PG太垃圾!2TB内存也OOM,你的生产库有这颗定时炸弹吗?

digoal 2026-05-17 09:49:00

你信吗? 2TB内存数据库发生OOM?

 

你以为把 work_mem 从 4MB 调到 64MB,是在“优化排序和哈希”?

 

很多时候,你不是在提速。

 

你是在把一颗延时爆炸的内存雷埋进生产库。更扎心的是:

 

就算你把 work_mem 设成 2MB,也不代表 PostgreSQL 一条查询最多只会吃 2MB。

 

一、先说结论:work_mem 最大的坑,不是你不会调,而是你以为自己懂它

 

PostgreSQL官方文档写得很清楚:work_mem 规定的是单个查询操作在写临时文件前可使用的基础内存上限,典型对象包括 sort 和 hash table;复杂查询里可能同时存在多个 sort/hash,多个会话还能并发执行, 总内存消耗可能是 work_mem 的很多倍。更要命的是,哈希类操作的上限还会再乘以 hash_mem_multiplier,默认就是 2.0。并行查询时,每个 worker 还会各自独立吃这份资源。官方甚至直接提醒:一个 4-worker 的并行查询,资源占用可能接近无并行时的 5 倍

 

翻译成人话就是:

 

work_mem 从来不是“每条 SQL 的内存上限”,更不是“这台机器的安全阈值”。

 

它只是执行计划里某个内存操作节点的预算起点。

 

所以,很多团队一上来就问:“我们 256GB 内存,work_mem 设 64MB 保守吗?”

 

这问题从第一性原理上就问错了。

 

你不先问并发数、执行计划里有多少内存节点、是否启用并行、哈希是否放大、是否有错误估算,单独讨论 work_mem 数字,基本等于闭眼开车。

 

二、为什么 work_mem 明明不大,库还是会被打爆?

 

这是最容易被误解的地方。

 

很多人知道一个常识:work_mem 不是“每查询”,而是“每操作”。这没错。

 

但只知道这一层,还不够。

 

最近一篇 PostgreSQL 实战文章里记录了一个非常典型的事故:生产集群曾被 OOM killer 干掉,机器吃掉了 2TB RAM;而 work_mem 当时只有 2MB。作者复盘时,在复现环境里通过 pg_log_backend_memory_contexts 抓到了后端内存上下文:单个 backend 的日志里,ExecutorState 约 235MB,HashTableContext 约 340MB,总计约 557MB,其中 ExecutorState 还出现了 524,059 chunks

 

这件事说明什么?

 

说明真正的危险,不只是“有很多 sort/hash 节点”,而是:这些内存未必会在过程中及时归还。

 

PostgreSQL 的内存管理核心机制是 memory context。它设计的优势就是:不按对象逐个 free,而是按上下文整块释放。

 

官方/社区对这一机制的解释非常一致:memory context 的主要优势,是能“一次性释放整个上下文里的内容”,这样更快,也更可靠。

 

问题也正出在这里。

 

如果某些分配挂在一个生命周期很长的上下文上,比如 ExecutorState,那它们就可能一路攒着,直到整个执行结束才释放。如果查询本身很大、执行很久、又因为函数封装或糟糕的 SQL 结构把本来可以切开的步骤揉成了“一锅大杂烩”,内存就会像滚雪球一样涨。

 

所以你看到的现象是:

 

  • 配置上 work_mem 并不夸张

  • 单看某个节点预算也不离谱

  • 但执行期间,多个节点、多个 worker、多个分配上下文叠加

  • 再加上长生命周期上下文延迟释放

  • 最后,操作系统出来“物理教你做人”

 

这不是 PostgreSQL “不遵守 work_mem”。而是你把 work_mem 当成了硬上限,而它从来都不是。

 

三、work_mem 真正控制的,是“是否开始落盘”;它控制不了“整条执行链最终占多少内存”

 

官方表述其实已经给出答案:work_mem 是“在写临时磁盘文件之前”的内存预算。

 

注意这句话的关键词:

 

before writing to temporary disk files

 

也就是说,它更像是一个节点级的内存-磁盘切换阈值

 

它能影响 sort / hash 是更愿意在内存里做,还是更早 spill 到磁盘。

 

