Kubernetes_OOM通俗详解_内存组成_指标公式与直观案例

Kubernetes OOM 通俗详解

先看懂内存账单和直观数值,再计算占比,最后定位 OOM

从内存组成、占比计算到容器为何被终止

通俗详解版 v7 · 2026 年 9 月

先直接回答:为什么还剩 5% 就 OOM

一句话答案

Cache 已经包含在容器 cgroup 总账里,不是发生 OOM 时才被 Kubernetes 临时加上。K8s 容器 OOM 与 Linux 节点 OOM 的区别是检查范围不同,不是 Cache 口径不同。

疑问

直接解释

K8s OOM 与 Linux OOM 有什么区别?

容器 memcg OOM 检查这个容器的 cgroup limit;Linux 全局 OOM 检查节点实际可分配内存。两者都由 Linux 内核执行。

两者是否计算 Cache?

都计算占用 RAM 的 Cache,并在 OOM 前尝试回收可回收部分。容器只统计归属于自己 cgroup 的 Cache,不会把节点全部 Cache 算进来。

为什么监控还有 5% 就 OOM?

95% 不是安全线,也不是提前触发阈值。监控可能显示 Working Set 或上一个采样点;下一次申请还可能大于真实剩余额度。

1000 MiB limit:一组完整的 95% OOM 示例
监控 Working Set = 950 MiB,inactive_file = 40 MiB
较宽口径的总 usage 近似值 ≈ 950 + 40 = 990 MiB,按本例实际只剩约 10 MiB
再申请 60 MiB,仅回收 20 MiB:990 + 60 - 20 = 1030 MiB
1030 > 1000,因此后续分配可能触发 memcg OOM

为什么图上可能永远没到 100%

超过 limit 的申请可能直接失败,失败部分不会成功记入 memory.current。

OOM 可在毫秒内发生,15~60 秒采样或平均值可能错过 99%~100% 的瞬时峰值。

若完整日志证明是节点全局 OOM,容器自身还剩多少 limit 都不能排除被选为 victim。

目录

1 先把容器内存账单看懂

1.1 容器内存到底由哪些部分组成

一级记账项

中文名称

常见来源

是否容易回收

anon

匿名内存

应用堆、线程栈、匿名 mmap、运行时对象

通常不容易直接丢弃;有 swap 时部分页面可换出

file

文件类内存

普通文件缓存(Page Cache)、文件映射、内存文件(tmpfs)、共享内存(shmem)

普通干净文件页较容易回收;tmpfs/shmem 不能当作普通干净缓存直接丢弃

kernel

内核内存汇总(字段存在时)

内核对象缓存(slab)、页表、内核栈、per-CPU、vmalloc 等

取决于具体子项;旧内核可能不提供 kernel 汇总字段

sock

网络套接字内存

TCP/UDP 发送接收缓冲、网络积压

受网络栈和连接状态约束,不能视为普通 Page Cache

这是用于排障的近似账单,不要求所有字段在每次采样中精确相加到最后一个字节。先记住:sock 与 kernel 是不同记账类别,不能重复相加;但部分旧内核不提供 memory.stat:kernel 汇总字段,此时不能把缺失字段当成 0,也不能仅靠若干子项重建完整 kernel 总量。swap 使用量应另看 memory.swap.current;它不能与 memory.current 机械相加,因为驻留 swapcache 可能同时出现在两种口径中。少见的条件性记账和统计差值放到第 6 章进阶说明。

1.2 memcg OOM 是什么,占比是多少

2 先把 OOM 相关名词全部讲清楚

2.1 第一次阅读必须懂的核心词

核心词

最简单的理解

cgroup

Linux 给一组进程建立的资源账本和控制范围;容器进程通常被放在相应 cgroup 中

memory.current

当前已经记入这个 cgroup 及其后代的总内存用量

memory.max / limit

这个 cgroup 允许使用的硬上限;输出 max 表示本层没有设置硬上限

anon

应用堆、线程栈等匿名内存,通常不能像干净文件缓存一样直接丢弃

file

普通文件缓存、tmpfs 和共享内存等文件类内存的总项

kernel

较新内核可提供的内核内存汇总字段,如页表、内核栈、slab 等;字段缺失不代表用量为 0

sock

TCP/UDP 等网络发送接收缓冲,通常与 kernel 分开统计

核心词

最简单的理解

Page Cache

读取普通文件后保留在内存中的文件页;干净、非活跃页面通常较容易回收

tmpfs / shmem

内存文件系统和共享内存;计入 file,但不能当成普通干净 Page Cache 随时丢弃

Working Set

监控侧用总用量减去 inactive_file 得到的工作集近似值,不是另一类内存

memcg OOM

某个内存 cgroup 回收后仍无法在额度内完成分配时进入的 OOM 状态

OOM kill

OOM Killer 实际选中并强制终止某个进程的动作

OOMKilled

容器运行时和 Kubernetes 展示的终止原因,需要与事件和日志一起定责

回收(reclaim)

内存紧张时,内核尝试释放可丢弃页面、写回脏页或换出可换出的页面

下面这些词属于进阶排障

第一次阅读可以先跳过;遇到节点 OOM、父 cgroup、页面冷热或监控口径问题时再回来查。

进阶词

最简单的理解

进程

正在运行的程序实例,是 Linux 实际调度和分配内存的对象

容器

被 namespace(隔离边界)和 cgroup 限制的一组进程;它不是独立虚拟机

内存页

Linux 管理内存的基本单位,常见为 4 KiB,也可能使用更大的页

记账(charge)

某个页面或内核对象被算到哪个 cgroup 名下;被记账就会占用相应额度

LRU

内核管理页面冷热状态的一组列表,不是绝对精确的“最久未使用排序”

swap-backed

内容可由 swap 作为后备存储的页面;tmpfs/shmem 常属于这一类

cAdvisor

采集容器资源指标的组件,许多 container_memory_* 指标来自它

badness

节点全局 OOM 时,内核评估哪个进程更适合被终止的分值逻辑

oom_score_adj

对 OOM victim 选择倾向的调整值;越高通常越容易被选中

两条阅读路径

初学者主线:先读第 1~5 章,理解名词、树状账单和 2 GiB 案例;实际排查时先读 6.2~6.5 和第 12 章。

进阶参考:节点 cgroup 精确定位、cgroup v1、父层级、HugeTLB、PromQL 和事件传播可按需查阅;第一次阅读可以跳过。

2.2 OOM

OOM 是 Out Of Memory 的缩写,意思是“无法满足内存分配”。它描述的是一次分配失败和处理过程,不是某一个 Prometheus 指标。

2.3 cgroup

cgroup 是 Linux Control Group。它可以把一组进程放进同一个资源控制范围,对 CPU、内存等资源做统计和限制。Kubernetes 创建容器时,容器运行时会把进程放入相应 cgroup。

2.4 memcg

memcg 是 memory cgroup 的简称,即 cgroup 的内存控制部分。memcg 负责记录该组进程、其后代 cgroup 及相关缓存使用了多少内存,并执行 memory.max 或 memory.limit_in_bytes 等限制。

2.5 memcg OOM

memcg OOM 指某个 cgroup 自己的内存额度不足。即使节点还有大量空闲内存,只要容器被限制为 2 GiB,而该 cgroup 在回收后仍无法把下一次分配控制在 2 GiB 内,就可能进入 memcg OOM 状态。进入该状态与实际发生 OOM kill 是两个步骤,应分别查看 oom 和 oom_kill。

2.6 Linux 节点全局 OOM

节点全局 OOM 指整台 Linux 主机的可用内存严重不足。它与“某个容器达到自己的 limit”不是一回事。全局 OOM 时,内核会在更大的范围内依据 badness、oom_score_adj 等选择进程。

2.7 OOMKilled

OOMKilled 是容器运行时和 Kubernetes 展示的终止原因,表示进程被 OOM Killer 杀死。它通常由 memcg OOM 引起,也可能与节点全局 OOM 有关,必须结合 cgroup 事件和内核日志判断来源。

2.8 Exit Code 137

137 = 128 + 9,表示进程收到了 SIGKILL。OOM Killer 会发送 SIGKILL,因此 OOM 常见退出码 137;但人工 kill -9、运行时强制停止也会产生 137,所以不能只凭退出码判定 OOM。

2.9 Evicted

Evicted 是 kubelet(每个 Kubernetes 节点上的管理代理)在节点资源压力下主动驱逐 Pod。它不是 Linux OOM Killer 直接杀进程。看到 Evicted,应查节点 memory.available(kubelet 估算的节点可用内存)、驱逐阈值和 kubelet 事件,而不是直接归因于容器 memory limit。

2.10 名词关系总表

名词

它是什么

有没有内存占比

主要看什么证据

anon / file / sock / kernel

常见主要记账项;kernel 取决于字段可用性

有,可以计算;缺失字段记为 N/A

memory.stat 与 memory.current

memcg

内存控制域

没有,它不是内存类型

cgroup 路径和限制

memcg OOM

cgroup 内存不足状态或事件

没有,它不是内存类型

oom 增量、限制层级、内核 memcg 日志

OOM kill

OOM Killer 实际终止进程

没有,它是动作和事件

oom_kill 增量、终止状态、内核日志

全局 OOM

节点内存不足事件

没有,它是事件

节点内核 OOM 日志、节点内存

OOMKilled

容器终止原因

没有

Pod 状态、事件、内核日志

Evicted

kubelet 驱逐结果

没有

Pod Event、kubelet 日志、节点 Condition

3 容器内存账单:主要一级项与常见子项

3.1 先看树状账单:一级项和子项不能混加

