Kubernetes 内存指标不是同一个数字:从 cgroup v2 到 OOM 与 Eviction
一个 Pod 的内存使用量,可能同时出现在 kubectl top pod、Prometheus、进程的 RSS 和容器的 cgroup 文件里。它们经常不是同一个数字,但这不一定表示监控或应用有问题。
问题在于「内存使用量」没有脱离测量目的的唯一答案:进程 RSS 关心进程当前驻留的页面,cgroup 关心一组进程及其后代的资源账本,kubelet 还要用另一个口径判断节点是否应该驱逐 Pod。把这些数字直接比较,容易得到错误结论,例如「Working Set 还没到 Limit,为什么已经 OOMKilled」或「MemAvailable 很高,为什么 Pod 还是被 Evict」。
本文以 Linux cgroup v2 为前提,整理这些数字之间的关系,并给出一条排查 OOM 和 Eviction 的路径。文章中的结论主要针对 kubelet 使用 cgroup v2、containerd 作为容器运行时的集群;cgroup v1、CRI-O 或云厂商定制的监控链路需要重新核对实现。
先记住四个结论
| 你想回答的问题 | 优先看的值 | 原因 |
|---|---|---|
| Pod 当前看起来用了多少内存 | container_memory_working_set_bytes | kubectl top pod 看到的也是 Working Set |
| 容器是否触发了自己的内存上限 | memory.current 对比 memory.max | cgroup 的硬限制不直接比较 Working Set |
| 节点是否正在逼近 Eviction | kubelet 的 memory.available | 它基于节点 Working Set,而不是 MemAvailable |
| Request 是否合理 | 一段时间内的 Working Set 峰值和分位数 | Request 影响调度、驱逐优先级和节点 OOM 时的保护程度 |
因此,排查时不要先问「哪个指标才是真的」,而应该先问「这次故障需要回答哪个问题」。
1. cgroup v2 到底记录了什么
Kubernetes 的容器资源限制最终要落到 Linux cgroup。cgroup v2 中,容器所在 cgroup 通常可以看到这些文件:
| |
memory.current 不是应用堆大小,也不是某一个进程的 RSS。Linux 会把匿名页、文件页缓存、tmpfs/共享内存,以及部分内核内存都纳入 cgroup 账本。memory.stat 中最常用的字段是:
| 字段 | 含义 | 排障时的关注点 |
|---|---|---|
anon | 匿名页,例如堆、栈 | 通常更接近应用真正不能被内核直接回收的内存 |
file | 文件页缓存,也包含 shmem | 文件读写、日志和内存卷可能让它增长 |
shmem | tmpfs、共享内存等 | 不要把它简单当成可回收的磁盘缓存 |
active_file | 最近仍活跃的文件页 | Working Set 会保留它 |
inactive_file | 不活跃的文件页 | Working Set 通常从 memory.current 中减去它 |
slab、socket 等 | 内核数据结构和网络缓冲 | 解释 anon + file 与 memory.current 的差额 |
file 与 active_file + inactive_file 不一定相等。shmem 会计入 file,但不一定出现在 file LRU 中;emptyDir 使用 medium: Memory 时,本质上也是 tmpfs,应该作为容器内存的一部分管理。
cgroup v2 不只有一个上限
memory.current 和 memory.max 解释了「用了多少」和「最多允许多少」,但 cgroup v2 还提供了内存保护和软限制:
| 文件 | 类型 | 超过或低于边界时的行为 |
|---|---|---|
memory.min | 强保护 | 在有效保护范围内的内存不会被回收;系统无法满足保护时可能触发 OOM |
memory.low | 弱保护 | 尽量避免回收,系统压力极高时仍然可以回收 |
memory.high | 软限制 | 触发更强的回收压力和节流,本身不等于 OOM Kill |
memory.max | 硬限制 | 先尝试回收,仍无法满足分配时触发 cgroup OOM |
这四个文件对应两类不同模型:memory.min / memory.low 是保护,memory.high / memory.max 是限制。不要把 memory.high 当成一个较早触发的 memory.max;它更接近「让这个 cgroup 变慢并主动回收」,而不是直接杀进程。
Kubernetes 的 Memory QoS 会把部分 Pod 的 Request 和 Limit 映射到这些 cgroup v2 接口。当前官方文档中,Memory QoS 已经是 Beta,但 kubelet 默认配置不会自动启用所有保护和节流行为:memoryThrottlingFactor 决定是否设置 memory.high,memoryReservationPolicy 决定是否设置 memory.min / memory.low。因此,看到 Pod 写了 Request,并不能仅凭 YAML 判断节点上一定已经出现对应的 cgroup 保护;需要直接检查 kubelet 配置和 cgroup 文件。Kubernetes Pod QoS 官方文档对此有完整说明。
memory.events 中的 high 计数增加,通常说明触发过 memory.high 的处理;max 记录触及 memory.max 的次数;oom 和 oom_kill 则用于进一步判断是否发生了 OOM。它们的语义不同,不能只看到 high 就认定容器已经 OOM。
Working Set 为什么和 RSS 不一样
cAdvisor 暴露的 Working Set 可以近似理解为:
| |
在 Prometheus 中,它通常对应 container_memory_working_set_bytes;Metrics Server 使用 kubelet 的资源指标,kubectl top pod 展示的也是这一类值。
Working Set 仍然包括:
- 匿名页;
- active 的文件页缓存;
- tmpfs 和共享内存;
- cgroup 计费范围内的其他内存。
所以它不是「应用分配的堆」,也不是「马上无法回收的全部内存」。它是 Kubernetes 用来观察资源使用和节点压力的一个工作口径。
container_memory_rss 也不能直接替代进程 RSS。cAdvisor 在 cgroup v2 下通常将它映射到 memory.stat 的 anon,而应用通过 /proc/<pid>/status 看到的 VmRSS 还可能包含文件映射页和共享内存,并且统计单位是单个进程。一个是 cgroup 维度,一个是进程维度,二者不一致是正常的。
2. 三条不同的故障路径
2.1 memcg OOM:自己的 cgroup 撞上了硬上限
当容器、Pod 或上层 cgroup 的 memory.max 不能容纳当前分配时,内核会先尝试回收可回收页面;回收后仍然无法满足分配,才会在对应 cgroup 内触发 OOM Kill。
这条路径的关键点是:
- 比较的是
memory.current和memory.max,不是 Working Set 和container_spec_memory_limit_bytes的简单视觉比较。 memory.current到达上限并不等于立刻 Kill,内核还会尝试回收页面。- 节点整体还有很多空闲内存,也不妨碍某个容器触发自己的 Limit。
memory.events中的oom和oom_kill可以帮助确认 cgroup 内是否发生过这类事件。
因此,看到 OOMKilled 时,先检查 container_memory_usage_bytes(通常对应 memory.current)和 cgroup 的 memory.max,不要只看 kubectl top 的 Working Set。
2.2 global OOM:节点整体没有可用内存
global OOM 发生在节点层面。即使某个容器没有超过自己的 Limit,只要节点整体无法满足新的内存分配,Linux OOM Killer 仍然可能选择该容器中的进程。
选择谁被 Kill 时,kubelet 设置的 oom_score_adj 会参与计算。QoS、Memory Request、进程实际占用量和系统进程优先级都会影响最终结果。这里没有一个「某个容器的 Working Set 达到 X 就一定 Kill」的单一阈值。
这解释了一个常见现象:Pod 的 Working Set 低于 Limit,却出现 OOMKilled。如果容器自己的 cgroup 没有撞上 memory.max,就应该把调查范围扩大到节点内存、内核日志和其他 Pod。
2.3 Eviction:kubelet 主动退 Pod
Eviction 不是 OOM Kill。它是 kubelet 观察到节点压力达到阈值后,主动终止并标记 Pod 失败,以便回收资源。
Linux 节点上的内存信号可以近似写成:
| |
kubelet 会把这个值和 evictionHard 或 evictionSoft 比较。由于节点 Working Set 会保留 active file cache,node_memory_MemAvailable_bytes 看起来还有余量时,kubelet 仍然可能认为节点已接近内存压力。
驱逐哪个 Pod,主要会看:
- Pod 的实际使用量是否超过 Request;
- Pod Priority;
- 使用量相对 Request 的超额程度。
所以 Memory Request 不只是调度字段。它还影响节点能放多少 Pod,以及发生压力后哪些 Pod 更容易被选中。
QoS、Request 和 Limit 不是同一个概念
Pod 的 QoS 类别由资源配置推导出来,不能只看内存这一项:
| QoS | 简化判断 | 内存压力下的含义 |
|---|---|---|
BestEffort | 没有容器设置 CPU/Memory Request 或 Limit | 没有资源请求保护,通常最容易被驱逐 |
Burstable | 至少有一个资源配置,但不满足 Guaranteed 条件 | Request 以下的部分相对更有保障,超过 Request 的部分更容易成为回收对象 |
Guaranteed | 每个容器的 CPU 和 Memory Request、Limit 都设置且相等 | 通常更晚被驱逐,但超过自身 Limit 仍可能 OOM |
上表是传统容器级资源配置的简化规则;如果集群启用了 Pod-level resources,还需要按 Pod 级别的资源字段判断。只把 memory.requests 和 memory.limits 设置成相等,并不能保证 Pod 一定是 Guaranteed;CPU 的 Request 和 Limit 也必须满足条件。QoS 类别影响调度和压力处理,但它不是一个内存使用量指标,也不能替代对 memory.current、Working Set 和节点状态的检查。
还要区分 Capacity 和 Allocatable:
- Scheduler 使用节点的
Allocatable判断 Pod 的 Request 是否能放下; - kubelet 的
memory.available以节点Capacity和 Working Set 计算压力信号; kube-reserved、system-reserved和 Eviction 阈值会减少真正适合工作负载使用的空间。
因此,「Request 总和没有超过 Allocatable」只能说明调度器认为配置可以放置,不代表节点在运行时不会因为 active file、内核内存或系统进程而进入 MemoryPressure。
3. 把常见指标放回同一张表
| 指标 | 常见来源 | cgroup v2 或内核中的对应物 | 适合回答的问题 |
|---|---|---|---|
container_memory_working_set_bytes | cAdvisor、kubelet、Metrics Server | memory.current - inactive_file | Pod 当前的工作集有多大 |
container_memory_usage_bytes | cAdvisor | memory.current | cgroup 计费量、Limit 相关问题 |
container_memory_rss | cAdvisor | memory.stat 的 anon | 匿名页是否持续增长 |
container_memory_cache | cAdvisor | memory.stat 的 file | 文件缓存、tmpfs 是否增长 |
pod_memory_working_set_bytes | kubelet /metrics/resource | Pod cgroup 的 Working Set | Pod 级别的实际工作集 |
node_memory_MemAvailable_bytes | node-exporter | /proc/meminfo 的 MemAvailable | 内核估计的节点可分配余量 |
container_memory_working_set_bytes{id="/"} | cAdvisor | 根 cgroup 的 Working Set | kubelet 视角的节点内存压力 |
kube_node_status_capacity{resource="memory"} | kube-state-metrics | Node 对象的 Capacity | 把节点使用量换算成比例时的分母 |
这张表里至少有三个不同层次:
- 容器 cgroup 的使用量;
- 节点根 cgroup 的 Working Set;
/proc/meminfo给出的内核估算值。
同一节点上,MemAvailable 使用率和 kubelet Eviction 使用率不一致,并不能单独证明哪一个组件有问题。要先确认它们的定义,再看差额主要来自 active file、tmpfs、共享内存还是其他内核记账。
容器指标之和也不一定等于 Pod cgroup 的使用量。多容器 Pod 可能包含 pause 容器、共享内存和 Pod 级别的 cgroup 开销;排查时可以同时查看容器累加值和 pod_memory_working_set_bytes,不要把两者再相加。
另外,container_* 这个指标前缀不保证数据永远直接来自 cAdvisor。启用 PodAndContainerStatsFromCRI 且容器运行时支持对应统计接口时,kubelet 可能从 CRI 获取 Pod 和容器统计。指标名称可以保持不变,但数据来源和边界应以当前 kubelet、运行时和采集配置为准。可参考 Kubernetes Node Metrics。
4. 一条可复用的排查路径
第一步:先确认故障类型
| |
重点区分:
reason: OOMKilled:某个进程被 Kill,仍需继续判断是 memcg OOM 还是 global OOM;status: Failed、reason: Evicted:kubelet 驱逐了 Pod;- 只有重启次数增加但没有留下明确状态:可能是短时尖峰,也可能是历史状态已被后续重启覆盖。
OOMKilled 只说明容器最终观察到的退出原因,不携带完整的 OOM 触发路径。不要把它直接等同于「超过了这个容器的 Memory Limit」。
第二步:检查节点事件和内核证据
先找到 Pod 所在节点:
| |
然后检查节点事件:
| |
SystemOOM 可以作为 global OOM 的强证据;没有这个 Event 不能反向证明一定是 memcg OOM,因为事件记录还依赖 kubelet 对内核日志的观察和解析。必要时应该同时查看节点上的内核日志:
| |
日志中出现 oom_memcg=、Memory cgroup out of memory,通常指向 memcg OOM;出现 constraint=CONSTRAINT_NONE 或 global_oom,通常指向节点级 global OOM。不同内核版本的日志格式可能略有变化,最终以 memory.events、Pod 状态和节点现场互相印证。
第三步:直接看 cgroup v2 的账本
先在节点上确认 cgroup 版本:
| |
如果输出是 tmpfs,节点使用的是 cgroup v1,本文关于 memory.current、memory.max 和部分 memory.stat 字段的映射不能直接套用。cgroup v1 通常要检查 memory.usage_in_bytes、memory.limit_in_bytes 等文件,并重新确认 kubelet 和 cAdvisor 的指标定义。
容器 cgroup 的具体路径取决于 cgroup driver、Pod UID 和容器运行时。找到目标 cgroup 后,优先读取:
| |
没有设置 Memory Limit 的容器,其 memory.max 通常是 max;对应的 container_spec_memory_limit_bytes 可能以 0 表示无限制。对这类容器直接计算「使用量 / Limit」没有意义,也不能因为它没有触发自己的 memcg OOM,就认为它对节点没有风险。
可以按下面的顺序解释结果:
memory.current是否接近或超过memory.max;memory.events中oom_kill是否增加;anon是否增长,判断是否更像应用匿名内存问题;file、active_file或shmem是否增长,判断是否更像文件缓存、tmpfs 或共享内存问题。
容器退出后,容器 cgroup 可能已经被清理,此时不能依赖现场文件恢复全部信息。应该提前采集 memory.events、节点级 node_vmstat_oom_kill、Pod 的最后退出状态,并保留足够细的 Prometheus 时间序列。
第四步:用 PromQL 看趋势,而不是只看瞬时值
查看 Pod 的 Working Set:
| |
查看匿名页和文件缓存的组成:
| |
如果已经采集 kube-state-metrics 的资源请求,可以把 Working Set 和 Request 放在一起比较:
| |
实际环境中要检查两侧的标签和采集范围,避免把 pause 容器、已结束容器或重复抓取的时间序列算进去。比例超过 1 只能说明当前使用量超过 Request,不能直接推导出即将 OOM。
kubectl top 适合快速定位当前谁的 Working Set 较高,但 Metrics Server 主要保留最近采样值,不能替代长期历史。Request 应该根据一段时间的峰值、分位数、业务启动阶段和异常流量共同确定;也可以让 VPA 根据历史使用量计算建议值,但仍要检查它与业务的发布、扩缩容和 Limit 策略是否兼容。
证据和推论要分开
排障时可以用下面的表避免过早下结论:
| 看到的现象 | 下一步证据 | 不能直接推出 |
|---|---|---|
OOMKilled | memory.events、节点 Event、内核日志 | 一定超过了这个容器的 Memory Limit |
| Working Set 持续上升 | 拆分 container_memory_rss、container_memory_cache、shmem | 一定是应用内存泄漏 |
MemAvailable 仍然较高但 Pod 被 Evict | kubelet Working Set、Eviction 阈值、Node Condition | kubelet 指标错误 |
| 使用量超过 Memory Request | Pod QoS、Priority、节点压力和实际 Limit | 一定马上 OOM |
memory.events 的 high 增加 | memory.high、PSI 和请求延迟 | 已经发生 OOM Kill |
5. 对应的处理方式
如果是 memcg OOM
- 检查 Limit 是否低于正常峰值,必要时提高 Limit,但要同时确认节点可承载容量;
- 优先看
anon是否持续增长,排查堆、线程栈、缓存和应用内部队列; - 检查
emptyDir: { medium: Memory }、tmpfs 和共享内存,它们也会计入内存; - 如果只是短时尖峰,不能只凭一分钟粒度的 Prometheus 曲线判断「没有超过 Limit」;
- 应用已经释放对象但 RSS 不下降时,继续确认运行时是否真的把页归还给内核。
提高 Request 本身不能解决容器撞上 memory.max 的问题;这条路径要先处理 Limit 和实际分配行为。
如果是 global OOM 或 Eviction
- 为工作负载设置接近实际使用量的 Memory Request,减少节点过度装箱;
- 结合
kube-reserved、system-reserved和 Eviction 阈值给 kubelet、containerd、内核留出空间; - 对文件 I/O、日志或内存卷使用量大的 Pod 单独观察
active_file和shmem; - 检查 QoS、Priority 和 Request 超额情况,确认为什么某个 Pod 成了优先回收对象;
- 节点经常出现 PSI 内存等待时,说明系统可能已经出现分配阻塞,即使还没有 OOM,也应该提前处理容量或工作负载。
Request 的作用可以概括为:它影响 Pod 能否被调度、节点压力时是否被认为超额,以及 global OOM 时的 oom_score_adj。它不会替代 Limit,也不会让 memcg OOM 消失。
6. 最容易误判的四种情况
「Working Set 没到 Limit,却 OOMKilled」
可能是 memory.current 到达了 cgroup 上限,也可能是节点发生 global OOM。先区分 cgroup 内的 memory.events、节点 Event 和内核日志。
「MemAvailable 还有很多,却发生 Eviction」
MemAvailable 和 kubelet 的 memory.available 不是同一个公式。后者基于节点 Working Set,active file 可能造成明显差异。
「Working Set 一直涨,是不是内存泄漏」
先拆分 container_memory_rss 和 container_memory_cache。RSS 涨更值得检查应用匿名内存;cache 涨则要继续拆分磁盘页缓存、tmpfs 和共享内存。只看 Working Set 不能完成判断。
「应用 RSS 和 container_memory_rss 对不上」
应用 RSS 是进程维度,container_memory_rss 通常是 cgroup 维度的匿名页统计。它们统计对象不同,不能要求数值相等。
结语
Kubernetes 内存排障最重要的不是背下某个指标名称,而是把「测量对象、统计范围、回收语义和触发动作」对应起来:
container_memory_working_set_bytes用于观察工作集,适合看kubectl top和节点压力;memory.current与memory.max决定 cgroup 限制路径是否可能触发 memcg OOM;- 节点的 Working Set 和 kubelet 的
memory.available决定 Eviction 视角的压力; - Memory Request 影响调度和节点压力下的选择,但不等于 Memory Limit。
当这些边界明确后,OOMKilled、Evicted、Working Set 增长和 RSS 不一致就不再是互相矛盾的现象,而是可以沿着不同证据路径分别验证的问题。
参考
- 原始阅读材料:Kubernetes のメモリ使用量と cgroup v2 の関係: OOM / Eviction の切り分けと Request の適正化
- About cgroup v2 - Kubernetes
- Resource Management for Pods and Containers - Kubernetes
- Node-pressure Eviction - Kubernetes
- Pod Quality of Service Classes - Kubernetes
- Resource metrics pipeline - Kubernetes
- kubectl top - Kubernetes
- Control Group v2 - Linux kernel documentation
- Concepts overview - OOM killer - Linux kernel documentation