Postgres 跑在 Kubernetes 上,别再只盯着 Working Set

2026-08-19 43 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

在 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_fileinactive_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 提供的自定义查询能力。

告警和资源配置该怎么改

监控侧至少应同时保留四类信息:

  1. memory.current / memory.max:cgroup 的总账和硬上限。
  2. anon + shmem:更接近 PostgreSQL 不易回收的核心占用。
  3. active_file + inactive_file:解释 working set 为什么升高,也能看出文件页缓存规模。
  4. 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 的 anonshmemactive_fileinactive_filememory.currentmemory.max
  • anon + shmem 以及“扣除全部文件 LRU 后的内存”建立图表和告警。
  • 在目标内核、真实并发和实际数据规模下测试 shared_bufferswork_mem
  • 将 OOM、Pod 重启、驱逐和 PSI 与内存曲线放在同一时间轴上分析。
  • 评估 MGLRU、swap 和 Kubernetes 节点配置,避免把某台测试机的表现直接外推到生产集群。

核心判断很简单:PostgreSQL 的共享内存和查询匿名内存决定了硬边界,Linux 文件页缓存更多是在解释性能与可回收空间。把这两类内存混成一条 working set 曲线,容易在最需要准确判断时给出错误方向。


相关推荐