DeepSeek总结的PostgreSQL检查点和写入风暴

来源: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 资源。

这就像试图从消防水管中喝水。

纸上谈兵不如实战演练

为了观察实际效果,我们设置了一个适中的测试环境:

  • 虚拟机监控程序: Proxmox
  • CPU: 4 个 AMD EPYC 9454 核心
  • 内存: 4GB
  • 数据库存储: 100GB @ 2,000 IOPS
  • WAL 存储: 100GB @ 2,000 IOPS
  • 操作系统: Debian 12 Bookworm

我们使用 pgbench 以 800 的缩放因子初始化了数据库,产生了大约 12GB 的数据(可用内存的 3 倍,以减少缓存命中)。我们还遵循传统建议,将 shared_buffers 设置为 RAM 的 25%,在本例中为 1GB。所有其他设置保持默认值。

每个测试都遵循相同的模式:执行手动 CHECKPOINT 以从干净状态开始,然后运行 pgbench 60 秒,每秒报告进度,并使用 16 个并发客户端来保持所有 CPU 核心繁忙:

bash 复制代码
pgbench --progress=1 --time=60 --clients=16 demo

我们从默认的 max_wal_size(1GB)开始,观察系统的行为。这个设置在优化过程中经常被忽视,因此它应该能很好地说明基线操作的情况。

在测试的前 41 秒,吞吐量稳定在 1,000 到 1,100 TPS 之间。缓冲区开始变热,IO 子系统跟上了步伐,延迟保持较低水平。在第 42 秒标记处,WAL 输出达到了 1GB,Postgres 强制执行了一个检查点。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 使用默认的 1GB max_wal_size 生成示例数据时,Postgres 触发了 18 次强制检查点。将 max_wal_size 设置为 20GB 后再次尝试,强制检查点的数量降为零。

那又怎样?这不过是初始化,对吧?但请考虑,同样的模式适用于任何批量数据操作:COPY 导入、大型 INSERT INTO ... SELECT 语句、大表上的 CREATE INDEXREINDEX,甚至是大量的 UPDATE 批次。如果这些操作中的任何一个与生产 OLTP 工作负载同时运行,那就意味着有 18 次 IO 风暴在与应用程序查询竞争。

一个每晚加载几 GB 数据的 ETL 作业可能会触发一连串的强制检查点,从而导致系统上所有其他查询的延迟飙升。批量操作本身也会变慢,因为它需要与自身的检查点 IO 争夺磁盘带宽。

当检查点无法分散写入活动时,所有人都是输家。

发现问题

Postgres 会跟踪检查点统计信息,检查这些信息应该成为任何常规健康评估的一部分。要使用的系统目录取决于 Postgres 版本。

在 Postgres 17 及更高版本中,使用 pg_stat_checkpointer

sql 复制代码
SELECT num_timed, num_requested, num_done,
       write_time, sync_time, buffers_written
  FROM pg_stat_checkpointer;

在旧版本中,相同的信息位于 pg_stat_bgwriter 中:

sql 复制代码
SELECT checkpoints_timed, checkpoints_req,
       checkpoint_write_time, checkpoint_sync_time,
       buffers_checkpoint
  FROM pg_stat_bgwriter;

这里的关键比率是:定时检查点与请求检查点的比率 。在一个调优良好的系统中,相对于 num_timednum_requested(或 checkpoints_req)应该接近于零。如果请求的检查点占总数的很大一部分,那么 max_wal_size 对于当前的写入工作负载来说太小了,性能很可能不是最优的。

同时关注 write_timesync_time 也很有价值。如果 sync_time 持续偏高,说明存储子系统在努力跟上检查点刷新的步伐,这进一步证实了检查点期间存在 IO 瓶颈。

至于日志记录,我们强烈建议设置 log_checkpoints = on 来记录检查点活动:

sql 复制代码
log_checkpoints = on

这会使 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; write=2.132 s, sync=0.092 s,
      total=2.282 s; sync files=35, longest=0.090 s, average=0.003 s;
      distance=553713 kB, estimate=553713 kB; lsn=6/FEBDB228,
      redo lsn=6/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 并重复此过程。

sql 复制代码
-- 重置统计信息,并在观察窗口后重新检查
SELECT pg_stat_reset_shared('checkpointer');

-- ... 一段时间后 ...
SELECT num_timed, num_requested FROM pg_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_checkpointerpg_stat_bgwriter 应该突出地显示在监控仪表板上。对任何请求检查点的增加都应持怀疑态度。

最后,max_wal_size 是那种罕见的参数,只需调整它一个,就能带来显著的性能提升,而且几乎没有任何负面影响。所以,去检查一下你的检查点统计信息吧。你可能会对发现的结果感到惊讶!

相关推荐
摸鱼仙人~2 小时前
OpenCode 长期记忆系统内容整理
jvm
码以致用2 小时前
Java垃圾回收器笔记
java·jvm·笔记
HealthScience3 小时前
clinvar数据集说明
数据库·oracle
cyber_两只龙宝3 小时前
【Oracle】Oracle之DQL中SELECT的基础使用
linux·运维·服务器·数据库·云原生·oracle
老苏畅谈运维3 小时前
Oracle 在线表重定义:将非分区表转换为分区表的最佳实践
数据库·oracle
treacle田3 小时前
达梦数据库-达梦数据库中link链接访问oracle 19c/11g-记录总结
数据库·oracle·达梦 link访问oracle
萌兰三太子3 小时前
RAG 向量数据库设计指南:从入门到生产
数据库·oracle
IT邦德4 小时前
Oracle 26ai搭建ADG Far Sync日志备库
数据库·oracle
sa100274 小时前
一键获取淘宝天猫商品评论:API 接口实战与多语言实现教程
数据库·oracle