memory.current(当前 cgroup 及其后代的总记账)
├─ anon(匿名内存)
├─ file(文件类内存)
│  ├─ 普通文件 Page Cache
│  └─ shmem / tmpfs(已经包含在 file)
├─ sock(网络套接字内存)
├─ kernel(较新内核提供的汇总字段,存在时)
│  ├─ slab
│  ├─ pagetables
│  ├─ kernel_stack
│  └─ percpu / vmalloc 等
└─ 其他条件记账项(例如启用相应挂载选项后的 HugeTLB)
另一个“页面状态视图”:
active_file / inactive_file / file_dirty / file_writeback
这些字段描述页面状态,不能与上面的一级项重复相加。
swap 另看 memory.swap.current,不加进 memory.current 组成式。

3.2 anon:匿名内存

anon 是 anonymous memory。它没有普通磁盘文件作为可重新读取的后备内容,最常见来源是应用 Heap(运行时动态分配对象的内存区)、线程栈和匿名 mmap(不以普通文件为后备的内存映射)。Java Heap、Python 对象、Go Heap 等最终大多体现在这一类或与之相关的匿名内存中。GC 是 Java、Go 等运行时清理不再使用对象的垃圾回收机制。

  • anon 高并持续上涨:优先检查应用对象、堆外内存、线程数、匿名 mmap 和 GC 回收效果。
  • anon 不容易像干净 Page Cache 一样直接丢弃:无 swap 的容器节点上,它通常是 memcg OOM 风险的核心部分。

3.3 RSS:为什么同名指标可能不一样

RSS 是 Resident Set Size,直译为驻留内存集。但不同工具的 RSS 口径不完全相同。ps/top 的进程 RSS 可能包含文件映射页和共享页;cgroup v1 memory.stat 的 rss 主要是匿名与 swap cache;cAdvisor 的 container_memory_rss 在常见实现中更接近容器匿名驻留内存,而不是 ps RSS 的简单求和。

3.4 file:文件类内存

file 是 filesystem cache 使用的内存。cgroup v2 官方定义明确指出,file 包括普通文件缓存,也包括 tmpfs 和 shared memory。它是一个大类。

3.5 Page Cache:页缓存

应用读取文件时,Linux 会把文件页缓存在内存中,后续读取可以直接从内存返回,这就是 Page Cache。普通干净、非活跃文件页通常可以在内存压力下丢弃,将来再从磁盘读取。

3.6 cache:监控中的缓存口径

container_memory_cache 是 cAdvisor 暴露的缓存类指标。它常用于说明 cgroup 中由文件页等形成的缓存,但具体映射会受 cgroup 版本和 cAdvisor 实现影响。文章中看到 cache 时,应先理解为“缓存类内存”,再到 memory.stat 查看 file、shmem、inactive_file 等细项。

3.7 active_file 与 inactive_file

字段

通俗解释

能否直接等同于可回收

active_file

近期更活跃的文件页,内核倾向于继续保留

不能

inactive_file

位于非活跃文件 LRU 的页面,是更可能被回收的候选

也不能保证全部立即回收

inactive_file 是 Working Set 公式中被扣除的部分。Working Set 可先理解为“总使用量减去非活跃文件页后得到的工作集近似值”,第 4.3 节会完整解释。但“非活跃”不是“现在一定能无成本回收”的同义词:页面可能正在写回、很快重新活跃,回收速度也可能赶不上应用分配速度。active_file 与 inactive_file 是文件页在 LRU 上的状态计数,也不能把两者相加后机械地当成完整 file:file 还包含 tmpfs/shmem,而这类页面可能进入匿名页 LRU;不同内核版本和瞬时采样也会形成差值。

3.8 file_dirty 与 file_writeback

file_dirty 表示已修改但尚未完成持久化的文件页;file_writeback 表示正在写回存储的文件页。这些是 file 内部的状态或属性,不能再与 file 相加。脏页通常需要先写回,回收速度受磁盘性能影响。

3.9 tmpfs 与 shmem

tmpfs 是以内存为主要存储介质的文件系统;shmem 是共享内存及 swap-backed 文件类内存统计。/dev/shm、System V/POSIX 共享内存、部分共享匿名映射、medium: Memory 的 emptyDir 都可能消耗这一类内存。IPC 是 inter-process communication,即进程间通信;共享内存就是常见 IPC 方式之一。

3.10 kernel:内核内存汇总字段

较新的 cgroup v2 内核可在 memory.stat 中提供 kernel 汇总字段,表示该 cgroup 及其后代使用的内核内存总量。常见明细包括 kernel_stack、pagetables、percpu、vmalloc、slab 等;这些明细不能与 kernel 再重复相加。sock 是独立记账类别,不属于 kernel 汇总字段中的同一笔 socket buffer charge。

先确认 kernel 字段是否存在

部分旧内核或厂商内核不提供 memory.stat:kernel 汇总字段。字段不存在不表示内核内存为 0,应显示 N/A,并查看 slab、pagetables、kernel_stack 等已有明细;但这些明细不能保证重建完整 kernel 总量。不要只按内核版本号猜测,应直接检查 memory.stat。

kernel 子项

中文解释

异常增长时优先检查

kernel_stack

内核态线程栈

线程/进程数量

pagetables

页表

大量进程、映射或碎片化地址空间

slab

内核对象缓存

文件对象、目录项、容器对象及其他内核子系统

percpu / vmalloc

per-CPU 与内核虚拟映射相关内存

CPU 数、驱动和内核子系统

3.11 sock:网络套接字内存

sock 是 TCP、UDP 等网络传输缓冲在该 cgroup 中的记账。在 cgroup v2 的 memory.stat 中,它通常与 kernel 分开统计,因此排障近似式应把 sock 单独列出。高连接数、发送或接收积压、吞吐突增都可能让它升高。

3.12 哪些字段可以相加,哪些不能

关系

是否可以相加

原因

anon + file + sock + kernel(若字段存在)

可以作为排障近似

主要一级项用于解释 memory.current;kernel 缺失时必须标为 N/A,仍可能有条件性记账项和统计差值

file + shmem

不可以

shmem 已包含在 file 中

file + inactive_file + active_file

不可以

active/inactive 是页面 LRU 状态,不是额外大类;两者也不保证完整等于 file

kernel + slab + pagetables

不可以

有 kernel 汇总字段时,slab、pagetables 是其明细;没有汇总字段时,这些明细也不能保证重建完整 kernel

kernel + sock

字段存在时可用于一级项近似

当前内核中 sock 与 kernel 是不同记账类别;先确认 kernel 字段存在

usage + working set

不可以

working set 是从 usage 派生出的另一种口径

4 usage、limit 与 working set 分别是什么

4.1 先把三个监控指标放进同一本账

memory.current 是 Linux cgroup v2 的内核字段,表示当前 cgroup 及其后代的总内存记账;container_memory_usage_bytes 是 cAdvisor 采集并导出的 Prometheus 总用量指标。两者观察的是相近的总账语义,但不是同一个字段,也不能在所有环境中写成严格等号。采样时刻、cgroup 层级、运行时、cAdvisor 与内核版本都可能造成差异。

指标或字段

先把它理解成什么

最容易误解的地方

memory.current

Linux cgroup v2 当前总记账

是内核字段,不是 Prometheus 指标;必须确认读取的是目标容器或正确父层级

container_memory_usage_bytes

cAdvisor 导出的较宽口径容器总用量

通常接近相应 cgroup 总账,但不是 OOM Killer 的直接输入,也不是 memory.current 的别名

container_memory_rss

cgroup/cAdvisor 的匿名驻留内存近似口径

不是 ps RSS;cgroup v2 通常接近 anon,cgroup v1 语义还可能包含 swap cache,均不代表全部文件映射驻留页

container_memory_cache

cAdvisor 导出的文件缓存相关用量

已经包含在 usage 中;不是 usage 之外再加一次

kernel memory

归属于该 cgroup 的内核内存

新 cgroup v2 中 kernel 与 sock 可分开统计;不同 exporter 不一定都提供同名指标

sock / 其他差值

网络缓冲、条件性记账和采样差异

真实环境各项相加不一定恰好等于 usage

为什么正文使用约等号,而不是永远使用等号

在一个专门为教学设计、其他差值设为 0 的例子中,可以写成 920 = 500 + 340 + 80;但真实监控还可能存在 sock、HugeTLB、页级统计差值、不同采样时刻和层级差异。

cgroup v2 现场排障时,更接近内核账本的组成式是 memory.current ≈ anon + file + kernel(字段存在时)+ sock + 其他条件性记账。

container_memory_usage_bytes ≈ memory.current 只能表达同一目标、相近时刻下的总账近似关系,不能据此将一个字段精确转换成另一个字段。

memory.current 与 swap.current 不是完全互斥的两块

不要机械计算 memory.current + memory.swap.current 作为唯一总量。驻留在内存中的 swapcache 可能同时出现在内存用量与 swap 用量口径中;分析 swap 时应把两个指标并列观察,而不是假定它们永远没有交集。

4.2 memory.max / memory limit

memory.max 是 Linux cgroup v2 的硬限制。Kubernetes limits.memory 通常最终落实为这一类 cgroup 上限。

达到上限后,内核会尝试回收;回收仍无法满足新的内存分配时,才可能进入 memcg OOM。

Prometheus 中常用 container_spec_memory_limit_bytes,或 kube_pod_container_resource_limits{resource="memory",unit="byte"} 表示配置的容器内存 limit;部分面板仍使用 kube_pod_container_resource_limits_memory_bytes。

对象

单位与维度

能否与 Limit 计算比例

准确用途

memory.current

字节;内核 cgroup 当前总记账

可以,前提是同一 cgroup 层级

current / max 表示内核总账水位

memory.max

字节或 max;内核 cgroup 硬限制

作为同层级分母

限制边界;不是 OOM 事件计数

container_memory_usage_bytes

字节;cAdvisor 容器总用量

