Kubernetes_OOM通俗详解_内存组成_指标公式与直观案例
Kubernetes OOM 通俗详解
先看懂内存账单和直观数值,再计算占比,最后定位 OOM
从内存组成、占比计算到容器为何被终止
先直接回答:为什么还剩 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 或上一个采样点;下一次申请还可能大于真实剩余额度。 |
| |
为什么图上可能永远没到 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 MiB4.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% 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/nullcgroup v2 的一组常见输出如下。0::/ 表示在当前 cgroup namespace(隔离后的 cgroup 视图)中,进程看到自己位于根位置;2147483648 是字节,等于 2048 MiB,也就是 2 GiB。若 memory.max 输出 max,表示当前看到的这一层没有硬上限,需要检查 Pod 父层级或节点上的真实路径。
cgroup2fs
0::/
21474836486.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))
doneawk -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=.lastTimestamp12.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 参考资料
[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
[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 指标导出源码

浙公网安备 33010602011771号