来源https://www.pgedge.com/blog/checkpoints-write-storms-and-you检查点、写入风暴肖恩·托马斯 | 2026年4月10日每个数据库都必须调和两个令人不适的事实内存快速但易失磁盘缓慢但持久。Postgres 通过其预写日志 (WAL) 来处理这种紧张关系该日志在每次更改发生前就记录下更改。但 WAL 不能无限增长。在某个时刻Postgres 需要将所有累积的脏页刷新到磁盘并声明一个干净的起点。这个过程称为检查点当它出错时可能会使系统吞吐量陷入瘫痪。关于检查点的一些知识在正常操作下Postgres 对检查点的处理是非常“礼貌”的。checkpoint_timeout参数默认 5 分钟告诉 Postgres 执行计划检查点的频率而checkpoint_completion_target参数默认 0.9告诉它将产生的写入操作分散到该间隔的 90% 时间内。因此5 分钟的检查点超时意味着 Postgres 将在大约 4.5 分钟内将脏页慢慢地写入磁盘从而将对 IO 的影响降至最低。这仅适用于定时检查点行为。max_wal_size参数设置了检查点之间可以累积的 WAL 数量的软限制。当 WAL 接近该阈值默认为 1GB时Postgres 不会等待下一个计划的检查点。相反它会立即强制执行一个检查点。这些强制或称为请求的检查点不遵循checkpoint_completion_target。Postgres 需要回收 WAL 空间因此它会以 IO 子系统允许的最快速度将每个脏缓冲区刷新到磁盘。在一个繁忙的系统上如果有一个装满修改页面的大的shared_buffers池这可能会在几秒钟内完全耗尽磁盘 IO 资源。这就像试图从消防水管中喝水。纸上谈兵不如实战演练为了观察实际效果我们设置了一个适中的测试环境虚拟机监控程序ProxmoxCPU4 个 AMD EPYC 9454 核心内存4GB数据库存储100GB 2,000 IOPSWAL 存储100GB 2,000 IOPS操作系统Debian 12 Bookworm我们使用pgbench以 800 的缩放因子初始化了数据库产生了大约 12GB 的数据可用内存的 3 倍以减少缓存命中。我们还遵循传统建议将shared_buffers设置为 RAM 的 25%在本例中为 1GB。所有其他设置保持默认值。每个测试都遵循相同的模式执行手动CHECKPOINT以从干净状态开始然后运行pgbench60 秒每秒报告进度并使用 16 个并发客户端来保持所有 CPU 核心繁忙pgbench--progress1--time60--clients16demo我们从默认的max_wal_size1GB开始观察系统的行为。这个设置在优化过程中经常被忽视因此它应该能很好地说明基线操作的情况。在测试的前 41 秒吞吐量稳定在 1,000 到 1,100 TPS 之间。缓冲区开始变热IO 子系统跟上了步伐延迟保持较低水平。在第 42 秒标记处WAL 输出达到了 1GBPostgres 强制执行了一个检查点。TPS 立即暴跌至大约 620——下降了近 40%在基准测试的剩余时间内它再也没有恢复过来。在第二次测试中我们将max_wal_size增加到 4GB。这是一个适度的提升但足以满足本次演示的目的。这次吞吐量开始时约为 1,000 TPS并随着共享缓冲区的预热逐渐攀升到测试结束时达到了 1,200 TPS。在此硬件上一分钟的pgbench活动不足以产生 4GB 的 WAL这意味着没有强制检查点。结果基本上不言自明pgbench TPS 对比强制检查点的伤害哎呀两次测试在前 40 秒的追踪轨迹几乎相同。然后1GB 配置撞上了墙而 4GB 配置继续攀升。强制检查点的代价在定时检查点期间Postgres 通常会使用checkpoint_completion_target来计算写入预算。如果检查点间隔为 5 分钟目标值为 0.9则它可以将其脏页写入分散到 270 秒内。这是一个将数据慢慢写入磁盘的漫长过程每秒的 IO 影响微乎其微。强制检查点则没有这种奢侈。WAL 已满或接近满Postgres 现在就需要回收空间。它会尽可能快地将脏缓冲区写入磁盘直接与活动查询竞争磁盘 IO。在一个限制为 2,000 IOPS 的系统上这种竞争非常激烈。每个用于刷新检查点数据的 IOPS 本质上都是从用户查询中“窃取”来的。严重程度在很大程度上取决于硬件。拥有快速 NVMe 存储和数万或数十万IOPS 的系统可能几乎不会注意到。但是云实例、虚拟化环境或任何有 IO 限制的环境这非常普遍将会感受到痛苦。我们为测试系统配置了每个卷 2,000 IOPS按云标准来说这已经相对慷慨了但仍然经历了显著的影响。基准测试本身只是故事的一半。在此之前我们不得不使用pgbench --initialize来初始化 12GB 的测试数据库。当pgbench使用默认的 1GBmax_wal_size生成示例数据时Postgres 触发了 18 次强制检查点。将max_wal_size设置为 20GB 后再次尝试强制检查点的数量降为零。那又怎样这不过是初始化对吧但请考虑同样的模式适用于任何批量数据操作COPY导入、大型INSERT INTO ... SELECT语句、大表上的CREATE INDEX、REINDEX甚至是大量的UPDATE批次。如果这些操作中的任何一个与生产 OLTP 工作负载同时运行那就意味着有 18 次 IO 风暴在与应用程序查询竞争。一个每晚加载几 GB 数据的 ETL 作业可能会触发一连串的强制检查点从而导致系统上所有其他查询的延迟飙升。批量操作本身也会变慢因为它需要与自身的检查点 IO 争夺磁盘带宽。当检查点无法分散写入活动时所有人都是输家。发现问题Postgres 会跟踪检查点统计信息检查这些信息应该成为任何常规健康评估的一部分。要使用的系统目录取决于 Postgres 版本。在 Postgres 17 及更高版本中使用pg_stat_checkpointerSELECTnum_timed,num_requested,num_done,write_time,sync_time,buffers_writtenFROMpg_stat_checkpointer;在旧版本中相同的信息位于pg_stat_bgwriter中SELECTcheckpoints_timed,checkpoints_req,checkpoint_write_time,checkpoint_sync_time,buffers_checkpointFROMpg_stat_bgwriter;这里的关键比率是定时检查点与请求检查点的比率。在一个调优良好的系统中相对于num_timednum_requested或checkpoints_req应该接近于零。如果请求的检查点占总数的很大一部分那么max_wal_size对于当前的写入工作负载来说太小了性能很可能不是最优的。同时关注write_time和sync_time也很有价值。如果sync_time持续偏高说明存储子系统在努力跟上检查点刷新的步伐这进一步证实了检查点期间存在 IO 瓶颈。至于日志记录我们强烈建议设置log_checkpoints on来记录检查点活动log_checkpointson这会使 Postgres 记录关于每个检查点的详细信息例如写入的缓冲区数量、花费的时间包括同步时间以及其他有用的指标。启用后Postgres 日志应该会显示如下所示的检查点活动LOG: checkpoint starting: wal LOG: checkpoint complete: wrote 2069 buffers (1.6%), wrote 2 SLRU buffers; 0 WAL file(s) added, 1 removed, 32 recycled; write2.132 s, sync0.092 s, total2.282 s; sync files35, longest0.090 s, average0.003 s; distance553713 kB, estimate553713 kB; lsn6/FEBDB228, redo lsn6/DFFFFC60 LOG: checkpoints are occurring too frequently (2 seconds apart) HINT: Consider increasing the configuration parameter max_wal_size.LOG: checkpoint starting: wal这一行就是确凿的证据。它表示这个检查点是因为 WAL 达到了限制而被强制执行的而不是因为超时到期。定时检查点会显示checkpoint starting: time。这是免费的法医信息。日志记录开销可以忽略不计并提供清晰的检查点行为轨迹。这是另一个应该默认启用的设置从 Postgres 15 开始已经如此。那些使用旧版本集群的用户将需要手动启用它。找到合适的 max_wal_size那么是不是每个人都应该将max_wal_size提高到一个巨大的值然后就不管了呢不完全是这样。这其中有权衡。更大的max_wal_size允许在检查点之间积累更多的 WAL这意味着在崩溃恢复期间必须重放更多的数据。如果 Postgres 崩溃时有 20GB 的 WAL 需要重放启动时间必然比只有 1GB 时要长。这种恢复时间的差异通常只有几秒钟但值得注意。另一个考虑因素是磁盘空间。WAL 文件会消耗存储空间而max_wal_size是一个软限制。在繁重的写入负载下WAL 可能会暂时超过它。WAL 卷上应有足够的空间余量来应对突发情况而不会完全耗尽空间。那将是一个比检查点缓慢严重得多的问题。对于写入密集型 OLTP 工作负载一个合理的起始点是 10GB 到 20GB。具有大量批量加载或大型批处理操作的系统可能会受益于 50GB 甚至更多。目标是让强制检查点变得足够罕见以至于基本上所有的检查点都是定时检查点并能优雅地分散在checkpoint_completion_target内。我们建议通过持续监控pg_stat_checkpointer或pg_stat_bgwriter来验证设置。让系统在典型负载下运行一天或一周然后检查比率。如果请求的检查点增加了就提高max_wal_size并重复此过程。-- 重置统计信息并在观察窗口后重新检查SELECTpg_stat_reset_shared(checkpointer);-- ... 一段时间后 ...SELECTnum_timed,num_requestedFROMpg_stat_checkpointer;如果你不想自己动手我实际上写了一个名为pg_walsizer的 Postgres 扩展。它会启动一个后台工作进程监控检查点活动并根据在配置的checkpoint_timeout内发生了多少个检查点自动增加max_wal_size。只需设置它然后就不用管了总结检查点是 Postgres 内部机制中那些大多数人直到出问题才会考虑的东西。毕竟周期性的延迟峰值可能有无数种原因。并非所有 DBA 都会检查 WAL 活动也不一定意识到它与磁盘刷新的关系——大多数人首先会归咎于 vacuum。按照传统max_wal_size的默认值 1GB 是一个保守的值。它最大限度地减少了崩溃恢复时间并且对于轻量级工作负载来说效果很好同时不会占用大量存储空间。不幸的是繁忙的系统会很快超过这个默认值并开始受到影响。我们的测试在适中的硬件上显示了 40% 的 TPS 暴跌具有更重负载和更紧张 IO 预算的生产系统情况可能会更糟。对于大多数生产环境我们建议从一个更合适的max_wal_size开始。如果log_checkpoints尚未启用也请优先启用它。最后pg_stat_checkpointer或pg_stat_bgwriter应该突出地显示在监控仪表板上。对任何请求检查点的增加都应持怀疑态度。最后max_wal_size是那种罕见的参数只需调整它一个就能带来显著的性能提升而且几乎没有任何负面影响。所以去检查一下你的检查点统计信息吧。你可能会对发现的结果感到惊讶