持久化稳定性架构(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)
LogRecoveryHints(上层注入:TailAddress/CommittedOffset/FileSize 等——快照场景)- meta 持久化水位(
LogMetaPayload:TailAddress/CommittedOffset/LastCommittedSeq/悬干裁决) - 扫盘验帧(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 追加与提交):时间(
CommitInterval10ms)/ 数据量(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. 相关文档
- IO 原语与介质:io.md、local-file-system.md、virtual-file-system.md(载体写穿/崩溃窗口谱系)、medium-parity-matrix.md
- 引擎与结构:storage-engine.md(持久化姿势/崩溃恢复/双尾)、log.md(EntryLog 页帧/水位/恢复)、structures.md、segment-table.md
- TierWal:tierwal.md(三条铁律/组提交/质量地板)
- 上层消费:raft 架构与稳定性 raft-architecture.md、产品 raft 核心保证 raft-consistency.md
- 内部设计稿(不随包发布):tierwal-design / tierwal-mirror-snapshot / ring-overflow-chunked-write