
容器被标记为 `OOMKilled` 时，节点可能仍有很多可用内存；Pod 显示 `Evicted` 时，应用也可能从未超过自己的 memory limit。这两种现象来自不同的执行者：Linux 内核负责内存分配与 OOM，kubelet 负责根据节点压力主动驱逐 Pod。

在 `zj` 的隔离 kind 集群中，一个 memory limit 为 `32Mi` 的容器尝试分配 `64Mi` 内存，退出原因是 `OOMKilled`、退出码为 `137`，容器打印的 `memory.max` 为 `33554432`。这个实验不需要耗尽节点内存，也不需要由 kubelet 先作出驱逐决定。

<!--more-->

## 从三个不同的失败入口判断

| 现象 | 作出决定的组件 | 主要依据 | 对象层面的结果 |
| --- | --- | --- | --- |
| 容器 cgroup OOM | Linux 内核 | cgroup 的内存限制、使用量与回收结果 | 容器进程被杀，运行时可能报告 `OOMKilled` |
| 节点全局 OOM | Linux 内核 | 节点内存分配失败、回收结果与进程 OOM 分数 | 被选择的进程被杀，可能影响业务或节点组件 |
| 节点内存压力驱逐 | kubelet | `memory.available` 与配置的驱逐阈值 | Pod 被终止并标记为 `Failed` / `Evicted` |

退出码 `137` 常见于进程因 `SIGKILL` 结束，不能独立证明 OOM。人工强杀、超时后强制终止也可能得到这个退出码，应用还可以主动返回相同数值。排障时需要同时查看容器的 `state.terminated` 或 `lastState.terminated`、节点内核日志和 kubelet 事件。只看到 `OOMKilled`，也不能据此断定是容器限制还是祖先 cgroup、节点层面的内存耗尽。

`Evicted` 位于 Pod 的 `status.reason`；`OOMKilled` 通常位于某个容器的终止状态。它们不是同一字段的两个可互换取值。一个 Pod 可能经历过容器 OOM，后来又因节点压力被驱逐，需要按 UID、容器 ID 和时间分别理解。

## request、limit 与 QoS 各自负责什么

memory request 参与调度容量核算，帮助 scheduler 判断节点还能承诺多少资源。它不会在进程开始运行时预先分配同样大小的物理内存。memory limit 则经 kubelet、CRI 和容器运行时转换为 Linux cgroup 的限制。

在 [v1.35.1 的 `generateLinuxContainerResources` 与 `calculateLinuxResources`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L275-L298) 中，kubelet 读取容器的 memory limit，填写 CRI `LinuxContainerResources.MemoryLimitInBytes`，并设置 `OomScoreAdj`。运行时负责把这些参数落实到 cgroup 与进程。使用 cgroup v2 时，`memory.max` 是观察这条配置链是否生效的直接入口。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L275-L298" >}}

`generateLinuxContainerResources` 调用这段资源转换后，另行写入进程 OOM 分数。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/kuberuntime/kuberuntime_container_linux.go#L136-L146" >}}

达到 limit 不意味着内核按一个固定使用率立即杀死进程。内核会尝试回收内存；无法在限制内满足分配时才可能触发 cgroup OOM。计量还包括相关的页缓存等内存，不能只用应用报告的堆大小与 limit 比较。具体内核语义见 [cgroup v2 内存控制器文档](https://docs.kernel.org/admin-guide/cgroup-v2.html#memory)。

传统的容器级资源配置下，QoS 分为：

- `Guaranteed`：所有容器都设置非零 CPU、memory request 与 limit，且两类资源各自的 request 等于 limit。
- `BestEffort`：所有容器都没有 CPU、memory request 与 limit。
- `Burstable`：其余情况。例如只设置 memory request 与 limit，即使两者相等，也不满足上述 `Guaranteed` 条件。

分类实现见 [`ComputePodQOS`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/apis/core/v1/helper/qos/qos.go#L151-L174)。本文实验不配置 Pod 级 `spec.resources`，也不启用 MemoryQoS；不能把这里的容器级判断直接套到所有 feature gate 组合。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/apis/core/v1/helper/qos/qos.go#L151-L174" >}}

QoS 会影响 OOM 保护程度，但 `Guaranteed` 不代表进程永远不会被杀。它仍可能触发自身 cgroup 的内存限制，也无法凭一个分类消除节点故障。

## 全局 OOM 与 kubelet 驱逐使用不同的排序

[`GetContainerOOMScoreAdjust`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/qos/policy.go#L45-L86) 为普通 `Guaranteed` 容器设置 `oom_score_adj=-997`，为 `BestEffort` 设置 `1000`，对 `Burstable` 则结合 memory request 与节点内存容量计算并限定范围。内核再结合进程实际占用等因素选择 OOM victim。它不是按 Pod QoS 名称调用 Kubernetes 的排序器。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/qos/policy.go#L45-L86" >}}

kubelet 的内存压力驱逐是另一条路径。[`synchronize`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/eviction_manager.go#L369-L399) 从节点统计读取压力信号，判断阈值及 soft threshold 的持续时间，再选择需要回收的资源。通用框架会调用 `reclaimNodeLevelResources`，但本版本的 `buildSignalToNodeReclaimFuncs` 只为文件系统信号注册容器或镜像垃圾回收，没有为内存信号注册回收函数。因此内存路径在这个调用中直接返回 false，继续对候选 Pod 排序；不能画成 kubelet 先执行一轮内存回收，再检查是否需要驱逐。内核在内存分配路径中的回收是另一套机制。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/eviction_manager.go#L369-L399" >}}

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/helpers.go#L1202-L1235" >}}

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/eviction_manager.go#L473-L504" >}}

[`rankMemoryPressure`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/helpers.go#L819-L821) 的核心排序是：

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/helpers.go#L819-L821" >}}

它依次比较是否超过 memory request、Pod 优先级、相对于 request 的内存使用量。因此，“先 BestEffort，再 Burstable，最后 Guaranteed”只能帮助理解某些常见结果，不能替代这段实际判断。一个超过 request 的高优先级 Pod，与一个未超过 request 的低优先级 Pod，不能仅按优先级比较。

```mermaid
flowchart TB
    A[内存分配或节点统计变化] --> B{谁发现问题}
    B -->|Linux 内核| C{限制或全局内存分配失败}
    C --> D[尝试回收内存]
    D --> E{回收后能否满足分配}
    E -->|不能| F[OOM 选择并终止进程]
    E -->|能| G[继续运行]
    B -->|kubelet| H[检查 memory.available 与驱逐阈值]
    H --> I{达到阈值及必要的持续时间}
    I -->|是| M[内存信号没有节点级回收函数 直接排序候选 Pod]
    M --> K[终止选中的 Pod 并记录 Evicted]
    I -->|否| G
    style C fill:#fff3df,color:#402800,stroke:#a96b00
    style I fill:#e3f0ff,color:#102d50,stroke:#386da6
```

图中的两条路径可以竞争发生。kubelet 需要采样和执行时间，突增的内存分配可能让内核先触发 OOM；不能把驱逐阈值当作“保证节点永远不发生 OOM”的屏障。hard eviction 使用零秒宽限；soft eviction 使用 `evictionMaxPodGracePeriod` 约束宽限，不能一律理解为应用都有完整的 `terminationGracePeriodSeconds`。节点压力驱逐由 kubelet 本地发起，不经过 Eviction API 的 PDB 预算检查；PDB 不能阻止这种驱逐。具体宽限值的选择见 `synchronize` 中终止 Pod 的分支。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/eviction_manager.go#L414-L439" >}}

## 一个不耗尽节点内存的实验

实验使用 kind v0.33.0、Kubernetes v1.35.1（提交 `8fea90b45245ef5c8ba54e7ae044d3e777c22500`）、containerd v2.2.1，运行在 Linux 主机 `zj`。分配程序最多尝试申请 `64Mi`，容器限制为 `32Mi`，使用 `restartPolicy: Never` 保留一次退出状态，不形成持续分配循环。

配置、校验脚本、证据和复现说明见 [k8sdev/article-resources-memory](https://github.com/jimyag/k8sdev/tree/3cfa654905a750b1716de8c32aa42d7573220954/article-resources-memory)。先按该目录 README 的固定提交准备步骤创建独立工作目录，再运行实验。

实验脚本同时检查 cgroup 配置与容器终止状态，关键断言如下。

{{< github-file url="https://github.com/jimyag/k8sdev/blob/3cfa654905a750b1716de8c32aa42d7573220954/article-resources-memory/run.py#L28-L38" >}}

实际记录为：

```text
memory.max=33554432
oom_score_adj=999
qosClass: Burstable
terminated.reason: OOMKilled
terminated.exitCode: 137
```

这里没有给容器配置 CPU request 与 limit，所以 QoS 是 `Burstable`。节点容量远大于 `16Mi` memory request，计算后的 `oom_score_adj` 接近上界。本次进程因自身 cgroup 限制退出，不能用这个实验声称已验证全局 OOM 的 victim 排序。

脚本同时检查终止原因与退出码，不把“Pod 不再 Running”当作成功条件。它也不能证明 kubelet 的压力驱逐顺序：本轮没有制造节点内存压力，相关结论来自上述固定版本源码。

## 退出之后，容器重启与 Pod 重建仍要分开

容器 OOM 后，kubelet 根据适用的重启策略决定是否重新启动容器。默认 `Always` 的普通工作负载通常会重启该容器，Pod UID 可以保持不变；反复失败可能进入退避。本文使用 `Never`，所以保留终止结果。启用容器级重启规则时，还应检查容器自身的配置。

Pod 被驱逐后，原 Pod 已失败，`restartPolicy` 不会让它在另一台节点上原地恢复。Deployment、StatefulSet 或 Job 等控制器可能创建替代 Pod，替代对象有新的 UID；直接创建的独立 Pod 没有这层副本恢复保证。

判断一次内存故障时，应把容器限制、节点压力、QoS、优先级和控制器恢复分别对齐到证据：先确认被终止的对象，再判断谁作出了终止决定，最后解释谁负责恢复。提高 memory limit 只能改变其中一层约束，不能替代对内存泄漏、节点容量或不合理 request 的检查。

