存储基础(4):WAL、崩溃恢复与一致性

少于 1 分钟阅读

发布于:

本文是「存储基础系列」的第 4 篇:为什么存储系统几乎都要 WAL,以及一个最小的崩溃恢复闭环应该长什么样。

WAL + 恢复:最小闭环

group commit:把多次 fsync 合并成一次

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 家族。