生产环境里,数据库“提交成功”并不等于数据页已经刷到表文件。PostgreSQL 用 Write-Ahead Logging(WAL,预写日志) 把变更先顺序写到日志,再异步回写堆/索引页;一旦进程或主机崩溃,就从最近检查点(checkpoint)开始 REDO 前滚,把还没落盘的变更重放回来。理解 WAL 与检查点,才能在“提交延迟、恢复时间、磁盘占用”之间做正确取舍,而不是盲目把 fsync 关掉。
问题背景:为什么不能每次提交都刷数据页?
一次事务可能修改很多表页。如果每次 COMMIT 都把相关数据页 fsync 到磁盘:
- 随机写成本高:小事务会在不同数据文件之间来回寻道。
- 吞吐难扩展:并发提交会互相争抢刷脏页。
- 恢复语义更复杂:缺少统一的“已持久化边界”。
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)。段文件通常 每段 16MB(initdb --wal-segsize 可改),段内再按页划分(常见 8kB)。文件名是递增编号,例如从 000000010000000000000001 起。
运维提示:
- WAL 磁盘与主数据盘分离,有助于把顺序写与随机写解耦(可在停库后迁移
pg_wal并用符号链接指回原路径)。 - 段文件在检查点之后、确认不再需要时会被 回收重命名 或删除;归档模式下必须先归档再回收。
3. 检查点:恢复的“安全锚点”
检查点是 WAL 序列中的一个保证点:该点之前的堆/索引变更,都已反映到数据文件并刷盘。检查点过程会:
- 把脏数据页写回磁盘;
- 在 WAL 中写入特殊的 checkpoint 记录;
- 把检查点位置写入
pg_control。
崩溃恢复时,服务器:
- 读
pg_control找到最新检查点; - 定位 checkpoint 记录中的 redo 起点;
- 从该 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_commit 与 fsync
这是最容易踩坑的一组旋钮,必须分清 丢最近事务 与 数据库损坏 的差别。
synchronous_commit(可会话级调整)
控制:在向客户端返回“成功”前,WAL 需要处理到什么程度。合法值包括:
remote_apply、on(默认)、remote_write、local、off。
关键语义:
- 非
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_time 与 buffers_written 的变化,理解检查点 I/O 的代价。
工程调参与排错清单
恢复时间 vs 运行期 I/O
- 缩短崩溃恢复:减小
checkpoint_timeout/max_wal_size→ 检查点更勤 → REDO 更少。 - 减轻检查点尖峰:增大二者,并把
checkpoint_completion_target维持在接近0.9,让刷脏页摊到整个间隔。 - 不要用检查点频率去“保证归档频率”:若关心归档 RPO,应调
archive_timeout,而不是把检查点拧得很紧。
WAL 目录膨胀
常见原因:
- 归档命令失败,段无法回收;
- 复制槽(replication slot)落后,强制保留旧段;
wal_keep_size/ 相关保留设置过大;- 批量导入、索引重建产生大量 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_size 或 checkpoint_timeout |
pg_wal 撑满磁盘 | 归档失败 / 复制槽落后 | 修归档、清理无用槽、扩容并监控 |
关 fsync “更快” | 用损坏风险换吞吐 | 生产禁止;可重建场景才临时关闭 |
| 异步提交后“丢单” | 崩溃窗口内事务未刷盘 | 业务上接受丢失,或关键路径保持 on |
| 检查点后 WAL 暴涨 | FPI(full_page_writes) | 避免检查点过密;观察 pg_stat_wal.wal_fpi |
总结
- WAL 先于数据页:提交持久化的关键路径是刷 WAL,而不是立刻刷所有脏表页。
- 检查点定义 REDO 起点:
checkpoint_timeout与max_wal_size共同决定自动检查点节奏,直接影响崩溃恢复时间与运行期 I/O。 synchronous_commit=off≠fsync=off:前者可能丢最近事务但保持一致;后者可能损坏。- 用 LSN 与统计视图观测:
pg_current_wal_lsn、pg_stat_checkpointer、pg_stat_wal把机制落到指标。 - 调参要绑定目标:RTO(恢复时间)、提交延迟、磁盘占用、复制/归档 RPO,不要只抄一组“神秘最优参数”。
把这些机制吃透后,再去谈高可用、PITR、逻辑复制,才会知道每条配置背后买的是什么、卖的是什么。
参考资料
- PostgreSQL 文档:Write-Ahead Logging (WAL)
- PostgreSQL 文档:WAL Configuration
- PostgreSQL 文档:Asynchronous Commit
- PostgreSQL 文档:WAL Internals
- PostgreSQL 文档:Write Ahead Log 运行参数(19.5)
- PostgreSQL 文档:CHECKPOINT
- PostgreSQL 文档:Monitoring Statistics(含
pg_stat_checkpointer/pg_stat_wal) - PostgreSQL 文档:系统管理函数(
pg_current_wal_lsn等)