
一个 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 通常可以看到这些文件：

```text
memory.current   当前使用量，包含该 cgroup 的后代
memory.max       硬上限，容器的 Memory Limit 通常会写到这里
memory.stat      当前使用量的分类统计
memory.events    low/high/max/oom/oom_kill 等事件计数
memory.pressure  PSI，记录进程因为内存而等待的时间
```

`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 官方文档](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/)对此有完整说明。

`memory.events` 中的 `high` 计数增加，通常说明触发过 `memory.high` 的处理；`max` 记录触及 `memory.max` 的次数；`oom` 和 `oom_kill` 则用于进一步判断是否发生了 OOM。它们的语义不同，不能只看到 `high` 就认定容器已经 OOM。

### Working Set 为什么和 RSS 不一样

cAdvisor 暴露的 Working Set 可以近似理解为：

```text
Working Set ≈ memory.current - inactive_file
```

在 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。

这条路径的关键点是：

1. 比较的是 `memory.current` 和 `memory.max`，不是 Working Set 和 `container_spec_memory_limit_bytes` 的简单视觉比较。
2. `memory.current` 到达上限并不等于立刻 Kill，内核还会尝试回收页面。
3. 节点整体还有很多空闲内存，也不妨碍某个容器触发自己的 Limit。
4. `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 节点上的内存信号可以近似写成：

```text
memory.available = node.status.capacity[memory] - node.stats.memory.workingSet
```

kubelet 会把这个值和 `evictionHard` 或 `evictionSoft` 比较。由于节点 Working Set 会保留 active file cache，`node_memory_MemAvailable_bytes` 看起来还有余量时，kubelet 仍然可能认为节点已接近内存压力。

驱逐哪个 Pod，主要会看：

1. Pod 的实际使用量是否超过 Request；
2. Pod Priority；
3. 使用量相对 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](https://kubernetes.io/docs/reference/instrumentation/node-metrics/)。

## 4. 一条可复用的排查路径

### 第一步：先确认故障类型

```bash
kubectl get pod <pod> -n <namespace> \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

kubectl describe pod <pod> -n <namespace>
```

重点区分：

- `reason: OOMKilled`：某个进程被 Kill，仍需继续判断是 memcg OOM 还是 global OOM；
- `status: Failed`、`reason: Evicted`：kubelet 驱逐了 Pod；
- 只有重启次数增加但没有留下明确状态：可能是短时尖峰，也可能是历史状态已被后续重启覆盖。

`OOMKilled` 只说明容器最终观察到的退出原因，不携带完整的 OOM 触发路径。不要把它直接等同于「超过了这个容器的 Memory Limit」。

### 第二步：检查节点事件和内核证据

先找到 Pod 所在节点：

```bash
kubectl get pod <pod> -n <namespace> \
  -o jsonpath='{.spec.nodeName}{"\n"}'
```

然后检查节点事件：

```bash
kubectl get events --field-selector involvedObject.kind=Node,involvedObject.name=<node> \
  --sort-by=.lastTimestamp

kubectl get events -A --field-selector reason=SystemOOM \
  --sort-by=.lastTimestamp
```

`SystemOOM` 可以作为 global OOM 的强证据；没有这个 Event 不能反向证明一定是 memcg OOM，因为事件记录还依赖 kubelet 对内核日志的观察和解析。必要时应该同时查看节点上的内核日志：

```bash
dmesg -T | grep -E 'oom-kill|Out of memory|Memory cgroup out of memory'
```

日志中出现 `oom_memcg=`、`Memory cgroup out of memory`，通常指向 memcg OOM；出现 `constraint=CONSTRAINT_NONE` 或 `global_oom`，通常指向节点级 global OOM。不同内核版本的日志格式可能略有变化，最终以 `memory.events`、Pod 状态和节点现场互相印证。

### 第三步：直接看 cgroup v2 的账本

先在节点上确认 cgroup 版本：

```bash
stat -fc %T /sys/fs/cgroup/
# cgroup2fs 表示 cgroup v2
```

如果输出是 `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 后，优先读取：

```bash
cg=/sys/fs/cgroup/<container-cgroup>

cat "$cg/memory.current"
cat "$cg/memory.max"
cat "$cg/memory.events"
grep -E '^(anon|file|shmem|active_file|inactive_file|slab) ' "$cg/memory.stat"
```

没有设置 Memory Limit 的容器，其 `memory.max` 通常是 `max`；对应的 `container_spec_memory_limit_bytes` 可能以 `0` 表示无限制。对这类容器直接计算「使用量 / Limit」没有意义，也不能因为它没有触发自己的 memcg OOM，就认为它对节点没有风险。

可以按下面的顺序解释结果：

1. `memory.current` 是否接近或超过 `memory.max`；
2. `memory.events` 中 `oom_kill` 是否增加；
3. `anon` 是否增长，判断是否更像应用匿名内存问题；
4. `file`、`active_file` 或 `shmem` 是否增长，判断是否更像文件缓存、tmpfs 或共享内存问题。

容器退出后，容器 cgroup 可能已经被清理，此时不能依赖现场文件恢复全部信息。应该提前采集 `memory.events`、节点级 `node_vmstat_oom_kill`、Pod 的最后退出状态，并保留足够细的 Prometheus 时间序列。

### 第四步：用 PromQL 看趋势，而不是只看瞬时值

查看 Pod 的 Working Set：

```promql
sum by (namespace, pod) (
  container_memory_working_set_bytes{
    container!="",
    image!=""
  }
)
```

查看匿名页和文件缓存的组成：

```promql
sum by (namespace, pod) (
  container_memory_rss{container!="", image!=""}
)

sum by (namespace, pod) (
  container_memory_cache{container!="", image!=""}
)
```

如果已经采集 kube-state-metrics 的资源请求，可以把 Working Set 和 Request 放在一起比较：

```promql
sum by (namespace, pod) (
  container_memory_working_set_bytes{container!="", image!=""}
)
/
sum by (namespace, pod) (
  kube_pod_container_resource_requests{
    resource="memory",
    unit="byte"
  }
)
```

实际环境中要检查两侧的标签和采集范围，避免把 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 内存排障最重要的不是背下某个指标名称，而是把「测量对象、统计范围、回收语义和触发动作」对应起来：

1. `container_memory_working_set_bytes` 用于观察工作集，适合看 `kubectl top` 和节点压力；
2. `memory.current` 与 `memory.max` 决定 cgroup 限制路径是否可能触发 memcg OOM；
3. 节点的 Working Set 和 kubelet 的 `memory.available` 决定 Eviction 视角的压力；
4. Memory Request 影响调度和节点压力下的选择，但不等于 Memory Limit。

当这些边界明确后，`OOMKilled`、`Evicted`、Working Set 增长和 RSS 不一致就不再是互相矛盾的现象，而是可以沿着不同证据路径分别验证的问题。

## 参考

- 原始阅读材料：[Kubernetes のメモリ使用量と cgroup v2 の関係: OOM / Eviction の切り分けと Request の適正化](https://qiita.com/yosshi_/items/46d8425b188675c102e5)
- [About cgroup v2 - Kubernetes](https://kubernetes.io/docs/concepts/architecture/cgroups/)
- [Resource Management for Pods and Containers - Kubernetes](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
- [Node-pressure Eviction - Kubernetes](https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/)
- [Pod Quality of Service Classes - Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/)
- [Resource metrics pipeline - Kubernetes](https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/)
- [kubectl top - Kubernetes](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_top/)
- [Control Group v2 - Linux kernel documentation](https://docs.kernel.org/admin-guide/cgroup-v2.html)
- [Concepts overview - OOM killer - Linux kernel documentation](https://docs.kernel.org/admin-guide/mm/concepts.html#oom-killer)

