容器环境下 PostgreSQL 的内存管理与 OOM 防治
让 shared_buffers、work_mem 和连接数装进 cgroup 内存 limit,避免 OOM killer 杀掉 postmaster
典型的容器 OOM 现场是这样的:PostgreSQL 每隔几小时重启一次,dmesg 里有 Out of memory: Killed process ... (postgres),而团队坚持 pod"内存很充足",因为容器里的 free -m 明明显示还有几个 GB 空闲。两个观察都是真的,也不矛盾:free 读的是 /proc/meminfo,它没有按容器命名空间隔离——容器里看到的是宿主机的内存,而不是内核真正执行的 cgroup limit。真正会杀掉你的那个 limit 在 cgroup 里,不在 /proc/meminfo 里。
真正的 limit 在哪里读
cgroup v2 主机上,生效的 limit 在 memory.max(当前记账在 memory.current);cgroup v1 上是 memory.limit_in_bytes 和 memory.usage_in_bytes:
# cgroup v2(Kubernetes 1.31+ 及多数现代发行版)
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
# cgroup v1
cat /sys/fs/cgroup/memory/memory.limit_in_bytes完整文件列表见内核文档 cgroup v2 和 cgroup v1 内存控制器。任何在容器里从 /proc/meminfo 推导"可用内存"的工具或运维手册,量的都是错误的盒子。
必须装进 limit 的内存预算
PostgreSQL 不会读 cgroup limit,它只按 postgresql.conf 给自己定尺寸,所以配置必须由容器 limit 推导,而不是由宿主机内存推导。必须装进去的部分(见 PostgreSQL 18 文档的 Resource Consumption):
shared_buffers——启动时固定分配;work_mem× 并发排序/哈希——它是每个操作而不是每个会话:一条带多个 hash join 的查询可以吃掉数倍work_mem,所以max_connections × work_mem只是粗略上限;maintenance_work_mem(或autovacuum_work_mem)× 并发的 vacuum worker 和维护命令;- 每个后端的
temp_buffers、WAL buffer,以及每个后端进程数 MB 的连接固定开销; - 再加上容器里其他东西(sidecar、监控 agent)以及 PostgreSQL 依赖的 page cache。
起步值而非铁律:专用 PostgreSQL 容器里 shared_buffers 取 cgroup limit 的 25% 左右,给连接和 page cache 留出空间,之后凭测量再上调。完整的配置清单见 服务器配置。
PG 认识的内存 vs cgroup 的记账
大多数"我们离 limit 还远着呢"的意外,来自两处记账差异:
- page cache 计入 cgroup。 对堆文件和 WAL 文件的读写由内核缓冲,页面记到第一个触碰它的 cgroup 名下。一个进程匿名内存很少的容器,也可能因为其余部分都是 page cache 而顶在 limit 上——这本身正常且大多可回收,直到匿名内存突发分配(
work_mem尖峰、连接潮)到来,而缓存此时是脏的或正被引用。 - RSS 随触碰过的页面增长。 后端进程共享
shared_buffers,把各进程 RSS 加总的工具会把共享页重复计算。不要用ps输出的总和来定 limit,要用峰值负载下的memory.current。
内核只在分配发生的瞬间没有可回收内存时才杀人,这就是为什么 OOM Kill 集中在负载尖峰而不是稳态。
OOM killer 打分与 oom_score_adj
当 cgroup(或宿主机)触顶时,内核按 oom_score 挑选牺牲者,可以通过 /proc/<pid>/oom_score_adj 按进程调整(范围 -1000 到 1000;-1000 完全豁免——见 proc 文件系统文档)。运维上有两个事实必须知道:
- 给 postmaster 设
-1000是标准做法,但oom_score_adj会被子进程继承:之后 fork 出来的每个后端同样不可被杀,这恰恰和你想要的相反。如果保护了 postmaster,后端必须重置自己的分值——例如在 wrapper 脚本里重新设置,或者接受在 Kubernetes 上这一层由 pod 粒度控制。 - Kubernetes 不支持按容器设置
oom_score_adj;kubelet 根据 pod 的 QoS 等级赋值:Guaranteed pod 得-997,BestEffort pod 得1000,Burstable 落在中间。这写在 node-pressure eviction 文档里。一个 pod 只放一个数据库实例,这层保护才有意义。
关于 vm.overcommit_memory = 2 的取舍要单独权衡:PostgreSQL 文档的 Linux Memory Overcommit 一节解释了这笔交易——把它调到 2 可以降低 OOM killer 被触发的概率,代价是内存分配会更早直接失败。
K8s 的 requests、limits 与 QoS
对数据库 pod,来自 Kubernetes 内存资源文档和 Pod QoS 等级的可辩护模式:
requests.memory与limits.memory设为相等,让 pod 成为 Guaranteed:节点压力下它会排在 Burstable/BestEffort 之后才被驱逐,并且拿到有利的oom_score_adj。postgresql.conf从 limit 推导(见上一节),不从节点规格推导——pod 会被重新调度到更大的节点上,环境会悄悄变化。- limit 里要给 page cache 和尖峰留余量:把
shared_buffers + 连接数配到接近 limit 的 100%,等于保证下一次work_mem突发是致命的。
诊断一次 OOM Kill
从内核证据入手,一路向内查到查询:
# 内核侧的 kill 记录(宿主机上,或通过节点日志)
dmesg -T | grep -i -E 'out of memory|oom_kill'
journalctl -k | grep -i oom
# cgroup v2:该 cgroup 每发生一次 kill,oom_kill 计数加一
cat /sys/fs/cgroup/memory.events
# low 0 / high 0 / max <n> / oom <n> / oom_kill <n>
# cgroup v1 等价文件
cat /sys/fs/cgroup/memory/memory.oom_controlmemory.events 里的 max 事件(触顶但靠回收挺过去、没有 kill)同样值得看——max 持续上涨而 oom_kill 为零,说明你已经长期贴在天花板上,被杀只是时间问题。
PostgreSQL 一侧,找 kill 发生前的内存消耗者。临时文件用量是 work_mem 溢出的经典签名;它按 database 统计,不按会话:
SELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp_written
FROM pg_stat_database
ORDER BY temp_bytes DESC;temp_bytes 是累计值,要比较事故窗口前后的增量。归因到具体语句要靠日志:设 log_temp_files = 0(或一个阈值),让每次落盘临时文件都记下肇事查询,再与 kill 时刻 pg_stat_activity 里的活跃会话对照。完整的监控管线见 监控与日志。
防治清单
- 一个 pod 一个 postmaster;Guaranteed QoS(
requests == limits)。 shared_buffers、work_mem、maintenance_work_mem、max_connections从 cgroup limit 推导而非宿主机内存——limit 变更后重新推导。- 数据库前面放连接池,让
max_connections × work_mem有界。 log_temp_files = 0(或阈值),并对temp_bytes增长告警。- 对
memory.events的oom_kill增量和max计数增长告警。 - 容量决策永远不信容器里的
free、top、htop。
不要只靠调大 limit 来修 OOM
不重新推导 postgresql.conf 就调大 limit,只是把崩溃往后挪。要么是配置对 limit 来说太大,要么是某个查询级消费者(work_mem 尖峰、连接潮)没有上界——先搞清楚是哪一种,再花钱买内存。
我的 PostgreSQL 18 跑在 Kubernetes pod 里,memory limit 为 <LIMIT_GB> GiB。 当前配置: - shared_buffers = <值> - work_mem = <值> - maintenance_work_mem = <值> - max_connections = <值> - autovacuum_max_workers = <值> 1. 估算最坏情况下的内存消耗,并对照 cgroup limit 检查。 2. 给出从 limit 推导出的修正值,逐个说明理由。 3. 给出验证当前真实用量的 kubectl / cgroup 命令(memory.current、memory.events)。 4. 列出能定位哪些会话制造临时文件和内存压力的 pg_stat_database / pg_stat_activity 查询。
把输出当作假设:先在测试 pod 应用,在峰值负载下观察 memory.current 和 memory.events,确认后再动生产。
相关页面
Last updated on