存储基础(4):WAL、崩溃恢复与一致性
发布于:
本文是「存储基础系列」的第 4 篇:为什么存储系统几乎都要 WAL,以及一个最小的崩溃恢复闭环应该长什么样。
1. 为什么要 WAL
WAL(Write-Ahead Log,写前日志)解决的是最朴素的问题:
机器突然断电/进程崩溃后,数据不能“写丢”或“写一半”
思路是:
- 先把写入意图顺序写入日志(WAL)
- 再把数据写入更复杂的结构(memtable / B+Tree / LSM 文件等)
顺序写日志通常更快、更稳定,也更容易做校验与截断。
1.1 WAL 解决的“不是性能”,而是确定性
WAL 的核心价值是:把“崩溃后的状态”变成可推理的。
没有 WAL,往往会遇到这些坑:
- 数据结构本身写入是“多处更新”的(例如 B+Tree 页、LSM 的元数据),崩溃时很难保证原子性
- page cache 里可能存在“看起来写了但其实没落盘”的数据
WAL 把复杂问题简化为:只要日志可用,就可以重放(至少能恢复到某个一致点)。
2. 一致性要先说清楚:需要的是什么保证
不同系统的目标不同,常见的保证有:
- durability(持久性):返回成功的写,崩溃后仍然存在
- atomicity(原子性):要么全成功,要么全失败(不出现半写入)
- ordering(顺序性):按某种顺序提交与可见(例如 LSN 单调递增)
WAL 通常负责 durability + 崩溃后的重放顺序,至于更强的一致性(事务/隔离级别)则需要更完整的机制。
2.1 “写成功”的定义必须明确
很多线上事故的本质是:应用以为“写成功”了,但系统其实只做到了“写进了缓存”。
需要明确:
- ack before fsync:吞吐更高,但允许崩溃丢最近一段写入(窗口)
- ack after fsync:强持久,但更依赖磁盘延迟与批量化策略
这不是对错问题,是产品/业务的选择:RPO(可接受丢数据窗口)与延迟目标之间的权衡。
3. 一个最小可用的恢复流程
一个“够用”的恢复流程一般是:
- 日志里每条记录带校验(CRC 等),能识别尾部半写
- 启动时从 WAL 读取:
- 找到最后一个合法位置(截断无效尾部)
- 重新把日志里的写入“重放”到内存结构
- 完成后服务对外可读可写
如果系统还会持久化更大结构(比如 LSM 的 SST、B+Tree 的 page),则需要:
- checkpoint / manifest / 元数据(知道哪些文件/page 是最新的)
- 与 WAL 的“截断点”配合(旧日志可以丢弃)
3.1 Checkpoint/截断点:否则 WAL 会无限长
光有 WAL 还不够,还需要一个“推进点”:
- checkpoint:把内存状态/脏页/元数据推进到磁盘结构中
- truncate point:从哪条日志之前的数据已经“安全落盘”,旧日志可以删
工程上常见做法是:给每条日志打序号(LSN/seq),checkpoint 记录“已持久化到哪里”。
3.2 恢复时间(RTO)怎么估:别等事故时才发现太慢
一个非常工程化的估算是:
replay_time ≈ WAL_to_replay_bytes / replay_throughput
需要确保在“最坏情况下”(例如上次 checkpoint 后写入很多)恢复时间仍在可接受范围内,否则就要更频繁 checkpoint、加快回放路径、或者做快照/分段恢复。
4. fsync:“落盘”了吗
工程上最容易踩坑的是:
- 写了文件/写了日志,但 没 fsync
- 系统崩溃后发现数据还是丢了
因为写入可能只在 page cache 里,还没真正落到磁盘介质。
所以需要明确:
- 返回成功前是否要 fsync(强持久 vs 弱持久)
- 批量 fsync(group commit)怎么做,才能兼顾吞吐与延迟
4.1 group commit:为什么它几乎是必选项
如果每次写都 fsync,吞吐通常会非常差,因为 fsync 有固定成本。
group commit 的核心是:把 N 次写的 fsync 合并成 1 次,代价是写入的可见性/延迟分布发生变化(但整体更可控)。
5. 读写路径里“日志”是如何影响性能的
WAL 的代价主要在:
- fsync 的固定成本(尤其是小写、高频写)
- 写放大(日志 + 数据结构各写一遍)
优化思路通常是:
- group commit(攒一批再 fsync)
- WAL 与数据文件分盘/分设备(减少竞争)
- 合理的刷盘策略(在吞吐与丢数据窗口之间权衡)
5.1 一个可落地的观测清单
排查 WAL/恢复相关问题时,建议你至少能回答:
- 平均/尾部 fsync 延迟是多少?(是否与写延迟高度相关)
- WAL 写入带宽占比多少?(是否与 compaction/flush 抢 IO)
- 崩溃恢复需要扫多少日志?恢复耗时是否可接受?
下一篇我们会聊“索引与查找”:B+Tree / LSM 读路径的差异,以及为什么范围查询通常更偏爱 B+Tree 家族。