可以,前提是同一容器、同一层级

usage / limit 表示监控总账水位

container_memory_working_set_bytes

字节;cAdvisor 派生工作集

可以,前提是同一容器、同一层级

working set / limit 表示压力与风险水位

Prometheus limit 指标

字节;Kubernetes 或 cAdvisor 暴露的配置值

作为匹配对象的分母

监控映射;不是 memory.max 文件本身

同为字节,只表示可以比较,不表示语义相同

这几个值都以字节表达,所以可以在标签、容器和 cgroup 层级正确匹配时计算比例;但单位相同不等于字段相同。

不能用 usage 与 Working Set 反推出一个第三种所谓“OOM 值”。两者只能近似得到 usage - working set ≈ inactive_file。

真正的 OOM 事件证据应查看 memory.events(.local) 的 oom/oom_kill、container_oom_events_total、容器终止状态和内核日志。

4.3 Working Set

Working Set 不是另一块新内存,也不是 usage 之外再加的一项。它是 cAdvisor 从总使用量中扣除选定 inactive_file 口径后得到的派生监控值,用来减少近期不活跃文件页对日常压力曲线的影响。

4.4 inactive_file 到底是什么

先记住这一句话

inactive_file 是内核对文件 LRU 上低热度页面的状态统计。这些页面仍驻留在 RAM、仍计入 usage,只是通常比 active_file 更优先接受回收检查。

inactive 不表示页面已经释放、不表示内容没有用,也不保证这些字节都能立刻回收。在普通文件读取场景中,它通常主要来自 Page Cache;真实内核还存在脏页、shmem 和 lazy-free 匿名页等边界。

阶段

这页内存发生了什么

是否仍计入 usage

接下来可能怎样

1. 第一次读取文件

磁盘内容被装入 RAM 成为 Page Cache,经典双 LRU 模型中通常先进入 inactive file LRU

是

单次读取不代表已经成为 active 页面

2. 页面被重复访问

页面再次被引用后,可能提升到 active file LRU

是

内核倾向继续保留近期较热的页面

3. 老化或回收扫描

若 active 页面之后未再被引用,可能被降回 inactive

是

不是经过固定时间就自动降级

4. 内存出现压力

若为普通、干净文件页,内核可丢弃其 RAM 副本

回收前是;回收后否

以后再次读取时从磁盘重新加载

5. 回收前又被读取

页面重新被引用,可能保持或恢复较活跃状态

是

曾经 inactive 不表示必须被回收

6. 页面是脏页或 shmem

可能需要先写回,或不能像普通干净文件页那样直接丢弃

是

回收速度和可回收程度受限制

上表用经典双 LRU 建立直觉;启用多代 LRU(MGLRU)的现代内核会用更复杂的代际和访问历史判断冷热,并不严格执行这套逐步状态机。某些内核中的 MADV_FREE lazy-free 匿名页也可能影响 inactive_file 口径。因此,只能在下方明确限定的教学案例中,把 260 MiB inactive_file 全部视为 340 MiB Cache 的子集;真实环境不能无条件照搬。

inactive_file 是页面状态统计,不是第五类内存。不能计算 usage = RSS + Cache + kernel + inactive_file;Working Set 只是按 cAdvisor 公式从 usage 中减去相应 inactive_file 口径。

4.5 一个 1000 MiB 的直观监控案例

场景设定

某容器的 memory limit 为 1000 MiB。为便于建立直觉,本例暂时把 sock 和其他差值设为 0,并假设 260 MiB inactive_file 都是普通、干净的文件页。真实环境必须保留差值,也不能预先假定所有 inactive_file 都能及时回收。

指标

示例值

怎样理解

container_memory_rss

500 MiB

本例按匿名驻留内存近似口径理解;不是 ps RSS,也不等于只看 Heap

container_memory_cache

340 MiB

文件缓存相关用量;其中 260 MiB 已进入 inactive_file 状态

kernel memory

80 MiB

页表、内核栈、slab 等被计入本例的内核用量

container_memory_usage_bytes

920 MiB

500 + 340 + 80;占 1000 MiB limit 的 92%

total_inactive_file

260 MiB

cgroup v1 常见层级累计字段;已包含在 340 MiB Cache 中

container_memory_working_set_bytes

660 MiB

920 - 260;占 limit 的 66%

本例按 cgroup v1/cAdvisor 常见字段 total_inactive_file 展示;如果环境是 cgroup v2,应读取 inactive_file。两者字段名和层级语义不同,但 cAdvisor 的 Working Set 计算思路都是从 usage 中扣除选定的 inactive_file 口径。

Prometheus/cAdvisor 指标和 cgroup 账本字段通常都以字节表示。把下列大整数连续除以 1024 两次,就能换算成表中的 MiB;注意 kernel 与 total_inactive_file 是本例直接从 cgroup 账本补充的值,不是这里假设存在的同名 Prometheus 指标:

# Prometheus / cAdvisor 指标
container_spec_memory_limit_bytes        = 1048576000  # 1000 MiB
container_memory_usage_bytes             =  964689920  #  920 MiB
container_memory_rss                     =  524288000  #  500 MiB
container_memory_cache                   =  356515840  #  340 MiB
container_memory_working_set_bytes       =  692060160  #  660 MiB
# cgroup 账本补充值
kernel(本例从 memory.stat 取得)        =   83886080  #   80 MiB
total_inactive_file(cgroup v1)          =  272629760  #  260 MiB
inactive_file(cgroup v2 对应字段)       =  272629760  #  260 MiB

4.6 把这组数逐行算一遍

① 总使用量:
container_memory_usage_bytes
= RSS + Cache + kernel memory
= 500 + 340 + 80
= 920 MiB
② Working Set:
container_memory_working_set_bytes
= usage - total_inactive_file
= 920 - 260
= 660 MiB
③ 两种监控比例:
usage / limit       = 920 / 1000 = 92%
working set / limit = 660 / 1000 = 66%

这时 usage 显示 920 MiB、kubectl top 更接近 660 MiB,二者都可能是正确的。差出的 260 MiB 没有凭空消失:它仍在 RAM 中、仍占容器总账,只是 Working Set 公式将这批近期不活跃的文件页扣除了。

4.7 从读文件到 OOM:同一批内存怎样变化

时刻

RSS

Cache

Kernel

Usage

inactive_file

Working Set

T0 初始

500

80

80

660

20

640

T1 读取并重复访问 260 MiB 文件

500

340

80

920

40

880

T2 经历老化或回收扫描,且文件未再访问

500

340

80

920

260

660

T3 新增 200 MiB 匿名驻留记账,同时回收 200 MiB 文件页

700

140

80

920

60

860

T1 到 T2 是两次教学快照,不是所有内核都必须遵循的定时转换:T1 表示读取期间部分文件页因重复引用而表现得更活跃;停止访问后,经过 LRU 老化或回收扫描,更多页面被统计为 inactive 候选,所以 Cache 和 Usage 仍不变,而 Working Set 从 880 MiB 降为 660 MiB。T2 到 T3 时,应用新增 200 MiB 匿名驻留记账,内核恰好回收 200 MiB 普通干净文件页,因此 RSS 近似口径增加、Cache 减少,而 Usage 仍为 920 MiB。

4.8 同一组容器数据:memcg OOM 与 Linux 节点全局 OOM 对比

两条分支共同使用的数据

都从 T3 开始:RSS 700 MiB、Cache 140 MiB、kernel memory 80 MiB、usage 920 MiB、inactive_file 60 MiB、Working Set 860 MiB。

同一个进程接下来都申请 160 MiB,并假设容器内最多只能及时回收这 60 MiB inactive_file。变化的只有限制边界和节点整体内存状态。

比较项

分支 A:容器 memcg OOM

分支 B:Linux 节点全局 OOM

容器硬限制

1000 MiB

1500 MiB

节点状态

节点 MemAvailable 仍较高,例如约 4 GiB,整机不紧张

教学设定:节点总内存 8192 MiB、简化已占用量 8132 MiB、swap 不可用或已耗尽,并暂时只计入本例 60 MiB 可及时回收文件页

若需求全部满足后的理论目标

920 + 160 - 60 = 1020 MiB

容器目标同为 1020 MiB;真实分配通常逐页记账,不保证能观察到这个快照

容器边界判断

理论目标 1020 > 1000;若回收后仍不能完成分配,会撞到容器硬限制

理论目标 1020 < 1500;容器硬限制本身仍有空间

节点容量教学模型

节点余量远大于本次需求,不属于整机容量不足

8132 - 60 + 160 = 8232 MiB,简化模型显示约 40 MiB 容量缺口;这只说明压力条件,不足以单独证明必然 OOM

若节点级回收最终失败并调用 OOM Killer

OOM domain 是触发硬限制的 memory cgroup

若完整日志确认 global_oom 或对应版本的 CONSTRAINT_NONE,才归为节点全局 OOM;若显示 cpuset、NUMA 等约束,应按受限分配 OOM 另行定责

victim 范围

通常从受该限制约束的任务中选择

可能从系统级约束范围内的进程中选择,不保证就是发起申请的进程

Kubernetes 可能看到什么

若容器进程被杀,可能显示 OOMKilled 和 137

若选中的 victim 属于某容器,该容器同样可能显示 OOMKilled 和 137

8232 MiB 不是全局 OOM 判决公式

这只是把相同 160 MiB 需求放进 8 GiB 节点后的容量缺口演示。Linux 是否真的调用全局 OOM Killer,还取决于 swap、zone 水位、保留页、GFP 分配标志、allocation order、NUMA/cpuset 限制、slab 与其他回收来源,以及本次分配是否允许调用 OOM Killer。

只有节点级分配与回收最终失败、调用 OOM Killer,并由完整日志确认 global_oom 或该内核版本的等价全局上下文,才能把分支 B 定义为 Linux 节点全局 OOM;不能只凭 8232 > 8192 直接下结论。若日志显示 cpuset、NUMA 等约束,则应改判为相应的受限分配 OOM。

4.9 两种 OOM 的数字证据怎样区分

证据

memcg OOM 更典型的表现

节点全局 OOM 更典型的表现

触发层级的 memory.events.local:oom

容器或 Pod 限制层级的 oom 增长

目标容器本地 oom 不一定增长,因为不是它的硬限制触发

victim 所属 memcg 的 oom_kill

实际发生 kill 时可能增长

若全局 OOM 选中该 cgroup 内进程,也可能增长;因此不能只凭 oom_kill 定责

节点 /proc/meminfo

MemAvailable 可以仍然很高,例如约 4 GiB

MemAvailable 通常已很低;它是综合水位、保留量、Page Cache 和可回收 slab 等因素的估算,不能按 free 加某个 Cache 字段直接计算

内核日志

检查完整 cgroup OOM 上下文、oom_memcg 和目标 cgroup 路径

检查完整全局 OOM 上下文、constraint、zone/NUMA 信息和 Killed process;不同内核字段不同,不能只凭一条短语定责

影响范围

主要是该限制域内的工作负载

整台节点上的工作负载都可能参与压力和 victim 选择

为什么不能只看 OOMKilled 或 137

两条分支最后都可能让 Kubernetes 显示 OOMKilled,退出码也都可能是 137。Kubernetes 展示的是容器终止结果,不足以单独证明是哪一层先耗尽;137 还可能来自其他 SIGKILL。

定责时要把相同时间窗口内的容器 memory.current 与 container_memory_usage_bytes、memory.max、memory.events.local 增量、节点 /proc/meminfo、swap 状态和完整内核日志放在一起判断。日志中可关注 constraint、oom_memcg、cgroup 路径和调用上下文,但具体字段会随内核版本变化。

上表中的 8132 MiB 是容量教学模型,不是由 MemAvailable 反推的精确已用量。真实 Linux 的 MemAvailable、可分配水位、各类内核页和回收能力,不能只靠 MemTotal 减去某一个 used 指标重建。

4.10 阿里云文档的建议如何使用

阿里云文档指出,kubectl top pod 通常反映 container_memory_working_set_bytes,而不是 container_memory_usage_bytes。这个说明适合解释为什么 kubectl top、Deployment 总览与单 Pod 详情可能出现不同百分比。差异来自仪表盘选用的监控口径,不代表 Deployment 和 Pod 使用了不同的 Linux OOM 机制。OOM 定责仍需回到 memory.current 与 container_memory_usage_bytes、memory.max、memory.stat、memory.events.local、节点内存状态和内核日志。

阿里云 Deployment 页面原始 PromQL(仅按版面换行)

label_replace(
  max(
    kube_pod_info{
      namespace="$namespace",
      created_by_kind="ReplicaSet",
      pod_ip!=""
    }
  ) by (created_by_name, uid, pod, pod_ip, node),
  "replicaset",
  "$1",
  "created_by_name",
  "(.+)"
)
* on(replicaset) group_left()
  max(
    kube_replicaset_owner{
      namespace="$namespace",
      owner_kind="Deployment",
      owner_name="$name"
    }
  ) by (replicaset)
* on(pod) group_right()
  max by(container, pod) (
    container_memory_working_set_bytes{
      namespace="$namespace",
      container!="",
      image!="",
      container!="POD"
    }
  )
/ max by(container, pod) (
    kube_pod_container_resource_limits{
      resource="memory",
      namespace="$namespace"
    }
  )

Deployment 查询片段

作用

不能误解成什么

kube_pod_info + label_replace

找到 ReplicaSet 创建的 Pod,并建立 replicaset 标签

不是在计算内存

kube_replicaset_owner

把 ReplicaSet 限定到目标 Deployment

不是把整个 Deployment 内存求和

max by(container, pod)

按 Pod 和容器保留一条监控用序列;使用前仍应核对采集拓扑

不是通用 HA 去重保证

working_set / limit

返回各 Pod/容器的工作集风险水位

不是 OOM Killer 直接使用的判决值

阿里云单 Pod 页面原始 PromQL

((max(
    container_memory_usage_bytes{
      container!="",
      container!="POD",
      namespace=~"$namespace",
      pod=~"$pod",
      node=~"$node"
    }
  ) by(pod,container,namespace)
  / max(
    kube_pod_container_resource_limits_memory_bytes{
      container!="",
      container!="POD",
      namespace=~"$namespace",
      pod=~"$pod",
      node=~"$node"
    }
  ) by(pod,container,namespace)) * 100)
<= 100 or on() vector(0)

单 Pod 查询的分子改用 container_memory_usage_bytes,因此它显示较宽口径的总账水位;分母 kube_pod_container_resource_limits_memory_bytes 是该面板使用的容器内存 limit 指标。它和 Deployment 页面使用 Working Set 的结果不同是正常现象,两者都只是字节值除以同一对象的字节 limit。

阿里云页面

分子口径

主要回答的问题

准确定位

Deployment 总览

container_memory_working_set_bytes

哪些副本的活跃压力和 OOM 风险较高

适合趋势观察和 85% 风险告警

单 Pod 详情

container_memory_usage_bytes

该 Pod 内各容器的较宽口径总账水位有多高

更接近完整用量视图,但仍不是 OOM Killer 直接输入

同一个容器为什么能同时显示 95% 和 75%

假设 limit=1000 MiB、container_memory_usage_bytes=950 MiB、inactive_file=200 MiB,则 Working Set≈950-200=750 MiB。

单 Pod 页面按 usage/limit 显示约 95%;Deployment 页面按 Working Set/limit 显示约 75%。少掉的 200 MiB 仍在 RAM、仍在总账中,只是 Working Set 公式将其作为 inactive_file 扣除。

这不表示单 Pod 更容易 OOM,也不表示 Deployment 有另一套限制;Linux 仍按实际 cgroup 记账、硬限制、回收结果和新分配是否成功处理。

原始单 Pod 查询中的 <= 100 不是封顶

PromQL 比较运算没有 bool 修饰符时默认执行过滤:小于等于 100 的样本保留原值,大于 100 的样本被删除;137 不会被改写成 100。

or on() vector(0) 不是按每个 Pod/容器补零。只有左侧整体没有可匹配结果时,才可能留下一个无原始容器标签的 0;这可能让面板看起来像 0,而不是显示被过滤掉的超过 100% 序列。

若展示需求确实是把数值封顶为 100,可另行评估 clamp_max(<百分比表达式>, 100)。本文保留阿里云原始查询,不擅自替换生产面板。

cAdvisor 的直接证据:Issue #3197 明确写道:

container_memory_working_set_bytes is not what the OOM killer uses,
but it is a better leading indicator of OOM risk than just the plain
container_memory_usage_bytes.
As long as the container's cgroup still has evictable filesystem cache pages,
it will try hard to avoid killing processes, and container_memory_working_set_bytes
subtracts some (but not all) of those pages.

这段话必须完整理解:前半句否定 Working Set 是 OOM Killer 的直接使用值;后半句肯定它通常比 plain usage 更适合作为 OOM 风险前置指标。因此,阿里云用 Working Set 做 85% 水位告警是合理的风险监控方案,但 85% 不是 Linux 的 OOM 触发阈值,告警本身也不会执行 Kill 或 Restart。

cAdvisor Issue #3197(直接原文);cAdvisor Usage 与 Working Set 计算源码

4.10.1 补充:Working Set 尚未升高,为什么容器已经 OOM

严格来说,也不能表述为“container_memory_usage_bytes 已经 OOM”。container_memory_usage_bytes 只是监控指标;真正发生的是相应 cgroup 或祖先限制域在回收后仍无法完成新的内存分配,或者节点进入全局 OOM。Usage 可以显示当时总账已经非常接近限制,但它本身不执行 OOM 判决。

一个 Working Set 只有 72%,但仍可能 OOM 的例子
memory limit = 1000 MiB
container_memory_usage_bytes = 970 MiB
inactive_file = 250 MiB
Working Set = 970 - 250 = 720 MiB,即 72%
随后申请 100 MiB,本轮只有 30 MiB 能及时回收
理论目标值 = 970 + 100 - 30 = 1040 MiB > 1000 MiB

为什么图上的 Working Set 可能一直只有 72%

Working Set 在每次采样时直接用 usage 减去 inactive_file。它不会等几分钟再计算,但会把整笔 inactive_file 从曲线上扣除;而 inactive_file 只是回收候选,不是本次分配必然能够立即释放的确定余额。

真实分配通常逐页申请和记账。上面的 1040 MiB 是解释容量缺口的理论值,不保证内核曾成功达到该值,也不保证 Prometheus 能采到这个快照。

如果突发申请、OOM Kill 和容器重启发生在两个抓取时刻之间,下一次采样可能已经来自重启后的容器,因此曲线上甚至不会出现 Working Set 峰值。

表现原因

为什么 Working Set 看起来偏低或滞后

现场应核对什么

扣除 inactive_file

Usage 中仍驻留 RAM 的 inactive_file 被公式扣除,但回收速度和本次实际可回收量并无保证

memory.current、memory.stat:inactive_file、脏页/回写与回收压力

Prometheus 离散采样

毫秒级突发、Kill 与重启可能全部发生在 15~60 秒抓取间隔内

抓取间隔、原始时序、事发前后容器实例 ID 和重启时间

容器重建后序列变化

面板可能已经展示重启后的低水位,而不是被杀进程最后一刻的值

lastState、OOMKilled、restart count、旧/新 container ID

OOM 边界不是当前容器

祖先 cgroup、Pod 层级或节点全局 OOM 都可能在单容器 Working Set 不高时终止进程

当前及祖先 memory.events(.local)、节点日志和 MemAvailable

“更好的 leading indicator”不等于“数值一定更早达到阈值”

cAdvisor 所说的 better leading indicator,主要表示 Working Set 扣除部分文件缓存噪声后,通常更适合识别持续性活跃内存压力,并减少仅因 Page Cache 较高产生的误报。

它不表示 Working Set 一定比 Usage 更早升高。按照公式,Working Set 通常小于或等于 Usage;当 inactive_file 较高时,相同的 85% 阈值天然可能更晚达到,甚至在 OOM 前达不到。

因此,阿里云使用 Working Set 做 Deployment 的 85% 风险告警是合理的,但不能把它当成覆盖所有 OOM 场景的唯一告警。

建议并行保留三层监控

监控层

建议观察项

主要用途

不能单独证明什么

持续压力

working_set / limit

降低文件缓存噪声,观察持续性风险;例如 85% 持续一段时间

不能保证所有突发 OOM 都提前告警

硬边界附近总账

usage / limit 或同层级 current / max

观察较宽口径总账离硬限制多近,并结合增长速度

高 Usage 可能包含可回收 Cache,也不是 OOM 事件

已发生的 OOM 事件

container_oom_events_total、memory.events(.local)、OOMKilled、内核日志

立即确认 OOM 事件并区分限制域

单一来源不能总是区分 memcg OOM 与节点全局 OOM

# 持续压力水位:第 7.2 节
container_memory_working_set_bytes / limit
# 较宽口径总账水位:第 7.1 节
container_memory_usage_bytes / limit
# OOM 事件:第 7.5 节
increase(container_oom_events_total[5m]) > 0

实际告警阈值和持续时间应按本环境负载、抓取间隔、突发幅度和历史 OOM 校准。对突发型容器,应同时看 Usage 水位、增长速度和 OOM 事件,而不是只等待 Working Set 达到 85%。

5 用一个 2 GiB 案例把所有占比算清楚

5.1 示例原始数据

以下比例仅用于教学,不是所有容器的标准比例

本例统一使用二进制单位:2 GiB = 2048 MiB,1 MiB = 1,048,576 字节。不同语言、负载和 I/O 模式差异很大,不存在“anon 必须占 60%、file 必须占 30%”这样的统一正常值。真实占比必须从本环境的 memory.stat 计算。

指标

示例值

它代表什么

memory.max

2048 MiB

容器硬限制 2 GiB

memory.current

1600 MiB

当前 cgroup 总记账

anon

900 MiB

应用堆、栈、匿名 mmap 等

file

550 MiB

普通 Page Cache、tmpfs/shmem 等文件类内存

kernel

100 MiB

slab、页表、内核栈等,不含下方独立列出的 sock

sock

50 MiB

网络发送接收缓冲

hugetlb

0 MiB

本示例未启用或未使用条件性的 HugeTLB 记账

inactive_file

300 MiB

更可能被回收的非活跃文件页

shmem

100 MiB

包含在 550 MiB file 中,不能再相加

本例假设 memory.stat 提供 kernel 汇总字段,因此 memory.current、memory.max、anon、file、kernel、sock、inactive_file 和 shmem 都可由第 6.3~6.4 节读取和计算。若真实内核没有 kernel 字段,应标记为 N/A,不能套用本例的四项闭合关系。先学会本章的手工公式,再把现场字节值代入即可。

5.2 本例四个主要一级项占 memory.current 的比例

anon 占比   = 900 / 1600 × 100% = 56.25%
file 占比   = 550 / 1600 × 100% = 34.38%
kernel 占比 = 100 / 1600 × 100% =  6.25%
sock 占比   =  50 / 1600 × 100% =  3.13%
56.25% + 34.38% + 6.25% + 3.13% ≈ 100%

这四个百分比回答的是“当前 1600 MiB 主要被谁占用了”。它们不是 memcg OOM 的概率,也不能单独说明一定会不会 OOM。本例 hugetlb 为 0,且为便于教学忽略批量记账等微小差值。

5.3 当前总记账占 limit 的比例

memory.current / memory.max
= 1600 / 2048 × 100%
= 78.13%

这表示当前总记账已经使用 78.13% 的硬限制,尚未扣除可回收页面。按总记账计算,距离 limit 还有 448 MiB。

5.4 Working Set 与 limit 的比例

working set = 1600 - 300 = 1300 MiB
working set / limit = 1300 / 2048 × 100% = 63.48%

kubectl top 可能更接近 1300 MiB,而不是 1600 MiB。它把 300 MiB inactive_file 扣掉了,因此看起来离 limit 更远。这里的 300 MiB 是“可能优先回收的候选”,不是内核承诺一定能瞬间释放的保险额度。

5.5 shmem 占比应该怎样算

shmem 占总记账 = 100 / 1600 × 100% = 6.25%
shmem 占 file   = 100 / 550  × 100% = 18.18%

shmem 可以计算自己的占比,但它仍然包含在 file 中。计算占比是为了理解 file 的内部结构,不是为了把 shmem 再加进总内存。

5.6 两次内存申请为什么结果不同

为了便于理解,假设 300 MiB inactive_file 能够全部及时回收,其他条件不变。先算回收后的基线和剩余额度:

当前硬额度剩余量是 2048 - 1600 = 448 MiB。只有在理想化假设 300 MiB inactive_file 能全部及时回收时,理论剩余额度才会变成 448 + 300 = 748 MiB;748 MiB 不是系统承诺的确定空闲内存。

理想回收后记账 = 1600 - 300 = 1300 MiB
理想剩余额度   = 2048 - 1300 = 748 MiB

场景

逐步计算

结果

应用新增 500 MiB anon

500 MiB < 748 MiB;1300 + 500 = 1800 MiB

低于 2048 MiB,分配可能成功

应用新增 800 MiB anon

800 MiB > 748 MiB;1300 + 800 = 2100 MiB

若无法继续回收,可能进入 memcg OOM

这里使用的是理想化上限:inactive_file 是回收候选,不是保证可立即释放的余额。真实分配过程中还会发生并发增长、脏页写回和其他记账变化,因此不能用 748 MiB 当作绝对安全额度。

这就是“Cache 会计入 limit,但有些 Cache 又能被回收”的完整含义。OOM 的关键不是某个标签是否叫 Cache,而是回收之后能否完成新的分配。

6 真实环境怎样查看和计算占比

6.1 先确认命令在哪里执行

位置

适合执行的命令

注意事项

运维终端

kubectl describe、kubectl get、kubectl exec

需要 kubeconfig(集群连接配置)和相应 RBAC 权限

目标容器内

读取 /sys/fs/cgroup 下的文件

先核对 memory.max,确认看到的是目标容器账本

目标节点上

crictl inspect、/proc/PID/cgroup、journalctl -k

用于精确定位 cgroup 和查看内核日志,属于进阶排查

Prometheus/Grafana 查询框

第 7 章 PromQL

PromQL 不是 Linux Shell 命令

占位符不能原样复制

本文命令中带有 replace-with-... 或 REPLACE_WITH_... 的值都是占位符,执行前必须替换成真实值。不要把占位符原样复制到 Shell。

最短路径是在运维终端先设置三个变量,再进入指定容器。下列 payments、order-api-7d9f6d8c5b-k2x7p、api 是完整的虚构示例,实际执行时替换为本环境名称。

NS=payments
POD=order-api-7d9f6d8c5b-k2x7p
CTR=api
kubectl exec -it -n "$NS" "$POD" -c "$CTR" -- sh

进入目标容器后执行:

stat -fc %T /sys/fs/cgroup
cat /proc/self/cgroup
cat /sys/fs/cgroup/memory.max 2>/dev/null

cgroup v2 的一组常见输出如下。0::/ 表示在当前 cgroup namespace(隔离后的 cgroup 视图)中,进程看到自己位于根位置;2147483648 是字节,等于 2048 MiB,也就是 2 GiB。若 memory.max 输出 max,表示当前看到的这一层没有硬上限,需要检查 Pod 父层级或节点上的真实路径。

cgroup2fs
0::/
2147483648

6.2 再确认 cgroup v1 还是 v2

下面这条命令应在目标容器内或目标节点上执行,不能在无关的运维电脑上判断目标工作负载。

stat -fc %T /sys/fs/cgroup

输出

版本

cgroup2fs

cgroup v2

tmpfs,并存在 memory 子目录

通常为 cgroup v1 或混合模式

进阶:从目标节点精确定位 cgroup

仅当容器内文件不可见、memory.max 与 Kubernetes limit 不符,或需要检查父 cgroup 时使用。本步骤要求登录目标节点的主机 cgroup namespace,并已安装 crictl、jq 和 findmnt。CRI 标准没有统一、稳定的容器 PID 字段,下面的 .info.pid 是 containerd 等运行时的常见扩展;其他运行时必须先检查实际 inspect 输出。

先在运维终端列出节点名以及每个容器的运行时 ID。命令会显示 containerd:// 或 cri-o:// 前缀,后续只复制前缀后面的 ID。

NS=payments
POD=order-api-7d9f6d8c5b-k2x7p
kubectl get pod -n "$NS" "$POD" -o wide
kubectl get pod -n "$NS" "$POD" -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.containerID}{"\n"}{end}'

随后登录目标节点,将真实 ID 写入 CID。先查看运行时扩展信息,确认存在有效 PID;再从 /proc/PID/cgroup 取得相对路径,并通过 findmnt 获取实际 cgroup2 挂载点。下面是 containerd 常见输出结构下的 cgroup v2 示例。注意:/proc/PID/cgroup 可读只能证明该 PID 当前存在,不能完全排除运行时返回的陈旧 PID 已被系统复用;若 inspect 状态异常,应同时核对运行时 task 信息或 runtimeSpec 中的 cgroup 路径,身份不一致时必须停止,不能使用该 PID 下结论。

CID=replace-with-container-id-without-runtime-prefix
crictl inspect -o json "$CID" | jq '.info'
PID=$(crictl inspect -o json "$CID" |
  jq -er '.info.pid | tonumber | select(. > 0)') || {
  printf '未取得有效 PID;请检查运行时 inspect 输出或容器状态\n' >&2
  exit 1
}
test -r "/proc/$PID/cgroup" || {
  printf 'PID 已失效或容器已经退出:%s\n' "$PID" >&2
  exit 1
}
REL=$(awk -F: '$1=="0" {print $3; found=1; exit} END {exit !found}' \
  "/proc/$PID/cgroup") || {
  printf '未在 PID %s 的 /proc cgroup 中找到 cgroup v2 路径\n' "$PID" >&2
  exit 1
}
[ "$REL" != "/" ] || {
  printf 'PID %s 映射到 cgroup2 根层级;请确认处于节点主机 cgroup namespace\n' "$PID" >&2
  exit 1
}
MNT=$(findmnt -rn -t cgroup2 -o TARGET | awk 'NR==1 {print; exit}')
test -n "$MNT" || { printf '未找到 cgroup2 挂载点\n' >&2; exit 1; }
CG="${MNT%/}${REL}"
if [ ! -r "$CG/memory.current" ] || [ ! -r "$CG/memory.stat" ]; then
  printf '计算出的路径不是可读的目标 memory cgroup:%s\n' "$CG" >&2
  exit 1
fi
printf 'PID=%s\nCG=%s\n' "$PID" "$CG"
cat "$CG/memory.max"

6.3 cgroup v2:先直接看原始值

如果在同一个 Shell 中完成第 6.2 节,应直接沿用其中计算出的 CG,不要重新赋值。若单独执行本节,CG 必须填写包含真实 cgroup2 挂载点在内的完整绝对路径,不能默认挂载点一定是 /sys/fs/cgroup。下面的检查会在 CG 未设置或关键文件不可读时立即停止。

# 单独执行本节时:取消下一行注释,并替换为完整绝对路径
# CG=/REPLACE_WITH_ABSOLUTE_TARGET_CGROUP_PATH
: "${CG:?请先按第 6.2 节设置目标 cgroup 的完整绝对路径}"
for required in memory.current memory.max memory.stat; do
  [ -r "$CG/$required" ] || {
    printf '文件不可读:%s/%s\n' "$CG" "$required" >&2
    exit 1
  }
done
cat "$CG/memory.current"
cat "$CG/memory.max"
cat "$CG/memory.stat"
cat "$CG/memory.events.local"
cat "$CG/memory.events"
cat "$CG/memory.swap.current"

6.4 cgroup v2:一键计算主要占比

脚本在目标节点执行,沿用第 6.2 节计算出的 CG;独立执行时须填写完整绝对路径。脚本会验证关键文件;kernel 汇总字段缺失时显示 N/A,HugeTLB 和 swap 单独展示,不重复相加。

# 单独执行本节时:取消下一行注释,并替换为完整绝对路径
# CG=/REPLACE_WITH_ABSOLUTE_TARGET_CGROUP_PATH
: "${CG:?请先按第 6.2 节设置目标 cgroup 的完整绝对路径}"
for required in memory.current memory.max memory.stat; do
  [ -r "$CG/$required" ] || {
    printf '文件不可读:%s/%s\n' "$CG" "$required" >&2
    exit 1
  }
done
stat_value() {
  awk -v key="$1" \
    '$1==key {print $2; found=1; exit} END {if (!found) print 0}' \
    "$CG/memory.stat"
}
has_stat_key() {
  awk -v key="$1" '$1==key {found=1; exit} END {exit !found}' \
    "$CG/memory.stat"
}
current=$(cat "$CG/memory.current")
limit=$(cat "$CG/memory.max")
anon=$(stat_value anon)
file=$(stat_value file)
sock=$(stat_value sock)
inactive=$(stat_value inactive_file)
shmem=$(stat_value shmem)
kernel=0
kernel_available=0
if has_stat_key kernel; then
  kernel=$(stat_value kernel)
  kernel_available=1
fi
swap=$(cat "$CG/memory.swap.current" 2>/dev/null || printf 0)
hugetlb=0
for f in "$CG"/hugetlb.*.current; do
  [ -f "$f" ] || continue
  value=$(cat "$f")
  hugetlb=$((hugetlb + value))
done
awk -v c="$current" -v a="$anon" -v f="$file" -v k="$kernel" \
    -v ka="$kernel_available" -v o="$sock" -v i="$inactive" \
    -v s="$shmem" -v m="$limit" -v w="$swap" -v h="$hugetlb" 'BEGIN {
  printf "current: %.2f MiB\n", c/1048576
  if (c > 0) {
    printf "anon:    %.2f%%\n", 100*a/c
    printf "file:    %.2f%%\n", 100*f/c
    printf "sock:    %.2f%%\n", 100*o/c
    if (ka == 1) {
      printf "kernel:  %.2f%%\n", 100*k/c
      printf "difference: %.2f MiB\n", (c-a-f-k-o)/1048576
    } else {
      printf "kernel:  N/A (summary field unavailable)\n"
      printf "unexplained incl. kernel: %.2f MiB\n", (c-a-f-o)/1048576
    }
    printf "shmem: %.2f%% of current (included in file)\n", 100*s/c
  }
  printf "working set approx: %.2f MiB\n", (c-i>0?c-i:0)/1048576
  if (m == "max") {
    printf "current/limit: N/A (memory.max=max)\n"
  } else if ((m + 0) > 0) {
    printf "current/limit: %.2f%%\n", 100*c/m
  } else {
    printf "current/limit: N/A (memory.max=0)\n"
  }
  printf "swap.current: %.2f MiB\n", w/1048576
  printf "hugetlb controller total: %.2f MiB\n", h/1048576
}'
printf 'cgroup2 mount options: '
findmnt -no OPTIONS -T "$CG" 2>/dev/null || printf 'unavailable\n'

6.5 示例输出怎样逐行看

下面示例来自提供 kernel 汇总字段的较新内核,因此能计算四项占比;旧内核显示 kernel: N/A 时,应按上一节的提示解读。

current: 1600.00 MiB
anon:    56.25%
file:    34.38%
sock:     3.13%
kernel:   6.25%
difference: 0.00 MiB
shmem: 6.25% of current (included in file)
working set approx: 1300.00 MiB
current/limit: 78.13%
swap.current: 0.00 MiB
hugetlb controller total: 0.00 MiB
cgroup2 mount options: rw,nosuid,nodev,noexec,relatime
  • 先看 current/limit:78.13% 表示总记账已经使用硬限制的 78.13%。
  • 再看主要一级项:本例 anon 最高、file 次之;kernel 与 sock 分别统计。若 kernel 显示 N/A,只能说明当前内核未提供汇总字段,不能判定内核内存为 0。
  • difference:仅在 kernel 汇总字段存在时,表示 current 与这些主要项合计之间的差值;可能来自其他条件记账、批量更新或采样时刻,不要求永远为 0。
  • shmem:它用于拆解 file;本例为 current 的 6.25%,但不能再加到总量。
  • swap 与 HugeTLB:两者单独判断。memory_hugetlb_accounting 是 cgroup2 文件系统挂载选项,不是普通内核命令行参数。启用后,新发生并成功完成的 HugeTLB charge 会计入 memory.current,memory.stat 中也会出现 hugetlb;独立 hugetlb.<size>.current 可能显示同一批内存,不能再次相加。启用前已经使用的 HugeTLB 页不保证追溯补记,应结合 findmnt 显示的挂载选项和控制器文件确认。

6.6 进阶参考:cgroup v1 主要字段

以下示例仅在已经确认使用 cgroup v1 的目标容器内执行。/sys/fs/cgroup/memory 是常见挂载路径,但必须先验证关键文件可读,并将 memory.limit_in_bytes 与 Kubernetes limit 对照;若路径不存在或数值不符,说明容器内视图没有映射到目标账本,应停止使用该路径。登录目标节点时必须另行定位目标容器的绝对 cgroup v1 路径,不能直接读取控制器根目录。

CG=/sys/fs/cgroup/memory
for required in memory.usage_in_bytes memory.limit_in_bytes memory.stat; do
  [ -r "$CG/$required" ] || {
    printf '文件不可读:%s/%s\n' "$CG" "$required" >&2
    exit 1
  }
done
cat "$CG/memory.usage_in_bytes"
cat "$CG/memory.limit_in_bytes"
cat "$CG/memory.stat"
cat "$CG/memory.oom_control"
cat "$CG/memory.failcnt"

cgroup v1 常见字段是 rss、cache、total_rss、total_cache、total_inactive_file 等。memory.usage_in_bytes 是内核为高效读取提供的近似值;failcnt 表示达到限制相关的累计次数,不等于 OOM kill 次数。total_* 通常包含子层级,读取时必须确认目标层级。

6.7 在运维终端查看 Kubernetes 容器终止状态

以下 kubectl 命令在已经配置 kubeconfig 且有查询权限的运维终端执行;将示例变量替换为真实名称。

NS=payments
POD=order-api-7d9f6d8c5b-k2x7p
kubectl describe pod -n "$NS" "$POD"
kubectl get pod -n "$NS" "$POD" -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

先确认 lastState.terminated.reason 是否为 OOMKilled。若只看到 137,仍需继续查看 Pod Event、memory.events.local 的事发前后增量、祖先 cgroup 和内核日志。

7 进阶参考:Prometheus 中怎样看组成与占比

7.1 Usage 占 Limit

100 *
max by (cluster, namespace, pod, container) (
  container_memory_usage_bytes{container!="",container!="POD"}
)
/
max by (cluster, namespace, pod, container) (
  kube_pod_container_resource_limits{resource="memory",unit="byte"} > 0
)

这个比例回答“监控中的较宽口径总账水位有多高”。分子和分母都是字节,因此可以相除;但它是监控比率,不是 OOM 事件,也不是 Linux OOM Killer 的直接触发器。

7.2 Working Set 占 Limit

100 *
max by (cluster, namespace, pod, container) (
  container_memory_working_set_bytes{container!="",container!="POD"}
)
/
max by (cluster, namespace, pod, container) (
  kube_pod_container_resource_limits{resource="memory",unit="byte"} > 0
)

这个比例回答“扣除部分 inactive_file 后的工作集压力和风险水位有多高”。它适合预警,但不是 OOM 判决值。阿里云 Deployment 与单 Pod 面板的完整口径差异及原始查询见第 4.10 节。

7.3 RSS 与 Cache 各占 Usage 多少

RSS 占 Usage:

100 *
max by (cluster, namespace, pod, container) (
  container_memory_rss{container!="",container!="POD"}
)
/
max by (cluster, namespace, pod, container) (
  container_memory_usage_bytes{container!="",container!="POD"}
)

Cache 占 Usage:

100 *
max by (cluster, namespace, pod, container) (
  container_memory_cache{container!="",container!="POD"}
)
/
max by (cluster, namespace, pod, container) (
  container_memory_usage_bytes{container!="",container!="POD"}
)

7.4 剩余部分怎样理解

可以用 usage - rss - cache 得到一个“剩余量”帮助发现内核内存或统计口径差异,但不要直接把它命名为精确 kernel,因为 cAdvisor 指标在不同版本下可能存在包含关系和层级差异。要精确拆分 kernel、shmem、slab、sock,应采集 cgroup memory.stat。

7.5 OOM 事件

sum by (cluster, namespace, pod, container) (
  max by (cluster, namespace, pod, container, id) (
    increase(container_oom_events_total{container!="",container!="POD"}[10m])
  )
) > 0

仅在 id 标签存在且稳定、并且已确认差异只来自重复抓取标签时,才可先用 max 按同一容器实例 ID 做实用聚合,再对观察窗口内可能出现的多个实例求和。container_oom_events_total 的来源和语义受 cAdvisor、Kubernetes 版本与启动方式影响,并不直接等同于 memory.events.local:oom;无法读取所需内核事件时也可能不暴露或漏报。这个指标和 memory.events(.local) 才是事件线索,不能由 usage 或 Working Set 的数值转换得到。即使该指标增长,仍需结合容器终止原因、cgroup 事件和节点内核日志区分 memcg OOM 与节点全局 OOM。

8 占比应该怎么看:没有统一正常值,但有明确判断顺序

8.1 为什么不存在固定的 anon、file、kernel、sock 标准占比

Java API 服务可能以 anon 为主;文件扫描任务可能以 file 为主;共享内存程序可能出现较高 shmem;高连接网关可能出现较高 sock,或伴随 slab 增长。只按一个固定比例判断所有容器,会把正常工作负载误报为异常。

8.2 先看四到五个比例

比例

回答的问题

持续升高时优先检查

current / limit

总记账距离硬上限多近

整体容量、峰值和 limit 是否合理

anon / current

应用匿名内存占多少

堆、对象、线程、匿名 mmap、GC

file / current

文件类内存占多少

文件 I/O、Page Cache、tmpfs/shmem

kernel / current(字段存在时)

内核内存占多少

slab、页表、内核栈和其他内核对象;字段缺失时记 N/A

sock / current

网络套接字内存占多少

连接数、吞吐、发送接收积压和 socket 配置

8.3 再看 file 内部

  • inactive_file 较高:通常说明存在较多非活跃文件页候选,但不是回收保证。
  • active_file 较高:说明文件页近期更活跃,工作负载可能依赖文件缓存。
  • shmem 较高:检查 /dev/shm、tmpfs、IPC 和 memory emptyDir;这部分不能按普通干净 Page Cache 理解。
  • file_dirty/writeback 较高:检查磁盘写入速度和脏页回写是否成为回收瓶颈。

8.4 可作为监控起点的 limit 比例

8.5 一眼判断表

看到的现象

先怀疑什么

下一步看什么

anon 占比高且单调上涨

应用内存或匿名 mmap 增长

GC、Heap、线程、进程映射

file 高、inactive_file 高、working set 不高

大量可回收倾向的文件缓存

I/O 模式、回收压力,不要立即判泄漏

file 高、shmem 高、inactive_file 不高

tmpfs/shared memory 占用

/dev/shm、emptyDir、IPC 生命周期

kernel 高且继续增长

slab、页表、内核栈等内核对象

拆分 kernel 明细并检查线程、映射和内核对象

sock 高且继续增长

网络发送接收缓冲或积压

连接数、吞吐、队列和 socket 配置

working set 未到 limit 却 OOM

突发分配、回收不足或采样遗漏

memory.events.local、内核日志、峰值采样

9 memcg OOM 是怎样一步一步发生的

9.1 完整流程

应用申请一块新内存
        ↓
memcg 尝试把新内存计入该 cgroup
        ↓
记账接近或将超过 memory.max
        ↓
Linux 尝试回收可回收页面,并按配置尝试换出
        ↓
回收足够:记账和分配成功,程序继续运行
回收不足:进入 memcg OOM 状态,oom 可能增加
        ↓
├─ 未发生进程终止:oom_kill 不增加
└─ OOM Killer 选择 victim:victim 所属 memcg 的 oom_kill 增加,并发送 SIGKILL
        ↓
若容器主进程被终止,运行时可记录 OOMKilled
        ↓
Kubernetes 根据 restartPolicy 决定是否重启容器

关键边界是:oom 记录触发 OOM 的 OOM domain(限制层级)进入 OOM 状态,oom_kill 记录 victim 实际所属 memcg 中发生的 OOM 杀进程动作。两者可以不在同一个 cgroup 增长,也可以不同时增加,因此不能把“进入 memcg OOM”直接等同于“同一层级一定出现 oom_kill”或“容器一定被杀死”。

9.2 为什么 Cache 算进 limit,却又可能不造成 OOM

因为“记账”和“能否回收”是两个不同问题。Page Cache 占用了真实物理内存,所以必须记账;干净文件页又可以在压力下丢弃,所以内核会先回收。若回收速度足够,容器继续运行;若无法回收、来不及回收或申请量仍然太大,就可能 OOM。

9.3 为什么 kubectl top 没到 limit 也可能 OOM

  • kubectl top 通常展示 working set,已经扣除了 inactive_file。
  • Prometheus 有抓取间隔,可能漏掉短时间峰值。
  • 突发匿名内存可能大于可及时回收的文件页。
  • tmpfs/shmem、脏页、内核内存不等于随时可释放。
  • 容器也可能在节点全局 OOM 中被选中,而不是达到自身 limit。

10 三种内存故障必须分开

事件

通俗解释

容器是否必须达到自己的 limit

典型表现

主要证据

memcg OOM

容器或上级 cgroup 自己的额度不够

通常涉及对应层级限制

oom 增长;实际 kill 后才可能 OOMKilled

事件计数增量、限制层级、memcg 内核日志

节点全局 OOM

整台机器内存不够

不必须

进程被 OOM Killer 杀死

节点全局 OOM 日志、节点内存曲线

kubelet Eviction

kubelet 发现节点压力后主动驱逐 Pod

不必须

Pod Reason=Evicted

Pod Event、kubelet 日志、MemoryPressure

10.1 memory.events.local 怎么看

[目标容器内或目标节点的精确 cgroup 路径] 先按第 6 章设置 CG。memory.events.local 中的数字都是 counter(累计只增型计数器):它们从 cgroup 创建开始累加,不是当前状态,也不是某一秒内的次数。必须保存事发前基线,再比较事发后的增量。

cat "$CG/memory.events.local"
low 0
high 15
max 8
oom 2
oom_kill 1
oom_group_kill 0

字段

含义

能否单独证明容器限额 OOM

high

触发 memory.high 节流或回收的累计次数

不能

max

达到 memory.max 相关边界的累计次数

不能等同于 OOM kill

oom

该 OOM domain/限制层级进入 OOM 状态的累计次数

不能说明一定发生 kill

oom_kill

victim 实际所属 memcg 中的进程被任一种 OOM Killer 杀死的累计次数

不代表该层自身 memory.max 一定是触发源;仍需日志区分 memcg 与全局 OOM

oom_group_kill

因 memory.oom.group 执行整组 OOM kill 的累计次数

需要结合配置、终止状态和日志

10.2 必须看增量,而不是只看数字是否大于 0

[目标容器内或目标节点] 确认 CG 已指向目标 cgroup 后执行。下面示例要在观察窗口前后各读取一次,中间不要让 CG 指向另一个已重建的 cgroup。

before_oom=$(awk '$1=="oom" {print $2}' "$CG/memory.events.local")
before_kill=$(awk '$1=="oom_kill" {print $2}' "$CG/memory.events.local")
# 事件发生后再次读取
after_oom=$(awk '$1=="oom" {print $2}' "$CG/memory.events.local")
after_kill=$(awk '$1=="oom_kill" {print $2}' "$CG/memory.events.local")
printf 'oom delta=%s\noom_kill delta=%s\n' \
  "$((after_oom-before_oom))" "$((after_kill-before_kill))"

例如 oom 原来就是 2,故障后仍为 2,不能把历史累计值当成本次证据;只有增量才说明观察窗口内发生了新事件。这个脚本只比较同一个 CG,不代表 oom 与 oom_kill 必须同时在该层增长;祖先限制场景必须按第 10.3 节沿层级检查。若事发前没有保存基线,可结合 cgroup 创建时间、监控 counter 的 increase()、容器重启时间和内核日志建立时间关联。

