Table of Contents

持久化稳定性架构(IO → 引擎 → WAL 三层崩溃一致性)

本文回答:Tier IO、存储引擎、TierWal 三层怎么拼出"崩溃后一致"的全套持久化稳定性—— 数据从"会丢的层"推进到"不会丢的层"的完整链条、各层水位体系、崩溃窗口语义,以及 "谁保证 fsync 顺序 / 谁保证原子性 / 谁保证恢复"的分工矩阵。

与各层使用指南的关系:

层 使用指南 本文补什么
Tier IO(Core/IO) io.md(§4.4 持久化概念)、virtual-file-system.md(崩溃窗口谱系) 它在持久化链中的角色与质量地板
存储引擎(Runtime) storage-engine.md(§3.4 持久化姿势 / §4 崩溃恢复)、log.md(EntryLog 页帧/水位/恢复) 三水位与双尾在链中的语义
TierWal(Products) tierwal.md(三条铁律/组提交/质量地板) 双水位如何消费下层保证并供给上层

代码引用为仓库相对路径(src/… 略去)。


1. 三层持久化模型总览

  上层(raft / 产品)          raft.WaitForPersistedAsync → 应答(多数派提交)
        │                            ↑
        ▼                            │ PersistedIndex
  ④ TierWal(Products)      双水位:AllocatedIndex(内存分配尾) / PersistedIndex(fsync 水位)
        │                            ↑
        ▼ CommitAsync / 组提交三维度   │ CommittedOffset
  ③ EntryLog(Runtime)      页帧流 + 双页 ping-pong + CommittedOffset/FlushedTail/TailAddress
        │                            ↑
        ▼ engine.Write + engine.Flush │ CommittedTail
  ② 存储引擎(Runtime)      三水位:MinAddress ≤ CommittedTail ≤ AllocatedTail + 段/页
        │                            ↑
        ▼ IFileHandle.Write / Flush   │ "不会丢的层"
  ① Tier IO(Core/IO)      IFileSystem 介质 + Flush 唯一入口 + 崩溃窗口谱系

核心命题(io.md §4.4):持久化 = 把数据从"会丢的层"推进到"不会丢的层"。每一层向上 暴露一个推进动作(Flush / CommitAsync / WaitForPersisted),向下消费一层的水位。 崩溃后,每一层用自己的持久化水位决定"哪些数据算数、哪些丢弃"——层与层的水位 不必相等(语义不同),但顺序不变量保证:上层声称已持久化的,下层必然已落盘。

2. 三层水位体系(一个崩溃点,三套语义)

层 水位 语义 崩溃后
引擎 MinAddress ≤ CommittedTail ≤ AllocatedTail CommittedTail = 真实已写水位(读/扫描/回收合法上界);AllocatedTail = 已预留水位(含未写空洞) 扫盘重建段表/地址表;半写帧按模式 B 截断
EntryLog FlushedTail / CommittedOffset(≤ FlushedTail) 落盘水位 / commit 边界水位 恢复三级回退(hints → meta 水位 → 扫盘验帧 CRC)
TierWal AllocatedIndex / PersistedIndex 内存分配尾 / 本地 fsync 水位 恢复按双水位 + 段 anchor 表

顺序不变量(跨层):EntryLog 内部 CommittedOffset ≤ FlushedTail ≤ TailAddress(log.md §6.4.3——已 commit 必已落盘,已落盘 ≤ 写游标);跨层方向性保证:上层只能把"下层已 fsync"当作"已持久化"——raft 经 WaitForPersistedAsync 确认(引擎 engine.Flush 完成) 才推进应答,上层永远无法观察到"下层未落盘却被上层当已持久化"的水位。

