来源:https://stormatics.tech/blogs/the-1-gb-limit-that-breaks-pg_prewarm-at-scale

导致 pg_prewarm 在大规模场景下失效的 1GB 限制

最近,我们遇到了一起生产事故,PostgreSQL 16.8 变得不稳定,导致应用程序无法建立数据库连接。我们在一个独立的测试环境中也复现了相同的行为,从而排除了基础设施和配置问题的可能。进一步的调查确定 pg_prewarm 扩展是问题的根源。

这篇博文将详细分析此次故障、其背后的限制、为何只在特定配置下才会显现,以及相应的短期缓解措施和长期修复方案。

pg_prewarm 的作用

每次 PostgreSQL 重启后,其共享缓冲区缓存(用于存放来自磁盘的频繁访问数据页的 RAM 区域)都会从完全清冷的状态开始。任何需要数据的查询都必须首先访问磁盘。在大型生产系统上,这种冷启动的代价可能非常高昂,有时需要数小时才能通过正常的业务流量自然预热缓存。

pg_prewarm 通过两种方式解决这个问题。首先,它提供了手动控制能力,允许你按需显式地预热特定的表或索引,这在执行繁重的批处理作业或已知的查询工作负载之前非常有用。其次,它附带了一个自动预热模式,启用后,会持续跟踪哪些页面驻留在共享缓冲区中,并在数据库重启后自动重放该列表,无需人工干预。对于拥有大型 shared_buffers 的高流量系统来说,这在操作上至关重要。

这个 Bug

为了让自动预热功能转储缓存页面的列表,PostgreSQL 必须在内存中构建一个数组,该数组为当前共享缓冲区中的每个页面包含一个条目。每个条目(一个 BlockInfoRecord 结构)占用 20 字节。

palloc(NBuffers * sizeof(BlockInfoRecord))

NBuffers 是共享缓冲区槽位的数量,直接由 shared_buffers 设置派生而来。PostgreSQL 的标准内存分配器 palloc 对任何单个分配都强制执行 1 GB 的硬性上限。任何请求超过 1 GB 的 palloc() 调用都会达到此限制并引发错误:

ERROR: invalid memory alloc request size

让我们来计算一下:

参数
palloc 硬性限制1,073,741,824 字节 (1 GB)
BlockInfoRecord 大小每个条目 20 字节
达到上限前的最大条目数 (1 GB / 20 B)~53,687,091 = 约 5370 万个条目
缓冲区页面大小8 KB
可覆盖的最大 shared_buffers (5370 万 x 8 KB)~429 GB

一旦 NBuffers × 20 字节 超过 1 GB 的分配限制,palloc() 调用就会失败。这个阈值大约在 shared_buffers 达到 429 GB 时触发。换句话说,如果 shared_buffers ≤ 429 GB,自动预热可以成功分配一个最大为 1 GB 的数组,其中每个条目存储一个页面/块引用,以便在重启后重新加载。然而,一旦 shared_buffers 超过这个阈值,所需的内存分配就超过了 1 GB 的限制,从而导致错误。

在拥有 TB 级内存的现代系统上,这不再是一个理论上的边缘情况。任何超过约 429 GB 的 shared_buffers 配置都会触发此限制,因为单次内存分配超过了允许的最大大小。

我们在生产环境中,于 PostgreSQL 16.8 上观察到了这个问题,该环境在一台 1.5 TB 的服务器上配置了 700 GB 的 shared_buffers。计算结果如下:

Buffers = shared_buffers / 8 KB = 700 GB / 8 KB = 91,750,400 个缓冲区槽位
palloc 请求大小 = 91,750,400 x 20 字节 = 1,835,008,000 字节 (~1.75 GB)

由于 1.75 GB > MaxAllocSize (1 GB),PostgreSQL 会报错:

ERROR:  invalid memory alloc request size 1835008000
LOG:   background worker "autoprewarm leader" exited with exit code 1

崩溃循环

崩溃循环是这个问题破坏性特别大的原因。当 PostgreSQL 中的一个后台工作进程以非零退出码退出时,postmaster 的设计会使其重启该工作进程。因此,以下序列会以每分钟数百次的频率重复:

  1. Autoprewarm 引导进程启动:在数据库启动或重启后,autoprewarm leader 后台工作进程开始运行。
  2. 尝试分配大数组:该进程计算所需内存(例如,700 GB shared_buffers 需要约 1.75 GB)并调用 palloc()
  3. 分配失败:由于 palloc() 的 1 GB 硬性限制,分配请求被拒绝。
  4. 工作进程退出:后台工作进程因错误而退出。
  5. Postmaster 重启工作进程:Postmaster 检测到非零退出码,并在短暂的延迟后重新启动该工作进程。
  6. 重复步骤 2-5:循环立即再次开始。

