Pending、OOMKilled、Evicted:requests 与 limits 各管什么
节点 CPU 占用只有百分之几,新 Pod 却一直
Pending,原因是Insufficient cpu;给服务加了 CPU limit,平均占用离上限还远,延迟却涨了几倍;两个 Pod 都「因为资源被杀」,一个状态是OOMKilled还在原地重启,另一个是Evicted再也没起来。这三类问题分别出在调度器、内核 cgroup 和 kubelet 三个地方。
容器的 resources 里有两组值。requests 是给调度器看的:调度器按它记账,决定 Pod 能不能放到某个节点上。limits 是给节点看的:kubelet 把它写进 cgroup,由内核在运行时强制执行。此外,kubelet 还会按节点资源和 Pod 的声明主动驱逐 Pod。把这几件事混为一谈,是很多资源问题查不下去的原因。
本文在 kind 搭建的 Kubernetes 1.36.4 集群(1 个控制面、1 个 6 核工作节点)里逐一验证。实验见文末。
一、Pending:调度器只看 requests
工作节点可分配 6 核,系统组件已经请求了 0.1 核。创建 3 个只执行 sleep、各请求 2 核 CPU 的 Pod:
- 前两个
Running,第三个Pending,事件是FailedScheduling: 0/2 nodes are available: 1 Insufficient cpu, 1 node(s) had untolerated taint(s); - 这时工作节点的实际 CPU 占用是 2.60%(占 Docker 虚拟机全部 CPU 的比例)。
调度器不看节点实际忙不忙,只看已经被 requests 占掉多少。requests 写得比实际需要大,集群就会在很闲的时候「装不下」;反过来,一个不写 requests 的 Pod 在这个「已满」的节点上照样被调度成功——它被记账为 0,QoS 等级是 BestEffort。它能放上去,不代表有资源给它用,资源紧张时它是最先被牺牲的。
FailedScheduling 的消息会列出每类节点被排除的原因:Insufficient cpu/memory 是 requests 不够;untolerated taint 是污点(这里是控制面节点);此外常见的还有节点选择器与亲和性不匹配、存储卷所在的可用区不对。先读这条消息,再决定是扩节点、调小 requests,还是改调度约束。
优先级抢占
给一个 Pod 设置 priorityClassName(值为 1000 的 PriorityClass),请求 2 核。节点已经放不下,调度器选中其中一个优先级更低的 Pod,事件为 Preempted by pod ... on node ...,它被删除后,高优先级 Pod 进入 Running。被抢占的 Pod 是直接创建的,没有控制器重建它;那个原本 Pending 的低优先级 Pod 仍然 Pending。关键业务与批处理任务混布时,优先级决定了资源紧张时谁让路。
二、limits 与 QoS:内核怎样执行
QoS 等级
QoS 等级由 requests 与 limits 的写法决定,kubelet 据此设置每个容器进程的 oom_score_adj——内核在整机内存不足、需要杀进程时参考的分值,越大越先被杀:
| 写法 | QoS | 实测 oom_score_adj |
|---|---|---|
| 每个容器 CPU、内存的 requests 都等于 limits | Guaranteed | -997 |
| 至少写了一个 request 或 limit,但不满足上一行 | Burstable | 996 |
| 什么都不写 | BestEffort | 1000 |
Burstable 的值按「内存 request 占节点内存的比例」计算,request 越大越靠近 Guaranteed。实验中请求 64Mi,只占节点内存的很小一部分,所以几乎和 BestEffort 一样。
内存 limit:超过就被杀
limit 128Mi 的容器不断申请内存到 320MB:内核在 cgroup 里触发 OOM,容器退出码 137,lastState.terminated.reason 是 OOMKilled。Pod 本身还在,kubelet 按 restartPolicy 原地重启容器,60 秒内重启了 3 次,重启间隔按退避时间逐渐拉长,就是常见的 CrashLoopBackOff。同样的程序把 limit 设成 512Mi,一直正常运行。容器内看到的内存与 JVM 如何按 limit 计算默认堆,见容器不是轻量虚拟机。
CPU limit:不杀进程,只让它等
CPU limit 写进 cgroup 的 cpu.max,含义是每 100ms 周期里最多运行多少时间:250m 就是每个周期 25ms。用完配额的进程被暂停到下一个周期。
模拟一个请求处理:每个任务单线程计算约 60ms,任务之间空闲 200ms,每种 limit 跑 100 个任务:
| CPU limit | 任务耗时 p50 | p99 | 平均占用 | 被限流的周期/总周期 |
|---|---|---|---|---|
| 不限 | 63ms | 74ms | 0.24 核 | 0/0 |
| 1 核 | 63ms | 70ms | 0.24 核 | 0/261 |
| 500m | 99ms | 118ms | 0.22 核 | 51/300 |
| 250m | 285ms | 318ms | 0.17 核 | 257/463 |
250m 这一行,平均占用 0.17 核,低于 limit,看监控上的平均 CPU 会以为还有余量;但单个任务需要的 CPU 超过了一个周期的配额,只能分几个周期跑完,p50 从 63ms 变成 285ms。500m 时配额够跑完大部分任务,但仍有 51 个周期被限流,p50 也变长了。多线程程序更明显:配额按容器内所有线程合计,4 个线程同时运行时,25ms 的配额 6ms 多就用完了。
所以判断 CPU limit 是否合适,要看 cgroup 的限流计数(cpu.stat 里的 nr_throttled,监控里通常叫 CPU throttling),而不是平均使用率。对延迟敏感的服务,常见做法是按实际用量设置足够的 CPU requests,CPU limit 放宽或不设;内存则应该设置 limit,因为内存用超了没法「等一等」,只能被杀。
三、Evicted:kubelet 驱逐整个 Pod
一个 Pod 声明 limits.ephemeral-storage: 100Mi,然后往容器的可写层写 200MB。15.9 秒后,Pod 状态变为 Failed,原因 Evicted,消息是 Pod ephemeral local storage usage exceeds the total limit of containers 100Mi.。
和 OOMKilled 对比:
| OOMKilled | Evicted | |
|---|---|---|
| 执行者 | 内核(cgroup OOM) | kubelet |
| 作用对象 | 一个容器进程 | 整个 Pod |
| 之后 | Pod 还在,容器按 restartPolicy 原地重启 | Pod 进入 Failed,不会再启动;由 Deployment 等控制器创建新的 Pod |
| 触发条件 | 容器内存超过 limit,或整机内存耗尽 | 临时存储超过 limit、emptyDir 超过 sizeLimit,或节点内存、磁盘、inode、PID 达到驱逐阈值 |
实验中被驱逐的 Pod 重启次数为 0,容器以 137 退出。被驱逐的 Pod 对象会留在 API 里,可以用 kubectl get pods --field-selector status.phase=Failed 找出来。
节点资源紧张(节点压力)时,kubelet 驱逐 Pod 的顺序是:先看使用量是否超过 requests,再看优先级,再看超出 requests 的多少。所以 requests 写得接近真实用量的 Pod,在节点压力下更安全。本实验没有在节点上制造内存压力,这部分以官方文档为准。
四、排查与设置建议
- Pending:读
FailedScheduling的消息,区分 requests 不足、污点、亲和性与存储约束;requests 按实际用量的高位设置,而不是随手写一个大数。 - OOMKilled:看
lastState.terminated.reason与退出码 137。Java 服务同时检查最大堆与堆外内存加起来是否小于 limit。 - 延迟变长但 CPU 不高:看限流计数;多线程、突发型负载尤其要注意 CPU limit。
- Evicted:看 Pod 的
status.message,区分是自身超过 limit,还是节点压力。前者调整 limit 或清理临时文件、日志;后者看节点。 - 关键服务使用 Guaranteed,或者至少让 requests 接近真实用量;给关键业务设置更高的优先级。
- 集群默认值可以用 LimitRange 给没写 requests 的容器补上,避免 BestEffort Pod 无约束地挤占节点。
配套实验
- codesphere-labs/kubernetes/resources-and-scheduling:kind 集群(Kubernetes 1.36.4)中按 requests 调度与 Pending、不写 requests 的 Pod、优先级抢占、三种 QoS 的
oom_score_adj、四种 CPU limit 下的任务延迟与限流计数、内存超限 OOMKilled、临时存储超限被驱逐(验证记录)
参考资料