3. 层 1:Tier IO(介质与崩溃窗口谱系)

  • 持久化唯一入口是 Flush(io.md 置顶一):内存/虚拟/网络文件系统的落盘语义差异巨大, 统一入口 = IFileHandle.Flush。FileOpenHints.WriteThrough(O_SYNC/WRITE_THROUGH)使每写 同步落盘——崩溃窗口归零(最重档)。
  • 崩溃窗口谱系(virtual-file-system.md §4.4):默认档(后台 flusher 50ms 轮询 / 75% 日志 占用触发检查点)→ FlushData(批落盘)→ Flush(单屏障日志提交)→ WriteThrough(零窗口)。 选择权在装配方:按"每笔必须稳"还是"批攒后稳"选档。
  • 载体写穿 CarrierWriteThrough(virtual-file-system.md:75):载体句柄以 WRITE_THROUGH/O_SYNC 打开——每写完成即达稳定存储,journal 提交免独立 fsync (写穿完成即单屏障),Flush 对已写穿数据短路。
  • DurabilityValidation 质量地板(tierwal.md 装配节):network:// 拒绝(远端对象存储无 fsync 语义);.tier 虚拟载体须挂载 CarrierWriteThrough;local:///memory: 通过 (mem = 零 fsync 地板,持久化取舍由部署方自权衡)。
  • IO hints(FileOpenHints):NoBuffering(DIO——页模型 4MB 大块顺序写甜区)+ WriteThrough(每写落盘)——磁盘 raft 部署建议 NoBuffering | WriteThrough(DIO+WT 全介质 最优,机械盘 p99.9 从 buffered/DIO 单独的百 ms 级降到 0.43ms)。

4. 层 2:存储引擎(三水位 / 双尾 / 崩溃恢复)

  • 三水位(storage-engine.md:43-45):CommittedTail(真实已写水位)= 读/扫描/回收合法上界; AllocatedTail(已预留,含未写空洞)= Append 内部起点;MinAddress 头部回收后移。
  • 双尾语义:Allocate 预留 ≠ 已写——CommittedTail 不越过真实写入;engine.Flush(upTo) 按 应用水位落盘(storage-engine.md:132)。
  • 崩溃恢复(storage-engine.md §4):Builder.Start* 内部扫盘重建段表与地址表 → 重建容量 计数 → 启动保护。模式 A(圈地复写):已写槽位原样恢复(Allocate 占位即 Committed+sparse); 模式 B(Append WAL):物理尾部自动截断半写帧,上层帧协议决策。
  • EngineRecoveryHints(CommittedTailHint/AllocatedTailHint,storage-engine.md:159-162): 上层比引擎知道更多时(如事务日志持有自己的提交水位)修正双尾——防尾部半写数据被当作 有效。属于直接构造引擎的消费者;数据结构内部建引擎一律不带 hints(物理真相引擎自恢复, 逻辑水位结构自管,两回事)。
  • 持久化姿势(storage-engine.md §3.4):Hints=None 写进页缓存攒批、调 Flush 才保证落盘 (吞吐高,按批次 Flush);WriteThrough 每写同步落盘(WAL 提交点);NoBuffering 直 IO (真实生效与否经 UnbufferedSupport 报告——探测结果,别当文件系统判断做分支)。

5. 层 3:EntryLog(页帧 / 双页 / 两层提交 / 恢复)

5.1 页帧与单 entry 不跨页(log.md §6.1)

[LogPageFrameHeader 8B][entry data 扇区对齐][Crc32Footer 4B][padding 到扇区对齐]
entry: [EntryLogHeader 22B(CRC64 in-header)][payload][padding 4B 对齐]
  • 页帧 footer CRC32C 覆盖 [header+data]——半写/撕裂帧扫盘时止步于前一有效帧(log.md §7.5)。
  • 单 entry 不跨页(超 PageSize 抛)——帧边界 = 页内 entry 边界,恢复可逐帧验帧推进。

5.2 双页 ping-pong 让渡(log.md §6.1.2)

批协议页满 → 组帧 → 先行推进地址 → 启动纯数据写任务 → 立即切另一页继续写(IO 重叠)—— 页满不阻塞写线程,唯一等待点 = 双页都在途的背压。_inFlightFlush 是纯数据写任务, 永不触碰写锁、永不内联跑提交链(提交链延迟到观察点 DrainInFlightSync/Async 补跑)。

5.3 两层提交模型(log.md §6.3)

层 触发 语义 可配置
底层页提交契约(恒成立) 跨页 Append / Flush / Dispose 前一页换出即持久化(数据 + meta + CommittedOffset 推进);最坏写满一页才提交 否
可选提前提交 ICommitPolicy(GroupCommitPolicy 三维度) 页未满提前触发降延迟 是

崩溃一致性顺序(log.md §6.4):data fsync 先于 meta fsync——CommitWithFlush = FlushUntil(落盘 data)→ CommitCore(AppendMeta + meta.Commit 落盘 meta)。断电时 meta 绝不会标记一个 data 尚未落盘的 commit 点。水位不变量:CommittedOffset ≤ FlushedTail ≤ TailAddress。

