From dde7f38af073b0f4dc933e56258361da725e2d8e Mon Sep 17 00:00:00 2001 From: dailz Date: Thu, 11 Jun 2026 20:26:51 +0800 Subject: [PATCH] update WAL batch resource limit design Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- docs/design.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/design.md b/docs/design.md index 3e85941..b2d417a 100644 --- a/docs/design.md +++ b/docs/design.md @@ -116,7 +116,7 @@ 默认模式下,写入成功发生在 WAL fsync 之后;但 fsync 的粒度是 WAL Batch,不是单条 `Put`。因此多条写入共享一次 fsync 成本,同时每条写入仍然只有在自身所在 WAL Batch 持久化后才算成功。 -步骤 ⑤ 是 WAL 副作用之前的强制容量关口:WAL writer 必须按 Batch 内所有 entry 的最大内存占用(key、value 或 ValueLogPointer、skiplist 节点、arena 对齐与层高开销)计算需要预留的 Arena 字节数。若当前可写 MemTable 剩余容量不足,必须先把它冻结为 Immutable MemTable,并创建新的可写 MemTable 后再继续。若 Immutable MemTable 队列已达上限,该 Batch 必须在 WAL write 之前等待后台 flush 释放容量;若单个 Batch 的最大可能内存占用超过 MemTable 上限,则该 Batch 在进入 WAL 前按普通错误拒绝。实现不得在 WAL write / append 已尝试之后,再因为 Arena 满而让 MemTable pending 写入失败。 +步骤 ④ 和 ⑤ 是 WAL 副作用之前的强制资源关口:WAL writer 必须先执行 WAL Batch 资源上限校验,再按 Batch 内所有 entry 的最大内存占用(key、value 或 ValueLogPointer、skiplist 节点、arena 对齐与层高开销)计算需要预留的 Arena 字节数。若 Batch 超过 WAL 资源上限,或单个 Batch 的最大可能内存占用超过 MemTable 上限,该 Batch 必须在 sequence 分配和 WAL write 之前按普通错误拒绝。若当前可写 MemTable 剩余容量不足,必须先把它冻结为 Immutable MemTable,并创建新的可写 MemTable 后再继续。若 Immutable MemTable 队列已达上限,该 Batch 必须在 WAL write 之前等待后台 flush 释放容量。实现不得在 WAL write / append 已尝试之后,再因为 Batch 超限或 Arena 满而让 MemTable pending 写入失败。 步骤 ⑧ 与 ⑩ 之间存在明确的内存序约束:MemTable skiplist / arena 节点必须先通过原子发布机制写入读路径可见结构(例如 `atomic.Pointer` store-release,或等价的 release publish),并且 Batch 内所有 entry 的节点都完成发布后,才能用 `atomic.Uint64.Store` 推进 `publishedSequence`。普通读者必须先 `Load` 当前 `publishedSequence`,再遍历 MemTable;读到 entry 后仍以 `entry.sequence <= loadedPublishedSequence` 判断可见性。该顺序保证弱内存序架构上读者不会先观察到已推进的 `publishedSequence`,却看不到对应已发布的 skiplist 节点。 @@ -371,6 +371,21 @@ Entry 的 sequence 由 batch 内位置推导: entry[i].sequence = baseSequence + i ``` +###### WAL Batch 资源上限 + +WAL Batch 解析和写入必须使用同一套可配置资源上限。默认上限如下: + +| 资源 | 默认上限 | 约束目的 | +|------|----------|----------| +| `entryCount` | 10,000 | 限制单个 batch 的解析循环次数和重放工作量 | +| `entriesSize` | 4MB | 限制 Entries 区域和完整 batch payload 的内存占用 | +| 单个 `keyLen` | 4KB | 防止单 key 分配过大 | +| 单个 Inline `valLen` | 4KB | 超过该大小的 value 必须走 ValueLogPointer | +| fragment buffer | 不超过 Batch Header 长度 + `entriesSize` 上限 | 防止 fragment 拼接阶段无限增长 | +| varint 编码长度 | 最大 5 bytes | 防止畸形 varint 长时间解析或解出超大长度 | + +写入侧必须在 sequence 分配、MemTable 预留和 WAL append 之前完成这些校验;超出任一上限时按普通错误拒绝写入,且不得产生 WAL 副作用。Recovery 侧超过任一上限时视为 WAL 损坏,并按“WAL 尾部可截断、中间损坏报错”的规则处理。 + ###### 事务边界与 commit sequence WAL 的 `baseSequence + i` 是物理 mutation sequence,用于保持 WAL 重放顺序和 `publishedSequence` 连续推进;它不能直接等同于多 key 事务的逻辑提交时间。 @@ -549,8 +564,8 @@ CollectingFragments | Idle | First | 开始收集 fragment,进入 CollectingFragments | | Idle | Middle | 非法 fragment 顺序 | | Idle | Last | 非法 fragment 顺序 | -| CollectingFragments | Middle | 追加 payload 到当前 fragment buffer | -| CollectingFragments | Last | 追加 payload,拼出完整 WAL Batch,解析重放后回到 Idle | +| CollectingFragments | Middle | 追加 payload 到当前 fragment buffer;追加后 buffer 不得超过 Batch Header 长度 + WAL Batch `entriesSize` 上限 | +| CollectingFragments | Last | 追加 payload;追加后 buffer 不得超过 Batch Header 长度 + WAL Batch `entriesSize` 上限,随后拼出完整 WAL Batch,解析重放后回到 Idle | | CollectingFragments | Full | 非法 fragment 顺序 | | CollectingFragments | First | 非法 fragment 顺序 | | Idle | segment 结束 | 正常结束当前 segment | @@ -565,26 +580,26 @@ Segment 边界不是合法的 fragment 边界。`First + Middle* + Last` 必须 ```text require flags 合法 -require entryCount > 0 -require entriesSize > 0 +require 0 < entryCount <= maxWalBatchEntryCount +require 0 < entriesSize <= maxWalBatchEntriesSize require entriesSize == 实际 Entries bytes 长度 require batch.baseSequence == expectedSequence ``` -然后顺序解析 Entries: +然后顺序解析 Entries。解析 `keyLen` / `valLen` varint 时,编码长度不得超过 `maxWalVarintBytes`;在分配 key/value buffer 或切片前,必须先完成长度上限和 Entry 边界校验: ```text for i in 0..entryCount: sequence = batch.baseSequence + i parse opType, valueKind, keyLen, valLen, key, value require opType ∈ {Put, Delete} - require keyLen > 0 + require 0 < keyLen <= maxWalKeyBytes require Entry 不越界 if opType == Put: require valueKind ∈ {Inline, ValueLogPointer} if valueKind == Inline: - valLen 可以为 0 + require valLen <= maxWalInlineValueBytes replay PutInline(key, value, sequence) if valueKind == ValueLogPointer: require valLen > 0