Phase 1 default state had two data-loss paths in WAL recovery.
C2: resolveRecoverySegmentID fell back to CURRENT when MANIFEST=0.
Since segment_manager writes CURRENT on every segment create/rotate,
the first recovery in Phase 1 (MANIFEST always 0 without flush) would
start from the active segment, skipping earlier unflushed segments.
C3: Recover called manifest.Save after every recovery, advancing
recoverySegmentID past segments that were still the only durable copy
of their data (no SSTable flush yet). Next restart would filter those
segments out and permanently lose the data.
Per design §3.2 line 280, recovery must not update MANIFEST; per line
604-06, CURRENT must not be used as recovery start. Both fixes are
required together — fixing C3 alone leaves C2's data-loss window open.
Changes:
- wal/recover.go: remove manifest.Save calls on both success and
tail-repair paths; remove CURRENT fallback in resolveRecoverySegmentID.
RecoveryResult.NextSegmentID is now in-memory only (consumed by DB.Open
to seed the new WalWriter, but never persisted to MANIFEST).
- wal/recover_test.go: rewrite TestRecoverUpdatesManifest as
TestRecoverDoesNotUpdateManifest; add TestRecoverPreservesExistingManifest,
TestRecoverIdempotentClean, TestRecoverIdempotentAfterTruncation,
TestRecoverIgnoresCurrentFallback.
- db_test.go: add TestOpenThreeTimesKeepsData (three opens to catch C3's
second-restart data loss; single-segment to avoid unrelated C8 bug
where segment_manager passes byte offset as startSequence).
Verified: each new test fails on pre-fix code and passes after the fix.
Full suite green including -race.
Audit context: docs/audit-3.2.md (with Oracle revisions from bg_ef425776
and bg_2e86d33b; C8 added). Plan: .omo/plans/fix-c2-c3-wal-recovery.md
(Momus + Oracle reviewed v1.2).
24 KiB
WAL 代码审核报告(对照 docs/design.md § 3.2)
审核范围:/wal/...、/memtable/...、/manifest/...、/config/...、db.go。
审核时间:基于 commit e34de4a(fix: implement group commit collection window)。
下面按严重程度排列,每条给出 设计要求 → 代码现状 → 后果 → 修复建议。
🔴 Critical(破坏持久化 / 恢复正确性,必须修)
C1. CRC 多项式错误:用 IEEE 而非 crc32c
- 设计要求(§3.2 Physical Record、WAL File Header):crc32c(Castagnoli,0x82F63B78),用于识别 torn write / partial write。
- 代码现状:
wal/record.go:30,55→crc32.ChecksumIEEEwal/header.go:44,83→crc32.ChecksumIEEE
- 后果:CRC 校验值与设计文档不一致;如果未来要做跨实现兼容(其他客户端 / 工具按 crc32c 校验),所有 segment 都会被判损坏。当前自洽但与规范脱钩。
- 修复:改用
hash/crc32.Castagnoli(即crc32.MakeTable(crc32.Castagnoli)),所有ChecksumIEEE替换为Checksum(data, castagnoliTable)。常数需要加测试固定。
C2. Recovery 用 CURRENT 作为兜底,违反"MANIFEST 是唯一权威"
- 设计要求(§3.2 CURRENT / MANIFEST 权威性,line 600-66):
MANIFEST 是 recovery 起点和 checkpoint 状态的权威源……CURRENT 只表示写入侧上次尝试记录的 active WAL segment hint……不能作为 recovery 起点、终点或排除 segment 的依据。
- 代码现状:
wal/recover.go:109-124if mf.RecoverySegmentID > 0 { return mf.RecoverySegmentID, nil } if segID, ok := manifest.ReadCurrent(dir); ok { return segID, nil } // ← 违规 return 0, nil - 后果:CURRENT 是 best-effort 写入、可能落后 / 指向已被截断或未 durable-ready 的 segment。用它做 recovery 起点,要么漏恢复(如果它指向比 MANIFEST 更新的 segment,而那个 segment 实际上没 durable-ready),要么恢复出空集(如果它指向已被 MANIFEST 覆盖删除的旧 segment)。这条路径在 MANIFEST.RecoverySegmentID==0 时(首次创建 DB 或刚 flush 后尚未写过 MANIFEST)会被触发。
- 修复:删掉 CURRENT 兜底,MANIFEST.RecoverySegmentID==0 时直接返回 0;CURRENT 仅在写入侧作为 hint 顺手写。
Oracle 修订(bg_ef425776):C2 在 Phase 1 比 C3 更严重。Phase 1 没有 flush,MANIFEST 一直保持 0;
segment_manager.go:47,90每次创建/轮转 segment 都会写 CURRENT 指向 active segment。默认状态下首次 Recover 就会触发 fallback,不需要"CURRENT 缺失/落后"这种特殊场景。修 C3 之前必须先修 C2,否则数据丢失窗口依然存在。
C3. Recovery 主动写 MANIFEST,违反"recovery repair 不更新 MANIFEST"
- 设计要求(§3.2 line 280):
MANIFEST 只作为 checkpoint / recovery 起点元数据;recovery repair 不更新 MANIFEST,也不通过 MANIFEST 记录恢复终点。
- 代码现状:
wal/recover.go:77,100无论恢复成功还是尾部截断,都会把 MANIFEST.RecoverySegmentID 改写为"最后一个 segment + 1"。if saveErr := manifest.Save(dir, result.NextSegmentID); saveErr != nil { ... } - 后果:直接数据丢失。设想 segment-5 是 active,写了若干 batch 后崩溃,恢复时只重放了部分 batch 并把尾部截断。代码把 MANIFEST 推进到 segment-6。下次启动时 recovery 直接从 segment-6 开始,跳过了 segment-5 中已经 durable 的 batch。设计明确要求 MANIFEST 只能由 checkpoint(MemTable flush 完成 + SSTable 元数据落盘)推进。
- 修复:删掉 recovery 中的
manifest.Save。MANIFEST 推进只能发生在 flush 完成后(未来 §3.3 实现)。
Oracle 修订(bg_ef425776):单修 C3 不足以解决 Phase 1 数据丢失,必须和 C2 一起修。测试方向需要区分干净 WAL 和尾部损坏 WAL 两种幂等性。
C4. 非尾段 fragment 状态被当成尾段损坏
- 设计要求(§3.2 Fragment 状态机 line 731):
Segment 边界不是合法的 fragment 边界……如果扫描到 WAL 尾部时仍处于 CollectingFragments 状态……丢弃该 incomplete batch。设计同时明确:CollectingFragments + 不是最后恢复 segment → 视为 WAL 中间损坏,报错。
- 代码现状:
wal/recovery.go:133-138在ReplaySegmentFile末尾,只要处于FragmentCollecting就返回TailCorruptionError,不区分当前 segment 是否是最后一个;RecoverFromSegments也直接透传。 - 后果:如果 segment-5 中间出现
First + Middle*但没有Last(中间损坏),但 segment-6 还存在,代码会把 segment-5 的中间损坏当尾部截断处理,截掉 segment-6 的有效数据。设计要求此时硬报错。 - 修复:
ReplaySegmentFile需要接收isLast bool参数,或者在RecoverFromSegments的 segment 循环里检查:非最后段返回CollectingFragments必须返回普通 corruption 错误,不是TailCorruptionError。
Oracle 修订(bg_ef425776):C4 还有一个更严重的失败模式。
wal/recover.go:61总是对segments[len(segments)-1]调用truncateSegment,但RecoverFromSegments的TailCorruptionError可能来自非尾段。结果:真正损坏的 segment 不动,最后一段的有效数据被错误截掉。
C5. 尾部截断不 fsync、不删空 segment、不 fsync 目录,且失败被吞
- 设计要求(§3.2 尾部截断持久化 line 786-800):
- ftruncate 当前 active segment 到 lastCompleteBatchEnd
- fsync 被截断的 segment
- 删除 startSequence == expectedSequence 且不含任何 complete batch 的后续空 segment
- fsync WAL directory 全部成功后 recovery 才能进入恢复完成状态。若任一步失败,recovery 必须报错,DB 不得进入可写状态。
- 代码现状:
wal/recover.go:60-69validOffset, truncErr := findValidOffset(lastSeg.FilePath) if truncErr != nil { result.TruncateError = fmt.Errorf("%w (find valid offset: %v)", err, truncErr) // 吞掉 } else if truncErr := truncateSegment(lastSeg.FilePath, validOffset); truncErr != nil { result.TruncateError = fmt.Errorf("%w (truncate: %v)", err, truncErr) // 吞掉 }truncateSegment只是os.Truncate,没有 fsync 文件、没有删后续空 segment、没有 fsync 目录,失败也只是写进result.TruncateError然后正常返回成功。 - 后果:崩溃恢复后 WAL 尾部可能再次暴露已被"截断"的脏字节,下次启动会重复 repair 或 repair 出不同边界;DB 已经接受新写入,破坏了"恢复后的 WAL 状态即权威"这一不变量。
- 修复:把 truncation 拆成独立函数,按设计 4 步串行执行,任何一步失败
return err,DB.Open 必须失败。
C6. SegmentWriter 创建时目录 fsync 失败被静默忽略
- 设计要求(§3.2 WAL 元数据持久化协议 line 248-273):durable-ready 协议第 5 步
fsync WAL directory是 segment 进入 durable-ready 的硬条件。目录 fsync 失败时 segment 不得承载可确认写入;若已有 Batch 依赖该 segment,进入 write-stopped。 - 代码现状:
wal/segment_writer.go:86-89if dirFD, derr := os.Open(dir); derr == nil { dirFD.Sync() // 错误被忽略 dirFD.Close() } - 后果:rename 已发生但目录元数据未落盘。掉电后恢复可能看不到这个 segment 文件,但 WAL writer 已经向调用方确认了该 Batch 成功("不丢已确认写入"被破坏)。这是 §3.2 Always 策略下最严重的 durability 漏洞之一。
- 修复:目录 fsync 错误必须返回,触发 write-stopped;同时显式管理 durable-ready 状态机(新字段
durableReady bool),未就绪时禁止 AppendBatch。
C7. Put/Delete vs Close 存在 send-on-closed-channel 竞态
- 代码现状:
wal/writer.go:314-326(Put)先检查writeStopped,再queue.Submit;而Close先writeStopped.Store(true)再queue.Close()。两者之间没有同步。 - 后果:并发调用 Put 和 Close 时,Put 通过 writeStopped 检查后、Submit 之前,Close 把 channel 关掉,Put 的
cq.ch <- req触发 panic。这不是 §3.2 设计直接约束,但破坏写入路径稳定性。 - 修复:Close 用 RWMutex 保护,Submit 用 RLock 检查 closed 标志;或者把 close 时机延后到所有 in-flight submit 完成。
C8. Segment 轮转时把字节偏移当 sequence number 传给新 segment(Oracle bg_2e86d33b 发现)
- 设计要求(§3.2 WAL File Header、Segment 连续性校验 line 639-663):每个 segment 的
startSequence必须严格衔接前一 segment 最后一个 batch 后的 sequence;recovery 时segment.StartSequence != expectedSequence必须报错。 - 代码现状:
wal/segment_manager.go:64-66if sm.active.RemainingPayload() < worstCaseSize { if err := sm.rotate(sm.active.CurrentOffset()); err != nil { ... } }rotate(newStartSequence uint64)的参数名是newStartSequence,但传入的sm.active.CurrentOffset()返回的是字节偏移(从WalFileHeaderSize=32累加),不是 sequence number。 - 后果:发生 segment 轮转后,新 segment 的 header 里
startSequence = 上一 segment 的字节偏移。Recovery 时wal/recovery.go:155-157:立刻失败。Phase 1 多 segment 场景的 recovery 实际上是坏的。if segment.StartSequence != nextSequence { error } - 修复:
SegmentManager需要跟踪当前nextSequence(或从 WalWriter 拿),rotate 时把它传下去,而不是CurrentOffset()。 - 对 C2+C3 patch 的影响:相关 e2e 测试(
TestOpenThreeTimesKeepsData)必须单 segment,不能强制轮转,否则会撞上 C8 而不是 C2+C3。
🟠 High(破坏格式约束或重要不变量)
H1. Physical Record 不校验 length > 0
- 设计要求(§3.2 Block 边界处理、Physical Record 解析规则 line 389, 681):
length必须> 0。 - 代码现状:
wal/record.go:38-67DecodePhysicalRecord只校验length <= len(data)-headerSize,没校验下界。wal/record_parser.go:69也没校验。 - 后果:攻击者 / 损坏数据可注入
length=0的 record,绕过 CRC(payload 为空时 CRC 只覆盖 length+type),恢复出空 batch 进而触发entry count is zero之类的错误,被误判为"尾部损坏可截断",实际上属于中间损坏。 - 修复:
DecodePhysicalRecord加if length == 0 { return ErrZeroLength },并在 parser 把它当中间损坏(非尾部不截断)。
H2. Physical Record 不校验 type != Invalid (0)
- 设计要求(§3.2 fragment 类型表 line 366-372):type=0 是 invalid,用于损坏检测。
- 代码现状:
wal/record.go解析时不校验 type;FragmentCollector.Append的 default 分支会拒绝,但ParseBlock收集 records 时不会。 - 后果:损坏的 type=0 record 会被加入 records 列表,传给 fragment collector 后才报错;这把"物理层损坏"推迟到"batch 层错误",错误分类可能错(按设计应该硬错,但实际可能被当 CRC 失败归为尾部损坏)。
- 修复:
DecodePhysicalRecord校验recType ∈ {1,2,3,4},否则返回错误。
H3. worstCaseSize 估算不足,segment 可能写超 MaxSegmentSize
- 设计要求(§3.2 Segment Rotation 约束、config 不变量 line 392-451):判断是否需要 rotate 时必须基于"真实最坏情况"开销:
numRecords * prHeaderSize + worstCaseBlockPadding,对默认配置最大 batch 是 903 + 7 = 910 bytes 开销。 - 代码现状:
wal/segment_manager.go:62只加 14 bytes(2 × 7)。对接近 4MB 的大 batch,少估了约 896 bytes。worstCaseSize := uint64(len(encodedBatch)) + uint64(PhysicalRecordHeaderSize) + uint64(PhysicalRecordHeaderSize) - 后果:当 active segment 剩余 payload 在
[encodedBatchSize + 14, encodedBatchSize + 910]区间时,代码认为放得下,实际写出后超过MaxSegmentSize。ValidateBatchLimits 只校验"空 segment 能放下",没校验"当前剩余能放下"。下一批次才会触发 rotate,期间 segment 文件实际大小超出配置上限。 - 修复:把
ValidateBatchLimits中已经算过的numRecords/overhead/padding计算抽成公共函数,segment_manager 用同样的公式判断 remaining。或者更稳:rotate 阈值改为if active.RemainingPayload() < minFreshSegmentPayload { rotate },即只要剩余不足以容纳最大合法 batch 就 rotate。
H4. MaxImmutableCount 配置存在但从不强制
- 设计要求(§3.2 写入流程 step ⑤ line 119):
若 Immutable MemTable 队列已达上限,该 Batch 必须在 WAL write 之前等待后台 flush 释放容量。
- 代码现状:
wal/writer.go:244-265reserveMemTable只在 active 满时 rotate,完全不检查 immutable 队列长度。MemTableList.rotateActive无脑 append。 - 后果:写入速度快于 flush 时,immutable 队列无限增长,OOM;同时也违反了"WAL write 前等待 flush"的设计约束。
- 修复:
reserveMemTable在 rotate 前检查len(immutable) >= MaxImmutableCount,是则阻塞等待条件变量(需要 flush goroutine 来唤醒)。Phase 1 没有 flush,至少应该在超限时返回ErrWriteStopped或阻塞,而不是无脑 append。
H5. Per-batch (segmentID, endOffset, endSequence) 跟踪缺失,durableSequence 推进模型错误
- 设计要求(§3.2 line 205-245):每个完整 Batch 必须记录
(segmentID, endOffset, endSequence);后台 fsync worker snapshot(segmentID, endOffset, endSequence);fsync 成功后只能推进到满足"segment 已 durable-ready + offset 覆盖 + endSequence 连续"的最大 batch。跨 segment 推进还需要目标 segment 的 header + rename + 目录都已 fsync。 - 代码现状:
wal/sequence.go:68-78MarkDurable只是 CAS 把 durableSequence 推到 seq;wal/writer.go:228每次processBatchfsync 后立刻调用MarkDurable(lastSequence)。 - 后果:当前 Phase 1 只有 Always 模式 + 单 writer 串行 fsync,功能上恰好正确(每次 fsync 覆盖且只覆盖当前 batch,durableSequence == publishedSequence)。但一旦未来引入 Periodic / 异步 fsync / 多 batch 合并 fsync,模型立刻崩溃。设计明确要求"durableSequence 不能只根据'最近一次 fsync 成功'模糊推进"。
- 修复:哪怕 Phase 1,也要按设计记录每 batch 的
(segmentID, endOffset, endSequence),并实现 fsyncSnapshot 比较逻辑。否则是对未来扩展的债务性违约。
H6. MemTable publish 机制不符合 release/acquire 模型
-
设计要求(§3.2 step ⑧-⑩ line 121):
MemTable skiplist / arena 节点必须先通过原子发布机制写入读路径可见结构(例如
atomic.Pointerstore-release,或等价的 release publish),并且 Batch 内所有 entry 的节点都完成发布后,才能用atomic.Uint64.Store推进publishedSequence。普通读者必须先Load当前publishedSequence,再遍历 MemTable;读到 entry 后仍以entry.sequence <= loadedPublishedSequence判断可见性。 -
代码现状:
memtable/memtable.go:87-127Publish:- 在
skiplist.mu下遍历收集 pending 节点; - 释放锁后,对每个 entry 调用
skiplist.Put(..., pending=false)创建新节点替换旧节点; mt.published.Store(upToSequence)。
读路径
skiplist.Get用node.pending判断可见性,完全不读publishedSequence。 - 在
-
后果:
- 读路径看到的可见性边界是"pending 标志位",不是
publishedSequence。Phase 1 单 key autocommit 下语义等价,但设计要求的是后者。 - Publish 创建新节点替换旧节点,旧 pending 节点成为 GC 垃圾;多个 batch 同时 publish(理论上)会竞争同一 key 的替换路径。
- 弱内存序架构上,新节点写入(通过
atomic.Pointer.Store)发生在published.Store之前,顺序对的;但设计要求的 publish-then-load-sequence 模式没有被代码使用,未来引入 MVCC 的commitSequence/visibleCommitSequence时会失配。 - MemTable.published 字段写完后没人读,是死代码。
- 读路径看到的可见性边界是"pending 标志位",不是
-
修复:要么按设计实现:skiplist 节点带 sequence,Put 时直接 published(不 pending),读路径 Load
publishedSequence后过滤;要么保留 pending 模型但在设计文档里明确改写,并删除/利用 MemTable.published。
H7. arena.Reserve 只是检查,不是真正预留
- 设计要求(§3.2 step ⑤ line 119):按 Batch 内所有 entry 的最大内存占用(key、value 或 ValueLogPointer、skiplist 节点、arena 对齐与层高开销)计算需要预留的 Arena 字节数。
- 代码现状:
memtable/arena.go:75-84Reserve只读 offset 不修改;memtable/memtable.go:55-70每条 entry 估算metadataOverhead = 32,没考虑maxLevel * sizeof(pointer) = 20 * 8 = 160bytes 的层高开销。skiplist 节点实际存在堆上(newSkipNode不用 arena)。 - 后果:Reserve 是个粗略的 budget gate,实际 heap 用量可能超过 MemTableSize;多线程并发 reserve 还可能 double-count(两个 batch 都看到 remaining 够,都写入,实际超限)。Phase 1 不用 arena 存节点,所以不会触发 ErrArenaFull,但容量预算失效。
- 修复:要么把 Reserve 改成真 atomic advance offset(commit 预留),要么显式承认 Phase 1 是软限制并在设计里标注;估算公式按最坏层高(maxLevel=20)算。
🟡 Medium(设计偏离但不立即致错)
H8. findValidOffset 不跟踪 batch 边界,截断点错位(Oracle bg_ef425776 升级 M1)
- 设计要求(§3.2 尾部截断持久化 line 786):截断目标必须是"最后一个完整 WAL Batch 的结束位置
lastCompleteBatchEnd"。 - 代码现状:
wal/recover.go:137-203的findValidOffset只用DecodePhysicalRecord校验物理记录,不跟踪完整 batch 边界 / fragment 状态机。对尾部First + Middle*没Last的情况:recovery.go:133在最后一个完整 batch 结尾报尾部损坏findValidOffset可能返回不完整 fragment 之后的 EOF,比真正应截断的位置更靠后
- 后果:残留半截 fragment 在文件尾部。下次启动 recovery 还会在同位置触发尾部损坏,反复 repair,每次结果可能不同。
- 修复:让
ParseRecordsFromFile直接返回lastValidOffset(基于完整 batch 边界),作为 truncation 的权威依据;删掉独立的findValidOffset重复解析。
原 M1("两次解析可能不一致")已升级为 H8,因为 Oracle 给出具体证据:不跟踪 batch 边界会让截断点错位,导致反复 repair。
M2. EncodeWalBatch(0, entries) 预校验后立刻丢弃,再编一次
wal/writer.go:192-204先用 baseSequence=0 编码一次只为校验,然后再用真实 baseSequence 编码。功能对(baseSequence 不影响 size),但白编一次 4MB buffer。- 修复:抽
validateEncodedSize(entries)函数,只算 size 不分配 buffer;或者直接信任ValidateBatchLimits已经覆盖的检查(其实已经覆盖了),删掉这次预编码。
M3. SyncMode 是字符串 "always",不是设计里的策略枚举
- 设计明确三种策略
Always/Periodic/Never,Phase 1 只支持 Always。代码用裸字符串,类型不安全,未来加 Periodic 时容易漏改地方。 - 修复:定义
type SyncMode int+ 常量,配置序列化层做字符串映射。
M4. CommitQueue 容量 = MaxBatchEntries,可能过大
wal/writer.go:82queueCapacity := max(1, int(cfg.MaxBatchEntries))默认 10000,意味着 channel 缓冲 10000 个*CommitRequest。每个 request 至少带 1 个 entry 的 key/value clone。高并发下内存占用被低估。- 修复:独立配置项
CommitQueueCapacity,默认更小(例如 1024)。
M5. SegmentWriter 没有 durableReady 状态字段
- 设计要求 segment 显式进入 durable-ready 状态才能承载可确认写入。代码隐式假设"构造函数返回即可写",没有状态机字段。
- 修复:加
durableReady bool字段,AppendBatch前断言;构造函数中所有 fsync 步聚通过后才置 true。
M6. GroupCommitDelay 计时器语义偏离设计
- 设计:"500µs 或 32KB,先到者触发"。代码
runLoop每次从 channel 读到第一个 req 后启 timer,500µs 内继续收集。这相当于"从第一个 req 起等待 500µs",与设计一致。但 timer 在每个 batch 循环重建,如果 channel 持续有 req,低负载时其实仍能 500µs 触发,OK。不算 bug,但建议注释清楚。
M7. MemTable.aborted 是 sync.Map 且从不清理
memtable/memtable.go:36。每次Abort(seq)写入,从不删除。长时间运行 + 频繁 abort 会内存泄漏。- 修复:Publish 时把已 publish 的 sequence 从 aborted 里删掉;或者用 bitmap。
M8. processRemaining 在 write-stopped 后仍会处理队列
wal/writer.go:165-173在 close 时 drain 队列。但processBatch内部会先检查writeStopped,如果已停止会直接 sendError。所以 drain 不会真正写 WAL,OK。但需要测试覆盖这个路径。
M9. 错误消息中"segment 损坏"和"尾部损坏"混用,难审计
- 多处
fmt.Errorf没用%w包装ErrWALCorrupted,调用者难以errors.Is。 - 修复:定义
ErrWALCorrupted/ErrWALTailCorrupted哨兵错误,所有 corruption 路径用%w包装。
🟢 Low / 建议
- L1:
wal/header.go把walFileHeaderSize等小写常量和wal/header.go与wal/constants.go中重复的大写常量合并,避免两套维护。 - L2:
wal/segment_writer.go:42os.O_EXCL在并发 Open 同一 tmp 名时直接失败,错误信息可加上"可能是上次崩溃残留"。 - L3:
memtable/skiplist.go:209rand.Float64()非并发安全(Go 1.20+ 默认 auto-seed 后是安全的,但显式用math/rand/v2更清楚)。 - L4:
config/config.go:124SyncMode != "always"错误消息建议列出合法值。 - L5:测试覆盖建议补:① crc32c 固定向量;② 非尾段 CollectingFragments 必须 hard error;③ MANIFEST 不被 recovery 修改;④ Put(k, []) 与 Delete(k) 的 Get 结果在恢复后仍能区分;⑤ dir fsync 失败时 segment 不可写。
总结(必须立刻修的)
| 编号 | 一句话 | 风险等级 |
|---|---|---|
| C1 | CRC 多项式错(IEEE → crc32c) | 格式不符规范 |
| C2 | Recovery 用 CURRENT 兜底 | 恢复起点错误 |
| C3 | Recovery 写 MANIFEST | 下次启动跳过有效 segment,丢数据 |
| C4 | 非尾段 CollectingFragments 当尾段损坏 | 中间损坏被静默截断 |
| C5 | 截断无 fsync / 不删空 segment / 失败被吞 | 重复 repair,DB 状态不一致 |
| C6 | SegmentWriter dir fsync 失败被吞 | 掉电后已确认写入消失 |
| C7 | Put vs Close 的 channel 竞态 | 偶发 panic |
| C8 | Segment 轮转把字节偏移当 startSequence | 多 segment recovery 直接失败 |
C3 和 C6 是最严重的:C3 直接导致数据丢失,C6 直接破坏 Always 模式的"不丢已确认写入"承诺。C8 是隐藏炸弹:单 segment 时一切正常,第一次轮转后就坏。建议优先修复 C1-C8 这 8 条,再处理 H1-H7。
修复优先级建议
按数据丢失风险排:C3 → C6 → C5 → C2 → C4 → C1 → C7,再处理 H1-H7。
Oracle 修订(bg_2e86d33b):实际上 C2 和 C3 必须捆绑修复(C2 的触发条件在 Phase 1 默认状态下就成立,比 C3 更宽松),单独修 C3 不止血。修订后优先级:C2 + C3(一起)→ C6 → C5 + H8(一起,同一文件)→ C4 → C8 → C1 → C7。