数据库本身是正常的。数据完好无损,但是一个与服务查询无关的单个后台工作进程却在疯狂地 thrashing(频繁分配失败并重启),其资源消耗足以拖垮整个应用程序。

修复方案

该错误在 PostgreSQL 16.8 中被多位用户报告,并且一个修复补丁已被提交到 PostgreSQL 代码库,并随 PostgreSQL 16.10 版本发布。该修复方案是用 palloc_extended 替换了标准的 palloc 调用,后者绕过了 1 GB 的硬性限制,允许对于任何实际可达到的 shared_buffers 值都能成功分配内存。

PostgreSQL 黑客邮件列表上的讨论探讨了几种备选方案,每种方案都有其值得理解的价值:

  • 流式写入:不预先分配整个数组,而是一次将一个条目写入转储文件。其障碍在于结构性的:文件格式将总条目数放在文件开头,而在写入完成之前你无法知道最终的计数。回头修补文件头很麻烦,并且缓冲区内容在写入期间可能会并发更改。
  • 使用 blkreftable:Robert Haas 建议重用 common/blkreftable.h,这是一个最初为增量备份构建的数据结构。对于密集的缓冲区集,它会收敛到每个块使用 1 位而不是 20 字节,这是一个巨大的改进。这是正确的长期方向,但其架构上的改动太大,无法安全地回溯到已发布的次要版本中。
  • 限制在 1 GB:分配 palloc 允许的最大值,并预热任何能装下的部分。但正如正确指出的那样:预热恰恰在那些会达到此限制的大内存系统上最为重要,因此限制大小就违背了其目的。

选择 palloc_extended 是因为它改动最小、正确,并且可以安全地回溯到所有受影响的次要版本,而这正是次要版本发布中的修复所需要具备的特性。

立即缓解措施

如果你正在运行一个受影响的版本并且无法立即升级,可以禁用自动预热工作进程。

第 1 步 – 停止崩溃循环

ALTER SYSTEM SET pg_prewarm.autoprewarm = off;
SELECT pg_reload_conf();
SHOW pg_prewarm.autoprewarm;  -- 验证:应返回 'off'

第 2 步 – 完全移除扩展(可选)

DROP EXTENSION pg_prewarm;
-- 验证移除
SELECT * FROM pg_extension WHERE extname = 'pg_prewarm';

第 3 步 – 从 shared_preload_libraries 中移除 pg_prewarm

为了防止它在下次重启时加载,请从 postgresql.conf 文件中的 shared_preload_libraries 里移除 pg_prewarm。对 shared_preload_libraries 的更改需要完全重启 PostgreSQL 才能生效。之后,验证没有残留的预 warm 进程处于活动状态:

SELECT pid, usename, application_name, backend_type
FROM pg_stat_activity
WHERE application_name ILIKE '%prewarm%';

应用此缓解措施后,崩溃循环将停止,CPU 使用率将恢复正常。但这会失去重启时的缓存预热功能,与完全不可用相比,这是一个可接受的运维权衡。

永久解决方案:版本升级

唯一永久的修复方法是升级到 PostgreSQL 16.10 或更高版本。由于这是一个次要版本升级,因此不需要进行数据迁移。它可以作为就地二进制升级来执行。

对于容器化部署,只需更新 Helm chart 中的 PostgreSQL 镜像标签,并对 Pod 执行滚动更新。请务必先在较低版本的环境中验证更改,然后在维护窗口期间安排生产环境的部署。升级后,如果需要,可以安全地重新启用 pg_prewarm

总结

PostgreSQL 次要版本滞后是一种可以避免的运维风险。正如本次事件所见,运行一个过时的版本(16.8)意味着会遇到一个在上游 16.10 版本中已经被识别并修复的错误。次要版本是严格向后兼容的,并且只包含错误修复,因此几乎没有理由推迟升级它们。环境应该保持在其主版本内的最新或接近最新的次要版本,以避免将已知缺陷带入生产环境。

参考文献

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