上一篇我们聊了 WAL 的写入与崩溃恢复,理解了"先写日志再写数据"如何保证持久性。这一篇,我们进入 WAL 机制的另一半:checkpoint 。如果说 WAL 是"记录变更的账本",那么 checkpoint 就是"宣布哪些账本可以合上"的那个动作。它决定了崩溃恢复需要从多早开始重放,也决定了日常写入的 I/O 是否平滑。checkpoint 调优,本质上是在恢复速度(RTO) 和运行时 I/O 稳定性之间做取舍。

一、Checkpoint 到底在做什么?
官方定义很精确:checkpoint 是事务序列中的一些点,在这些点上,堆和索引数据文件已经更新为在检查点之前写入的所有信息。执行时,所有脏数据页被刷到磁盘,同时一条特殊的 checkpoint 记录被写入 WAL。
这条记录的位置就是崩溃恢复的起点 。崩溃发生后,恢复过程会找到最新的 checkpoint 记录,确定 redo 记录 的位置,从那里开始重放 WAL。在 redo 记录之前的所有数据页修改,保证已经在磁盘上,因此那些更早的 WAL 段可以被回收或删除。
所以 checkpoint 的核心产出不是"刷了多少页",而是一个已知良好的恢复起点。它把"永远从头重放"的问题,变成了"从最近的安全点重放"。
二、触发条件:时间与 WAL 量,谁先到谁触发
checkpoint 的触发由两个参数控制,以先到者为准:
ini
checkpoint_timeout = 5min # 默认 5 分钟
max_wal_size = 1GB # 默认 1 GB
每一次 checkpoint 会在 checkpoint_timeout 秒后开始,或者当 max_wal_size 即将被超出时开始。还有一个容易被忽略的细节:如果自上次 checkpoint 以来没有写入任何 WAL,即使 checkpoint_timeout 已过,新的 checkpoint 也会被跳过。空闲实例不会产生无意义的 checkpoint 刷盘。
你也可以手动强制执行:CHECKPOINT;。但这通常只用于运维场景,日常不应依赖手动触发。
三、平滑刷脏:为什么 checkpoint 不是"瞬间倾泻"
checkpoint 最原始的形态是"把所有脏页一口气刷完"。但这样会瞬间淹没 I/O 子系统,导致查询延迟飙升。PostgreSQL 的做法是把脏页写入分散到一段时间内 ,由 checkpoint_completion_target 控制。
这个参数是一个分数 ,表示 checkpoint 希望在 checkpoint_timeout 的多大比例 时间内完成。I/O 速率会被调整,使得检查点在"给定的 checkpoint_timeout 分数过去时"完成,或者在 max_wal_size 被超出之前完成,以更早者为准。
早期默认值是 0.5 ,意味着 checkpoint 用大约一半的间隔时间完成,留下另一半作为缓冲。后来的版本(包括许多云托管服务的默认配置)将默认值提高到了 0.9,让写入更加平滑。
但不要把它设到 1.0。官方明确建议保持在 0.9 以下,因为 checkpoint 除了写脏页,还有其他活动;设为 1.0 很可能导致 checkpoint 无法按时完成,反而引起 WAL 段数量的意外波动和性能损失。
还有两个辅助参数值得了解:
checkpoint_flush_after:在 Linux/POSIX 平台上,强制操作系统在写入一定字节后把页刷出,避免页在 OS page cache 中积累,导致 checkpoint 末尾的fsync出现长停顿。checkpoint_warning:如果 checkpoint 发生的间隔小于这个值,日志会输出警告,提示你考虑增大max_wal_size。这是检查 checkpoint 是否"过于频繁"的最直接信号。
四、频繁 checkpoint 的代价:恢复更快,但 WAL 更多
回到核心问题:checkpoint 过于频繁会有什么问题?
直觉上,"更频繁 checkpoint = 恢复更快"是成立的。减少 checkpoint_timeout 和/或 max_wal_size 确实会让 checkpoint 更频繁,崩溃后需要重做的工作更少,恢复更快。
但代价被两个因素放大。
第一,刷脏本身的 I/O 成本。 checkpoint 需要写出所有当前脏缓冲区,频繁执行意味着更密集的写盘压力。
第二,full_page_writes 带来的 WAL 放大。 这是最容易被低估的代价。为了在部分写损坏时能够恢复,每次 checkpoint 之后,对某个数据页的第一次修改,WAL 会记录整个 8KB 页的内容 ,而不只是变更。因此,checkpoint 越密集,全页写在 WAL 总量中的占比就越高,WAL 输出量反而可能增加,部分抵消了"更频繁 checkpoint 让恢复更快"的目标,并且无论如何都会带来更多磁盘 I/O。
所以"频繁 checkpoint"的收益不是免费的:恢复时间缩短了,但日常运行的 WAL 量和 I/O 压力上升了。 这就是 checkpoint 调优的核心矛盾。
五、如何平衡:把 RTO 和 I/O 平滑放在同一张桌上
调优的起点不是"抄一个推荐值",而是先明确你的 RTO 预算 和 I/O 承受能力。
如果你的系统对恢复时间有严格要求,可以接受日常更多的 WAL 和写 I/O,那么适当降低 checkpoint_timeout 或 max_wal_size 是合理的。如果你更在意运行时延迟的平稳,不希望 checkpoint 周期性地造成 I/O 尖峰,那么应该把参数设得足够高,避免 checkpoint 过于频繁 ,同时依靠 checkpoint_completion_target 把写入摊平。
在严格 RTO 要求 的场景下,这个权衡必须通过真实的崩溃测试来验证,而不是靠假设。你可以人为制造一个 checkpoint,记录从崩溃到恢复完成的时间,与你的 RTO 目标对比,再决定是否调整参数。
一个实用的检查顺序:
- 先看日志有没有
checkpoint_warning。如果频繁出现"checkpoints are occurring too frequently"的提示,说明max_wal_size偏小,checkpoint 被 WAL 量推着走,而不是按时间平滑触发。 - 确认
checkpoint_completion_target在合理区间(通常 0.7--0.9)。过低会导致写入过于集中,过高(接近 1.0)可能导致 checkpoint 无法按时完成。 - 用真实崩溃恢复演练验证 RTO。不要用"参数看起来合理"替代实测。
六、总结
| 主题 | 核心结论 |
|---|---|
| Checkpoint 的本质 | 创建一个"数据页已全部落盘"的恢复起点,决定崩溃恢复的 redo 位置 |
| 触发条件 | checkpoint_timeout 或 max_wal_size,先到为准;无 WAL 写入时跳过 |
| 平滑刷脏 | checkpoint_completion_target 把脏页写入摊到间隔的分数时间内,默认常见 0.9,不要设 1.0 |
| 频繁 checkpoint 的收益 | 崩溃恢复更快,重做 WAL 更少 |
| 频繁 checkpoint 的代价 | 刷脏 I/O 更密集;full_page_writes 导致 WAL 输出量增加,部分抵消恢复速度收益 |
| 平衡方式 | 先看 checkpoint_warning,再调 completion_target,最后用真实崩溃测试验证 RTO |
| 辅助参数 | checkpoint_flush_after 减少 fsync 停顿;checkpoint_warning 是"过于频繁"的告警信号 |
一句话:Checkpoint 不是一个"越勤快越好"的维护动作。它用日常的 I/O 成本和 WAL 放大量,购买崩溃后的恢复速度。调优的本质,是明确你的 RTO 预算,然后让 checkpointer 在这个预算内,把脏页以最平稳的方式送到磁盘上。
下一篇,我们进入 PostgreSQL 的后台写进程:Background Writer 与 Checkpointer 到底如何分工?bgwriter 的"渐进刷脏"如何为 checkpoint 减负?
思考题:
你检查过自己数据库的日志里有没有 checkpoint_warning 吗?你的 checkpoint_completion_target 设的是多少------有没有想过它为什么不是 1.0?另外,你有没有真正做过一次崩溃恢复演练,测过从"拔电"到"可连接"的实际 RTO?欢迎在评论区聊聊。