概述

Kubernetes 通过 Linux cgroup(Control Groups)机制实现容器的资源隔离和限制。本文通过实验深入探索容器内存限制的工作原理,以及在何种情况下容器会被 OOM Killer 杀死。

核心内容:

  • 🔍 cgroup 内存限制机制
  • ⚡ OOM Killer 工作原理
  • 🧪 压力测试与故障模拟
  • 📊 oom_score 计算方法

技术背景:

  • cgroup 是容器资源控制的基础
  • 具有层级结构,可继承父级属性
  • Kubernetes 基于 cgroup 实现 Pod 的资源限制

原文来源: https://cloud.tencent.com/developer/article/1495508

扩展阅读: 深入理解 Kubernetes 资源限制:内存

cgroup 基础知识

什么是 cgroup

定义:

cgroup(Control Groups)是 Linux 内核提供的一种机制,用于限制、记录和隔离进程组使用的物理资源(CPU、内存、I/O 等)。

核心特性:

  • 📊 资源限制:限制进程组使用的资源上限
  • 📈 优先级控制:控制进程组的 CPU 和磁盘 I/O 吞吐量
  • 📝 资源统计:监控进程组对资源的占用情况
  • 🔒 进程控制:将进程挂起或恢复

层级结构:

1
2
3
4
5
6
7
8
root cgroup
└── kubepods
├── burstable (QoS: Burstable)
│ └── pod-xxx
│ ├── container-1
│ └── container-2
└── besteffort (QoS: BestEffort)
└── pod-yyy

cgroup 资源类型

子系统功能应用场景
memory内存资源限制限制容器内存使用
cpuCPU 时间分配限制容器 CPU 使用
cpusetCPU 核心绑定绑定特定 CPU 核心
blkio块设备 I/O 限制限制磁盘读写速度
devices设备访问控制控制设备访问权限
net_cls网络流量分类网络 QoS 控制

实验环境准备

创建测试 Pod

创建内存限制为 123Mi 的 Pod:

1
2
3
4
5
6
7
kubectl run \
--restart=Never \
--rm \
-it \
--image=ubuntu \
--limits='memory=123Mi' \
sh

参数说明:

  • --restart=Never: 不自动重启
  • --rm: 退出后自动删除
  • -it: 交互式终端
  • --limits='memory=123Mi': 设置内存限制

查找 Pod UID

获取 Pod 的唯一标识:

1
2
3
4
5
6
7
8
# 方法一:使用 kubectl
kubectl get pods sh -o yaml | grep uid

# 输出示例
# uid: bc001ffa-68fc-11e9-92d7-5ef9efd9374c

# 方法二:使用 jsonpath
kubectl get pod sh -o jsonpath='{.metadata.uid}'

定位 cgroup 目录

cgroup 文件系统路径结构:

1
2
3
4
5
6
7
8
/sys/fs/cgroup/memory/
└── kubepods/
└── burstable/
└── pod<UID>/
├── memory.limit_in_bytes # 内存限制
├── memory.usage_in_bytes # 当前使用量
├── memory.stat # 详细统计
└── <container-id>/ # 容器子 cgroup

查看内存限制:

1
2
3
4
5
6
7
# 进入 Pod 的 cgroup 目录
cd /sys/fs/cgroup/memory/kubepods/burstable/pod<UID>

# 查看内存限制(单位:字节)
cat memory.limit_in_bytes

# 输出:128974848 (等于 123 * 1024 * 1024)

验证计算:

1
2
3
# Python 验证
python3 -c "print(123 * 1024 * 1024)"
# 输出:128974848

cgroup 目录结构

Pod 级别 cgroup

列出 cgroup 目录内容:

1
ll /sys/fs/cgroup/memory/kubepods/burstable/pod<UID>

关键文件说明:

文件说明用途
memory.limit_in_bytes内存硬限制设置最大可用内存
memory.usage_in_bytes当前内存使用监控实时使用量
memory.max_usage_in_bytes历史最大使用峰值使用量
memory.stat详细统计信息内存分类统计
memory.oom_controlOOM 控制配置 OOM 行为
memory.failcnt超限次数达到限制的次数

容器级别 cgroup

Pod 的每个容器都有独立的子 cgroup:

1
2
3
pod-xxx/
├── <pause-container-id>/ # pause 容器
└── <business-container-id>/ # 业务容器

验证容器 cgroup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 查找容器 ID(使用 crictl,如果是 Docker 用 docker ps)
crictl ps | grep "sh"

# 输出示例:
# 64ae20d221399 ubuntu CONTAINER_RUNNING sh

# 2. 获取容器 PID
crictl inspect 64ae20d221399 | grep '"pid"'

# 输出示例:
# "pid": 32308

# 3. 查看进程的 cgroup 路径
cat /proc/32308/cgroup | grep memory

# 输出示例:
# 4:memory:/kubepods/burstable/pod<UID>/<container-id>

# 4. 查看容器的内存限制
cat /sys/fs/cgroup/memory/kubepods/burstable/pod<UID>/<container-id>/memory.limit_in_bytes

# 输出:128974848

重要发现:

容器级别的 cgroup 继承了 Pod 级别的内存限制,两者数值相同。

内存压力测试

安装测试工具

在容器内安装 stress 工具:

1
2
3
4
5
# 更新软件包列表
apt update

# 安装 stress 压力测试工具
apt install -y stress

stress 工具参数:

参数说明示例
--vm N启动 N 个内存测试进程--vm 1
--vm-bytes SIZE每个进程分配内存大小--vm-bytes 100M
--cpu N启动 N 个 CPU 测试进程--cpu 4
--timeout N运行 N 秒后停止--timeout 60s

准备监控

在另一个终端窗口监控系统日志:

1
2
3
4
5
# 实时查看内核日志(包含 OOM 事件)
dmesg -Tw

# 或使用
journalctl -kf

第一次压力测试

测试场景:分配 100MB 内存(后台运行)

1
2
3
4
5
6
# 启动第一个压力测试(后台)
stress --vm 1 --vm-bytes 100M &

# 输出:
# [1] 271
# stress: info: [271] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

当前状态:

  • 内存使用:~100MB
  • 剩余可用:~23MB
  • 进程 ID:271

第二次压力测试(触发 OOM)

测试场景:再分配 50MB 内存(前台运行)

1
2
3
4
5
6
7
8
# 启动第二个压力测试
stress --vm 1 --vm-bytes 50M

# 输出:
# stress: info: [273] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
# stress: FAIL: [271] (415) <-- worker 272 got signal 9
# stress: WARN: [271] (417) now reaping child worker processes
# stress: FAIL: [271] (451) failed run completed in 7s

发生了什么?

  • 💥 总内存使用:100MB + 50MB = 150MB
  • ⚠️ 超过限制:150MB > 123MB
  • 🔴 触发 OOM Killer
  • ⚡ 进程 272 被 signal 9(SIGKILL)杀死

OOM Killer 详解

系统日志分析

