Skip to content

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 冲击」之间找平衡。

从崩溃恢复的视角理解检查点 ​

text
时间轴(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 尖峰越多,但恢复越快。

一个完整检查点包含哪些步骤 ​

text
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 而数据页还在缓存里,崩溃后就会丢失数据。

sql
-- 手动触发一次检查点(需要超级用户或 pg_checkpoint 角色)
CHECKPOINT;

-- 查看命令返回
text
CHECKPOINT
bash
# 更明确地观察检查点的完成后,pg_control 中记录的变化
pg_controldata "$PGDATA" | head -n 20
text
pg_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 命令用于备份前、大操作后等场景
sql
-- 查看检查点相关参数
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;
text
            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   | default

checkpoint_warning 是「检查点太频繁」的预警

checkpoint_warning(默认 30s)表示:如果两次检查点之间的间隔短于该值,就在日志中输出警告:

text
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 尖峰摊平 ​

这是检查点最重要的调优参数。它决定了检查点的刷脏工作量要在多长时间内完成:

text
目标完成时间 = checkpoint_timeout × checkpoint_completion_target

默认 checkpoint_timeout = 5min,completion_target = 0.9:
    目标完成时间 = 5min × 0.9 = 4.5min

也就是说,检查点应当用 4.5 分钟把全部脏页刷完,
剩下的 0.5 分钟「留白」,为下一次检查点做准备。

对比不同取值的 I/O 形态:

text
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,而是分批推进:

text
每完成一批页的 write:
  → 检查「距检查点开始已过多少时间」
  → 如果进度落后于「(已过时间 / 目标完成时间) × 总脏页数」,就加快
  → 如果进度超前,就主动睡眠,避免超过目标节奏

全部页 write 完成后:
  → 统一 fsync 所有涉及的段文件
  → 更新 pg_control

这个「进度跟踪 + 动态调速」是 PostgreSQL 的 CheckpointWriteDelay 逻辑。它使得检查点的 I/O 输出近似匀速,而不是「先快后慢」或「忽快忽慢」。

sql
-- 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;
text
 num_timed | num_requested | restartpoints_timed | restartpoints_req | restartpoints_done | write_time | sync_time | buffers_written
-----------+---------------+---------------------+-------------------+--------------------+------------+-----------+-----------------
        31 |             4 |                   0 |                 0 |                  0 |    1842190 |    302118 |         5218422
sql
-- 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 是否下降。

检查点期间的「后端协作」 ​

检查点进程并不独自承担全部刷脏工作。当后端进程需要缓冲区却发现脏页时,它会:

  1. 自己把脏页刷掉(buffers_backend 计数)。
  2. 若当前正处于检查点期间,还可能帮忙 fsync 该页对应的段文件(buffers_backend_fsync)。

这是有意的设计——让检查点更快完成。所以在检查点期间看到 buffers_backend_fsync 增长是正常的。

text
检查点期间的三方协作:

  Checkpointer          后端进程 A        后端进程 B
       │                     │                  │
       │ 遍历缓冲区刷脏       │                  │
       │                     │ 需要缓冲区        │
       │                     │ 发现脏页 → 自己刷  │
       │                     │ + 顺手 fsync      │
       │                     │                  │
       ▼                     ▼                  ▼
   ─────────────────────────────────────────────────
              数据文件(最终被 fsync 保证落盘)

与 WAL 段回收的关系 ​

检查点完成后,redo point 之前的 WAL 段文件就不再是恢复所必需的了(但仍可能需要用于归档或复制)。WAL 段的回收逻辑:

text
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 磁盘占用,它实际上也在控制检查点的触发频率。

sql
-- 查看当前 WAL 目录占用与段文件数
SELECT
  count(*)                                              AS wal_files,
  pg_size_pretty(sum(size))                             AS total_size
FROM pg_ls_waldir();
text
 wal_files | total_size
-----------+------------
        40 | 640 MB
sql
-- 查看当前 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();
text
      current_wal_file       |        redo_wal_file         | wal_to_replay
-----------------------------+------------------------------+---------------
 00000001000000000000001B    | 00000001000000000000001A      | 512 MB

pg_control_checkpoint() 是 PG 10+ 的函数

它把 pg_control 的关键字段以 SQL 函数的形式暴露出来,比自己解析二进制文件方便得多:

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_timeout5minSIGHUP30s ~ 1h。调大减少检查点频率,但增加恢复时间
max_wal_size1GBSIGHUP写入量大时调大(4GB ~ 32GB)。这是最常用的调优项
min_wal_size80MBSIGHUP保持默认;若 WAL 反复扩容收缩可适当调大
checkpoint_completion_target0.9SIGHUP保持 0.9
checkpoint_flush_after32 × 8kB = 256kBSIGHUP机械盘可保持;SSD 可设为 0
checkpoint_warning30sSIGHUP保持默认,它是「检查点太频繁」的信号灯
checkpoint_wal_buffers-1(自动)postmasterPG 18 新增:检查点专用的 WAL 缓冲,缓解检查点期间的 WAL 写入竞争

PG 18 新增 checkpoint_wal_buffers

在 PG 18 之前,检查点进程在记录 checkpoint 记录时会与后端进程争抢 WAL 插入锁(WALInsertLock),在高写入负载下可能造成短暂的写入停滞。

PG 18 引入了 checkpoint_wal_buffers 参数(-1 表示自动,默认按 wal_buffers 的一定比例推算),让检查点拥有一块专用的 WAL 缓冲区,从而降低这种竞争。该参数是 postmaster 级,修改需要重启实例。

恢复时间与检查点间隔的关系 ​

这是检查点调优的核心权衡。用一个具体例子说明:

text
场景:写入速率为 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

结论:这不是「哪个更好」的问题,而是「你能接受多长的恢复时间」的问题。

决策框架:

text
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
sql
-- 用 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();
text
 replay_bytes_now | replay_human
------------------+--------------
        536870912 | 512 MB
sql
-- 测量 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;
text
 wal_bytes_per_sec | rate_human
-------------------+------------
          20971520 | 20 MB

有了「WAL 生成速率」与「可接受的恢复时间」,就能算出 max_wal_size 的合理区间:

text
max_wal_size ≈ WAL 生成速率 × 可接受恢复时间

检查点与复制槽的交互 ​

当存在复制槽(物理或逻辑)时,检查点不能回收仍被复制槽需要的 WAL 段,即使 redo point 已经推进。这会导致 pg_wal 不断增长。

sql
-- 查看复制槽保留的 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;
text
 slot_name  | slot_type | active | retained_wal | retained_bytes
------------+-----------+--------+--------------+----------------
 standby_1  | physical  | t      | 12 MB        |       12582912
 sub_order  | logical   | f      | 84 GB        |   90194313216
text
上例中 sub_order 是一个「非活跃的逻辑复制槽」,保留了 84GB WAL。
这就是典型的「磁盘被复制槽吃满」故障。检查点在这里无能为力 ——
问题不在检查点,而在于该槽没有被消费。
sql
-- 处理:判断是否还需要该槽
SELECT * FROM pg_logical_slot_peek_changes('sub_order', NULL, 10);
sql
-- 确认不再需要后删除(危险操作,需确认订阅端状态)
SELECT pg_drop_replication_slot('sub_order');

检查点无法解决「WAL 被复制槽钉住」的问题

如果 pg_wal 持续膨胀且 pg_current_wal_lsn() 与 redo_lsn 的差距很小(说明检查点工作正常),那么原因几乎必然是复制槽或有 pg_replication_slots 之外的保留机制。

排查顺序:

  1. pg_replication_slots 中 active = false 的槽。
  2. pg_stat_archiver 中归档失败的 WAL(归档未成功也可能保留)。
  3. wal_keep_size 设置过大。
  4. 备份工具的 pg_backup_start() 未正确 pg_backup_stop()(PG 15 前为 pg_start_backup / pg_stop_backup),导致 WAL 被强制保留。

实战示例 ​

示例 1:完整的检查点健康检查 ​

sql
-- 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;
text
            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
sql
-- 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;
text
 num_timed | num_requested | pct_requested | write_time | sync_time | buffers_written
-----------+---------------+---------------+------------+-----------+-----------------
        31 |             4 |          11.43 |    1842190 |    302118 |         5218422
sql
-- 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();
text
      uptime      | total_checkpoints | avg_interval_sec
------------------+-------------------+------------------
 01:52:04.113     |                35 |              192
sql
-- 4) 恢复代价评估
SELECT
  pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), redo_lsn)) AS wal_to_replay
