Featured image of post PostgreSQL WAL、检查点与崩溃恢复:原理、参数与工程实践
数据库

PostgreSQL WAL、检查点与崩溃恢复:原理、参数与工程实践

PostgreSQL WAL、检查点与崩溃恢复:原理、参数与工程实践

生产环境里,数据库“提交成功”并不等于数据页已经刷到表文件。PostgreSQL 用 Write-Ahead Logging(WAL,预写日志) 把变更先顺序写到日志,再异步回写堆/索引页;一旦进程或主机崩溃,就从最近检查点(checkpoint)开始 REDO 前滚,把还没落盘的变更重放回来。理解 WAL 与检查点,才能在“提交延迟、恢复时间、磁盘占用”之间做正确取舍,而不是盲目把 fsync 关掉。

问题背景:为什么不能每次提交都刷数据页?

一次事务可能修改很多表页。如果每次 COMMIT 都把相关数据页 fsync 到磁盘:

  1. 随机写成本高:小事务会在不同数据文件之间来回寻道。
  2. 吞吐难扩展:并发提交会互相争抢刷脏页。
  3. 恢复语义更复杂:缺少统一的“已持久化边界”。

WAL 的核心规则(官方表述):对数据文件的修改,必须在描述该修改的 WAL 记录刷到永久存储之后才能写入数据页。遵循这条规则后,提交时只需保证相关 WAL 已落盘,崩溃时再用日志重做(roll-forward / REDO)尚未写入数据页的变更。

这带来两个工程收益:

  • 提交路径偏顺序写:同步成本主要在 WAL 文件,而不是整页随机写。
  • 支持在线备份与 PITR:归档 WAL + 基准备份,可回放到任意覆盖时间点。

核心原理:LSN、段文件、检查点与 REDO

1. LSN:日志里的“字节尺”

每条 WAL 记录写入后,插入位置用 Log Sequence Number(LSN) 描述,本质上是 WAL 中单调递增的字节偏移。PostgreSQL 用 pg_lsn 类型表示它,可用于比较两点之间产生了多少 WAL、衡量复制与恢复进度。

常用函数:

-- 当前 WAL 写位置
SELECT pg_current_wal_lsn();

-- 计算两个 LSN 之间的字节差(备份/复制场景常用)
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0');

2. 段文件布局:pg_wal 里的 16MB 段

WAL 默认存放在数据目录下的 pg_wal/(历史版本曾叫 pg_xlog)。段文件通常 每段 16MBinitdb --wal-segsize 可改),段内再按页划分(常见 8kB)。文件名是递增编号,例如从 000000010000000000000001 起。

运维提示:

  • WAL 磁盘与主数据盘分离,有助于把顺序写与随机写解耦(可在停库后迁移 pg_wal 并用符号链接指回原路径)。
  • 段文件在检查点之后、确认不再需要时会被 回收重命名 或删除;归档模式下必须先归档再回收。

3. 检查点:恢复的“安全锚点”

检查点是 WAL 序列中的一个保证点:该点之前的堆/索引变更,都已反映到数据文件并刷盘。检查点过程会:

  1. 把脏数据页写回磁盘;
  2. 在 WAL 中写入特殊的 checkpoint 记录;
  3. 把检查点位置写入 pg_control

崩溃恢复时,服务器:

  1. pg_control 找到最新检查点;
  2. 定位 checkpoint 记录中的 redo 起点
  3. 从该 LSN 向前扫描 WAL 做 REDO

因此:检查点越频繁,崩溃后需要重放的 WAL 通常越少,恢复更快;但刷脏页更勤,运行期 I/O 更重

默认自动触发条件(二者先到为准):

参数含义默认(官方文档)
checkpoint_timeout两次自动检查点的最长时间间隔5 分钟
max_wal_size自动检查点期间 WAL 增长的软上限1 GB
checkpoint_completion_target检查点 I/O 在间隔内的目标完成比例0.9
min_wal_size低于该占用时优先回收复用段,而不是删除80 MB

CHECKPOINT SQL 可强制立即检查点,但文档明确:不适合作为常规业务操作,一般留给维护窗口或排障。

4. 全页写(Full Page Writes)与页撕裂

默认 full_page_writes = on:检查点之后,某个数据页 第一次被修改 时,会把 整页内容 记入 WAL(FPI,full page image)。目的是防止崩溃时页只写了一半(torn page),仅靠“增量修改记录”无法正确还原。

