PostgreSQL Field Guide

容器环境下 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_bytesmemory.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 v2cgroup 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 按进程调整(范围 -10001000-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.memorylimits.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_control

memory.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_bufferswork_memmaintenance_work_memmax_connections 从 cgroup limit 推导而非宿主机内存——limit 变更后重新推导。
  • 数据库前面放连接池,让 max_connections × work_mem 有界。
  • log_temp_files = 0(或阈值),并对 temp_bytes 增长告警。
  • memory.eventsoom_kill 增量和 max 计数增长告警。
  • 容量决策永远不信容器里的 freetophtop

不要只靠调大 limit 来修 OOM

不重新推导 postgresql.conf 就调大 limit,只是把崩溃往后挪。要么是配置对 limit 来说太大,要么是某个查询级消费者(work_mem 尖峰、连接潮)没有上界——先搞清楚是哪一种,再花钱买内存。

对照容器 limit 检查我的内存预算
我的 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.currentmemory.events,确认后再动生产。

相关页面

Last updated on

On this page