Checkpoint(检查点)
上一节讲了 WAL 记录怎么写进磁盘。但 WAL 是只增不减的:如果永远不清理,pg_wal 会无限膨胀,崩溃恢复也要从建库那天开始重放。把这两件事同时解决的机制就是检查点(Checkpoint)。
前文(Checkpointer(检查点进程))已经从「进程」的角度讲过检查点的执行者。本节换一个视角:从 WAL 的角度看检查点做了什么——它在 WAL 里留下什么记录、redo point 为什么这么定义、它如何决定「哪些 WAL 可以删」「恢复要从哪里开始」。
前置知识
阅读本节前,建议先了解:WAL 写入流程(Write Flow)
基础概念
检查点涉及几个容易混淆的「位点」,先统一术语:
| 术语 | 英文 | 说明 |
|---|---|---|
| 检查点 | Checkpoint | 一次「刷脏数据页 + 记录恢复起点」的完整操作 |
| 重做点 | Redo Point | 检查点开始前夕的 WAL 位置,崩溃恢复的起点 |
| 检查点记录 | Checkpoint Record | 写入 WAL 的特殊记录,携带 redo point 等信息 |
| 检查点开始记录 | XLOG_CHECKPOINT_ONLINE | 检查点启动时写的 WAL 记录 |
| 检查点完成记录 | Checkpoint Done | 检查点成功结束后写的 WAL 记录 |
| 控制文件 | pg_control | 二进制文件,保存「最后一次成功检查点」的 redo point |
| 超时检查点 | Timed Checkpoint | 由 checkpoint_timeout 到期触发 |
| 请求检查点 | Requested Checkpoint | 由 WAL 量超限或显式 CHECKPOINT 命令触发 |
| 检查点间隔 | Checkpoint Interval | 两次检查点之间生成的 WAL 量 |
| 恢复一致性 | Recovery Consistency | 从 redo point 重放到 WAL 末尾即可得到一致状态 |
检查点在 WAL 中留下了什么
检查点不是纯内存操作,它的每一步都在 WAL 里留痕:
WAL 逻辑顺序 ──────────────────────────────────────────────────────►
...记录... │ XLOG_CHECKPOINT_ONLINE │ ...本次检查点期间的记录... │ Checkpoint Done │ ...新记录...
▲ ▲
│ │
redo point 位置 「检查点完成,可删旧 WAL」
(实际比这条记录更早) 从这条起,之前的 WAL 才允许回收三个关键点:
redo point不等于检查点记录的位置。redo point 被刻意取在「检查点开始之前」,原因见下一小节。- 检查点期间的 WAL 是照常写的。检查点不阻塞 DML,期间的修改照样生成 WAL 记录。
- 只有
Checkpoint Done之后,早于 redo point 的 WAL 段才允许回收。回收判定的依据是pg_control里的 redo point,而不是「最新 LSN」。
-- 查看当前检查点记录承载的信息(PG 10+)
SELECT checkpoint_lsn, -- 检查点记录自身的 LSN
redo_lsn, -- 崩溃恢复的起点
timeline_id,
next_xid,
oldest_active_xid,
checkpoint_time
FROM pg_control_checkpoint(); checkpoint_lsn | redo_lsn | timeline_id | next_xid | oldest_active_xid | checkpoint_time
----------------+-----------+-------------+------------+-------------------+-------------------------
0/1A3E2B70 | 0/1A3DFF08| 1 | 0:18421903 | 0:18421801 | 2026-09-22 11:41:02+08结果解读:
checkpoint_lsn = 0/1A3E2B70是检查点记录的落点;redo_lsn = 0/1A3DFF08明显更早(差值0x2C68= 11368 字节)。这就是「redo point 被前移」的直观证据。next_xid与oldest_active_xid说明检查点同时固化了事务相关的元信息,next_xid还会写回pg_control(见后文)。
redo point 为什么必须「往前挪一点」
这是检查点设计中最容易被忽略、也最容易被面试考到的细节。
时间轴(同一把时间轴上有两个活动):
① 数据页变脏 ② WAL 记录生成
UPDATE users ... → XLogInsert() 写 WAL
│ │
│ 两者不是原子的! │
▼ ▼
页在 shared_buffers 里 记录在 WAL 里
变脏的瞬间 落盘的瞬间
两者之间存在「页已脏但 WAL 还没写完」的窗口假设检查点取 redo point 时,某个页已经在缓冲区里变脏,但对应的 WAL 记录还没写完。如果 redo point 定得太靠后(晚于那条 WAL),崩溃恢复时就不会重放那条记录,而这个脏页在检查点里恰好还没被刷盘(检查点刚开始),结果这个修改就永久丢失了。
结论:
- redo point 必须取在「检查点开始时所有活跃事务的最小 LSN 之前」,才能保证任何「尚未刷盘的脏页」所依赖的 WAL 都在重放范围内。
- 这也是检查点要「两次扫描脏页」的原因(见后文)。
-- 量化检查点记录与 redo point 的距离
SELECT checkpoint_lsn,
redo_lsn,
pg_size_pretty(pg_wal_lsn_diff(checkpoint_lsn, redo_lsn)) AS redo_ahead
FROM pg_control_checkpoint(); checkpoint_lsn | redo_lsn | redo_ahead
----------------+-----------+------------
0/1A3E2B70 | 0/1A3DFF08| 11 kB核心机制
触发检查点的三种情况
| 触发方式 | 触发条件 | 对应源码标志 | 典型场景 |
|---|---|---|---|
| 超时检查点 | 距上次检查点超过 checkpoint_timeout | CHECKPOINT_CAUSE_TIME | 低频写入实例,防止 WAL 长期不回收 |
| 请求检查点 | WAL 量接近 max_wal_size | CHECKPOINT_CAUSE_XLOG | 高写入压力下的自动保护 |
| 手动检查点 | 执行 CHECKPOINT 命令 | CHECKPOINT_CAUSE_XLOG | 备份前、大操作后、迁移前 |
-- 查看检查点相关参数的实际取值与来源
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
'checkpoint_timeout',
'checkpoint_completion_target',
'checkpoint_flush_after',
'checkpoint_warning',
'max_wal_size',
'min_wal_size'
)
ORDER BY name; name | setting | unit | source
-----------------------------+---------+------+--------------------
checkpoint_completion_target| 0.9 | | default
checkpoint_flush_after | 256 | 8kB | default
checkpoint_timeout | 300 | s | default
checkpoint_warning | 30 | s | default
max_wal_size | 1024 | MB | default
min_wal_size | 80 | MB | default一次检查点的七个步骤(WAL 视角)
① 将 XLOG_CHECKPOINT_ONLINE 记录写入 WAL
└─ 记录内容:redo point、next_xid、oldest_active_xid、时间线等
② 确定 redo point
└─ 取「检查点开始时最老的活跃事务 LSN」与「重放位置」的较小值
③ 第一遍扫描共享缓冲区:把所有脏页挑出来,打上「本次检查点要刷」的标记
└─ 这一步是瞬时的,不阻塞太久
④ 第二遍扫描:按 checkpoint_completion_target 的节奏,把脏页 write() 出去
└─ 摊平到「检查点间隔的 90%」内完成,避免 I/O 尖峰
└─ 【WAL 视角】这一阶段仍在持续产生新 WAL,WAL 量并不会下降
⑤ fsync 所有已 write 的数据文件
└─ 保证数据真正落到持久存储
⑥ 刷 WAL 并更新 pg_control
└─ 把新的 redo point、next_xid、时间线、full_page_writes 状态写进 pg_control
└─ 【关键顺序】必须先完成⑤,才能写⑥
⑦ 写入 Checkpoint Done 记录,允许回收 redo point 之前的 WAL 段第⑥步的顺序是正确性的核心:如果先更新 pg_control 而数据页还在内核缓存里,此时断电,恢复会从「新 redo point」开始重放,那些还没落盘的脏页所依赖的 WAL 已经被判定为「不需要了」——数据就丢了。
-- 手动执行一次检查点并计时(需要超级用户或 pg_checkpoint 角色)
\timing on
CHECKPOINT;Time: 128.471 ms
CHECKPOINT手动 CHECKPOINT 不是免费操作
CHECKPOINT 会强制把所有脏页刷盘,可能造成 I/O 尖峰。在写入繁忙的实例上慎用,尤其是备份脚本里「为了保险先 CHECKPOINT 一下」的写法——它会显著拉长备份准备时间,且对备份正确性没有额外帮助(pg_basebackup 自己会处理检查点)。
checkpoint_completion_target:把刷脏摊平
检查点的真正难点不是「刷脏」,而是「刷得太快」。
completion_target = 0.5(旧默认,PG 9.5 之前)
检查点开始 下一个检查点
│◄────────────── checkpoint_timeout ─────────────►│
│◄──── 刷脏窗口 (50%) ────►│ │
▼ ▼ ▼
████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ← 刷脏速率
↑ I/O 尖峰(集中在半个周期内)
completion_target = 0.9(现代默认)
│◄────────────── checkpoint_timeout ─────────────►│
│◄────────── 刷脏窗口 (90%) ──────────────────► │
▼ ▼ ▼
██████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ← 刷脏速率(更平缓)
↑ I/O 摊平到几乎整个周期checkpoint_completion_target 的含义是:把本次检查点的刷脏动作,摊平到「距下一个检查点还剩 target × checkpoint_timeout」的时间窗口内完成。
| 取值 | 行为 | 代价 |
|---|---|---|
0.5 | 刷脏集中在半个周期,I/O 尖峰明显 | 尖峰大,但检查点完成早 |
0.9(默认,PG 14+) | 刷脏摊平到 90% 周期 | I/O 平缓,但检查点几乎不「休息」 |
1.0 | 摊平到整个周期 | 极平缓,但存在「上一个检查点没做 perfect 就到期」的风险 |
# postgresql.conf:用 SSD 的 OLTP 实例起点
checkpoint_timeout = 15min # 调大间隔,减少检查点次数
max_wal_size = 4GB # 与 timeout 一起决定实际间隔
checkpoint_completion_target = 0.9 # 摊平刷脏
checkpoint_flush_after = 512kB # 让内核及时回写,减少集中 fsync检查点如何决定「哪些 WAL 可以删」
WAL 段的回收不是按「文件年龄」,而是按位点:
pg_wal 目录
┌──────────────────┬──────────────────┬──────────────────┬───────────────┐
│ 段 ...00000009 │ 段 ...0000000A │ 段 ...0000000B │ 段 ...0000000C │
└────────▲─────────┴────────▲─────────┴────────▲─────────┴───────────────┘
│ │ │ ▲
可回收(早于 redo point 当前写入位置 保留(未来)
redo point 所在段) ▲
└─ pg_control 中记录的重放起点回收需要同时满足多个「门槛」,任何一个不满足都不能删:
| 门槛 | 决定者 | 说明 |
|---|---|---|
| 早于 redo point | 检查点 | 恢复不再需要 |
| 已归档 | Archiver | 若 archive_mode = on,未归档的段不能删 |
| 不被复制槽需要 | 复制槽的 restart_lsn | 复制槽不活跃会让 WAL 无限堆积 |
高于 min_wal_size | min_wal_size | 至少保留这么多,避免频繁重建 |
-- 观察「谁在阻止 WAL 被回收」
SELECT slot_name, slot_type, active, restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
ORDER BY retained DESC NULLS LAST; slot_name | slot_type | active | restart_lsn | retained
-----------+-----------+--------+-------------+-----------
standby1 | physical | t | 0/1A000000 | 26 MB
report_db | logical | f | 0/15000000 | 512 MB ← 不活跃,危险结果解读:
standby1活跃,只保留26 MB,健康。report_db不活跃却保留了512 MB,且会继续增长。这是生产环境pg_wal磁盘写满的第一大原因:逻辑复制槽被弃用后没人清理。
检查点与恢复时间(RTO)的量化关系
崩溃恢复需要重放的 WAL 量,就是「pg_control 的 redo point」到「WAL 末尾」的距离:
恢复时间 RTO ≈ (WAL 末尾 LSN - redo point LSN) ÷ 重放速率
其中:
(WAL 末尾 - redo point) ≈ 检查点间隔内生成的 WAL 总量
检查点间隔 = min(checkpoint_timeout 到期的 WAL 量, max_wal_size 触发的 WAL 量)-- 估算当前崩溃恢复需要重放的 WAL 量
SELECT pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(),
(SELECT redo_lsn FROM pg_control_checkpoint()))
) AS wal_to_replay; wal_to_replay
---------------
1842 MB结果解读:1842 MB 意味着崩溃后需要重放约 1.8 GB 的 WAL。若实测重放速率约 200 MB/s,则 RTO 约 9 秒(不含实例启动、一致性校验等固定开销)。把 max_wal_size 从 1GB 调到 8GB 会让这个数字涨到约 70 秒——这就是「恢复时间 vs I/O 冲击」的取舍。
相关参数
| 参数 | 默认值 | 作用 | 调优方向 |
|---|---|---|---|
checkpoint_timeout | 5min | 两次检查点的最大时间间隔 | 高写入实例可减到 1~2min,低写入可增 |
max_wal_size | 1GB | 触发请求检查点的 WAL 量阈值,也是 WAL 目录软上限 | 写入量大时调大,代价是 RTO 变长 |
min_wal_size | 80MB | WAL 段回收的下限,避免频繁重建 | 写入平稳时可保持默认 |
checkpoint_completion_target | 0.9 | 刷脏摊平比例 | 现代实例保持 0.9,不建议改 1.0 |
checkpoint_flush_after | 256kB | 每刷这么多数据就通知内核回写 | SSD 可调大到 512kB~1MB |
checkpoint_warning | 30s | 检查点间隔短于该值则打日志警告 | 出现警告说明 max_wal_size 偏小 |
wal_level | replica | 影响 WAL 记录粒度,间接影响检查点期间 WAL 量 | 需要逻辑复制时设 logical |
# postgresql.conf:写入量大、可接受 30~60s 恢复时间的实例
checkpoint_timeout = 15min
max_wal_size = 8GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9
checkpoint_flush_after = 1MB
checkpoint_warning = 30s详细说明
检查点统计在哪个视图里
PG 17 拆分了检查点统计
PostgreSQL 17 起,检查点统计从 pg_stat_bgwriter 拆分到独立的 pg_stat_checkpointer 视图。PG 16 及更早版本请查 pg_stat_bgwriter 的 checkpoints_timed / checkpoints_req / checkpoint_write_time / checkpoint_sync_time 列。
-- PostgreSQL 17+:独立的检查点统计视图
SELECT num_timed, -- 超时触发的检查点次数
num_requested, -- 请求触发的检查点次数
restartpoints_timed,
restartpoints_req,
restartpoints_done,
write_time, -- 刷脏消耗的毫秒数
sync_time, -- fsync 消耗的毫秒数
buffers_written,
stats_reset
FROM pg_stat_checkpointer; num_timed | num_requested | restartpoints_timed | restartpoints_req | restartpoints_done | write_time | sync_time | buffers_written | stats_reset
-----------+---------------+---------------------+-------------------+--------------------+------------+-----------+-----------------+-------------------------------
87 | 13 | 0 | 0 | 0 | 412038 | 88104 | 2411883 | 2026-09-20 08:00:01.123456+08结果解读:
num_timed = 87远多于num_requested = 13,说明大部分检查点是被checkpoint_timeout触发的,而不是 WAL 量。这是健康状态:说明max_wal_size够大,没被写入压垮。- 若反过来(
num_requested占多数),说明max_wal_size偏小,检查点被 WAL 量频繁触发,且通常会伴随checkpoint_warning日志。 write_time = 412038 ms与sync_time = 88104 ms的比例约 4.7:1。若sync_time占比很高,说明存储的 fsync 效率低,应考虑带断电保护的写缓存。buffers_written = 2411883是检查点累计刷出的缓冲区数,可用来估算平均单次检查点的刷脏量。
与 pg_stat_bgwriter、pg_stat_io 交叉验证
PG 16/17 的两处统计搬家
PG 16 起,pg_stat_bgwriter.buffers_backend 被移除,改由 pg_stat_io 按 backend_type 分类统计;PG 17 起,检查点相关列全部移到 pg_stat_checkpointer。在 PG 18 上,pg_stat_bgwriter 只剩 buffers_clean / maxwritten_clean / buffers_alloc / stats_reset。写监控脚本时必须按版本分支。
-- PostgreSQL 18:bgwriter 剩余统计与后端刷脏的归属
SELECT (SELECT buffers_clean FROM pg_stat_bgwriter) AS bgwriter_clean,
(SELECT maxwritten_clean FROM pg_stat_bgwriter) AS bgwriter_maxwritten,
(SELECT buffers_alloc FROM pg_stat_bgwriter) AS buffers_alloc,
(SELECT writes FROM pg_stat_io
WHERE backend_type = 'client backend'
AND object = 'relation' AND context = 'normal') AS backend_writes; bgwriter_clean | bgwriter_maxwritten | buffers_alloc | backend_writes
----------------+---------------------+---------------+----------------
913402 | 0 | 48310244 | 142087结果解读:
backend_writes = 142087表示有 14 万次写是后端进程被迫自己做的(因为找不到干净页可用,无法仅靠bgwriter补充)。这个数字若持续快速增长,说明bgwriter_lru_maxpages偏小或共享缓冲区压力大。bgwriter_maxwritten = 0说明bgwriter从未因为达到bgwriter_lru_maxpages而中断过扫描,配置充裕。buffers_alloc = 48310244是累计分配的缓冲区数,可作为「实例启动以来累计访问量」的粗略刻度。- 检查点的刷脏量看
pg_stat_checkpointer.buffers_written,不要再从pg_stat_bgwriter找buffers_checkpoint——在 PG 17+ 上这个列已不存在,查询会直接报错。
检查点期间的 I/O 特征
检查点期间的写入有两个叠加来源,必须区分:
| I/O 来源 | 目标 | 特征 |
|---|---|---|
| 刷脏数据页 | 数据文件 | 随机写为主,速率受 checkpoint_completion_target 控制 |
| 写 WAL 记录 | pg_wal 段文件 | 顺序写,速率由业务事务量决定 |
-- 用两次采样计算检查点刷脏速率(毫秒级,须间隔一段时间再采第二次)
SELECT buffers_written,
write_time,
sync_time,
clock_timestamp() AS sampled_at
FROM pg_stat_checkpointer; buffers_written | write_time | sync_time | sampled_at
-----------------+------------+-----------+----------------------------
2424081 | 413900 | 88742 | 2026-09-22 11:55:03.882641+08# 更实际的做法:用系统工具按秒采样,观察检查点刷脏的节奏
iostat -x 1 60 | awk '/^Device|^sd|^nvme/ {print}'Device r/s w/s rkB/s wkB/s await %util
nvme0n1 120.4 890.2 4821.0 142088.0 0.42 31.2
nvme0n1 118.9 895.7 4756.0 143210.4 0.45 31.8
nvme0n1 121.2 288.3 4844.8 46126.1 0.18 10.4 ← 检查点刚结束结果解读:wkB/s 在 142 MB/s 与 46 MB/s 之间的周期性起伏,就是检查点刷脏的「呼吸」。如果起伏过于剧烈(如从 400 MB/s 直接掉到 10 MB/s),说明 checkpoint_completion_target 偏小或刷脏窗口被压缩。
实战示例
示例 1:量化一次检查点的完整代价
-- 步骤 1:记录检查点前的位点与统计
SELECT (SELECT redo_lsn FROM pg_control_checkpoint()) AS redo_before,
pg_current_wal_lsn() AS lsn_before,
(SELECT num_timed + num_requested FROM pg_stat_checkpointer) AS cp_count_before; redo_before | lsn_before | cp_count_before
-------------+------------+-----------------
0/1A3DFF08 | 0/1B02A1C8 | 100-- 步骤 2:制造一批脏页(写入约 100MB 数据)
CREATE TABLE chk_demo AS
SELECT g AS id, md5(g::text) AS payload, repeat('x', 400) AS filler
FROM generate_series(1, 200000) AS g;
SELECT pg_size_pretty(pg_total_relation_size('chk_demo')) AS table_size; table_size
------------
104 MB-- 步骤 3:手动触发检查点并计时
\timing on
CHECKPOINT;
\timing offTime: 642.318 ms
CHECKPOINT-- 步骤 4:对比检查点前后的位点与统计
SELECT (SELECT redo_lsn FROM pg_control_checkpoint()) AS redo_after,
pg_current_wal_lsn() AS lsn_after,
(SELECT num_timed + num_requested FROM pg_stat_checkpointer) AS cp_count_after,
(SELECT buffers_written FROM pg_stat_checkpointer) AS buffers_written,
(SELECT write_time FROM pg_stat_checkpointer) AS write_time_ms,
(SELECT sync_time FROM pg_stat_checkpointer) AS sync_time_ms; redo_after | lsn_after | cp_count_after | buffers_written | write_time_ms | sync_time_ms
------------+------------+----------------+-----------------+---------------+--------------
0/1B02A1C8 | 0/1B02B4A0 | 101 | 2424081 | 413900 | 88742结果解读:
redo_after = 0/1B02A1C8恰好等于步骤 1 记录的lsn_before——印证了「redo point 取在检查点开始前的位点」。这不是巧合,而是设计保证。cp_count从100增到101,确认本次检查点被计入统计。buffers_written从2411883增到2424081,增量 12198 个缓冲区,约12198 × 8kB ≈ 95 MB,与步骤 2 写入的 104MB 表基本吻合(部分页可能已在检查点前被bgwriter刷走)。write_time增量1862 ms与sync_time增量638 ms之比约 2.9:1。在 SSD 上这个比例正常;在机械盘上sync_time往往占比更高。- 一个重要观察:检查点耗时 642ms,但统计里的 write_time 增量是 1862ms,大于实际耗时。这是因为
write_time统计的是所有刷脏动作的累计耗时(含bgwriter的部分),而不是单次检查点的墙钟时间。
示例 2:复现「检查点太频繁」并消除
-- 步骤 1:制造高频小事务,快速消耗 WAL
CREATE TABLE freq_demo (id serial PRIMARY KEY, v int);
DO $$
BEGIN
FOR i IN 1..200000 LOOP
INSERT INTO freq_demo (v) VALUES (i);
END LOOP;
END $$;-- 步骤 2:查看检查点触发构成与参数
SELECT (SELECT num_timed FROM pg_stat_checkpointer) AS timed,
(SELECT num_requested FROM pg_stat_checkpointer) AS requested,
(SELECT setting FROM pg_settings WHERE name = 'max_wal_size') AS max_wal_size,
(SELECT setting FROM pg_settings WHERE name = 'checkpoint_timeout') AS timeout_s; timed | requested | max_wal_size | timeout_s
-------+-----------+--------------+-----------
87 | 13 | 1024 | 300-- 步骤 3:观察检查点是否频繁(相邻两次 redo point 的距离)
SELECT redo_lsn,
pg_size_pretty(pg_wal_lsn_diff(redo_lsn, lag(redo_lsn) OVER (ORDER BY checkpoint_time))) AS gap
FROM pg_control_checkpoint(); redo_lsn | gap
------------+---------
0/1B02A1C8 | 1.1 GB# 步骤 4:消除频繁检查点——调大 max_wal_size(需重启或 reload)
# postgresql.conf
max_wal_size = 4GB
checkpoint_timeout = 15min# 步骤 5:reload 后确认生效
psql -d demo -c "SELECT name, setting, source FROM pg_settings WHERE name = 'max_wal_size';" name | setting | source
--------------+---------+---------
max_wal_size | 4096 | configuration file结果解读:
- 步骤 3 显示相邻检查点之间的 WAL 量约
1.1 GB,与max_wal_size = 1GB基本吻合——说明当前检查点是被 WAL 量触发的(num_requested占比高)。 - 每次检查点刷脏都会制造 I/O 尖峰,若业务对延迟敏感(P99),应通过调大
max_wal_size拉长间隔。 - 调大后需接受恢复时间变长:
4GB意味着最坏情况下要重放约 4GB WAL。若 RTO 有硬约束(如 30 秒内),需配合更快的存储或降低max_wal_size。 - 注意
checkpoint_timeout与max_wal_size谁先到谁触发:把 timeout 调到15min而 max_wal_size 仍是1GB,检查点仍会按 WAL 量触发,可能比 15 分钟频繁得多。
示例 3:验证检查点后旧 WAL 被回收
-- 步骤 1:制造大量 WAL,记录当前段
SELECT pg_current_wal_lsn() AS before_lsn,
pg_walfile_name(pg_current_wal_lsn()) AS before_seg; before_lsn | before_seg
------------+--------------------------
0/1C0010A8 | 00000001000000000000001C# 步骤 2:记录 pg_wal 目录当前文件数与总大小
ls -1 "$PGDATA/pg_wal" | wc -l
du -sh "$PGDATA/pg_wal"68
1.1G-- 步骤 3:触发检查点,让 redo point 前移
CHECKPOINT;
SELECT (SELECT redo_lsn FROM pg_control_checkpoint()) AS new_redo; new_redo
------------
0/1C0010A8# 步骤 4:稍等片刻,再看 pg_wal 的文件数与大小
sleep 5
ls -1 "$PGDATA/pg_wal" | wc -l
du -sh "$PGDATA/pg_wal"66
1.0G结果解读:
redo point前移到0/1C0010A8,意味着早于该位点的 WAL 段不再需要。pg_wal文件数从68降到66,大小从1.1G降到1.0G。下降幅度不大,因为:① 回收受min_wal_size约束(默认 80MB,此处远未触及);② 回收由Checkpointer在检查点后周期性执行,需要一点时间。- 若文件数完全没有下降,按顺序排查:是否有不活跃的复制槽?
archive_mode = on时 Archiver 是否卡住(pg_stat_archiver.failed_count持续增长)?这二者是最常见的「WAL 不回收」原因。 - 注意:段文件回收是改名复用(
wal_recycle = on)而不是删除,所以可能出现「文件数不变但内容被覆盖」的情况,用pg_walfile_name()的段号变化判断更准确。
注意事项
- 不要把
CHECKPOINT当成「保存数据」的手段。数据持久性由 WAL 保证,检查点只影响恢复起点与 WAL 回收。频繁手动CHECKPOINT只会制造 I/O 尖峰。 checkpoint_completion_target = 1.0不建议使用。它意味着理论上刷脏要占据整个检查点周期,容易和下一个检查点重叠,出现「检查点叠检查点」的恶性循环。
# 错误倾向:追求绝对平滑而设成 1.0
# checkpoint_completion_target = 1.0
# 正确做法:保持默认 0.9,留出 10% 的缓冲
checkpoint_completion_target = 0.9max_wal_size不是硬限制。它只是触发检查点的阈值;在检查点来不及完成时(如刷脏很慢),实际 WAL 用量会超过它。真正确保pg_wal不写满需要监控 + 设置磁盘告警。checkpoint_timeout与max_wal_size共同决定检查点间隔,只调其中一个往往无效。调优时应同时观察num_timed与num_requested的变化。- PG 16 及更早版本没有
pg_stat_checkpointer。写监控脚本时要按版本分支,否则查询会直接报错。
-- 兼容写法:按版本选择统计来源
SELECT current_setting('server_version_num')::int AS ver,
CASE WHEN current_setting('server_version_num')::int >= 170000
THEN (SELECT num_requested FROM pg_stat_checkpointer)
ELSE (SELECT checkpoints_req FROM pg_stat_bgwriter)
END AS requested_cp; ver | requested_cp
--------+--------------
180000 | 13- 检查点期间的查询延迟会上升,因为刷脏占用了 I/O 与
BufferAlloc的锁竞争。若业务对延迟极敏感,可考虑把 WAL 与数据文件放到不同设备。 - 不要在看不懂
checkpoint_lsn与redo_lsn差值含义时就去调参。差值过大(如几十 MB)通常意味着有长事务或大批量操作,而不是参数问题。
最佳实践
- 用
num_requested / (num_timed + num_requested)作为核心健康指标,控制在 30% 以内;超过则优先调大max_wal_size。
-- 建议固化为监控查询:检查点健康度一屏看清
SELECT now() AS ts,
num_timed,
num_requested,
ROUND(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 1) AS req_pct,
ROUND(sync_time::numeric / NULLIF(buffers_written, 0), 3) AS ms_sync_per_buf,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),
(SELECT redo_lsn FROM pg_control_checkpoint()))) AS wal_to_replay
FROM pg_stat_checkpointer;- 把「待重放 WAL 量」与 RTO 目标绑定。先定 SLO(如「崩溃恢复 60 秒内」),再反推可接受的
max_wal_size,而不是凭感觉设数值。
max_wal_size 上限估算:
目标 RTO = 60s
实测重放速率 ≈ 200 MB/s
→ max_wal_size 不超过 60 × 200 MB ≈ 12 GB(留 50% 余量取 6~8 GB)checkpoint_flush_after在 SSD 上适当调大(512kB~1MB),减少fsync()调用次数;在虚拟化环境下尤其有效。- 监控
pg_wal目录大小并设硬告警,阈值取max_wal_size的 2 倍。WAL 盘写满会导致数据库完全停止写入,是最高优先级的告警项。 - WAL 与数据目录使用不同物理设备。检查点的随机写与 WAL 的顺序写混在同一设备上会互相拖累。
- 备份前不要手动
CHECKPOINT,pg_basebackup与pg_backup_start()内部会正确处理。手动加一次只会延长停机窗口。 - 长事务是检查点健康度的隐形杀手。长事务会持续钉住 oldest XID,使某些清理无法推进,也间接影响检查点效果;应设置
idle_in_transaction_session_timeout兜底。 - 升级到 PG 17+ 后把监控切到
pg_stat_checkpointer,字段语义更清晰,且不再与bgwriter的统计混在一起。
下一节