Files
go-kv/docs/issues/oracle-wal-3.2-review.md
T
dailz cf913b1d52 feat: initialize project structure and error types
- 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
2026-06-12 13:23:27 +08:00

298 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 可以跨 segmentrecovery 必须在 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。
```
方案 BRecovery 能识别并忽略空 segmentstartSequence == 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 写入 MemTablepending 状态),步骤 ⑧ 通过更新 `publishedSequence` 使 entry 对无锁读可见。
在 Go 内存模型中:
1. **Skiplist 节点发布**:Mutex 保护下的写入对未持锁的并发读者不一定可见。需要 `atomic.Pointer` 或等效发布机制确保节点对读者可见。
2. **`publishedSequence` 更新**:作为普通变量写入,无锁读者可能看到过时值或部分写入。必须是 atomic 操作。
**影响**: 在 ARM 架构(弱内存序)上可能出现读者看到 `publishedSequence` 已更新但对应 skiplist 节点尚未可见的情况,导致读到不一致数据。
**建议修复**:
在 Section 3.2 或 3.3 中明确内存序要求:
```text
1. MemTable skiplist 节点必须通过 atomic storeatomic.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。
方案 BMemTable 写入失败后按 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 bytesu64 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 |