Skip to content

PostgreSQL 中的 WAL — 3. 检查点(Checkpoint)

原文:https://habr.com/en/companies/postgrespro/articles/494464/ (作者 Egor Rogov,PostgresPro)

上一篇讲到,崩溃恢复要从"上一个检查点"开始重放 WAL。这一篇专门讲清楚检查点到底是什么、它是怎么产生和推进的,以及和它协同工作的后台写进程(background writer)是怎么回事,最后落到实际的调优和监控参数上。

检查点解决什么问题

如果 WAL 一直无限增长、恢复时永远要从数据库诞生那一刻开始重放,这既浪费磁盘空间,也会让崩溃后的恢复时间长到无法忍受。检查点的作用就是定期在 WAL 上打一个"安全标记":检查点完成之后,可以保证在检查点开始之前产生的所有改动,对应的脏页都已经确实写到磁盘上了。这样一来,恢复时只需要从最近一次检查点的位置开始重放之后的日志即可,再往前的 WAL 文件可以安全地删除或复用,恢复所需的时间也被大致限定在了"检查点间隔"这个量级上。

检查点不是一个瞬间,而是一段过程

容易望文生义地以为"打检查点"是一个原子的、瞬间发生的动作,但实际上它被设计成一个有始有终、持续一段时间的过程,这样可以把随之而来的大量磁盘 I/O 摊开,避免瞬间写爆磁盘造成性能抖动。它大致分几步:

  1. 启动阶段:检查点开始时,先把事务状态缓冲区(XACT/CLOG)里的内容立即刷盘;
  2. 标记阶段:遍历当前所有缓冲区,把此刻已经是"脏"的那些页面在缓冲区头部打上一个特殊标志,表示"这些页面属于本次检查点需要处理的范围";
  3. 渐进写出阶段:checkpointer 进程按照一个由参数控制的节奏,慢慢地把被标记的脏页依次写回磁盘。这个过程中,如果某个业务后端进程恰好也要驱逐或者需要写这个被标记的页面,它自己动手写掉也是允许的,写完之后同样会把那个标志位清掉——checkpointer 和普通后端进程可以并发地为同一批"检查点范围内"的脏页干活,不会重复劳动;
  4. 新产生的脏页不算数:检查点开始之后才变脏的页面不会被打上标志,也不会被这次检查点强制写出,它们要等到下一次检查点才会被处理;
  5. 完成阶段:所有被标记的脏页都确认写盘之后,检查点算是完成。此时会向 WAL 里写入一条"检查点记录",内容包含这次检查点开始时的 LSN 位置;同时,pg_control 控制文件也会被更新,记下"最近一次成功完成的检查点"信息,供下次可能的崩溃恢复使用。

可以手动触发一次检查点:

sql
CHECKPOINT;

文章配合几张示意图说明了这个过程:一张展示检查点区间与 checkpoint_completion_target 的关系(写出阶段占据两次检查点之间时间的比例,比如取默认值 0.5 时大约占一半时间);一张展示检查点开始时打标记、期间陆续写出、新脏页不受影响、最终完成的时间线;还有一张展示 WAL 和 pg_control 之间的关系——检查点开始位置的 LSN 既写进 WAL 的检查点记录里,也写进控制文件里。

崩溃恢复流程回顾(结合检查点视角)

结合检查点后再看恢复流程会更清楚:startup 进程通过 pg_control 里的状态标志判断是否需要恢复;如果需要,就从控制文件里记录的"最近一次检查点的起始 LSN"(如果存在 backup_label 文件,则优先使用其中记录的位置,这与物理备份场景有关)开始,顺序应用后续的 WAL 记录,不打日志的表则通过初始化分支清空。恢复完成后,checkpointer 会立即主动执行一次新的检查点,把刚刚恢复出来的状态重新固化下来,确保万一再次崩溃,恢复起点已经推进到了最新状态,而不用从很久以前重新开始。

正常关闭(而非异常宕机)时,数据库会先断开客户端连接,然后做一次"关闭检查点"(在 WAL 里对应 CHECKPOINT_SHUTDOWN 类型的记录,区别于日常运行期间的 CHECKPOINT_ONLINE),把 pg_control 标记为"已正常关闭",这样下次启动就不需要走恢复流程。可以用立即模式关闭来人为模拟一次异常崩溃,从而观察恢复过程:

bash
sudo pg_ctlcluster 11 main stop -m immediate --skip-systemctl-redirect
sudo pg_ctlcluster 11 main start

后台写进程(Background Writer):另一路独立的脏页清理

除了 checkpointer 之外,PostgreSQL 还运行着一个独立的后台写进程(bgwriter),它的目的是提前、主动地把一些"看起来快要被驱逐"的脏页预先写掉,从而避免正在执行查询的普通后端进程在申请缓冲区、恰好撞上一个脏页需要驱逐时,被迫自己停下来同步等待这次写盘完成——那样会直接拖慢用户可感知的查询延迟。