5.4 恢复三级回退(log.md §7)

  1. LogRecoveryHints(上层注入:TailAddress/CommittedOffset/FileSize 等——快照场景)
  2. meta 持久化水位(LogMetaPayload:TailAddress/CommittedOffset/LastCommittedSeq/悬干裁决)
  3. 扫盘验帧(engine.MinAddress 前向走帧到最后一个有效帧尾——逐帧 magic+dataLen+CRC, 撕裂/坏帧止步前一有效帧)

恢复后 ReconcileEngineTail 退到真实尾 + 帧边界 padded end 对齐(meta/hints 记录的尾是末帧 数据尾,padding 之后才能续写首个新 frame);EntryLog OnLogRecovered 设 CommittedOffset = TailAddress(恢复后天然一致)+ 启动提前提交循环。

6. 层 4:TierWal(双水位 / 组提交 / meta 原子替换 / 快照)

  • 三条铁律(tierwal.md 心智模型):index = 起点 + 顺序计数;双水位分离(AllocatedIndex = 内存分配尾 / PersistedIndex = fsync 水位);分配与持久化分离(Append 只写不 fsync,持久化 时机 = 组提交或显式 CommitAsync)。PersistedIndex 不是集群 commitIndex(多数派确认归协议层)。
  • 组提交三维度(tierwal.md 追加与提交):时间(CommitInterval 10ms)/ 数据量(64KB)/ 条数(1000)——任一满足即提交(一次 fsync = 一批持久化);WaitForPersistedAsync(index) 阻塞到已持久化(raft 推进 commitIndex 的依据)。
  • meta Opaque 原子替换(tierwal.md 元数据槽):WriteMetaAsync 搭 EntryLog 水位线落盘 + CRC 校验兜底——段表与协议元数据(term/vote/config)共用容器,单槽覆盖原子语义。
  • 快照先快照后截断(tierwal.md 镜像快照):SnapshotAsync = 镜像生成 → 本地存储 → 增量压缩截断(先快照后截断——被截区有覆盖);本地存储 = IncrementalSnapshot 段增量。
  • 持久化质量地板:见 §3(DurabilityValidation + IO hints 选型)。

7. 端到端:一次 raft 写入的持久化保证链

raft.ReplicateAsync(leader)
  → TierWal.AppendBatchAsync(分配,PersistedIndex 未动——数据在页缓冲)
  → 组提交触发 / raft 攒批尾 CommitAsync(TierWal 侧)
      → EntryLog.CommitAsync:FlushUntil(engine.Write + engine.Flush 落盘 data)
        → CommitCore(AppendMeta + meta.Commit 落盘 meta)   ← data fsync 先于 meta fsync
      → TierWal.PersistedIndex 推进
  → raft 复制 lane 读 PersistedIndex → 发送 AppendEntries(已持久化内容)
  → follower 侧同样追加 + WaitForPersisted → 应答
  → leader 多数派 matchIndex → commitIndex 推进

崩溃窗口的语义(这条链上任一点断电):

断电点 数据状态 恢复后
Append 后、组提交前 entry 在页缓冲未 fsync 丢弃(WAL 语义——未 commit 即未持久化,raft 重放看不到)
data fsync 后、meta fsync 前 data 已落盘、CommittedOffset 未推进 恢复扫盘/meta 水位 → 该批按未 commit 处理(meta 不标记 data 未落盘的 commit)
提交完成后 data+meta 均落盘 重放完整可见(raft 日志前缀一致)
半数以上节点均完成提交 多数派日志含该条 raft 提交安全(不会丢已提交条目)

8. 谁保证什么(分工矩阵)

保证 承担层 机制
fsync 顺序(data 先于 meta) EntryLog CommitWithFlush 两段序(log.md §6.4)
半写/撕裂防护 引擎 + EntryLog 页帧 CRC32C footer + CRC64 in-header + 扫盘止步有效帧
原子替换(meta) EntryLog/TierWal opaque 搭水位线单块落盘 + 单槽覆盖(log.md §6.5 / tierwal.md 元数据槽)
水位修正(上层知更多) 引擎 EngineRecoveryHints CommittedTail/AllocatedTail 双尾 hint(storage-engine.md §4)
崩溃恢复 引擎(段表/地址表)+ EntryLog(三级回退)+ TierWal(anchor 表) 各层恢复协议(§4/§5.4/§6)
介质质量地板 Tier IO + TierWal DurabilityValidation + 载体写穿 + IO hints 选型(§3)
落盘时机决策 TierWal 组提交 + EntryLog 两层提交 三维度提前提交 / 页契约兜底(§5.3/§6)

9. 相关文档