From 5e9836c93118b38c5026c5428cd044ff948c5846 Mon Sep 17 00:00:00 2001 From: dailz Date: Thu, 11 Jun 2026 19:53:17 +0800 Subject: [PATCH] update MemTable arena reservation design Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- docs/design.md | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/docs/design.md b/docs/design.md index 8d68d4c..e6e5d3c 100644 --- a/docs/design.md +++ b/docs/design.md @@ -105,29 +105,33 @@ ② WAL writer 收集当前队列中的多条写入,组成 WAL Batch ③ 等待组提交触发(500µs 或 32KB,先到者触发) ④ 完成 Batch 校验与 WAL 编码前置准备 -⑤ 为 Batch 内每条写入分配递增的 sequence number,并在内存中编码 WAL Batch -⑥ 将编码后的 WAL Batch 拆分为一个或多个 Physical Record,追加写入 WAL 文件 -⑦ 写入 MemTable,但标记为 pending / unpublished -⑧ WAL Batch fsync 落盘 -⑨ 发布 publishedSequence,使 Batch 内写入对普通读可见 -⑩ 唤醒 Batch 内每个调用方,分别返回客户端"写入成功" +⑤ 预留 MemTable Arena 容量;不足时先冻结当前 MemTable 并切换到新的可写 MemTable +⑥ 为 Batch 内每条写入分配递增的 sequence number,并在内存中编码 WAL Batch +⑦ 将编码后的 WAL Batch 拆分为一个或多个 Physical Record,追加写入 WAL 文件 +⑧ 写入 MemTable,但标记为 pending / unpublished +⑨ WAL Batch fsync 落盘 +⑩ 发布 publishedSequence,使 Batch 内写入对普通读可见 +⑪ 唤醒 Batch 内每个调用方,分别返回客户端"写入成功" ``` 默认模式下,写入成功发生在 WAL fsync 之后;但 fsync 的粒度是 WAL Batch,不是单条 `Put`。因此多条写入共享一次 fsync 成本,同时每条写入仍然只有在自身所在 WAL Batch 持久化后才算成功。 -步骤 ⑦ 与 ⑨ 之间存在明确的内存序约束:MemTable skiplist / arena 节点必须先通过原子发布机制写入读路径可见结构(例如 `atomic.Pointer` store-release,或等价的 release publish),并且 Batch 内所有 entry 的节点都完成发布后,才能用 `atomic.Uint64.Store` 推进 `publishedSequence`。普通读者必须先 `Load` 当前 `publishedSequence`,再遍历 MemTable;读到 entry 后仍以 `entry.sequence <= loadedPublishedSequence` 判断可见性。该顺序保证弱内存序架构上读者不会先观察到已推进的 `publishedSequence`,却看不到对应已发布的 skiplist 节点。 +步骤 ⑤ 是 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 文件或任何可能被 recovery 看到的 WAL 缓冲区。若 Batch 校验或内存编码在步骤 ⑤ 失败,且尚未分配 sequence,该 Batch 可以按普通错误返回,调用方可安全地认为本次提交没有进入 WAL。若实现已经分配了 sequence 且不复用,即使失败尚未触碰 WAL,也必须进入 write-stopped 状态;否则 `publishedSequence` 无法跳过失败 Batch 推进,后续成功 Batch 也会因为 sequence 空隙而不可见。因此实现应尽量把可失败的校验与编码前置到 sequence 分配之前,减少无 WAL 副作用的普通错误触发 write-stopped。 +步骤 ⑧ 与 ⑩ 之间存在明确的内存序约束:MemTable skiplist / arena 节点必须先通过原子发布机制写入读路径可见结构(例如 `atomic.Pointer` store-release,或等价的 release publish),并且 Batch 内所有 entry 的节点都完成发布后,才能用 `atomic.Uint64.Store` 推进 `publishedSequence`。普通读者必须先 `Load` 当前 `publishedSequence`,再遍历 MemTable;读到 entry 后仍以 `entry.sequence <= loadedPublishedSequence` 判断可见性。该顺序保证弱内存序架构上读者不会先观察到已推进的 `publishedSequence`,却看不到对应已发布的 skiplist 节点。 -步骤 ⑥ 一旦尝试 WAL write / append,后续错误的判断标准不再是 Batch 是否已经进入 MemTable,而是 WAL bytes 是否可能已经到达 recovery 可见状态。`write()` 返回错误时,部分或全部 bytes 可能已经进入 OS page cache、WAL 文件或实现内部的 WAL buffer;即使 Batch 尚未进入 MemTable,也不能把它当作 definitely failed。除非实现能证明 zero bytes reached WAL state(例如未调用 write,或明确的内部缓冲协议证明没有任何 byte 被提交给 WAL 状态),否则 Batch 内调用方必须返回 `ErrCommitUnknown`,引擎进入 write-stopped 状态。 +步骤 ⑥ 必须只做内存内编码,不得触碰 WAL 文件或任何可能被 recovery 看到的 WAL 缓冲区。若 Batch 校验、MemTable 容量预留或内存编码在步骤 ⑥ 之前失败,且尚未分配 sequence,该 Batch 可以按普通错误返回,调用方可安全地认为本次提交没有进入 WAL。若实现已经分配了 sequence 且不复用,即使失败尚未触碰 WAL,也必须进入 write-stopped 状态;否则 `publishedSequence` 无法跳过失败 Batch 推进,后续成功 Batch 也会因为 sequence 空隙而不可见。因此实现应尽量把可失败的校验、容量预留与编码前置到 sequence 分配之前,减少无 WAL 副作用的普通错误触发 write-stopped。 + +步骤 ⑦ 一旦尝试 WAL write / append,后续错误的判断标准不再是 Batch 是否已经进入 MemTable,而是 WAL bytes 是否可能已经到达 recovery 可见状态。`write()` 返回错误时,部分或全部 bytes 可能已经进入 OS page cache、WAL 文件或实现内部的 WAL buffer;即使 Batch 尚未进入 MemTable,也不能把它当作 definitely failed。除非实现能证明 zero bytes reached WAL state(例如未调用 write,或明确的内部缓冲协议证明没有任何 byte 被提交给 WAL 状态),否则 Batch 内调用方必须返回 `ErrCommitUnknown`,引擎进入 write-stopped 状态。 WAL 写入路径的失败分类如下: | 失败点 | WAL 副作用 | 客户端结果 | 引擎状态 | |--------|------------|------------|----------| -| Batch 校验 / 内存编码失败,且尚未分配 sequence | 无 | 普通错误 | 可继续写入 | -| Batch 校验 / 内存编码失败,但 sequence 已分配且不复用 | 无 | 普通错误 | write-stopped | +| Batch 校验 / MemTable 容量预留 / 内存编码失败,且尚未分配 sequence | 无 | 普通错误 | 可继续写入 | +| Batch 校验 / MemTable 容量预留 / 内存编码失败,但 sequence 已分配且不复用 | 无 | 普通错误 | write-stopped | | WAL write / append 已尝试后失败,且无法证明 zero bytes reached WAL state | 可能有 | `ErrCommitUnknown` | write-stopped | +| WAL write / append 已成功,但 MemTable pending 写入违反预留保证仍然失败 | 可能有 | `ErrCommitUnknown` | write-stopped | | WAL bytes 已写入,MemTable pending 写入后 fsync 失败 | 可能有 | `ErrCommitUnknown` | write-stopped | `ErrCommitUnknown` 的 Batch 在当前运行期仍不发布、不确认成功;如果已经写入 MemTable,其 entries 保持 pending / unpublished 或转为 aborted,仅作为内部状态存在,对普通读不可见。`publishedSequence` 始终保持连续 high-water mark,普通读仍可使用 `sequence <= publishedSequence` 判断可见性。重启后,如果 recovery 在 WAL 中发现该 Batch 完整、CRC 合法且 sequence 连续,可以按正常 WAL 规则重放;如果只留下尾部 partial write,则按 WAL 尾部截断规则处理。 @@ -666,6 +670,23 @@ MemTable (可写, 64MB) 释放 Immutable MemTable ``` +#### Arena 容量预留 + +MemTable 必须提供写入前容量预留能力,供 WAL writer 在追加 WAL 之前调用。预留必须覆盖整个 WAL Batch,而不是逐条 entry 边写边试:一旦预留成功,后续把 Batch 内所有 entry 写入 pending / unpublished MemTable 时,不得再因为 Arena 满返回错误。预留应绑定到具体的可写 MemTable generation;如果后续在 sequence 分配和 WAL append 之前发生 Batch 校验或内存编码失败,预留必须可以回滚,或通过丢弃该 generation 的方式保证不会泄漏 Arena 容量。 + +预留计算至少包含:key bytes、value bytes 或 ValueLogPointer bytes、skiplist node 固定字段、随机层高带来的 next 指针数组、arena 对齐 padding,以及实现需要的 entry metadata。为避免随机层高导致预留不足,预留必须按该 MemTable 最大层高的最坏情况计算,或在预留阶段一次性确定并记住每个 entry 的层高。 + +写入侧必须先用 checked arithmetic 判断 Batch 最坏情况预留量是否超过空 MemTable 的可用容量上限;这里的上限是扣除 MemTable 固定元数据后的 usable capacity,而不是原始配置的 Arena 字节数。若单个 Batch 本身不可能放入一个空 MemTable,必须在 sequence 分配和 WAL append 之前拒绝该 Batch,不得先等待 flush 或追加 WAL。 + +当 Batch 可以放入空 MemTable、但当前可写 MemTable 剩余容量不足时,写入侧必须在 WAL write 之前执行 freeze + switch: + +1. 如果 Immutable MemTable 队列未满,冻结当前 MemTable,创建新的可写 MemTable,并在新 MemTable 上重新执行预留。 +2. 如果 Immutable MemTable 队列已满,阻塞新 Batch,等待后台 flush 释放队列名额;等待期间不得先追加 WAL。 + +如果实现违反上述预留协议,导致 WAL append 已经成功后 MemTable pending 写入仍因 Arena 满或 generation mismatch 失败,该 Batch 不能按普通错误处理;必须按 `ErrCommitUnknown` 返回并进入 write-stopped,因为 WAL bytes 已可能被 recovery 观察到。 + +该约束把 Arena 满从“WAL 已有副作用后的不确定提交”前移为“WAL 前的容量/背压决策”,避免出现 WAL 中存在完整 Batch、但当前运行期 MemTable 没有对应 pending entry 的状态。 + #### Flush 过滤规则 MemTable 可能包含已写入内存但尚未发布的 pending entry,也可能包含 fsync 失败后保留的 aborted entry。Flush 到 SSTable 时必须过滤这些 entry: