4. cgroup OOM 控制:cgroup v1 的 memory.oom_control、cgroup v2 的 memory.oom.group、容器组 OOM 行为
当容器内存使用超过 cgroup 限制时,内核必须决定“杀谁”。cgroup v1 和 v2 提供了不同的 OOM 控制策略,直接影响容器组(Pod)内进程的存活概率。本章深入分析两种版本的 OOM 控制机制,并给出生产环境的最佳配置。
4.1 cgroup v1:memory.oom_control 与 OOM 通知
在 cgroup v1 中,每个 memory cgroup 都有一个 memory.oom_control 文件,控制 OOM 行为并暴露状态。
| 参数 | 类型 | 说明 |
|---|---|---|
oom_kill_disable |
0 / 1 | 0=启用 OOM Killer(默认),1=禁用 OOM Killer |
under_oom |
0 / 1 | 只读,表示当前是否处于 OOM 状态 |
oom_kill |
整数 | 只读,累计 OOM 杀进程次数 |
关键行为:当
oom_kill_disable = 1 时,cgroup 不会触发 OOM Killer,而是让进程进入不可中断的睡眠(D 状态),直到内存被释放或手动干预。这通常用于保护关键进程不被误杀,但可能导致系统级死锁。
典型配置示例:
# 查看当前 OOM 控制状态
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.oom_control
# 输出示例:oom_kill_disable 0 under_oom 0 oom_kill 0
# 禁用 OOM Killer(保护该容器内的进程不被内核杀死)
echo 1 > /sys/fs/cgroup/memory/docker/<container_id>/memory.oom_control
生产警告:禁用 OOM Killer 后,如果容器内存持续超限,进程会卡在 D 状态,无法响应信号,也无法被
docker stop 正常停止。必须通过 kill -9 或重启节点恢复。
4.2 cgroup v2:memory.oom.group 与组级 OOM
cgroup v2 引入了 memory.oom.group 参数,这是 v1 不具备的组级 OOM 控制能力。
| 参数 | 类型 | 说明 |
|---|---|---|
memory.oom.group |
0 / 1 | 0=只杀触发 OOM 的进程(默认),1=杀死整个 cgroup 的所有进程 |
核心差异:
- v1 行为:OOM Killer 只杀死触发内存超限的单个进程,同 cgroup 的其他进程继续运行。
- v2 行为(oom.group=1):当 cgroup 内任意进程触发 OOM 时,内核杀死该 cgroup 的所有进程,确保整个容器组原子性退出。
适用场景:Kubernetes Pod 中的 sidecar 容器。如果主容器 OOM,sidecar 继续运行会导致 Pod 状态不一致。启用
memory.oom.group 后,整个 Pod 的 cgroup 被清理,kubelet 可以快速重建。
配置示例:
# 在 cgroup v2 系统中启用组级 OOM
echo 1 > /sys/fs/cgroup/kubepods/besteffort/pod<uid>/memory.oom.group
# 验证当前设置
cat /sys/fs/cgroup/kubepods/besteffort/pod<uid>/memory.oom.group
# 输出:1
4.3 容器组(Pod)OOM 行为对比
在 Kubernetes 环境中,Pod 内的多个容器共享同一个 cgroup 层级。不同版本的 cgroup 和配置会导致截然不同的 OOM 行为。
4.4 生产环境最佳实践
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| Kubernetes Pod(cgroup v2) | memory.oom.group = 1 |
确保 Pod 原子性退出,避免 sidecar 残留 |
| 关键数据库容器 | oom_kill_disable = 1(v1) |
防止内核误杀,配合外部监控手动处理 |
| 批量任务容器 | 默认(oom_kill_disable=0) | 允许内核快速回收内存,任务可重试 |
| sidecar 代理容器 | memory.oom.group = 1 |
避免代理容器在主容器 OOM 后空转 |
内核版本依赖:
memory.oom.group 从 Linux 4.19 开始支持,cgroup v2 需要内核 5.2+ 稳定运行。在 containerd 1.5+ 和 CRI-O 1.20+ 中已默认启用 cgroup v2 支持。
4.5 OOM 优先级调整:oom_score_adj
除了 cgroup 级别的控制,每个进程还有独立的 oom_score_adj 值,范围 -1000 到 1000,值越高越容易被 OOM Killer 选中。
| 值 | 含义 | 典型用途 |
|---|---|---|
| -1000 | 完全禁用 OOM Killer | init 进程、系统关键服务 |
| -500 | 极低优先级 | 监控代理、日志收集 |
| 0 | 默认值 | 普通应用进程 |
| 500 | 较高优先级 | 缓存、批处理任务 |
| 1000 | 最高优先级 | 内存泄漏的测试进程 |
在容器中设置 oom_score_adj:
# Docker 运行时
docker run --oom-score-adj=-500 myapp
# containerd 通过 CRI 配置(Kubernetes Pod)
# 在 Pod spec 中设置:
containers:
- name: myapp
resources:
limits:
memory: "512Mi"
# 通过 securityContext 调整
securityContext:
oomScoreAdj: -500
注意:
oom_score_adj 是进程级别的设置,与 cgroup 级别的 OOM 控制独立。即使 cgroup 禁用了 OOM Killer,进程的 oom_score_adj 仍然影响其他 cgroup 的 OOM 选择。
4.6 监控与调试 OOM 事件
当 OOM 发生时,内核会记录事件到 /dev/kmsg 和 dmesg。以下是常见的调试方法:
# 查看最近的 OOM 事件
dmesg | grep -i "oom"
# 输出示例:
# [12345.678] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0,oom_memcg=/kubepods/besteffort/pod123,...
# [12345.678] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
# [12345.678] [ 1234] 0 1234 256000 50000 1234567 0 -500 myapp
# 实时监控 OOM 事件(需要 root)
cat /dev/kmsg | grep "oom-kill"
关键指标解读:
oom_memcg:触发 OOM 的 cgroup 路径oom_score_adj:被杀死进程的优先级调整值rss:进程实际物理内存使用量(单位:页)constraint:约束类型(CONSTRAINT_MEMCG 表示 cgroup 内存限制)
生产监控建议:在 Prometheus 中配置
node_oom_kills_total 指标(来自 node_exporter),或使用 oom_kill 计数器(来自 cgroup 的 memory.events)触发告警。当 OOM 频率超过 1 次/分钟时,应立即检查内存配置。