代价是:检查点刚结束后的一段时间内 WAL 量会冲高。高负载系统常通过合理调大 max_wal_size / checkpoint_timeout,避免检查点过于密集导致 FPI 风暴。

提交耐久性:synchronous_commitfsync

这是最容易踩坑的一组旋钮,必须分清 丢最近事务数据库损坏 的差别。

synchronous_commit(可会话级调整)

控制:在向客户端返回“成功”前,WAL 需要处理到什么程度。合法值包括:

remote_applyon(默认)、remote_writelocaloff

关键语义:

  • off 的本地行为:等待 本地 WAL 刷盘
  • off(异步提交):逻辑提交完成后即可返回,不等待 WAL 落盘;崩溃可能丢失最近若干已“成功”的事务。
  • 官方强调:异步提交的风险是 数据丢失,不是数据损坏。恢复仍会重放到最后已刷盘的 WAL,库状态自洽,只是像最近事务被干净回滚一样。

典型用法:

-- 会话级:日志/埋点等可接受极短窗口丢失
SET synchronous_commit = off;

INSERT INTO event_log(payload) VALUES ('...');
-- 默认仍是 on;按事务开始时的参数值决定该事务提交模式

银行出钞、扣款等“客户端会据此做外部动作”的场景,不要关同步提交。

fsync(集群级,危险)

fsync = on 时,PostgreSQL 通过 fsync() 或等价方法(见 wal_sync_method)尽量把更新真正写到磁盘,保证 OS/硬件崩溃后仍可恢复到一致状态。

关掉 fsync 可能换来吞吐,但在掉电/系统崩溃时可能导致 不可恢复损坏。文档只建议在可整体重建的场景(初始装载、用完即丢的批处理库、只读克隆等)临时关闭。

工程对照:

设置主要风险适用
synchronous_commit=off丢最近事务,库仍一致可丢失的短事务
fsync=off可能损坏,需重建几乎仅限可重建环境
默认两者开启提交稍慢,耐久性强绝大多数生产

可复现观测实验(实验室)

下面命令假设你有一个可写的测试实例。目标是把“看不见的 WAL 机制”变成可观察指标。

1)看当前 WAL 与检查点统计

SHOW wal_level;
SHOW fsync;
SHOW synchronous_commit;
SHOW checkpoint_timeout;
SHOW max_wal_size;
SHOW min_wal_size;
SHOW full_page_writes;
SHOW checkpoint_completion_target;

SELECT pg_current_wal_lsn() AS current_lsn;

-- 新版本检查点统计在 pg_stat_checkpointer
SELECT num_timed, num_requested, num_done,
       write_time, sync_time, buffers_written
FROM pg_stat_checkpointer;

-- WAL 生成量与 FPI
SELECT wal_records, wal_fpi, wal_bytes, wal_buffers_full
FROM pg_stat_wal;

说明:

  • num_timed / num_requested 会计入完成与跳过;num_done 只计真正完成的检查点。
  • 空闲实例可能因“自上次检查点以来没有 WAL”而跳过定时检查点。

2)制造 WAL 并观察 LSN 前进

psql 中:

CREATE TABLE IF NOT EXISTS wal_lab(
  id bigserial PRIMARY KEY,
  payload text
);

-- 记录写入前 LSN(psql 变量)
SELECT pg_current_wal_lsn() AS before_lsn \gset

INSERT INTO wal_lab(payload)
SELECT repeat('x', 200)
FROM generate_series(1, 5000);

SELECT pg_current_wal_lsn() AS after_lsn,
       pg_wal_lsn_diff(pg_current_wal_lsn(), :'before_lsn') AS wal_bytes;

pg_wal_lsn_diff 返回两个 LSN 之间的字节差,便于量化一次批量写入产生了多少 WAL。若不用 \gset,可先手工抄下 before_lsn 再代入比较。

3)对比同步/异步提交延迟(概念实验)

\timing on

BEGIN;
SET LOCAL synchronous_commit = on;
INSERT INTO wal_lab(payload) VALUES ('sync');
COMMIT;

BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO wal_lab(payload) VALUES ('async');
COMMIT;

在高并发小事务压测下,异步提交通常降低提交等待;但要清楚这是 用丢失窗口换延迟,不是免费午餐。

4)强制检查点(仅实验)

