Checkpointer(检查点进程)
Checkpointer(检查点进程)自 PostgreSQL 9.2 起从 postmaster 的职责中独立出来,负责周期性创建检查点(Checkpoint):把共享缓冲区中的全部脏页刷到磁盘,并把「到这里为止的数据都已落盘」这个信息记录到 pg_control 与 WAL 中。本节将讲解检查点的原理、触发条件、写入节奏控制,以及如何诊断检查点引发的 I/O 尖峰。
前置知识
阅读本节前,建议先了解:Autovacuum(自动清理进程)
基础概念
| 术语 | 英文 | 说明 |
|---|---|---|
| 检查点 | Checkpoint | 一次「把全部脏数据页刷盘 + 记录恢复起点」的操作 |
| 重做点 | Redo Point | 检查点记录写下的那一刻的 WAL 位置,崩溃恢复的起点 |
| 检查点记录 | Checkpoint Record | 写入 WAL 的一条特殊记录,指向 redo point |
| 控制文件 | pg_control | 记录当前重做点、集群状态、检查点元信息的二进制文件 |
| 超时检查点 | Timed Checkpoint | 由 checkpoint_timeout 到期触发 |
| 请求检查点 | Requested Checkpoint | 由 max_wal_size 超限或显式 CHECKPOINT 命令触发 |
| 完成目标 | checkpoint_completion_target | 把检查点期间的刷脏摊平到「检查点间隔的该比例」内完成 |
| 全页写入 | Full Page Write(FPW) | 检查点后首次修改页时,把整页镜像写进 WAL(见后续章节) |
| 恢复终点 | Recovery Consistency | 崩溃恢复只需重放 redo point 之后的 WAL |
检查点不是「为了持久性」,而是「为了缩短恢复时间」
初学者常误以为检查点是让数据落盘以保证不丢数据。不是的。持久性始终由 WAL 保证。
检查点真正的目的是:
- 限制崩溃恢复需要重放的 WAL 量:如果没有检查点,崩溃恢复需要从建库以来的所有 WAL 重放,可能耗时数小时。
- 限制 WAL 文件总量:
max_wal_size的检查点机制让老 WAL 段可以被回收。
检查点的代价是 I/O 尖峰。因此「检查点调优」的本质是在「恢复时间」与「I/O 冲击」之间找平衡。
从崩溃恢复的视角理解检查点
时间轴(WAL 持续追加):
... │ 检查点A │ ...WAL... │ 检查点B │ ...WAL... │ 崩溃 ✗
▲ ▲ ▲ ▲
redo_A redo_B WAL 末尾
崩溃后恢复过程:
1. 从 pg_control 读出「最后一次检查点的 redo point」= redo_B
2. 从 redo_B 开始,向后重放所有 WAL 记录到 WAL 末尾
3. 恢复完成,集群回到一致状态
关键结论:恢复时间 ∝ (WAL 末尾 - redo_B) 的字节量
而 (WAL 末尾 - redo_B) 由「检查点间隔内的 WAL 生成量」决定
这个量被 max_wal_size 约束所以:
max_wal_size越大 → 检查点间隔越长 → 检查点越少、I/O 越平缓,但崩溃恢复越慢、WAL 磁盘占用越大。max_wal_size越小 → 检查点越频繁、I/O 尖峰越多,但恢复越快。
一个完整检查点包含哪些步骤
1. 在 WAL 中写入一条「checkpoint start」记录(标记检查点开始)
2. 记下 redo point 的位置
—— 注意:redopoint 是「检查点开始前」的位置
因为检查点期间产生的脏页可能对应更早的 WAL
3. 刷脏(按 checkpoint_completion_target 摊平到一段时间内):
遍历共享缓冲区,把所有脏页 write() 到数据文件
4. fsync 所有被刷的数据文件
—— 这一步保证数据真正落盘
5. 刷 fsync WAL 到「checkpoint 记录的 LSN」
6. 把新的 redo point 与检查点信息写入 pg_control
(Unlink 之前的 pg_control 备份文件,写入新的)
—— 这一步完成意味着「这次检查点生效了」,恢复将从这里开始
7. 在 WAL 中写入「checkpoint done」记录
8. 回收不再需要的 WAL 段文件(由后续逻辑处理)第 6 步的顺序至关重要:必须先保证所有数据页 fsync 完成,再更新 pg_control。否则如果先更新 pg_control 而数据页还在缓存里,崩溃后就会丢失数据。
-- 手动触发一次检查点(需要超级用户或 pg_checkpoint 角色)
CHECKPOINT;
-- 查看命令返回CHECKPOINT# 更明确地观察检查点的完成后,pg_control 中记录的变化
pg_controldata "$PGDATA" | head -n 20pg_control version number: 1300
Catalog version number: 202307071
Database system identifier: 7341289012345678901
Database cluster state: in production
pg_control last modified: Mon 22 Sep 2026 11:41:02 AM CST
Latest checkpoint location: 0/1A3E2B70
Latest checkpoint's REDO location: 0/1A3DFF08
Latest checkpoint's REDO WAL file: 00000001000000000000001A
Latest checkpoint's TimeLineID: 1
Latest checkpoint's PrevTimeLineID: 1
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID: 0:18421903
Latest checkpoint's NextOID: 24612
Latest checkpoint's NextMultiXactId: 1
Latest checkpoint's NextMultiOffset: 0
Latest checkpoint's oldestXID: 18421903
Latest checkpoint's oldestXID's DB: 5
Time of latest checkpoint: Mon 22 Sep 2026 11:41:02 AM CST核心机制
触发检查点的三种方式
| 触发方式 | 条件 | 说明 |
|---|---|---|
| 超时触发 | 距上次检查点超过 checkpoint_timeout | 默认 5min。保证「无论写入多少,至少这个间隔会有一次检查点」 |
| WAL 量触发 | 自上次检查点以来生成的 WAL 超过 max_wal_size 的某个比例 | 默认 max_wal_size = 1GB;实际在 WAL 使用达到 max_wal_size 时触发「请求检查点」 |
| 手动触发 | 执行 CHECKPOINT 命令 | 用于备份前、大操作后等场景 |
-- 查看检查点相关参数
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
'checkpoint_timeout',
'max_wal_size',
'min_wal_size',
'checkpoint_completion_target',
'checkpoint_flush_after',
'checkpoint_warning',
'checkpoint_wal_buffers' -- PG 18 新增(见下方说明)
)
ORDER BY name; name | setting | unit | source
-----------------------------+---------+------+---------
checkpoint_completion_target| 0.9 | | default
checkpoint_flush_after | 32 | 8kB | default
checkpoint_timeout | 300 | s | default
checkpoint_warning | 30 | s | default
max_wal_size | 1024 | MB | default
min_wal_size | 80 | MB | defaultcheckpoint_warning 是「检查点太频繁」的预警
checkpoint_warning(默认 30s)表示:如果两次检查点之间的间隔短于该值,就在日志中输出警告:
LOG: checkpoints are occurring too frequently (18 seconds apart)
HINT: Consider increasing the configuration parameter "max_wal_size".看到这条日志就说明 max_wal_size 相对写入量太小了,应直接调大它,而不是去调 checkpoint_timeout。
checkpoint_completion_target:把 I/O 尖峰摊平
这是检查点最重要的调优参数。它决定了检查点的刷脏工作量要在多长时间内完成:
目标完成时间 = checkpoint_timeout × checkpoint_completion_target
默认 checkpoint_timeout = 5min,completion_target = 0.9:
目标完成时间 = 5min × 0.9 = 4.5min
也就是说,检查点应当用 4.5 分钟把全部脏页刷完,
剩下的 0.5 分钟「留白」,为下一次检查点做准备。对比不同取值的 I/O 形态:
completion_target = 0.0(旧默认,5min 内几乎瞬间刷完)
检查点触发瞬间 ████████████████████████ ← I/O 尖峰,业务受影响
──────────────────────────────────────▶ 时间
completion_target = 0.5
检查点触发后 ████░░░░████░░░░████░░░░████ ← 一半时间刷完
──────────────────────────────────────▶ 时间
completion_target = 0.9(PG 14+ 默认)
检查点触发后 ██░░██░░██░░██░░██░░██░░██░░██ ← 平稳摊开
──────────────────────────────────────▶ 时间
completion_target = 1.0
检查点触发后 █░░██░░██░░██░░██░░██░░██░░██░░ ← 一直刷到下一次检查点前
──────────────────────────────────────▶ 时间 (风险:跟不上则堆积)不要设成 1.0
checkpoint_completion_target = 1.0 意味着「一直刷到下一次检查点即将开始」。一旦某段时间写入量突然增大,检查点无法在预定窗口内完成,导致:
- 检查点「拖尾」到下一次检查点期间,I/O 持续饱和。
- WAL 段回收延迟,
pg_wal目录膨胀。
0.9 是 PG 14 起的默认值,在平滑性与安全性之间已是最佳平衡点。不要动它,除非有明确的压测数据支持。
检查点的刷脏是「渐进」的,但 fsync 是「集中」的
一个容易被忽略的细节:检查点进程不是一次性遍历完缓冲区再统一 fsync,而是分批推进:
每完成一批页的 write:
→ 检查「距检查点开始已过多少时间」
→ 如果进度落后于「(已过时间 / 目标完成时间) × 总脏页数」,就加快
→ 如果进度超前,就主动睡眠,避免超过目标节奏
全部页 write 完成后:
→ 统一 fsync 所有涉及的段文件
→ 更新 pg_control这个「进度跟踪 + 动态调速」是 PostgreSQL 的 CheckpointWriteDelay 逻辑。它使得检查点的 I/O 输出近似匀速,而不是「先快后慢」或「忽快忽慢」。
-- PG 17+:pg_stat_checkpointer 提供专门的检查点统计
SELECT
num_timed, -- 超时触发的检查点数
num_requested, -- 请求(WAL 量/手动)触发的检查点数
restartpoints_timed, -- 崩溃恢复期间的超时重启点
restartpoints_req, -- 崩溃恢复期间的请求重启点
restartpoints_done, -- 已完成的重启点
write_time, -- 写脏页总耗时(ms)
sync_time, -- fsync 总耗时(ms)
buffers_written, -- 写出的缓冲区数
stats_reset
FROM pg_stat_checkpointer; num_timed | num_requested | restartpoints_timed | restartpoints_req | restartpoints_done | write_time | sync_time | buffers_written
-----------+---------------+---------------------+-------------------+--------------------+------------+-----------+-----------------
31 | 4 | 0 | 0 | 0 | 1842190 | 302118 | 5218422-- PG 16 及以前:这些计数都在 pg_stat_bgwriter 里
SELECT
checkpoints_timed,
checkpoints_req,
checkpoint_write_time,
checkpoint_sync_time,
buffers_checkpoint
FROM pg_stat_bgwriter;关键比值:num_requested vs num_timed
这是判断检查点配置是否合理最直接的方法:
num_requested << num_timed(如 4 : 31)——健康。说明max_wal_size足够大,检查点主要由时间驱动,与写入量解耦。num_requested ≈ num_timed或更多——max_wal_size偏小。检查点被 WAL 量逼着触发,I/O 会更加频繁和不规律。num_requested持续激增——写入量远超预期,或存在大量 WAL 消费不掉的原因(如full_page_writes产生的 FPI 过多、批量导入)。
处理顺序:先调大 max_wal_size,观察 num_requested 是否下降。
检查点期间的「后端协作」
检查点进程并不独自承担全部刷脏工作。当后端进程需要缓冲区却发现脏页时,它会:
- 自己把脏页刷掉(
buffers_backend计数)。 - 若当前正处于检查点期间,还可能帮忙 fsync 该页对应的段文件(
buffers_backend_fsync)。
这是有意的设计——让检查点更快完成。所以在检查点期间看到 buffers_backend_fsync 增长是正常的。
检查点期间的三方协作:
Checkpointer 后端进程 A 后端进程 B
│ │ │
│ 遍历缓冲区刷脏 │ │
│ │ 需要缓冲区 │
│ │ 发现脏页 → 自己刷 │
│ │ + 顺手 fsync │
│ │ │
▼ ▼ ▼
─────────────────────────────────────────────────
数据文件(最终被 fsync 保证落盘)与 WAL 段回收的关系
检查点完成后,redo point 之前的 WAL 段文件就不再是恢复所必需的了(但仍可能需要用于归档或复制)。WAL 段的回收逻辑:
pg_wal/ 中的段文件:
① 被 redo point 之后的恢复所需 → 必须保留
② 尚未被归档(archive_mode = on) → 必须保留
③ 尚未被复制槽消费(有复制槽时) → 必须保留
④ 以上都不需要的 → 可回收(删除或重命名复用)
min_wal_size / max_wal_size 决定了「保留多少 WAL 段备用」:
- 段文件数低于 min_wal_size 对应量时,不删而是重命名保留备用
- 段文件数超过 max_wal_size 对应量时,触发「请求检查点」以推进 redo point这就是 max_wal_size 与检查点之间的耦合点:max_wal_size 不只是限制 WAL 磁盘占用,它实际上也在控制检查点的触发频率。
-- 查看当前 WAL 目录占用与段文件数
SELECT
count(*) AS wal_files,
pg_size_pretty(sum(size)) AS total_size
FROM pg_ls_waldir(); wal_files | total_size
-----------+------------
40 | 640 MB-- 查看当前 redo point 与最新 WAL 位置的距离(恢复代价的直接度量)
SELECT
pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file,
pg_walfile_name(redo_lsn) AS redo_wal_file,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), redo_lsn)) AS wal_to_replay
FROM pg_control_checkpoint(); current_wal_file | redo_wal_file | wal_to_replay
-----------------------------+------------------------------+---------------
00000001000000000000001B | 00000001000000000000001A | 512 MBpg_control_checkpoint() 是 PG 10+ 的函数
它把 pg_control 的关键字段以 SQL 函数的形式暴露出来,比自己解析二进制文件方便得多:
SELECT
checkpoint_lsn,
redo_lsn,
timeline_id,
prev_timeline_id,
full_page_writes,
next_xid,
next_oid,
oldest_xid,
oldest_xid_dbid,
checkpoint_time,
restartpoint_time
FROM pg_control_checkpoint();wal_to_replay 就是「如果现在崩溃,需要重放多少 WAL」——它直接决定了恢复时间。这个值在正常情况下应该在 0.5 × max_wal_size 到 max_wal_size 之间。
详细说明
检查点参数全表与取值建议
| 参数 | 默认值 | 生效方式 | 建议 |
|---|---|---|---|
checkpoint_timeout | 5min | SIGHUP | 30s ~ 1h。调大减少检查点频率,但增加恢复时间 |
max_wal_size | 1GB | SIGHUP | 写入量大时调大(4GB ~ 32GB)。这是最常用的调优项 |
min_wal_size | 80MB | SIGHUP | 保持默认;若 WAL 反复扩容收缩可适当调大 |
checkpoint_completion_target | 0.9 | SIGHUP | 保持 0.9 |
checkpoint_flush_after | 32 × 8kB = 256kB | SIGHUP | 机械盘可保持;SSD 可设为 0 |
checkpoint_warning | 30s | SIGHUP | 保持默认,它是「检查点太频繁」的信号灯 |
checkpoint_wal_buffers | -1(自动) | postmaster | PG 18 新增:检查点专用的 WAL 缓冲,缓解检查点期间的 WAL 写入竞争 |
PG 18 新增 checkpoint_wal_buffers
在 PG 18 之前,检查点进程在记录 checkpoint 记录时会与后端进程争抢 WAL 插入锁(WALInsertLock),在高写入负载下可能造成短暂的写入停滞。
PG 18 引入了 checkpoint_wal_buffers 参数(-1 表示自动,默认按 wal_buffers 的一定比例推算),让检查点拥有一块专用的 WAL 缓冲区,从而降低这种竞争。该参数是 postmaster 级,修改需要重启实例。
恢复时间与检查点间隔的关系
这是检查点调优的核心权衡。用一个具体例子说明:
场景:写入速率为 20 MB/s 的 WAL(约 1.2 GB/min)
配置 A:checkpoint_timeout = 5min,max_wal_size = 1GB
→ 每 5min 超时触发,但 WAL 量在 50s 时就达到 1GB
→ 实际由 WAL 量驱动,每 ~50s 一次检查点
→ num_requested 远大于 num_timed,日志会出现 "checkpoints are occurring too frequently"
→ 崩溃恢复需要重放最多 1GB WAL,约 10~30s
配置 B:checkpoint_timeout = 5min,max_wal_size = 16GB
→ 每 5min 超时触发一次(16GB 需要 13min 才达,超时先到)
→ 检查点间隔 5min,I/O 平稳
→ 崩溃恢复需要重放最多 5min 的 WAL = 6GB,约 1~3min结论:这不是「哪个更好」的问题,而是「你能接受多长的恢复时间」的问题。
决策框架:
Step 1:确定 RTO(恢复时间目标),比如 5 分钟
Step 2:测量恢复速度(大致 = 磁盘顺序读速度,或通过实际恢复测试得到)
假设 200 MB/s,则 5 分钟内可重放约 60GB WAL
Step 3:max_wal_size 取 RTO × 恢复速度的 0.5 倍左右
→ 30GB(留出一半安全余量)
注意:实际重放速度远低于磁盘顺序读速度
因为要按页随机访问 + 应用每条记录的逻辑。
经验值:机械盘约 20~50 MB/s,SSD 约 100~500 MB/s-- 用 pg_wal_lsn_diff 测量「当前需要重放多少」以及增长速度
SELECT
pg_wal_lsn_diff(pg_current_wal_lsn(), redo_lsn) AS replay_bytes_now,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), redo_lsn)) AS replay_human
FROM pg_control_checkpoint(); replay_bytes_now | replay_human
------------------+--------------
536870912 | 512 MB-- 测量 WAL 生成速率(采样两次,间隔 60 秒)
SELECT pg_current_wal_lsn() AS lsn_t0 \gset
SELECT pg_sleep(60);
SELECT
pg_wal_lsn_diff(pg_current_wal_lsn(), :'lsn_t0') / 60 AS wal_bytes_per_sec,
pg_size_pretty((pg_wal_lsn_diff(pg_current_wal_lsn(), :'lsn_t0') / 60)::bigint) AS rate_human; wal_bytes_per_sec | rate_human
-------------------+------------
20971520 | 20 MB有了「WAL 生成速率」与「可接受的恢复时间」,就能算出 max_wal_size 的合理区间:
max_wal_size ≈ WAL 生成速率 × 可接受恢复时间检查点与复制槽的交互
当存在复制槽(物理或逻辑)时,检查点不能回收仍被复制槽需要的 WAL 段,即使 redo point 已经推进。这会导致 pg_wal 不断增长。
-- 查看复制槽保留的 WAL 量
SELECT
slot_name,
slot_type,
active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes
FROM pg_replication_slots
ORDER BY retained_bytes DESC NULLS LAST; slot_name | slot_type | active | retained_wal | retained_bytes
------------+-----------+--------+--------------+----------------
standby_1 | physical | t | 12 MB | 12582912
sub_order | logical | f | 84 GB | 90194313216上例中 sub_order 是一个「非活跃的逻辑复制槽」,保留了 84GB WAL。
这就是典型的「磁盘被复制槽吃满」故障。检查点在这里无能为力 ——
问题不在检查点,而在于该槽没有被消费。-- 处理:判断是否还需要该槽
SELECT * FROM pg_logical_slot_peek_changes('sub_order', NULL, 10);-- 确认不再需要后删除(危险操作,需确认订阅端状态)
SELECT pg_drop_replication_slot('sub_order');检查点无法解决「WAL 被复制槽钉住」的问题
如果 pg_wal 持续膨胀且 pg_current_wal_lsn() 与 redo_lsn 的差距很小(说明检查点工作正常),那么原因几乎必然是复制槽或有 pg_replication_slots 之外的保留机制。
排查顺序:
pg_replication_slots中active = false的槽。pg_stat_archiver中归档失败的 WAL(归档未成功也可能保留)。wal_keep_size设置过大。- 备份工具的
pg_backup_start()未正确pg_backup_stop()(PG 15 前为pg_start_backup/pg_stop_backup),导致 WAL 被强制保留。
实战示例
示例 1:完整的检查点健康检查
-- 1) 参数现状
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('checkpoint_timeout', 'max_wal_size', 'min_wal_size',
'checkpoint_completion_target', 'checkpoint_flush_after')
ORDER BY name; name | setting | unit
-----------------------------+---------+------
checkpoint_completion_target| 0.9 |
checkpoint_flush_after | 32 | 8kB
checkpoint_timeout | 300 | s
max_wal_size | 1024 | MB
min_wal_size | 80 | MB-- 2) 检查点触发构成(PG 17+)
SELECT
num_timed,
num_requested,
round(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 2) AS pct_requested,
write_time,
sync_time,
buffers_written
FROM pg_stat_checkpointer; num_timed | num_requested | pct_requested | write_time | sync_time | buffers_written
-----------+---------------+---------------+------------+-----------+-----------------
31 | 4 | 11.43 | 1842190 | 302118 | 5218422-- 3) 检查点间隔(两个检查点之间的平均时间)
-- 结合 pg_control_checkpoint 的 checkpoint_time 与 uptime 粗算
SELECT
now() - pg_postmaster_start_time() AS uptime,
(SELECT num_timed + num_requested FROM pg_stat_checkpointer) AS total_checkpoints,
(extract(epoch FROM now() - pg_postmaster_start_time())
/ NULLIF((SELECT num_timed + num_requested FROM pg_stat_checkpointer), 0))::int AS avg_interval_sec
FROM pg_control_checkpoint(); uptime | total_checkpoints | avg_interval_sec
------------------+-------------------+------------------
01:52:04.113 | 35 | 192-- 4) 恢复代价评估
SELECT
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), redo_lsn)) AS wal_to_replay
FROM pg_control_checkpoint(); wal_to_replay
---------------
512 MB判定:pct_requested = 11.43% 健康(说明大部分检查点由超时驱动),平均间隔 192s(checkpoint_timeout = 300s,接近但略短,可能因为实例启动初期有几次请求检查点),重放量 512MB ≈ max_wal_size 的一半。整体健康。
# 5) 日志中是否有「检查点太频繁」的警告
grep -c "checkpoints are occurring too frequently" /var/log/postgresql/postgresql-18-main.log0示例 2:复现并消除「检查点太频繁」
# postgresql.conf —— 初始配置(max_wal_size 过小)
checkpoint_timeout = 5min
max_wal_size = 256MB # 偏小,写入量稍大就会被请求检查点驱动
checkpoint_warning = 30s# 施加写入负载(pgbench 的 insert 场景会产生大量 WAL)
pgbench -i -s 50 postgres
pgbench -c 16 -j 4 -T 120 -f insert_only.sql postgres-- 观察检查点构成:请求检查点占比会很高
SELECT num_timed, num_requested,
round(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 2) AS pct_requested
FROM pg_stat_checkpointer; num_timed | num_requested | pct_requested
-----------+---------------+---------------
12 | 38 | 76.00# 日志中会出现大量警告
grep "checkpoints are occurring too frequently" /var/log/postgresql/postgresql-18-main.log | tail -n 32026-09-22 11:02:14.113 CST [12040] LOG: checkpoints are occurring too frequently (14 seconds apart)
2026-09-22 11:03:02.882 CST [12040] LOG: checkpoints are occurring too frequently (19 seconds apart)
2026-09-22 11:04:11.004 CST [12040] LOG: checkpoints are occurring too frequently (16 seconds apart)# postgresql.conf —— 修正配置
checkpoint_timeout = 5min
max_wal_size = 8GB # 256MB → 8GB,让检查点回归「超时驱动」
min_wal_size = 1GB # 相应上调,减少段文件反复创建删除
checkpoint_warning = 30s-- reload 生效(这两个参数都是 SIGHUP 级)
SELECT pg_reload_conf();-- 清零统计后重新观察
SELECT pg_stat_reset_shared('checkpointer');-- 一段时间后再看:请求检查点占比应大幅下降
SELECT num_timed, num_requested,
round(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 2) AS pct_requested
FROM pg_stat_checkpointer; num_timed | num_requested | pct_requested
-----------+---------------+---------------
23 | 2 | 8.00pct_requested 从 76% 降到 8%,日志警告消失。
调大 max_wal_size 的副作用
不要把 max_wal_size 一次性调到极大(如 64GB),因为:
- 崩溃恢复时间线性增长——恢复会重放这么多 WAL。
- WAL 磁盘占用增长——
pg_wal可能占满磁盘。 pg_wal所在磁盘需要预留空间,否则会因 WAL 写满而宕机。
建议的上调路径:先翻倍观察,确认 pct_requested 与恢复时间都在可接受范围,再继续上调。
示例 3:定位检查点引发的 I/O 尖峰
-- 需要 track_io_timing = on
SHOW track_io_timing; track_io_timing
-----------------
on-- PG 16+:按进程类型查看数据文件的写入(检查点的 I/O 一目了然)
SELECT
backend_type,
writes,
write_bytes,
write_time,
fsyncs,
fsync_time
FROM pg_stat_io
WHERE object = 'relation' AND backend_type IN ('checkpointer', 'background writer', 'client backend')
ORDER BY writes DESC; backend_type | writes | write_bytes | write_time | fsyncs | fsync_time
------------------+---------+-------------+------------+--------+------------
checkpointer | 1842100 | 12079398821 | 2842190 | 8821 | 302118
background writer| 421033 | 2751742901 | 412033 | 0 | 0
client backend | 31201 | 255703808 | 121003 | 812 | 21908诊断结论:checkpointer 的 write_time 高达 2.84s,是三者之和的绝大部分——说明检查点是 I/O 的主要来源。若同时观察到业务延迟尖峰与检查点时间重合,可采取:
# postgresql.conf —— 降低检查点的瞬时冲击
checkpoint_completion_target = 0.9 # 确认不为 0 或 1.0
checkpoint_flush_after = 256kB # 机械盘保持;SSD 可设 0 提高吞吐
max_wal_size = 16GB # 减少检查点频率
checkpoint_timeout = 15min # 或直接拉长间隔(代价:恢复更慢)-- 对比调整前后的 checkpointer write_time 增长率
SELECT pg_stat_reset_shared('io');
-- 等待相同时间的负载后
SELECT backend_type, writes, write_time
FROM pg_stat_io
WHERE object = 'relation' AND backend_type = 'checkpointer';示例 4:检查点与全页写入(FPW)的关系观察
检查点的一个隐性代价是:检查点之后,每个数据页的首次修改都要写整页镜像(8KB)到 WAL。检查点越频繁,FPW 产生的 WAL 就越多。
-- PG 14+:pg_stat_wal 视图提供 wal_fpi(全页镜像数)
SELECT
wal_records,
wal_fpi,
pg_size_pretty(wal_bytes) AS wal_bytes,
round(100.0 * wal_fpi / NULLIF(wal_records, 0), 2) AS fpi_per_100_records,
round(100.0 * wal_fpi * 8192 / NULLIF(wal_bytes, 0), 2) AS pct_wal_by_fpi
FROM pg_stat_wal; wal_records | wal_fpi | wal_bytes | fpi_per_100_records | pct_wal_by_fpi
-------------+-----------+-----------+---------------------+----------------
8421033 | 421033 | 2841 MB | 5.00 | 11.86解读:
wal_fpi = 421033表示产生了约 42 万次全页写入,占用约421033 × 8KB ≈ 3.2GB的 WAL 量(但注意wal_bytes是压缩后统计,不完全线性)。pct_wal_by_fpi反映了 FPW 在 WAL 总量中的比例。
-- 对比:把检查点间隔调长(max_wal_size 调大)后,FPW 占比应下降
-- 因为「检查点次数变少 → 页首次修改的机会变少 → FPW 变少」FPW 是「检查点间接触发的 WAL 放大」
这个关系容易被忽略:调小 max_wal_size(检查点更频繁)不仅增加检查点本身的 I/O,还会通过 FPW 机制增加 WAL 生成量。反过来,调大 max_wal_size 有「双重收益」:
- 检查点数减少 → 检查点 I/O 减少。
- FPW 减少 → WAL 总量减少 → 归档/复制压力减小。
代价是恢复时间变长。这也是为什么 pg_stat_wal.wal_fpi 值得纳入监控——它能揭示「WAL 量偏大」是否由过于频繁的检查点造成。
示例 5:用 pg_controldata 判断上次关闭状态
# 停止服务后检查
pg_ctl -D "$PGDATA" stop -m fast
pg_controldata "$PGDATA" | grep -E "cluster state|Latest checkpoint|Time of latest checkpoint"Database cluster state: shut down
Latest checkpoint location: 0/1A3E2B70
Time of latest checkpoint: Mon 22 Sep 2026 11:41:02 AM CST# 模拟异常退出:用 immediate 模式停止(不执行正常关闭)
pg_ctl -D "$PGDATA" stop -m immediate
pg_controldata "$PGDATA" | grep -E "cluster state"Database cluster state: in production# 重新启动:会触发崩溃恢复
pg_ctl -D "$PGDATA" start# 日志中会看到恢复过程
LOG: database system was interrupted; last known up at 2026-09-22 11:41:02 CST
LOG: database system was not properly shut down; automatic recovery in progress
LOG: redo starts at 0/1A3DFF08
LOG: consistent recovery state reached at 0/1A3E2B70
LOG: database system is ready to accept connections关键日志行解读:
last known up at ...:来自pg_control中记录的最后一次检查点时间。redo starts at 0/1A3DFF08:就是 redo point,恢复从这里开始重放。consistent recovery state reached at 0/1A3E2B70:恢复到一致状态,可接受连接。
-m immediate 是模拟崩溃,不要在生产的正常运维中使用
pg_ctl stop -m immediate 相当于 kill -QUIT(postmaster 立即退出,不做任何清理)。它会让下次启动走崩溃恢复流程。
这只应用于:
- 测试恢复流程与恢复时间。
- 极端紧急情况下(如 postmaster 已无响应)。
- 复现「异常关闭」相关的问题。
正常停库应使用 -m fast(回滚未完成事务,干净关闭)或 -m smart(等待客户端断开)。
注意事项
- 检查点不保证业务数据「不丢」:持久性由 WAL 的同步提交保证,与检查点无关(
synchronous_commit = off时才会丢数据)。 max_wal_size与checkpoint_timeout是 SIGHUP 级:reload即可生效,无需重启。checkpoint_timeout的实际上限约为 1 小时(内部有约束),不要试图设成几小时。checkpoint_completion_target保持 0.9:不要设 1.0,也不要为了「快点刷完」设 0。CHECKPOINT命令会强制刷全部脏页:在大库上可能造成显著 I/O 冲击,不要在业务高峰期手动执行。- 检查点期间
pg_stat_activity可能看到后端处于wait_event = 'CheckpointDone':这是后端在等待检查点完成(某些操作需要),属正常现象。 - 检查点不回收磁盘空间:它只把脏页写回已有文件,不会缩小表空间。
pg_stat_checkpointer需要 PG 17+:更早版本使用pg_stat_bgwriter的checkpoints_timed/checkpoints_req等列。- 复制槽会阻止 WAL 回收:无论检查点怎么配置,只要存在不活跃的复制槽,
pg_wal就会持续增长。 pg_control是集群的关键文件:不要手动编辑、不要用不同版本的 PostgreSQL 二进制去启动同一个PGDATA。
# postgresql.conf —— 不推荐:为了「安全」把检查点设得极频繁
# checkpoint_timeout = 30s # 不推荐:检查点过于频繁,I/O 持续紧张
# max_wal_size = 128MB # 不推荐:会被 WAL 量持续驱动,且 FPW 放大 WAL# postgresql.conf —— 不推荐:为了「少 I/O」把检查点设到极限
# checkpoint_timeout = 1h # 不推荐:恢复时间将长达数十分钟
# max_wal_size = 200GB # 不推荐:需 200GB WAL 空间,且磁盘耗尽风险极高-- 不推荐:业务高峰期手动触发检查点
-- CHECKPOINT; -- 不推荐:会瞬时产生大量 I/O,拖慢业务最佳实践
max_wal_size是最优先调优的参数:把num_requested占比压到 10%~20% 以下。- 用「WAL 生成速率 × 可接受恢复时间」反推
max_wal_size:不要凭感觉设值。 - 关注
checkpoint_warning日志:出现「occurring too frequently」就调大max_wal_size。 checkpoint_completion_target保持 0.9:这是 PG 14+ 的默认值,已是平滑性与安全性的最佳平衡点。- 把
pg_wal放在独立磁盘或至少监控其占用:WAL 写满磁盘会导致实例停机。 - SSD/NVMe 上可考虑
checkpoint_flush_after = 0:减少人为的写入停顿,提升吞吐。 - 定期检查不活跃复制槽:这是
pg_wal膨胀最常见的非检查点原因。 track_io_timing仅用于诊断:用pg_stat_io观察checkpointer的 I/O 占比后再决定是否调参。- 做恢复演练:把恢复时间实测出来,而不是理论估算。这是唯一可靠的方法。
- PG 18 可关注
checkpoint_wal_buffers:高写入负载下若观察到检查点引起的写入停顿,可评估该参数。
下一节