但它并不能替代下面这些问题:

 

  • 执行计划是不是错了?

  • 行数估算是不是低得离谱?

  • 一个查询里是不是同时堆了太多 hash/sort/memoize?

  • 并行是不是把放大器打开了?

  • SQL 是否把多个阶段硬捏成了一个长生命周期执行体?

  • 函数、CTE、子查询、JOIN 方式是否让释放时机变晚?

 

所以, 把 work_mem 调大,常常不是解决问题,而是在隐藏问题。

 

在小并发、强控制、偏分析型 workload 里,调大 work_mem 当然可能有收益;PostgreSQL 官方也明确说了,在 hash 经常 spill,而提高 work_mem 又带来内存压力时,可以考虑用更高的 hash_mem_multiplier,甚至在某些环境中提升到 2.0–8.0 以上。

 

但这里有一个不可跳过的前提:

 

  • 前提一:并发是可控的

  • 前提二:查询形态是可控的

  • 前提三:执行计划足够稳定

 

如果这三个前提不成立,调大 work_mem 往往不是优化,而是把单点慢 SQL 升级成整库级故障。

 

四、真正该背锅的,常常不是参数,而是 SQL 结构和统计信息

 

这才是 DBA 和开发最应该正视的现实。

 

原文里的事故,本质上不是“参数设错了”,而是查询写法和执行生命周期设计出了问题。复盘中提到:一个 SELECT 调用了 plpgsql 函数,函数内部还做了 COPY 相关操作,再把结果拿去 JOIN。语法上合法,但从执行器角度看,这可能把原本应该切分、释放、落盘的东西,拼成了一个超长生命周期的大操作。

 

这背后有两个基本规律:

 

 
1、错误估算会让执行器做出危险决策

 

如果统计信息失真,Planner 低估行数,就更容易选出本该 spill 却没及时 spill、或者本不该 hash 却选了 hash 的计划。PostgreSQL 官方也明确建议,复杂查询可能同时进行多个 sort/hash;而是否会落盘、落到什么时候,本来就强依赖计划和执行路径。

 

 
2、“像写面向对象代码一样封装 SQL”,经常会把数据库拖进深坑

 

很多开发喜欢把逻辑包进函数、子查询、CTE、视图,然后再层层 JOIN,觉得“代码优雅了”。

 

但数据库不是应用层对象系统。

 

数据库优化器看的是数据流、算子、生命周期、代价模型,不是“抽象是否漂亮”。

 

对 PostgreSQL 来说, 好维护的代码结构,不一定等于好执行的 SQL 结构

 

五、DBA 最该告诉业务方的一句话:你不能给 PostgreSQL 设“每个 backend 的硬内存上限”

 

这是很多团队直到出事故都没真正接受的事实。

 

在这类问题上,很多人本能会问:“那有没有一个参数,限制单连接最多吃多少内存?”

 

很遗憾,没有。

 

至少没有一个像很多人想象中的、简单直接、可靠的“per-backend hard cap”配置。原文对此说得很直白: 你不能对 PostgreSQL 每个 backend 设一个硬性内存封顶。

 

你能做的,是一整套“风险治理”组合拳,而不是迷信一个参数:

 

  • 控制并发

  • 修正统计信息

  • 改写 SQL

  • 限制并行

  • 设置超时

  • 观察实际内存上下文

  • 把大查询和高峰流量隔离

 

这才是工程化思路。不是“把 work_mem 调小一点,求神保佑”。

 

六、遇到可疑内存暴涨,别猜,直接看证据

 

PostgreSQL 已经给了你非常好用的工具:pg_log_backend_memory_contexts(pid)。

 

官方文档说明,它会把指定 backend 的 memory contexts 以 LOG 级别打进日志;而且要注意,这个函数可能带来明显开销,因为它会生成大量日志。

 

这类工具为什么重要?

 

因为过去很多团队排查内存问题,方法都非常原始:

 

  • 看操作系统 RSS

  • 看 pg_stat_activity

  • 看慢 SQL

  • 猜是 sort 爆了还是 hash 爆了

 

但真正的关键问题是:

 

  • 内存到底堆在哪个上下文里?

  • 是谁分配的?

  • 为什么没释放?

 

没有 memory context 视角,你看到的只是“高血压”;有了它,你才能找到“是哪根血管堵了”。

 

另外,如果你想看当前会话自己的内存上下文,PostgreSQL 还提供了 pg_backend_memory_contexts 视图;默认只有 superuser 或具备 pg_read_all_stats 权限的角色可读。

 

