在 Kubernetes 中,container_memory_working_set_bytes 经常被当作容器真实内存占用的代表,并参与内存压力判断、驱逐和告警。但对 PostgreSQL 来说,这个指标可能严重失真:数值更高的实例未必更接近 OOM,数值看起来平稳的实例也可能在下一次大排序时直接崩溃重启。
问题不只是监控面板画错了一条线,而是 PostgreSQL 的匿名内存、共享缓冲区和 Linux 文件页缓存具有完全不同的回收属性。要判断数据库距离内存上限还有多远,必须把它们分开看。
Working Set 到底计算了什么
在 cgroups v2 环境中,Kubernetes 常见的 working set 计算思路可以概括为:
container_memory_working_set_bytes = memory.current - memory.stat[inactive_file]
它从当前 cgroup 内存用量中减去 inactive_file,也就是 Linux 文件页缓存中位于 inactive LRU 的部分。这个设计隐含了一个判断:inactive 文件页较容易回收,因此不应全部算作不可释放的工作内存。
对只持续分配匿名内存的普通压力程序,这个指标通常表现得不错。匿名内存不断增长,memory.current 和 working set 一起逼近 memory.max,随后 cgroup 发生 OOM。
PostgreSQL 的内存构成却更复杂:
anon:排序、哈希、连接进程以及其他查询执行内存通常会反映在这里。shmem:PostgreSQL 的shared_buffers由内核记作共享内存。这些页面一旦实际分配,不能像普通文件页缓存那样被内核直接回收。active_file与inactive_file:数据库读写产生的 Linux 文件页缓存。即使页面当前位于 active LRU,其中相当一部分仍可能在内存压力下被回收。
因此,只减去 inactive_file 会把 active_file 留在 working set 中。对于 I/O 活跃的数据库,这可能把大量可回收页缓存误算为高风险内存。
PostgreSQL 为什么会让指标失去方向感
一个限制为 512 MiB 的 PostgreSQL 容器,可以通过不断增大内存排序规模来观察 OOM 边界:
SET work_mem = '1GB';
SET max_parallel_workers_per_gather = 0;
SELECT count(*)
FROM (
SELECT md5(n::text) AS sort_key
FROM generate_series(1, 3500000) AS input(n)
ORDER BY sort_key
) AS sorted_values;
这里把 work_mem 故意调得很高,只是为了复现实验。生产环境不要照搬:work_mem 并不是整个实例的统一额度,它可能被多个连接、多个排序或哈希节点同时使用。
当数据库还没有真实负载时,大排序主要推高 anon,working set 与 OOM 风险大体同步。加入 shared_buffers 后,可用空间会进一步减少。需要特别注意的是,配置了 shared_buffers=128MB 并不代表启动瞬间就一定驻留 128 MB;相关共享内存页通常随着访问逐步分配。
可以用 pg_prewarm 主动把一张表装入共享缓冲区:
CREATE TABLE shared_buffer_filler AS
SELECT n, repeat(md5(n::text), 8) AS payload
FROM generate_series(1, 440000) AS input(n);
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT pg_prewarm('shared_buffer_filler'::regclass, 'buffer');
共享缓冲区实际占用增大后,同样的排序会更早碰到容器上限。与文件页缓存不同,这部分 shmem 不能指望内核在压力出现时自动回收。
更容易误判的是实际数据库负载。表扫描会建立较大的 Linux 文件页缓存,其中一些页面进入 active_file。Kubernetes working set 没有扣除这些 active 文件页,于是监控值可能很高;但这些页面仍然比 anon 和 PostgreSQL shmem 更容易回收。
这会产生一个反直觉结果:缩小 shared_buffers 后,数据库真正可用于查询排序的余量增加了,working set 却可能因为更多数据经过 Linux 页缓存而变高。此时只看 working set,会把更安全的配置判断成内存压力更大。
建立一个更贴近 OOM 风险的指标
针对 PostgreSQL,一个实用的利用率指标是从 memory.current 中同时排除 active 和 inactive 文件页:
postgres_non_file_memory_bytes =
memory.current - active_file - inactive_file
它不是“PostgreSQL 精确使用了多少字节”的完整会计口径,但更接近当前无法通过回收普通文件页缓存快速释放的内存。判断 OOM 风险时,还应直接观察:
anon + shmem
前者适合形成通用时间序列,后者能更清楚地解释 PostgreSQL 查询内存和共享缓冲区如何逼近上限。两者都不应脱离 memory.max 单独展示。
下面的脚本可在使用 cgroups v2 的容器内或节点对应 cgroup 目录中运行。默认读取当前进程所在的 cgroup;如需检查特定 Pod,应把 CGROUP_DIR 改成对应目录。
#!/usr/bin/env bash
set -euo pipefail
CGROUP_DIR="${CGROUP_DIR:-/sys/fs/cgroup}"
CURRENT=$(<"$CGROUP_DIR/memory.current")
MAX=$(<"$CGROUP_DIR/memory.max")
read_stat() {
awk -v key="$1" '$1 == key { print $2; found=1 } END { if (!found) print 0 }' \
"$CGROUP_DIR/memory.stat"
}
ANON=$(read_stat anon)
SHMEM=$(read_stat shmem)
ACTIVE_FILE=$(read_stat active_file)
INACTIVE_FILE=$(read_stat inactive_file)
WORKING_SET=$((CURRENT - INACTIVE_FILE))
NON_FILE=$((CURRENT - ACTIVE_FILE - INACTIVE_FILE))
printf 'memory.current: %12d bytes\n' "$CURRENT"
printf 'memory.max: %12s\n' "$MAX"
printf 'k8s-style working set: %12d bytes\n' "$WORKING_SET"
printf 'current minus all file LRUs: %12d bytes\n' "$NON_FILE"
printf 'anon + shmem: %12d bytes\n' "$((ANON + SHMEM))"
printf 'active_file: %12d bytes\n' "$ACTIVE_FILE"
printf 'inactive_file: %12d bytes\n' "$INACTIVE_FILE"
运行方式:
chmod +x ./postgres-cgroup-memory.sh
./postgres-cgroup-memory.sh
如果是从节点侧排查,可以先找到容器对应的 cgroup 路径,再传入目录:
CGROUP_DIR=/sys/fs/cgroup/path/to/pod-or-container \
./postgres-cgroup-memory.sh
生产监控中,可以由节点 exporter、自定义采集器,或 PostgreSQL 监控扩展读取同样的 cgroup 字段,再导出以下序列:
recommended_series:
- name: postgres_cgroup_non_file_memory_bytes
expression: memory.current - memory.stat.active_file - memory.stat.inactive_file
- name: postgres_cgroup_anon_shmem_bytes
expression: memory.stat.anon + memory.stat.shmem
- name: postgres_cgroup_memory_limit_bytes
expression: memory.max
这段 YAML 是监控模型示意,不是可直接提交给 Kubernetes 的资源对象;具体接入方式取决于 Prometheus exporter 或 PostgreSQL operator 提供的自定义查询能力。
告警和资源配置该怎么改
监控侧至少应同时保留四类信息:
memory.current / memory.max:cgroup 的总账和硬上限。anon + shmem:更接近 PostgreSQL 不易回收的核心占用。active_file + inactive_file:解释 working set 为什么升高,也能看出文件页缓存规模。- OOM、重启次数和 PSI:分别确认事故结果与内存压力过程。
资源配置也要考虑数据库的特殊性。不要简单地让 PostgreSQL 的 memory request 明显低于 limit,再期待 Kubernetes 在节点压力下温和处理。Pod 被驱逐或触发 cgroup OOM,对 PostgreSQL 都意味着整个数据库进程退出、恢复并重新预热缓存,而不是一次无感的内存回收。
shared_buffers 也不应机械套用裸机上的经验比例。容器硬上限较小时,它与并发查询内存、后台进程、连接开销和内核页缓存争夺同一份额度。应通过真实并发压测确定余量,并给 work_mem 留出乘数效应空间。
还要明确这个简化指标的边界。MGLRU 会改变文件页在 active、inactive 世代中的表现;启用 swap 后,内存回收与 OOM 时机也会变化;PSI 描述的是压力和阻塞,不等同于利用率。因此,memory.current - active_file - inactive_file 很有价值,但不能替代负载测试、OOM 事件监控和数据库级指标。
落地检查清单
- 不把
container_memory_working_set_bytes当作 PostgreSQL 唯一的 OOM 预测指标。 - 采集 cgroups v2 的
anon、shmem、active_file、inactive_file、memory.current和memory.max。 - 为
anon + shmem以及“扣除全部文件 LRU 后的内存”建立图表和告警。 - 在目标内核、真实并发和实际数据规模下测试
shared_buffers与work_mem。 - 将 OOM、Pod 重启、驱逐和 PSI 与内存曲线放在同一时间轴上分析。
- 评估 MGLRU、swap 和 Kubernetes 节点配置,避免把某台测试机的表现直接外推到生产集群。
核心判断很简单:PostgreSQL 的共享内存和查询匿名内存决定了硬边界,Linux 文件页缓存更多是在解释性能与可回收空间。把这两类内存混成一条 working set 曲线,容易在最需要准确判断时给出错误方向。