CHECKPOINT;
SELECT num_done, write_time, sync_time, buffers_written
FROM pg_stat_checkpointer;

观察 write_time / sync_timebuffers_written 的变化,理解检查点 I/O 的代价。

工程调参与排错清单

恢复时间 vs 运行期 I/O

  • 缩短崩溃恢复:减小 checkpoint_timeout / max_wal_size → 检查点更勤 → REDO 更少。
  • 减轻检查点尖峰:增大二者,并把 checkpoint_completion_target 维持在接近 0.9,让刷脏页摊到整个间隔。
  • 不要用检查点频率去“保证归档频率”:若关心归档 RPO,应调 archive_timeout,而不是把检查点拧得很紧。

WAL 目录膨胀

常见原因:

  1. 归档命令失败,段无法回收;
  2. 复制槽(replication slot)落后,强制保留旧段;
  3. wal_keep_size / 相关保留设置过大;
  4. 批量导入、索引重建产生大量 WAL。

排查思路:

-- 归档相关(若启用)
SHOW archive_mode;
SHOW archive_command;
SHOW archive_timeout;

-- 当前是否在恢复(备库)
SELECT pg_is_in_recovery();

同时检查 pg_wal 磁盘占用、复制槽延迟与归档日志报错。

wal_level 选型

级别大致用途
minimal仅崩溃恢复所需信息,体积最小;不足以 PITR/流复制
replica(默认)支持归档与物理复制、备库只读查询
logical在 replica 基础上增加逻辑解码所需信息

生产若要做流复制或连续归档,不要用 minimal;文档还指出:max_wal_senders 非 0 时甚至不能以 minimal 启动。

硬件与虚假“写成功”

WAL 要求日志先于数据页持久化。若磁盘/控制器 谎报写完成(只进缓存未真正落盘),掉电仍可能造成不可恢复损坏。务必确认 WAL 所在存储关闭了危险写缓存,或有电池备份写缓存(BBU)。

常见坑对照表

现象可能原因处理方向
提交延迟高、小事务 TPS 低每次提交都在等 WAL fsync评估 synchronous_commit=off(可丢)或提升 WAL 盘延迟;关注 group commit
检查点期间延迟尖刺脏页集中刷写调大 max_wal_size/checkpoint_timeout,保持 checkpoint_completion_target≈0.9
崩溃恢复很久检查点过稀,REDO 区间过长适度降低 max_wal_sizecheckpoint_timeout
pg_wal 撑满磁盘归档失败 / 复制槽落后修归档、清理无用槽、扩容并监控
fsync “更快”用损坏风险换吞吐生产禁止;可重建场景才临时关闭
异步提交后“丢单”崩溃窗口内事务未刷盘业务上接受丢失,或关键路径保持 on
检查点后 WAL 暴涨FPI(full_page_writes避免检查点过密;观察 pg_stat_wal.wal_fpi

总结

  1. WAL 先于数据页:提交持久化的关键路径是刷 WAL,而不是立刻刷所有脏表页。
  2. 检查点定义 REDO 起点checkpoint_timeoutmax_wal_size 共同决定自动检查点节奏,直接影响崩溃恢复时间与运行期 I/O。
  3. synchronous_commit=offfsync=off:前者可能丢最近事务但保持一致;后者可能损坏。
  4. 用 LSN 与统计视图观测pg_current_wal_lsnpg_stat_checkpointerpg_stat_wal 把机制落到指标。
  5. 调参要绑定目标:RTO(恢复时间)、提交延迟、磁盘占用、复制/归档 RPO,不要只抄一组“神秘最优参数”。

把这些机制吃透后,再去谈高可用、PITR、逻辑复制,才会知道每条配置背后买的是什么、卖的是什么。

参考资料

  1. PostgreSQL 文档:Write-Ahead Logging (WAL)
  2. PostgreSQL 文档:WAL Configuration
  3. PostgreSQL 文档:Asynchronous Commit
  4. PostgreSQL 文档:WAL Internals
  5. PostgreSQL 文档:Write Ahead Log 运行参数(19.5)
  6. PostgreSQL 文档:CHECKPOINT
  7. PostgreSQL 文档:Monitoring Statistics(含 pg_stat_checkpointer / pg_stat_wal
  8. PostgreSQL 文档:系统管理函数(pg_current_wal_lsn 等)
使用 Hugo 构建
主题 StackJimmy 设计