FROM pg_control_checkpoint();
text
 wal_to_replay
---------------
 512 MB

判定:pct_requested = 11.43% 健康(说明大部分检查点由超时驱动),平均间隔 192s(checkpoint_timeout = 300s,接近但略短,可能因为实例启动初期有几次请求检查点),重放量 512MB ≈ max_wal_size 的一半。整体健康。

bash
# 5) 日志中是否有「检查点太频繁」的警告
grep -c "checkpoints are occurring too frequently" /var/log/postgresql/postgresql-18-main.log
text
0

示例 2:复现并消除「检查点太频繁」 ​

ini
# postgresql.conf —— 初始配置(max_wal_size 过小)
checkpoint_timeout = 5min
max_wal_size = 256MB      # 偏小,写入量稍大就会被请求检查点驱动
checkpoint_warning = 30s
bash
# 施加写入负载(pgbench 的 insert 场景会产生大量 WAL)
pgbench -i -s 50 postgres
pgbench -c 16 -j 4 -T 120 -f insert_only.sql postgres
sql
-- 观察检查点构成:请求检查点占比会很高
SELECT num_timed, num_requested,
       round(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 2) AS pct_requested
FROM pg_stat_checkpointer;
text
 num_timed | num_requested | pct_requested
-----------+---------------+---------------
        12 |            38 |         76.00
