- go.mod with github.com/dailz/go-kv, Go 1.26.3, testify - config/config.go with WalConfig, Validate() with checked arithmetic - errors.go with sentinel errors (ErrCommitUnknown, ErrWriteStopped, etc.) - wal/constants.go with all WAL format constants and enums - wal/header.go with WAL File Header encode/decode (CRC32 IEEE) - wal/record.go with Physical Record codec, block boundary, SplitIntoRecords - wal/entry.go with WAL Entry codec (varint keys/values, OpType, ValueKind) - wal/sequence.go with SequenceManager (atomic, CAS, overflow-safe) - manifest/manifest.go with MANIFEST stub (Load/Save atomic) - manifest/current.go with CURRENT file (WriteCurrent/ReadCurrent) - Comprehensive tests for all modules - .golangci.yml configuration
12 KiB
WAL Section 3.2 Oracle 审核报告 — Issues 清单
来源:Oracle 对
docs/design.mdSection 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" 返回普通错误。但实际上存在两种不同情况:
- Encode 失败(未触及 syscall):确实是 definitely failed,可以安全返回普通错误
write()失败(bytes 可能已进入 OS page cache 或部分写入文件):不是 definitely failed。Recovery 后可能发现该 batch CRC 合法并被重放,导致语义矛盾——调用方收到错误认为写入失败,但数据实际被恢复
影响: 调用方可能基于"写入失败"做非幂等业务决策(如放弃、走替代路径),但数据实际持久化了。
建议修复:
拆分 WAL write failure 处理:
- 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 明确添加约束:
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:
新 segment 只在当前 active segment 写完一个完整 WAL Batch 后才创建。
确保 segment-N 永远在完整 batch 边界结束,segment-N+1 的创建不先于该 sealing。
方案 B:Recovery 能识别并忽略空 segment(startSequence == expectedSequence 但无任何 batch):
如果 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。
场景:
- WAL bytes 已通过
write()写入 OS page cache - 进程崩溃(非掉电),OS 将 page cache 刷盘
- 重启后 recovery 发现该 batch 完整、CRC 合法、sequence 连续
- 该 batch 被重放并标记为 published
- 但调用方从未收到成功确认
影响: Always 策略下,调用方收到的语义是 "Put 返回成功 = 已持久化"。但如果进程 crash(非掉电),未确认的写入可能变成已发布。这是一个 API 语义问题而非数据安全问题。
建议修复:
在 Section 3.2 "持久化策略" 或 "恢复完成状态" 中显式声明:
进程崩溃(非掉电)后恢复时,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 内存模型中:
- Skiplist 节点发布:Mutex 保护下的写入对未持锁的并发读者不一定可见。需要
atomic.Pointer或等效发布机制确保节点对读者可见。 publishedSequence更新:作为普通变量写入,无锁读者可能看到过时值或部分写入。必须是 atomic 操作。
影响: 在 ARM 架构(弱内存序)上可能出现读者看到 publishedSequence 已更新但对应 skiplist 节点尚未可见的情况,导致读到不一致数据。
建议修复:
在 Section 3.2 或 3.3 中明确内存序要求:
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。设计文档未覆盖此场景。
建议修复:
方案 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 又不依赖它。
建议修复:
选择一种并保持一致:
方案 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 扫描流程" 末尾添加:
截断持久化步骤:
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 添加硬性限制:
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 关系。
建议修复:
在 "可见性语义" 小节补充:
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 |