目录

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 时间点恢复。