# WAL Section 3.2 Oracle 审核报告 — Issues 清单 > 来源:Oracle 对 `docs/design.md` Section 3.2 WAL 的架构审核 > 日期:2026-06-09 --- ## Critical Issues ### C1. WAL write failure 语义过于简化:`write()` 失败后 bytes 可能已落盘 **严重程度**: Critical **位置**: Section 3.2 "写入流程" 步骤 ⑤ 及后续错误处理段落(~line 117) **问题描述**: 当前设计将 WAL encode/write 失败统一当作 "definitely failed" 返回普通错误。但实际上存在两种不同情况: 1. **Encode 失败**(未触及 syscall):确实是 definitely failed,可以安全返回普通错误 2. **`write()` 失败**(bytes 可能已进入 OS page cache 或部分写入文件):不是 definitely failed。Recovery 后可能发现该 batch CRC 合法并被重放,导致语义矛盾——调用方收到错误认为写入失败,但数据实际被恢复 **影响**: 调用方可能基于"写入失败"做非幂等业务决策(如放弃、走替代路径),但数据实际持久化了。 **建议修复**: 拆分 WAL write failure 处理: ```text - encode 失败(未调用 write()): 普通错误,write-stopped - write() 失败(bytes 可能已交给 OS): ErrCommitUnknown + write-stopped 除非实现能证明零 bytes 到达文件(例如 write 返回 0 且无副作用) ``` --- ### C2. Segment 边界跨 Batch 行为未定义 **严重程度**: Critical **位置**: Section 3.2 "Block 边界处理"(~line 303)与 "Recovery 扫描流程"(~line 436) **问题描述**: WAL Batch 可拆成多个 Physical Record 跨多个 Block,但未定义 WAL Batch 是否可以跨 segment 文件。Recovery 中 incomplete fragment 的处理取决于是否位于"最后一个需要恢复的 segment"(~line 493): - 如果 Batch 可以跨 segment:recovery 必须在 segment 间携带 fragment 收集状态(CollectingFragments),这增加了恢复复杂度 - 如果 Batch 不可跨 segment:需要显式约束 segment rotation 时机 当前 recovery 流程按 segment 顺序独立扫描,未定义跨 segment fragment 收集。 **影响**: 可能导致合法的跨 segment batch 被误判为中间损坏,或需要引入复杂的跨 segment 状态管理。 **建议修复**: 在 Section 3.2 明确添加约束: ```text WAL Batch 不得跨 segment 文件。Segment rotation 只在 WAL Batch 边界发生。 当前 segment 写入完一个完整 WAL Batch 后,如果需要轮转,在下个 Batch 写入前切换到新 segment。 ``` --- ### C3. 预创建 segment 可破坏尾部截断逻辑 **严重程度**: Critical **位置**: Section 3.2 "WAL 元数据持久化协议"(~line 173)与 "Physical Record 解析规则"(~line 491) **问题描述**: 新 segment 创建协议(步骤 1-8)允许 `segment-N+1.wal` 在文件系统上可见(已完成 rename + directory fsync),即使它尚未承载任何 batch。如果此时进程崩溃,`segment-N.wal` 可能有 crash-torn tail。 Recovery 扫描时,因为 `segment-N+1.wal` 存在且 header 合法,`segment-N` 不再被视为"最后一个需要恢复的 segment"。按照当前的尾部/中间损坏分类规则,`segment-N` 的尾部损坏会被升级为"WAL 中间损坏"→ 报错而非截断。 **影响**: 一个本应可截断恢复的尾部 partial write 场景被错误升级为不可恢复的中间损坏,导致整个 DB 无法启动。 **建议修复**: 方案 A(推荐):禁止预创建未来 segment,直到当前 segment 在完整 batch 边界 sealed: ```text 新 segment 只在当前 active segment 写完一个完整 WAL Batch 后才创建。 确保 segment-N 永远在完整 batch 边界结束,segment-N+1 的创建不先于该 sealing。 ``` 方案 B:Recovery 能识别并忽略空 segment(startSequence == expectedSequence 但无任何 batch): ```text 如果 segment header 合法但不含任何 complete batch,且 startSequence == expectedSequence, 视为空 segment,跳过或删除,继续扫描下一个 segment。 ``` --- ### C4. 恢复后未 fsync 确认的 Batch 可能变成已发布 **严重程度**: Critical **位置**: Section 3.2 "恢复完成状态"(~line 575)与 "持久化策略"(~line 96) **问题描述**: Recovery 重放所有 CRC 合法、sequence 连续的 WAL batch,并在恢复完成后将其全部标记为 `published`。但其中可能包含崩溃前从未 fsync 确认(调用方未收到成功)的 batch。 场景: 1. WAL bytes 已通过 `write()` 写入 OS page cache 2. 进程崩溃(非掉电),OS 将 page cache 刷盘 3. 重启后 recovery 发现该 batch 完整、CRC 合法、sequence 连续 4. 该 batch 被重放并标记为 published 5. 但调用方从未收到成功确认 **影响**: `Always` 策略下,调用方收到的语义是 "Put 返回成功 = 已持久化"。但如果进程 crash(非掉电),未确认的写入可能变成已发布。这是一个 API 语义问题而非数据安全问题。 **建议修复**: 在 Section 3.2 "持久化策略" 或 "恢复完成状态" 中显式声明: ```text 进程崩溃(非掉电)后恢复时,OS page cache 中已写入但尚未 fsync 的完整 WAL Batch 可能被恢复并视为已发布。这不是数据丢失,而是数据可见性前移。 调用方必须理解:进程崩溃重启后,比掉电场景可能多恢复一些写入。 如果需要严格区分"调用方已确认"与"未确认但存在于 WAL",需要后续引入 durable commit marker 或 confirmed-sequence 元数据。 ``` --- ### C5. Go 内存模型:lock-free read 需要原子发布机制 **严重程度**: Critical **位置**: Section 3.3 MemTable "并发策略"(~line 600)与 Section 3.2 步骤 ⑥⑧(~line 109-112) **问题描述**: Section 3.3 声明 MemTable 使用 "Mutex 写 + 无锁读",Section 3.2 步骤 ⑥ 在 fsync 前将 entry 写入 MemTable(pending 状态),步骤 ⑧ 通过更新 `publishedSequence` 使 entry 对无锁读可见。 在 Go 内存模型中: 1. **Skiplist 节点发布**:Mutex 保护下的写入对未持锁的并发读者不一定可见。需要 `atomic.Pointer` 或等效发布机制确保节点对读者可见。 2. **`publishedSequence` 更新**:作为普通变量写入,无锁读者可能看到过时值或部分写入。必须是 atomic 操作。 **影响**: 在 ARM 架构(弱内存序)上可能出现读者看到 `publishedSequence` 已更新但对应 skiplist 节点尚未可见的情况,导致读到不一致数据。 **建议修复**: 在 Section 3.2 或 3.3 中明确内存序要求: ```text 1. MemTable skiplist 节点必须通过 atomic store(atomic.Pointer 或自定义 release 操作)发布, 确保无锁读者看到完整的节点内容。 2. publishedSequence 必须是 atomic 变量(atomic.Uint64), 且其 Store 必须在所有 batch entries 的 skiplist 节点都已原子发布之后执行。 这保证读者先看到节点,再通过 publishedSequence 筛选可见 entry。 3. 读者必须先 atomic Load publishedSequence,再遍历 skiplist。 ``` --- ## Important Issues ### I1. MemTable 写入失败(Arena 满)在 WAL 写入后未覆盖 **严重程度**: Important **位置**: Section 3.2 "写入流程" 步骤 ⑥(~line 109) **问题描述**: 写入流程步骤 ⑤(WAL encode/write)成功后,步骤 ⑥ 写入 MemTable 可能因为 Arena 满而失败。此时 WAL bytes 已持久化(或已在 page cache),但 MemTable 中没有对应 entry。设计文档未覆盖此场景。 **建议修复**: ```text 方案 A(推荐):在 WAL write 之前保证 MemTable 有足够容量。 写入前检查 Arena 剩余空间,不足时先冻结 MemTable 并创建新 MemTable。 Arena 预留必须考虑最大可能的 batch size。 方案 B:MemTable 写入失败后按 ErrCommitUnknown + write-stopped 处理。 因为 WAL bytes 可能已持久化,不能按普通错误处理。 ``` --- ### I2. `CURRENT` 文件权威性与实际恢复模型不一致 **严重程度**: Important **位置**: Section 3.2 "CURRENT / MANIFEST 权威性"(~line 420)与 "WAL 元数据持久化协议"(~line 181) **问题描述**: 设计明确声明 `CURRENT` 只是写入侧辅助文件,recovery 权威源是 `MANIFEST + 目录扫描`。但 durable-ready 协议要求在 segment 可承载写入前更新 `CURRENT` 并 fsync(步骤 6-7)。这意味着 `CURRENT` 更新是 batch 确认成功的前提之一,但 recovery 又不依赖它。 **建议修复**: 选择一种并保持一致: ```text 方案 A(推荐):简化 durable-ready 协议,移除 CURRENT 更新作为 batch 确认前提。 Recovery 通过 MANIFEST + 目录扫描发现 segment,CURRENT 仅作为写入侧快速定位优化。 新 segment 只需 rename + WAL directory fsync 即可进入 durable-ready。 方案 B:让 CURRENT 成为 recovery 的必要组件。 这样需要处理 CURRENT 损坏/缺失的 fallback,增加恢复复杂度。不推荐。 ``` --- ### I3. 尾部截断后缺少持久化步骤 **严重程度**: Important **位置**: Section 3.2 "Physical Record 解析规则" 尾部损坏处理(~line 495) **问题描述**: Recovery 允许截断最后一个 segment 的尾部损坏。但截断操作本身(`ftruncate` + 删除后续空 segment)需要 fsync 才能在再次崩溃时保持一致性。设计文档未说明截断后的持久化步骤。 **建议修复**: 在 "恢复完成状态" 之后或 "Recovery 扫描流程" 末尾添加: ```text 截断持久化步骤: 1. ftruncate active segment 到 lastCompleteBatchEnd 2. fsync truncated segment 3. 删除 startSequence == expectedSequence 但无 complete batch 的后续空 segment 4. fsync WAL directory 5. 更新 MANIFEST 记录恢复终点 6. fsync metadata directory 以上完成后,引擎才能开始接受新写入。 ``` --- ### I4. Batch 校验缺少资源上限 **严重程度**: Important **位置**: Section 3.2 "WAL Batch 校验与重放"(~line 530) **问题描述**: Recovery 校验 `entryCount`、`entriesSize` 和 entry 边界,但未定义任何资源上限。恶意或损坏的 WAL 可能包含极大的 `entryCount` 或 `entriesSize`,导致 recovery OOM 或无限循环。 **建议修复**: 在 Section 3.2 添加硬性限制: ```text WAL Batch 资源上限(可配置,建议默认值): - entryCount: 最大 10,000 - entriesSize: 最大 4MB - 单个 keyLen: 最大 4KB(不含 value) - 单个 valLen (Inline): 最大 4KB(超过走 ValueLogPointer) - fragment buffer: 最大 entriesSize 上限 - varint: 最大 5 bytes(u64 varint 上限) Recovery 解析时,超过任何上限即视为 WAL 损坏。 写入侧也必须遵守这些限制,超出拒绝写入。 ``` --- ### I5. `publishedSequence` 需要明确的内存序约束 **严重程度**: Important **位置**: Section 3.2 "可见性语义"(~line 148) **问题描述**: `publishedSequence` 作为普通变量描述其语义,但未说明其在 Go 内存模型中的操作类型。多 goroutine 并发读写需要明确的 happens-before 关系。 **建议修复**: 在 "可见性语义" 小节补充: ```text publishedSequence 的内存序约束: 1. 类型:atomic.Uint64(或等效原子变量) 2. 写入侧:Store 只在 WAL durability 和所有 MemTable 节点原子发布都完成后执行 3. 读取侧:Load 在遍历 MemTable 前执行,获得可见性 high-water mark 4. Happens-before 关系: WAL fsync 完成 → MemTable 节点原子发布 → publishedSequence.Store → 读者 publishedSequence.Load → 遍历 MemTable 筛选可见 entry ``` --- ## 变更追踪 | Issue | 类型 | 优先级 | 状态 | |-------|------|--------|------| | C1 | 语义正确性 | Critical | Open | | C2 | 格式完整性 | Critical | Open | | C3 | 恢复正确性 | Critical | Open | | C4 | API 语义 | Critical | Open | | C5 | 内存安全 | Critical | Open | | I1 | 错误处理完整性 | Important | Open | | I2 | 设计一致性 | Important | Open | | I3 | 持久化完整性 | Important | Open | | I4 | 安全性/鲁棒性 | Important | Open | | I5 | 内存序正确性 | Important | Open |