pgsql-32 WAL与检查点机制
1. 概述
**WAL(Write-Ahead Logging,预写日志)**是 PostgreSQL 保证数据持久性(Durability,ACID 的 D)和崩溃恢复的核心机制,也是复制、备份、时间点恢复(PITR)的底层基础。面试中它常与"如何保证宕机不丢数据"一起被问到。
核心原则(预写日志规则):
任何对数据页的修改,必须先把变更记录写入 WAL 日志并落盘,然后才能修改内存中的数据页。
为什么这样能不丢数据又快?因为——
- WAL 是顺序追加写(磁盘顺序 IO 极快),而数据页是随机分布的(随机 IO 慢)。
- 事务提交时只需保证 WAL 落盘即可返回成功,脏数据页可以稍后再慢慢刷盘。
- 即使此时宕机,重启后可用 WAL **重放(replay)**把未刷盘的修改恢复出来。
这就是"用一次顺序写换取持久性"的经典设计。
2. WAL 的工作流程
1. 事务修改数据 → 生成 WAL 记录写入 WAL 缓冲区(wal_buffers)
2. 事务 COMMIT → 把 WAL 缓冲区 fsync 落盘(保证持久)→ 返回客户端成功
3. 被修改的数据页(脏页)仍在 shared_buffers 内存中,暂不落盘
4. 后台进程(bgwriter / checkpointer)稍后把脏页刷到数据文件
5. 若第3步后宕机:重启时从上个检查点开始重放 WAL,恢复未落盘的脏页
2.1 LSN(Log Sequence Number)
WAL 中每条记录都有一个 LSN——日志的字节偏移量,全局单调递增,用来标识"日志位置"。数据页头也记录了"最后修改它的 LSN",恢复时据此判断某个修改是否需要重放。复制中主从也用 LSN 衡量同步进度。
-- 查看当前 WAL 写入位置(LSN)
SELECT pg_current_wal_lsn();
-- 计算两个 LSN 之间的字节差(衡量主从延迟)
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) FROM pg_stat_replication;
2.2 WAL 文件
WAL 存放在 pg_wal/ 目录,默认每个段文件 16MB,写满切换下一个。文件会被循环复用或归档。
3. 检查点(Checkpoint)
如果 WAL 无限增长、脏页永不落盘,那崩溃恢复就要重放海量日志。检查点是一个同步点:
在检查点时刻,把此前所有脏页强制刷到数据文件,并记录"到此为止的修改都已持久化到数据文件"。之后崩溃恢复只需从最近一个检查点之后的 WAL 开始重放,而不是从头。
3.1 触发条件
- 时间触发:
checkpoint_timeout(默认 5min)。 - WAL 量触发:写入的 WAL 达到
max_wal_size(默认 1GB)。 - 手动:
CHECKPOINT; - 关库、备份开始等。
3.2 检查点调优(重要权衡)
检查点是一把双刃剑:
- 太频繁 → 脏页反复刷盘,IO 尖峰,影响性能。
- 太稀疏 → 恢复时要重放很多 WAL,崩溃恢复慢;且脏页积压导致检查点时一次性刷盘造成"IO 风暴"。
# postgresql.conf 关键参数
max_wal_size = 4GB # 调大 → 检查点更少 → 吞吐更好,但恢复更慢
checkpoint_timeout = 15min # 适当调大
checkpoint_completion_target = 0.9 # 把刷盘摊平到整个周期的90%,避免IO尖峰
checkpoint_completion_target=0.9是几乎必调的项:让检查点的脏页刷写平滑分布,而不是集中在一瞬间打爆磁盘。
4. 持久化与 fsync
- fsync = on(默认):COMMIT 时确保 WAL 真正写入磁盘介质,绝不能在生产关闭(关了性能高但宕机会丢数据/损坏)。
- synchronous_commit:控制"提交是否等待 WAL 落盘":
on(默认):等 WAL 落盘才返回,最安全。off:不等待,提交更快,但宕机可能丢失最近几百毫秒已提交事务(注意:不会损坏数据,只是丢最新的几笔)。高吞吐、可容忍少量丢失时可用。
-- 会话级临时关闭同步提交(如批量导入时提速)
SET synchronous_commit = off;
5. WAL 的三大用途
WAL 不只是崩溃恢复,它是 PG 很多高级功能的底座:
| 用途 | 说明 | 相关篇 |
|---|---|---|
| 崩溃恢复 | 宕机后重放 WAL 恢复未落盘修改 | 本篇 |
| 流复制 / 逻辑复制 | 把 WAL 传给从库重放,实现主从同步 | 主从复制与高可用 |
| PITR 时间点恢复 | 基础备份 + 归档 WAL,恢复到任意时刻 | 备份与恢复 |
5.1 WAL 归档
archive_mode = on
archive_command = 'cp %p /archive/%f' # 把写满的 WAL 段复制到归档目录
归档的 WAL 配合基础备份即可实现 PITR。
5.2 wal_level
wal_level = replica # minimal(仅恢复) / replica(默认,支持复制) / logical(支持逻辑复制)
6. 对比 MySQL:WAL vs redo log + binlog(面试高频)
MySQL 把 PostgreSQL 里 WAL 承担的职责拆成了两套日志,这个对比极常考:
| 维度 | PostgreSQL WAL | MySQL |
|---|---|---|
| 崩溃恢复 | WAL(一套搞定) | redo log(InnoDB 引擎层,物理日志) |
| 复制/binlog | 同一个 WAL 复用 | binlog(Server 层,逻辑日志) |
| 日志套数 | 一套 WAL | 两套(redo + binlog),需两阶段提交保证一致 |
| 日志内容 | 物理/物理逻辑混合 | redo 物理、binlog 逻辑 |
| 提交一致性 | 单日志,天然一致 | 需 XA 两阶段提交协调 redo 与 binlog |
面试金句:
MySQL 需要 redo log 做崩溃恢复、binlog 做复制和归档,两套日志靠两阶段提交保持一致,机制较复杂;PostgreSQL 只有一套 WAL,同时承担崩溃恢复、复制、PITR,设计更统一,但也因此没有 binlog 那种独立的逻辑变更流(逻辑复制是后来通过 wal_level=logical 解码 WAL 实现的)。
7. 监控
-- WAL 生成速率、归档状态
SELECT * FROM pg_stat_archiver;
-- 检查点统计(PG 15 前在 pg_stat_bgwriter,之后在 pg_stat_checkpointer)
SELECT * FROM pg_stat_bgwriter;
8. 面试高频问答
Q1:什么是 WAL?为什么能保证不丢数据还快? 预写日志:修改数据前先把变更顺序写入 WAL 并落盘。顺序 IO 快,提交只需保证 WAL 落盘即可返回,脏页延后刷;宕机后重放 WAL 恢复。用一次顺序写换持久性。
Q2:检查点是什么?为什么需要它? 把此前所有脏页刷到数据文件的同步点。有了它,崩溃恢复只需从最近检查点之后重放 WAL,而不是从头,控制恢复时间;同时限制 WAL 增长。
Q3:checkpoint_completion_target 的作用? 把一次检查点的脏页刷写平摊到整个检查点周期(如 90%),避免集中刷盘造成 IO 尖峰影响业务。
Q4:synchronous_commit=off 会丢数据吗?会损坏数据库吗? 可能丢失最近几百毫秒已提交但 WAL 未落盘的事务,但不会损坏数据库(WAL 顺序保证一致性),适合可容忍少量丢失的高吞吐场景。
Q5:PostgreSQL 的 WAL 和 MySQL 的 redo log/binlog 有什么区别? MySQL 用 redo log(引擎层,崩溃恢复)+ binlog(Server 层,复制/归档)两套日志,靠两阶段提交保持一致;PG 只用一套 WAL 同时承担崩溃恢复、复制、PITR。
Q6:LSN 是什么? Log Sequence Number,WAL 中的字节位置偏移,全局单调递增,用于标识日志位置、判断数据页修改是否需重放、衡量主从复制延迟。
9. 下一步
学习 备份与恢复,基于 WAL 实现 PITR 时间点恢复。
xingliuhua