内核日志输出(关键信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
[2019-04-27 22:56:09] stress invoked oom-killer: 
gfp_mask=0x14000c0(GFP_KERNEL), order=0, oom_score_adj=939

[2019-04-27 22:56:09] Task in /kubepods/burstable/pod<UID>/<container-id>
killed as a result of limit of /kubepods/burstable/pod<UID>

[2019-04-27 22:56:09] memory: usage 125952kB, limit 125952kB, failcnt 3632

[2019-04-27 22:56:09] Memory cgroup out of memory:
Kill process 32308 (stress) score 1718 or sacrifice child

[2019-04-27 22:56:09] Killed process 32308 (stress)
total-vm:110644kB, anon-rss:99620kB

日志解读:

字段说明数值
usage当前内存使用125952kB (≈123MB)
limit内存限制125952kB (123MB)
failcnt超限尝试次数3632 次
oom_scoreOOM 评分1718
signal终止信号9 (SIGKILL)

OOM 候选进程列表

系统列出的可杀死进程:

1
2
3
4
5
6
7
[ pid ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[25160] 0 25160 256 1 28672 0 -998 pause
[25218] 0 25218 4627 872 77824 0 939 bash
[32307] 0 32307 2060 275 57344 0 939 stress
[32308] 0 32308 27661 24953 253952 0 939 stress
[32331] 0 32331 2060 304 53248 0 939 stress
[32332] 0 32332 14861 5829 102400 0 939 stress

进程分析:

PID进程名RSS (内存)oom_score_adj说明
25160pause1 页-998Pause 容器,几乎不会被杀
25218bash872 页939Shell 进程
32308stress24953 页939内存使用最多,被选中
32332stress5829 页939第二个 stress 进程

oom_score_adj 计算

Kubernetes 官方计算公式:

1
oom_score_adj = min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)

公式说明:

  • memoryRequestBytes: Pod 请求的内存
  • machineMemoryCapacityBytes: 节点总内存
  • 结果范围:[2, 999]

实际计算示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看节点总内存
kubectl describe nodes k3s | grep Allocatable -A 5

# 输出:
# Allocatable:
# memory: 2041888Ki

# 计算 oom_score_adj
# 如果只设置 limits,requests 会自动设置为相同值
# oom_score_adj = 1000 - (1000 * 123 * 1024 / 2041888)
# = 1000 - 61.68
# ≈ 938.32
# ≈ 939(取整)

验证:

1
2
3
4
# 查看进程的 oom_score_adj
cat /proc/32308/oom_score_adj

# 输出:939

oom_score 计算

oom_score 计算规则:

1
oom_score = (进程RSS内存 / 节点总内存) * 1000 + oom_score_adj

为什么选择 PID 32308?

进程内存使用基础分数oom_score_adj最终 oom_score结果
pause极小~0-998~-998❌ 不会杀
bash872 页~42939~981⚠️ 低优先级
stress-124953 页~779939~1718被选中
stress-25829 页~182939~1121⚠️ 低优先级

查看进程 oom_score:

1
2
3
cat /proc/32308/oom_score

# 输出:1718

结论:

在相同 oom_score_adj 的进程中,内存使用量最大的进程 oom_score 最高,最容易被 OOM Killer 选中杀死。

QoS 与 OOM 优先级

Kubernetes QoS 分类

QoS 等级与 oom_score_adj 的关系:

QoS 类别条件oom_score_adj优先级
Guaranteedrequests == limits-998最高(最不易被杀)
Burstable0 < requests < limits[2, 999]中等
BestEffort未设置 requests/limits1000最低(最易被杀)

pause 容器为什么不会被杀

特殊保护机制:

1
2
3
4
# 查看 pause 容器的 oom_score_adj
cat /proc/<pause-pid>/oom_score_adj

# 输出:-998

pause 容器作用:

  • 🌐 创建并持有 Pod 的 network namespace
  • 📡 为其他容器提供网络共享
  • 🔄 保持 Pod 生命周期稳定

oom_score_adj 范围:

  • -1000: 永不杀死(内核级保护)
  • -999 ~ -1: 极低优先级被杀
  • 0: 默认值
  • 1 ~ 1000: 递增的被杀优先级

最佳实践

合理设置资源限制

推荐配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: myapp:latest
resources:
requests:
memory: "256Mi" # 请求值:保证分配
cpu: "250m"
limits:
memory: "512Mi" # 限制值:不可超过
cpu: "500m"

配置原则:

  • requests 应接近实际使用量
  • limits 应留有适当余量(1.5-2倍 requests)
  • ⚠️ 避免设置过高的 limits(浪费资源)
  • ⚠️ 避免设置过低的 requests(频繁 OOM)

监控内存使用

实时监控命令:

1
2
3
4
5
6
7
8
# 查看 Pod 内存使用
kubectl top pod <pod-name>

# 查看容器内存使用
kubectl top pod <pod-name> --containers

# 查看 cgroup 内存统计
cat /sys/fs/cgroup/memory/kubepods/burstable/pod<UID>/memory.stat

关键指标:

指标说明告警阈值
usage_in_bytes当前使用量> 80% limit
max_usage_in_bytes历史峰值接近 limit
failcnt超限次数> 0

故障排查

OOM 排查步骤:

  1. 查看 Pod 事件
1
kubectl describe pod <pod-name> | grep -A 10 Events
  1. 查看容器日志
1
kubectl logs <pod-name> --previous
  1. 查看节点内核日志
1
dmesg -T | grep -i "killed process"
  1. 分析 OOM 原因
1
2
3
4
5
# 查看内存使用趋势
kubectl top pod <pod-name> --containers

# 查看 cgroup 统计
cat /sys/fs/cgroup/memory/.../memory.stat

避免 OOM 的建议

应用层面:

  • 🔍 使用内存分析工具(pprof、heapdump)
  • 🗑️ 及时释放不用的对象
  • 💾 合理使用缓存大小
  • 📊 监控内存泄漏

配置层面:

  • ✅ 为 JVM 应用设置合适的堆大小
  • ✅ 预留系统内存(limits 大于应用需求)
  • ✅ 启用内存限流(如 Go 的 GOMEMLIMIT)
  • ✅ 配置 HPA 自动扩缩容

总结

核心要点

cgroup 内存限制机制:

  • 📊 Kubernetes 通过 cgroup 实现容器内存隔离
  • 🌳 cgroup 具有层级结构(Pod → Container)
  • 🔒 超过 limit 时触发 OOM Killer

OOM Killer 选择策略:

  • 🎯 基于 oom_score = 内存使用比例 + oom_score_adj
  • 💪 Guaranteed QoS 优先级最高(-998)
  • ⚠️ BestEffort QoS 最容易被杀(1000)
  • 🔴 相同优先级下,内存使用最多的进程被选中

实战经验:

  • ✅ 合理设置 requests 和 limits
  • ✅ 监控内存使用趋势
  • ✅ 区分 OS 级别 OOM 和 Pod 级别 OOM
  • ✅ 使用压力测试验证配置

深入学习

推荐阅读:

相关工具:

  • kubectl top: 资源使用监控
  • crictl: 容器运行时调试
  • dmesg: 内核日志查看
  • stress: 压力测试工具