重放与影分身:崩溃恢复的两条路线
04-1 用一张对比表带过了 WAL 和 shadow paging,各只给了一段。这两条路线撑起了半个数据库和文件系统世界,值得展开成篇:它们从同一个恐惧出发,走向两个相反的哲学,最后还在互相抄作业。
恐惧有个具体的名字:torn page(页断裂)。
1. 一切恐惧的源头:torn page
"断电后要么旧状态、要么新状态、不能是半个"——这句崩溃一致性说起来轻巧,难点在于 磁盘的一次页写入不是原子的。
8KB 的页落盘要写十几个扇区;断电恰好卡在中间,就是半新半旧的页。更糟的是这页可能同时毁了:新数据没写全,原内容也被覆盖了一半。校验和能 发现它,不能修复 它——旧数据已经没了。
两条恢复路线,本质上是对"页断裂怎么办"的两种回答:别在原地写(shadow paging),或者 留一份怎么重演都行的剧本(WAL)。
2. 路线 A:WAL——先记后做,重放兜底
Write-Ahead Logging 的规矩两条:
- 改数据页之前,描述这次修改的日志必须先持久化(write ahead 的字面意思)
- 恢复时从头 重复历史(repeating history):日志在手,从检查点把历史重演一遍,页面自然回到一致点——这是 ARIES(C. Mohan,1992,IBM)给整个行业立的法
checkpoint 治的是 WAL 的天然病:日志无限增长,重放时间跟着无限增长。PostgreSQL 的解法是定期推进——checkpoint_timeout 到点或 WAL 写满 max_wal_size,就刷脏页、把检查点位置写进控制文件,之后的恢复只重放这点之后的 WAL。
最妙的一手是 full_page_writes:checkpoint 之后某页第一次被修改时,把 整页镜像 写进 WAL。恢复时先铺镜像、再重放增量,torn page 的危害被掐死在起跑线。注意这一手的血统——整页镜像就是"旧版本完整保留",shadow paging 的思想借给了 WAL 当保镖。两条路线不是敌人。
3. 路线 B:Shadow Paging——不改原地,原子切根
另一派的答案更直接:既然原地写有 torn page 风险,那就永远不原地写。
- 要改一页?新页写到别处,旧页原封不动
- 都改完了?原子切换根指针,新页转正,旧页变垃圾
- 崩溃在任何时刻?根指针没切成功就等于整个事务没发生过,恢复是 O(1) 的——直接用旧根
祖师爷是 IBM System R(1970 年代,SQL 诞生地),但当年败给了 WAL:每次改页要复制"根到叶的整条路径",随机 I/O 与碎片化在高写入场景被打得没有还手之力。
复兴在低写入、要快照、要原子的场景:
- ZFS:全 COW 文件系统,根指针叫 uberblock,存在磁盘首尾各两份标签的 环形槽位 里循环写入。一个事务组(txg)内先把数据块全部落盘,新 uberblock 才写进下一个槽;崩溃后扫描环形数组,取校验通过且 txg 最大的那个。04-1 讲过:环形缓冲 + 双代交替 + 先数据后索引,三合一
- BoltDB(etcd 底层存储):文件头固定 page 0 和 page 1 两个 meta 轮换写。事务提交时脏页写新位置,然后往另一个 meta 写入新根页号 + txid + 校验和。打开数据库时读两个 meta,取 txid 更大且校验合法的那个;单页损坏最多废一个 meta,另一个兜底
4. 两条路线的本质
| 维度 | WAL + checkpoint | Shadow paging |
|---|---|---|
| 写路径 | 顺序追加,吞吐王者 | COW + 路径复制,写放大 |
| 崩溃恢复 | 重放日志,耗时随日志量 | 换根即恢复,O(1) |
| torn page | full_page_writes 兜底 | 结构上不可能(旧页不动) |
| 空间 | 日志要归档清理 | 旧版本占空间,依赖 GC |
| 门派 | PostgreSQL、InnoDB、LevelDB | ZFS、Btrfs、BoltDB |
InnoDB 还有一手 doublewrite buffer:脏页先顺序写进一块共享区再刷各处——明面上是 WAL 门派,防 torn page 的这招又是影子的思路。混用是常态:ZFS 是影子分页的根 + 事务意图日志的辅助;04-1 的槽位设计是位图切根 + checkpoint 节奏。
Tip
工程只问"哪段风险用哪种武器":高写入走 WAL 拿吞吐,低写入要快照走影子拿恢复速度,torn page 风险高就给 WAL 垫一层影子式的全页镜像。
小结
两条路线同一个出发点(页写入不原子),两个相反的方向:WAL 用 顺序追加 + 重放买写吞吐,代价是恢复时间与日志量挂钩;shadow paging 用写时复制 + 原子切根 买 O(1) 恢复,代价是写放大。而 full_page_writes 和 doublewrite 说明,真到工程里两派互抄作业毫无心理负担——思想是别人的,组合是自己的。
下一篇顺着 WAL 的账继续查:日志说了"先记后做",那数据页的"做"什么时候落盘——dirty page 的欠条经济学。
最后更新:2026-09-08