七、那到底该怎么管 work_mem?给 DBA 和开发两套答案

 

 
1、给 DBA / 架构师的答案

 

1)不要全局盲调大 work_mem

 

全局大 work_mem,本质是在给所有会话、所有复杂查询、所有并行 worker批量发放内存信用卡。官方文档已经明确说明,总体消耗可能是很多倍。

 

2)优先做分层配置,而不是“一刀切”

 

把高并发 OLTP、低并发分析、批处理、报表查询分开。能按 role、database、session、事务级设置的,就不要全局放开。

 

3)先压风险,再谈提速

 

高峰业务链路上,先配 statement_timeout,因为它能在语句执行时间过长时直接中止;官方还明确提示,不建议在 postgresql.conf 里对所有 session 粗暴统一设置。

 

4)把统计信息治理当成容量治理的一部分

 

不是只盯 CPU、IO、shared buffers。行数估算偏差,本质上就是容量预算偏差。

 

5)对并行保持敬畏

 

并行会加速,也会放大。官方直接说了:资源限制是按 worker 单独生效的,4 worker 的并行查询可能吃出接近 5 倍资源。

 

 
2、给应用开发者 / 数据库用户的答案

 

1)别把数据库函数、CTE、子查询当成“代码封装美学”工具乱嵌套

 

你看到的是“复用”;优化器看到的可能是“更长的执行生命周期”和“更差的释放时机”。

 

2)能分阶段物化的,就别硬拼成一个超级 SQL

 

有些中间结果,就该落临时表、物化结果集或拆分步骤。数据库最怕的是“逻辑优雅、执行灾难”。

 

3)你以为的“小查询”,在执行器眼里可能是一条大工程流水线

 

一个 SELECT 里套函数、函数里再跑复杂逻辑,不代表数据库会把它当“小事”。

 

4)SQL 性能问题,最终都不是字符串问题,而是资源调度问题

 

内存、磁盘、CPU、锁、并行度、统计信息、数据分布,缺一不可。

 

八、什么时候“调大 work_mem”是对的?

 

也不是说 work_mem 永远不能调大。这篇文章最容易被误读成“work_mem 一调就死”,这也不对。

 

从第一性原理出发,下面这些前提成立时,提升 work_mem 往往是合理的:

 

  • 并发数低且稳定

  • 查询主要是分析型,排序/聚合/哈希代价高

  • 有足够内存余量

  • 并行度可控

  • 执行计划稳定

  • 已确认瓶颈真的是 spill 到临时文件,而不是估算失真或 SQL 结构错误

 

如果这些前提成立,调大 work_mem 是优化。

如果这些前提不成立,调大 work_mem 是赌博。

 

一字之差,结果天差地别。

 

九、最后一句狠话:没有任何硬件,能拯救一条设计错误的查询

 

原文里最值得反复咀嚼的一句判断是:

 

不是硬件不够大,而是查询写法错了。

 

哪怕你有 2TB RAM,照样可能被一条“看起来没什么”的 SQL 拖垮。

 

所以真正成熟的 PostgreSQL 团队,不会把 work_mem 当成万能旋钮。

 

他们会把它看成:

 

  • 一个执行器节点预算参数

  • 一个磁盘/内存切换阈值

  • 一个会被并发、并行、哈希放大、生命周期拖长的风险源

 

说白了:

 

work_mem 不是性能开关。

它是资源杠杆。

杠杆用对了,省力;用错了,砸库。

 

你们线上出过和 work_mem、临时文件、Hash Join、并行查询有关的事故吗?

 

你更认同“参数先行”,还是“SQL 与统计信息优先治理”?

 

作者丨digoal
来源丨公众号:digoal德哥(ID:gh_eb6d10476af2
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

活动推荐

5月22日,2026 XCOPS 智能运维管理人年会「广州站」重磅来袭!聚焦大模型迭代、AI Agent 深度应用等技术热点,邀请一众行业领军人物、技术大咖,从技术架构、实战案例到科研成果,与大家一起探索AI应用于智能运维与数据库的最佳方式,共同破解垂类智能体落地、多Agent协同、数据库自治技术工程化、核心系统信创与智能化平衡等现实难题。扫描下方二维码可了解大会详情及报名↓
 

最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告