10.3 local、层级事件和祖先 cgroup

  • memory.events.local:只统计当前精确 cgroup 层级的本地事件。
  • memory.events:默认包含从子层级传播上来的层级事件,适合观察整棵子树;但若 cgroup2 使用 memory_localevents 挂载选项,它会改为本地语义。可用 findmnt -no OPTIONS -T "$CG" 检查挂载选项。
  • 祖先限制:容器子 cgroup 未设置硬限制时,Pod 父 cgroup 或更高层仍可能达到 memory.max。此时应沿路径向上逐级读取 memory.max 和 memory.events.local。

因此,单独看到当前容器的 oom_kill=0,不能在尚未检查父层级和内核日志时排除 memcg 问题;单独看到 oom_kill 增长,也只能说明该层中的进程成为 victim,既不能证明该层自己的 memory.max 是触发源,也不能排除节点全局 OOM。

11 四个典型占比案例

11.1 案例一:应用匿名内存持续增长

limit

current

anon

file

kernel

sock

2048 MiB

1900 MiB

1650 MiB(86.84%)

150 MiB(7.89%)

80 MiB(4.21%)

20 MiB(1.05%)

判断:anon 占绝对多数并持续上涨,优先查应用堆、堆外内存、线程和匿名 mmap。此类场景通常不能指望 Page Cache 回收解决根因。

11.2 案例二:普通文件缓存较高

limit

current

anon

file

kernel

sock

inactive_file

2048 MiB

1900 MiB

600 MiB(31.58%)

1200 MiB(63.16%)

70 MiB(3.68%)

30 MiB(1.58%)

900 MiB

判断:file 高且 inactive_file 很高,working set 约为 1000 MiB。可能是大文件扫描、备份或大量读取造成的缓存,不应仅凭 usage 高就判定应用泄漏;仍需看回收压力和后续匿名分配。

11.3 案例三:shmem/tmpfs 占用

limit

current

anon

file

shmem

kernel

sock

inactive_file

2048 MiB

1800 MiB

700 MiB

1000 MiB(55.56%)

800 MiB(44.44% of current,已包含在 file)

80 MiB

20 MiB

100 MiB

判断:file 看似很高,但其中 800 MiB 是 shmem,不能把它当成普通可丢弃 Page Cache。应检查 /dev/shm、tmpfs、memory emptyDir 和 IPC 数据清理。

11.4 案例四:kernel 与 sock 必须分开看

limit

current

anon

file

kernel

sock

2048 MiB

1800 MiB

1000 MiB(55.56%)

300 MiB(16.67%)

350 MiB(19.44%)

150 MiB(8.33%)

判断:kernel 和 sock 都明显高于该容器自身历史基线。kernel 应继续拆分 slab、pagetables、kernel_stack 等;sock 应单独检查连接数、网络积压、吞吐和 socket 配置。二者不能混成一个 kernel 数值,也不能只调小应用 Heap。

12 一套从零开始的 OOM 排查步骤

12.1 第一步:确认看到的是 OOMKilled 还是 Evicted

[运维终端] 下面使用一组虚构名称,执行时替换变量值:

NS=payments
POD=order-api-7d9f6d8c5b-k2x7p
kubectl describe pod -n "$NS" "$POD"
kubectl get events -n "$NS" --field-selector involvedObject.name="$POD" --sort-by=.lastTimestamp

12.2 第二步:记录四个总量

  • memory.current 或 container_memory_usage_bytes。
  • memory.max 或 Kubernetes memory limit。
  • container_memory_working_set_bytes。
  • OOM 前后的峰值、增长速度和采样间隔。

12.3 第三步:计算主要一级项占比

anon / current × 100%
file / current × 100%
sock / current × 100%
kernel / current × 100%  # 仅当 memory.stat 存在 kernel
current / limit × 100%

先检查 memory.stat 是否存在 kernel。存在时可计算 difference = current - anon - file - kernel - sock;不存在时必须把 kernel 标为 N/A,current - anon - file - sock 只能称为“包含未汇总 kernel 在内的未解释部分”。差值较大时,应检查已有内核明细、HugeTLB 等条件性记账、cgroup 层级、字段采样时刻和内核实现,而不是直接把差值命名为某一种固定内存。

12.4 第四步:继续拆分异常大项

  • anon 高:查 Heap、GC、线程、匿名 mmap。
  • file 高:查 inactive_file、active_file、shmem、dirty/writeback。
  • kernel 高(字段存在时):查 slab、pagetables、kernel_stack、percpu、vmalloc;字段缺失时只能分别观察已有明细。
  • sock 高:查连接数、吞吐、发送接收积压和 socket 配置。

12.5 第五步:确认是哪一种 OOM

[目标节点] 查看内核与 kubelet 日志:

journalctl -k --since "-1h" | grep -Ei 'oom|out of memory|killed process|memory cgroup'
journalctl -u kubelet --since "-1h"

[运维终端] 查看 Kubernetes 节点状态;将 NODE 替换为第 6.1 节 kubectl get pod -o wide 显示的节点名:

NODE=replace-with-target-node-name
kubectl describe node "$NODE"

memory.events.local:oom_kill 的增量能够证明观察窗口内该层中的某个进程成为 OOM Killer 的 victim,但不能证明该层自己的 memory.max 是触发源,也不能单独区分 memcg OOM 与节点全局 OOM。应沿父级逐层检查:oom 可能增长在触发限制的 OOM domain,而 oom_kill 可能增长在 victim 所属的容器子 cgroup;最后结合内核日志中的 Memory cgroup out of memory 或系统级 Out of memory 信息定责。

12.6 第六步:决定怎么修

根因

优先措施

anon/应用内存增长

修复泄漏、调整 Heap/并发/线程、为堆外和峰值留余量

普通 Page Cache 高

评估 I/O 模式和回收压力,不要仅为降低 usage 强行清缓存

shmem/tmpfs 高

限制容量、清理临时数据、修复 IPC 生命周期

kernel 高

检查 slab、页表、内核栈等内核对象和相关进程行为

sock 高

检查连接、网络积压、吞吐和 socket 配置

节点全局 OOM

增加节点容量、修正 requests/limits、系统预留和 Pod 分布

kubelet Eviction

调整节点容量、驱逐阈值、requests 和调度布局

13 最容易误解的十句话

错误理解

正确理解

memcg OOM 是一种内存,占比需要计算

memcg OOM 是状态或事件;计算的是 OOM 时 anon、file、sock,以及字段存在时的 kernel 占比

容器总内存就是进程 RSS

cgroup 还会记账文件类内存、内核内存、sock 和其他条件项

file + shmem 才是文件类总内存

shmem 已包含在 file 中,不能重复相加

active_file + inactive_file 一定等于 file

它们是文件页 LRU 状态;file 还包含可能位于匿名页 LRU 的 shmem 等内容

sock 已包含在 kernel,不用单独看

cgroup v2 memory.stat 通常把 sock 与 kernel 分开列为一级统计项

Cache 不算 memory limit

归属于 cgroup 的 Cache 会占用 limit

Cache 算进 limit,所以超过就一定 OOM

内核会先尝试回收;回收后仍不足才可能进入 OOM

进入 memcg OOM 就一定会杀进程

oom 与 oom_kill 是两个计数;进入 OOM 状态不保证发生 kill

Working Set 就是 RSS

Working Set 通常是 usage 扣除 inactive_file 的派生近似

Exit Code 137 就是 OOM

137 只证明 SIGKILL,需要 terminated.reason、事件增量和日志定责

14 最终总结:只按这张表看即可

要回答的问题

应该看什么

不要误用什么

当前总共用了多少

memory.current 与 container_memory_usage_bytes

只看进程 RSS,或把两个字段当成严格同一个值

总记账由谁组成

anon、file、sock,以及字段存在时的 kernel 占 current 比例

遗漏 sock、把缺失 kernel 当成 0,或将 shmem、slab 等子项重复相加

工作集近似值多少

working set = max(usage - inactive_file, 0)

把 working set 当内核硬限制或 OOM 直接判定值

总账水位和风险水位分别多高

usage/limit 看总账;working_set/limit 看扣除部分 inactive_file 后的风险水位

把 Working Set 水位当成硬限制剩余额度

是否进入 OOM/发生 Kill

oom 与 oom_kill 的事发前后增量、终止原因、内核日志

只看累计值、usage/Working Set 或退出码 137

是哪一种内存故障

精确及祖先 cgroup 事件、memcg 日志、节点 OOM 日志、Evicted Event

把所有情况都叫 Kubernetes OOM

15 参考资料

[1] 阿里云:为什么在容器中得到的内存值不一致?

[2] Linux Kernel:Control Group v2

[3] Linux Kernel:Memory Resource Controller(cgroup v1)

[4] Kubernetes:Resource Management for Pods and Containers

[5] Kubernetes:Node-pressure Eviction

[6] Kubernetes:Pod Quality of Service Classes

[7] cAdvisor:Working Set 计算说明

[8] Linux Kernel 5.15:Control Group v2

[9] Prometheus:Operators and aggregation

[10] Thanos Querier:Replica deduplication

[11] Kubernetes:Debugging nodes with crictl

[12] Linux 源码:memcontrol.c(kernel 与 sock 记账)

[13] cAdvisor:容器 Usage 与 Working Set 计算源码

[14] cAdvisor:Prometheus 指标导出源码

[15] Linux Kernel:cgroup v2 memory.current 与 memory.max 定义

[16] Prometheus:clamp_max 函数

posted @ 2026-09-14 20:45  小家电维修  阅读(7)  评论(0)    收藏  举报