概述
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 | root cgroup |
cgroup 资源类型
| 子系统 | 功能 | 应用场景 |
|---|---|---|
| memory | 内存资源限制 | 限制容器内存使用 |
| cpu | CPU 时间分配 | 限制容器 CPU 使用 |
| cpuset | CPU 核心绑定 | 绑定特定 CPU 核心 |
| blkio | 块设备 I/O 限制 | 限制磁盘读写速度 |
| devices | 设备访问控制 | 控制设备访问权限 |
| net_cls | 网络流量分类 | 网络 QoS 控制 |
实验环境准备
创建测试 Pod
创建内存限制为 123Mi 的 Pod:
1 | kubectl run \ |
参数说明:
--restart=Never: 不自动重启--rm: 退出后自动删除-it: 交互式终端--limits='memory=123Mi': 设置内存限制
查找 Pod UID
获取 Pod 的唯一标识:
1 | # 方法一:使用 kubectl |
定位 cgroup 目录
cgroup 文件系统路径结构:
1 | /sys/fs/cgroup/memory/ |
查看内存限制:
1 | # 进入 Pod 的 cgroup 目录 |
验证计算:
1 | # Python 验证 |
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_control | OOM 控制 | 配置 OOM 行为 |
| memory.failcnt | 超限次数 | 达到限制的次数 |
容器级别 cgroup
Pod 的每个容器都有独立的子 cgroup:
1 | pod-xxx/ |
验证容器 cgroup:
1 | # 1. 查找容器 ID(使用 crictl,如果是 Docker 用 docker ps) |
重要发现:
容器级别的 cgroup 继承了 Pod 级别的内存限制,两者数值相同。
内存压力测试
安装测试工具
在容器内安装 stress 工具:
1 | # 更新软件包列表 |
stress 工具参数:
| 参数 | 说明 | 示例 |
|---|---|---|
--vm N | 启动 N 个内存测试进程 | --vm 1 |
--vm-bytes SIZE | 每个进程分配内存大小 | --vm-bytes 100M |
--cpu N | 启动 N 个 CPU 测试进程 | --cpu 4 |
--timeout N | 运行 N 秒后停止 | --timeout 60s |
准备监控
在另一个终端窗口监控系统日志:
1 | # 实时查看内核日志(包含 OOM 事件) |
第一次压力测试
测试场景:分配 100MB 内存(后台运行)
1 | # 启动第一个压力测试(后台) |
当前状态:
- 内存使用:~100MB
- 剩余可用:~23MB
- 进程 ID:271
第二次压力测试(触发 OOM)
测试场景:再分配 50MB 内存(前台运行)
1 | # 启动第二个压力测试 |
发生了什么?
- 💥 总内存使用:100MB + 50MB = 150MB
- ⚠️ 超过限制:150MB > 123MB
- 🔴 触发 OOM Killer
- ⚡ 进程 272 被 signal 9(SIGKILL)杀死
OOM Killer 详解
系统日志分析
内核日志输出(关键信息):
1 | [2019-04-27 22:56:09] stress invoked oom-killer: |
日志解读:
| 字段 | 说明 | 数值 |
|---|---|---|
| usage | 当前内存使用 | 125952kB (≈123MB) |
| limit | 内存限制 | 125952kB (123MB) |
| failcnt | 超限尝试次数 | 3632 次 |
| oom_score | OOM 评分 | 1718 |
| signal | 终止信号 | 9 (SIGKILL) |
OOM 候选进程列表
系统列出的可杀死进程:
1 | [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name |
进程分析:
| PID | 进程名 | RSS (内存) | oom_score_adj | 说明 |
|---|---|---|---|---|
| 25160 | pause | 1 页 | -998 | Pause 容器,几乎不会被杀 |
| 25218 | bash | 872 页 | 939 | Shell 进程 |
| 32308 | stress | 24953 页 | 939 | 内存使用最多,被选中 |
| 32332 | stress | 5829 页 | 939 | 第二个 stress 进程 |
oom_score_adj 计算
Kubernetes 官方计算公式:
1 | oom_score_adj = min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999) |
公式说明:
memoryRequestBytes: Pod 请求的内存machineMemoryCapacityBytes: 节点总内存- 结果范围:[2, 999]
实际计算示例:
1 | # 查看节点总内存 |
验证:
1 | # 查看进程的 oom_score_adj |
oom_score 计算
oom_score 计算规则:
1 | oom_score = (进程RSS内存 / 节点总内存) * 1000 + oom_score_adj |
为什么选择 PID 32308?
| 进程 | 内存使用 | 基础分数 | oom_score_adj | 最终 oom_score | 结果 |
|---|---|---|---|---|---|
| pause | 极小 | ~0 | -998 | ~-998 | ❌ 不会杀 |
| bash | 872 页 | ~42 | 939 | ~981 | ⚠️ 低优先级 |
| stress-1 | 24953 页 | ~779 | 939 | ~1718 | ✅ 被选中 |
| stress-2 | 5829 页 | ~182 | 939 | ~1121 | ⚠️ 低优先级 |
查看进程 oom_score:
1 | cat /proc/32308/oom_score |
结论:
在相同 oom_score_adj 的进程中,内存使用量最大的进程 oom_score 最高,最容易被 OOM Killer 选中杀死。
QoS 与 OOM 优先级
Kubernetes QoS 分类
QoS 等级与 oom_score_adj 的关系:
| QoS 类别 | 条件 | oom_score_adj | 优先级 |
|---|---|---|---|
| Guaranteed | requests == limits | -998 | 最高(最不易被杀) |
| Burstable | 0 < requests < limits | [2, 999] | 中等 |
| BestEffort | 未设置 requests/limits | 1000 | 最低(最易被杀) |
pause 容器为什么不会被杀
特殊保护机制:
1 | # 查看 pause 容器的 oom_score_adj |
pause 容器作用:
- 🌐 创建并持有 Pod 的 network namespace
- 📡 为其他容器提供网络共享
- 🔄 保持 Pod 生命周期稳定
oom_score_adj 范围:
-1000: 永不杀死(内核级保护)-999 ~ -1: 极低优先级被杀0: 默认值1 ~ 1000: 递增的被杀优先级
最佳实践
合理设置资源限制
推荐配置:
1 | apiVersion: v1 |
配置原则:
- ✅
requests应接近实际使用量 - ✅
limits应留有适当余量(1.5-2倍 requests) - ⚠️ 避免设置过高的 limits(浪费资源)
- ⚠️ 避免设置过低的 requests(频繁 OOM)
监控内存使用
实时监控命令:
1 | # 查看 Pod 内存使用 |
关键指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| usage_in_bytes | 当前使用量 | > 80% limit |
| max_usage_in_bytes | 历史峰值 | 接近 limit |
| failcnt | 超限次数 | > 0 |
故障排查
OOM 排查步骤:
- 查看 Pod 事件
1 | kubectl describe pod <pod-name> | grep -A 10 Events |
- 查看容器日志
1 | kubectl logs <pod-name> --previous |
- 查看节点内核日志
1 | dmesg -T | grep -i "killed process" |
- 分析 OOM 原因
1 | # 查看内存使用趋势 |
避免 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: 压力测试工具
