WAL 预写日志与崩溃恢复
校招考察频度:★ 高频(存储底层·崩溃一致性) 一句话速记:改数先落 WAL 日志,顺序追加变随机;LSN 比对保幂等,全页写防扇区撕;检查点刷脏定起点,平滑调至零点九;归档基备合为一,任意秒级做 PITR。
数据库在运行过程中,随时可能面临物理服务器断电、操作系统内核崩溃或进程被强杀等灾难。PostgreSQL 是如何做到在重启后**“已提交的事务一条不丢,未提交的事务彻底回滚”**的?其背后的绝对功臣就是 WAL(Write-Ahead Logging,预写日志) 体系。
一、WAL 机制的底层物理哲学
面试官:“为什么写数据前必须先写 WAL?为什么先写日志比直接写数据页快上千倍?”
1. 核心设计原则
WAL 的第一法则:任何修改在写入磁盘上的数据堆表之前,必须先将记录该修改的 WAL 日志安全刷入磁盘。
2. 为什么写 WAL 极快?
- 数据页刷盘是“随机 I/O”:表中的数据页分散在磁盘的不同扇区中。如果每次事务提交都要把修改过的 8KB 数据页同步写回磁盘,磁头需要频繁寻道,SSD 会引发极大的写入放大和磨损,并发吞吐极低(每秒几百次)。
- WAL 写入是“顺序追加 I/O(Sequential Append)”:WAL 日志永远在文件末尾顺序追加写入。操作系统和磁盘对顺序写具有极致的硬件吞吐优化,单机可轻松承载每秒数万次写入。
WAL 极速提交模型
- 事务提交流程:当执行
COMMIT时,只要对应的 WAL 记录被持久化刷到磁盘上,数据库便立刻返回客户端成功。内存中被修改的数据页(脏页)可以留在shared_buffers里慢慢由后台线程刷盘。即使下一微秒机器断电,重启后靠 WAL 重放即可完全恢复。
二、LSN(日志序列号)与崩溃恢复幂等性
- 定义:LSN(Log Sequence Number) 是一个全局单调递增的 64 位无符号整数,代表某条 WAL 日志记录在整个数据库 WAL 历史字节流中的物理绝对偏移量(例如
16/B374D848)。 - 页面标记:每个 8KB 数据页的头部都包含一个系统属性:
PageHeaderData.pd_lsn,记录“最近一次修改该数据页的那条 WAL 记录的 LSN”。
恢复过程的精准幂等比对
系统崩溃重启后,恢复引擎扫描 WAL 日志,拿正在重放的 WAL 记录与磁盘上的数据页进行比对:
- 若
WAL.LSN <= Page.pd_lsn:说明这条修改在宕机前已经被操作系统成功刷入磁盘了,直接跳过,绝不重复写入; - 若
WAL.LSN > Page.pd_lsn:说明宕机时该修改仅留在内存缓冲区,磁盘数据页还是旧的,立即执行 Redo 重放更新,并把页头的pd_lsn推进更新。
三、full_page_writes 防半写撕裂页(Torn Page · 必考手撕点)
面试官深入拷问:“为什么 PostgreSQL 中有一个关键参数叫 full_page_writes?开启它有什么系统代价?为什么生产环境绝对不能关闭?”
1. 硬件层面的“半写/撕裂页”矛盾
- 数据库层面:PostgreSQL 读写数据页的最小单位是 8KB;
- 操作系统与硬件层面:Linux 文件系统与底层磁盘控制器保证原子写入的物理扇区大小通常仅为 4KB(或 512B)。
断电灾难还原:
1. PostgreSQL 正在将一个 8KB 脏页刷写回磁盘;
2. 操作系统刚写完了前 4KB 数据,突然发生意外断电!
3. 重启后读取该页:前 4KB 是最新数据,后 4KB 依然是历史垃圾数据;
4. 结果:该数据页的二进制校验和直接破坏,页面彻底损坏(Torn Page)!
5. 致命后果:由于数据页基础结构已碎,WAL 中诸如“在某偏移量写入 5 字节”的增量重放记录根本无法应用在破碎的页上!2. 解法:全页写入(Full Page Writes)
- 内核机制:在每次 Checkpoint(检查点)完成之后,任何一个数据页在第一次被修改并产生 WAL 时,PostgreSQL 会将整个 8KB 的完整数据页镜像原封不动地完整写入 WAL 日志中!
- 自愈过程:崩溃恢复时,若发现某数据页在 Checkpoint 后损坏,恢复引擎首先从 WAL 日志中提取出该 8KB 的全页快照,原样覆盖还原该数据页,随后再按部就班应用后续的增量 WAL 变更。
- 代价与权衡:在 Checkpoint 刚结束的瞬间,会有大量数据页首次被修改,引发 WAL 日志量的短暂激增(WAL 浪涌)。但为了防范不可逆的数据损坏,生产环境必须始终保持
full_page_writes = on!
四、Checkpoint 检查点机制与平滑调优
1. Checkpoint 的核心职责
- 将
shared_buffers内存中当前所有的脏数据页强制安全刷写到磁盘; - 将当前的检查点元信息(包括本次 Checkpoint 对应的 REDO 起始 LSN)写入全局控制文件(
global/pg_control); - 回收或删除那些已经被检查点超越的、不再需要的历史 WAL 段文件。
2. 崩溃恢复的范围
系统重启时,引擎读取 pg_control 文件,获知上一次成功的 Checkpoint 位置。恢复只需要从最近一次 Checkpoint 的 REDO LSN 开始向后扫描重放 WAL 即可,无需重放整个历史日志!
3. 生产 I/O 尖刺与平滑刷盘调优(I/O Smoothing)
- 痛点:若每次 Checkpoint 瞬间把所有脏页一股脑写盘,会导致磁盘 I/O 瞬间飙至 100%,引发业务请求出现严重的响应时间毛刺(Latency Spike)。
- 关键调优参数组合:
checkpoint_timeout = 15min # 检查点最大间隔时间 max_wal_size = 32GB # 两次检查点间允许产生的最大 WAL 量 checkpoint_completion_target = 0.9 # 核心调优项:平滑刷盘系数 - 原理解析:将
checkpoint_completion_target设为 0.9,通知内核将原本集中的刷盘任务,均匀平摊在两次检查点周期的 90% 时间段内完成。这彻底熨平了磁盘 I/O 尖刺,保证了业务全天候平稳低延迟。
五、PITR(基于时间点的数据恢复)
除了宕机自愈,WAL 还是支撑企业级灾难备份恢复 PITR(Point-in-Time Recovery) 的核心底座:
PITR 基于时间点的数据还原
- 组合公式:
基础备份(pg_basebackup)+持续 WAL 归档(archive_command / pg_receivewal)。 - 应对场景:当开发人员不小心在生产执行了
DROP TABLE orders;时,主从高可用流复制会实时同步删除,无法防范逻辑误操作。 - PITR 救援流程:
- 找一份误删前的全量物理备份恢复到测试实例;
- 配置
recovery_target_time = '2026-09-18 13:59:59'(精准定位在执行删表前 1 秒); - 启动数据库,PostgreSQL 会持续应用归档日志,直到指定时间戳前刹车停止;
- 完整救回被误删的所有核心业务数据!