bash
# 日志中会出现大量警告
grep "checkpoints are occurring too frequently" /var/log/postgresql/postgresql-18-main.log | tail -n 3
text
2026-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)
ini
# postgresql.conf —— 修正配置
checkpoint_timeout = 5min
max_wal_size = 8GB        # 256MB → 8GB,让检查点回归「超时驱动」
min_wal_size = 1GB        # 相应上调,减少段文件反复创建删除
checkpoint_warning = 30s
sql
-- reload 生效(这两个参数都是 SIGHUP 级)
SELECT pg_reload_conf();
sql
-- 清零统计后重新观察
SELECT pg_stat_reset_shared('checkpointer');
sql
-- 一段时间后再看:请求检查点占比应大幅下降
SELECT num_timed, num_requested,
       round(100.0 * num_requested / NULLIF(num_timed + num_requested, 0), 2) AS pct_requested
FROM pg_stat_checkpointer;
text
 num_timed | num_requested | pct_requested
-----------+---------------+---------------
        23 |             2 |          8.00

pct_requested 从 76% 降到 8%,日志警告消失。

调大 max_wal_size 的副作用

不要把 max_wal_size 一次性调到极大(如 64GB),因为:

  1. 崩溃恢复时间线性增长——恢复会重放这么多 WAL。
  2. WAL 磁盘占用增长——pg_wal 可能占满磁盘。
  3. pg_wal 所在磁盘需要预留空间,否则会因 WAL 写满而宕机。

建议的上调路径:先翻倍观察,确认 pct_requested 与恢复时间都在可接受范围,再继续上调。

示例 3:定位检查点引发的 I/O 尖峰 ​

sql
-- 需要 track_io_timing = on
SHOW track_io_timing;
text
 track_io_timing
-----------------
 on
sql
-- 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;
text
   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 的主要来源。若同时观察到业务延迟尖峰与检查点时间重合,可采取:

ini
# postgresql.conf —— 降低检查点的瞬时冲击
checkpoint_completion_target = 0.9     # 确认不为 0 或 1.0
checkpoint_flush_after = 256kB         # 机械盘保持;SSD 可设 0 提高吞吐
max_wal_size = 16GB                    # 减少检查点频率
checkpoint_timeout = 15min             # 或直接拉长间隔(代价:恢复更慢)
sql
-- 对比调整前后的 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 就越多。

sql
-- 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;
text
 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 总量中的比例。
sql
-- 对比:把检查点间隔调长(max_wal_size 调大)后,FPW 占比应下降
-- 因为「检查点次数变少 → 页首次修改的机会变少 → FPW 变少」

FPW 是「检查点间接触发的 WAL 放大」

这个关系容易被忽略:调小 max_wal_size(检查点更频繁)不仅增加检查点本身的 I/O,还会通过 FPW 机制增加 WAL 生成量。反过来,调大 max_wal_size 有「双重收益」:

  1. 检查点数减少 → 检查点 I/O 减少。
  2. FPW 减少 → WAL 总量减少 → 归档/复制压力减小。

代价是恢复时间变长。这也是为什么 pg_stat_wal.wal_fpi 值得纳入监控——它能揭示「WAL 量偏大」是否由过于频繁的检查点造成。

示例 5:用 pg_controldata 判断上次关闭状态 ​

bash
# 停止服务后检查
pg_ctl -D "$PGDATA" stop -m fast
pg_controldata "$PGDATA" | grep -E "cluster state|Latest checkpoint|Time of latest checkpoint"
text
Database cluster state:               shut down
Latest checkpoint location:           0/1A3E2B70
Time of latest checkpoint:            Mon 22 Sep 2026 11:41:02 AM CST
bash
# 模拟异常退出:用 immediate 模式停止(不执行正常关闭)
pg_ctl -D "$PGDATA" stop -m immediate
pg_controldata "$PGDATA" | grep -E "cluster state"
text
Database cluster state:               in production
bash
# 重新启动:会触发崩溃恢复
pg_ctl -D "$PGDATA" start
text
# 日志中会看到恢复过程
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。
ini
# postgresql.conf —— 不推荐:为了「安全」把检查点设得极频繁
# checkpoint_timeout = 30s          # 不推荐:检查点过于频繁,I/O 持续紧张
# max_wal_size = 128MB              # 不推荐:会被 WAL 量持续驱动,且 FPW 放大 WAL
ini
# postgresql.conf —— 不推荐:为了「少 I/O」把检查点设到极限
# checkpoint_timeout = 1h           # 不推荐:恢复时间将长达数十分钟
# max_wal_size = 200GB              # 不推荐:需 200GB WAL 空间,且磁盘耗尽风险极高
sql
-- 不推荐:业务高峰期手动触发检查点
-- 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:高写入负载下若观察到检查点引起的写入停顿,可评估该参数。

参考链接 ​