bgwriter 和检查点的驱逐逻辑有几个关键区别:

  • 它维护着一个和时钟扫描算法(见第一篇)里"next victim"指针方向一致、但独立移动的指针,在缓冲区上巡视;
  • 它只关心同时满足"脏、未被钉住、使用计数为 0"这三个条件的缓冲区,把它们提前写盘、清理干净;
  • 它在巡视过程中不会像正常的驱逐算法那样去递减经过的缓冲区的使用计数,只是单纯地找、写,不影响时钟扫描本身的状态;
  • 它按照"睡眠—工作"的节奏周而复始地运行,每一轮之间休眠一段由参数控制的时间。

调优相关参数

检查点触发条件共有两类:定时触发和按 WAL 用量触发。

  • checkpoint_timeout:两次计划内检查点之间的最大时间间隔,默认 5 分钟;
  • max_wal_size:一旦本次检查点以来产生的 WAL 总量超过这个上限,就会提前触发一次"计划外"的检查点,而不必等到 checkpoint_timeout 到点;这个参数是一个偏好性质的软上限,不是硬边界——实际保留的 WAL 量在某些情况下(比如存在复制槽、归档没跟上)仍然可能超出它;
  • min_wal_size:只要当前 WAL 总量在这个值以下,旧的 WAL 段文件会被直接重命名复用而不是删除再重新创建,减少文件系统层面的开销。

写出节奏控制

  • checkpoint_completion_target:一个 0 到 1 之间的比例值,默认 0.5,表示把检查点的写出阶段摊开到"下一次计划检查点到来之前"这段时间的多大比例上完成,值越接近 1,写出过程越平缓、峰值 I/O 越低,但也意味着检查点几乎要一直持续到下一个检查点周期开始前才结束。原文给出一个和这个参数相关的 WAL 保留量估算:从 PostgreSQL 11 开始,大致的 WAL 保留量约为每个检查点周期产生量乘以 (1 + checkpoint_completion_target)(11 之前的旧版本系数是 2 + checkpoint_completion_target,行为略有不同)。

  • checkpoint_warning:默认 30 秒,如果两次"因为 WAL 用量超限而被迫提前触发"的检查点之间间隔过短(小于这个阈值),说明 max_wal_size 设置得可能偏小、检查点触发过于频繁,服务器日志会打印一条警告提醒管理员。

后台写进程相关

  • bgwriter_delay:bgwriter 每一轮巡视之间的休眠时长,默认 200 毫秒;
  • bgwriter_lru_maxpages:每一轮最多写出多少页,默认 100;
  • bgwriter_lru_multiplier:一个系数(默认 2.0),bgwriter 会参考最近一段时间"平均每轮有多少缓冲区被正常业务申请/换入"这个滑动平均值,乘以这个系数来估算这一轮大概需要预先写出多少页,既不过量浪费 I/O,也尽量跟上实际的换入速度,最终这个估算值仍然会被 bgwriter_lru_maxpages 封顶。

监控

打开详细的检查点日志,能观察到每次检查点实际写了多少缓冲区、耗时多久、是被什么原因触发的:

sql
ALTER SYSTEM SET log_checkpoints = on;
SELECT pg_reload_conf();

统计视图 pg_stat_bgwriter 是长期监控和调优的核心依据,它把 checkpointer 和 bgwriter 各自的工作量汇总在一起,可以观察诸如"计划内检查点次数 vs. 因为 WAL 超限被迫触发的次数"(后者过多说明 max_wal_sizecheckpoint_timeout 需要调大)、"由 checkpointer/bgwriter 写出的缓冲区数量 vs. 普通后端进程被迫自己写出的数量"(buffers_backend 占比过高说明 bgwriter 没有跟上节奏,业务查询在替它擦屁股,拖慢了自身延迟)等关键信号:

sql
SELECT * FROM pg_stat_bgwriter \gx

统计数据是累计值,重置后可以重新观察某个时间窗口内的行为:

sql
SELECT pg_stat_reset_shared('bgwriter');

文章还结合 pg_waldumppg_controldata 演示了如何在检查点前后分别观察 WAL 记录内容和控制文件里记录的最新检查点位置变化,从原理上验证了前面讲的整个机制。

小结

检查点把"崩溃后要重放多少 WAL"这件事控制在了一个可预期的范围内:它不是瞬时动作,而是"标记—渐进写出—完成"的完整过程,写出节奏由 checkpoint_completion_target 摊开,触发时机由 checkpoint_timeout(定时)和 max_wal_size(WAL 用量)共同决定;检查点完成后把起始 LSN 写入 WAL 记录并更新 pg_control,恢复时正是从这里开始前滚。与检查点并行工作的后台写进程(bgwriter)则专注于提前把"看起来快要被换出"的脏页悄悄写掉,减轻业务查询临时被迫同步写盘的概率。调优这一整套机制离不开 pg_stat_bgwriter 视图提供的长期观测数据,需要结合实际的 WAL 生成速率、可接受的恢复时间窗口来综合权衡。下一篇会进一步展开 WAL 相关的可靠性与性能参数(wal_levelfull_page_writessynchronous_commit 等),把整个体系的调优图景补完整。