Skip to content

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 的比例)。
调度器的账本idle-1 请求 2 核idle-2 请求 2 核剩 1.9 核idle-3 请求 2 核放不下,Pending6 核实际 CPU 占用只有百分之几:三个 Pod 都在 sleep不写 requestsbesteffort:记账为 0节点「满了」也能放上去,资源紧张时最先被牺牲
图 1 · 工作节点可分配 6 核。系统组件请求 0.1 核,两个各请求 2 核的 Pod 放上去之后只剩 1.9 核,第三个请求 2 核的 Pod 就 Pending,尽管这些 Pod 只在 sleep,节点实际 CPU 占用只有百分之几。不写 requests 的 Pod 记账为 0,照样被放上去

调度器不看节点实际忙不忙,只看已经被 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 都等于 limitsGuaranteed-997
至少写了一个 request 或 limit,但不满足上一行Burstable996
什么都不写BestEffort1000

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。用完配额的进程被暂停到下一个周期。

0ms100ms200ms300ms不限 CPU约 60ms 完成limit 250m跨三个周期才完成运行配额用完,被暂停(throttled)
图 2 · CPU limit 在 cgroup 里是每 100ms 周期的配额:250m 就是每个周期最多运行 25ms。一个需要约 60ms CPU 的任务,不限 CPU 时一次跑完;限到 250m 时跑 25ms 就被暂停到下个周期,要跨三个周期才能完成。任务之间有空闲,平均占用低于 limit,延迟照样被拉长

模拟一个请求处理:每个任务单线程计算约 60ms,任务之间空闲 200ms,每种 limit 跑 100 个任务:

CPU limit任务耗时 p50p99平均占用被限流的周期/总周期
不限63ms74ms0.24 核0/0
1 核63ms70ms0.24 核0/261
500m99ms118ms0.22 核51/300
250m285ms318ms0.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 对比:

OOMKilledEvicted
执行者内核(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,在节点压力下更安全。本实验没有在节点上制造内存压力,这部分以官方文档为准。

四、排查与设置建议 ​

  1. Pending:读 FailedScheduling 的消息,区分 requests 不足、污点、亲和性与存储约束;requests 按实际用量的高位设置,而不是随手写一个大数。
  2. OOMKilled:看 lastState.terminated.reason 与退出码 137。Java 服务同时检查最大堆与堆外内存加起来是否小于 limit。
  3. 延迟变长但 CPU 不高:看限流计数;多线程、突发型负载尤其要注意 CPU limit。
  4. Evicted:看 Pod 的 status.message,区分是自身超过 limit,还是节点压力。前者调整 limit 或清理临时文件、日志;后者看节点。
  5. 关键服务使用 Guaranteed,或者至少让 requests 接近真实用量;给关键业务设置更高的优先级。
  6. 集群默认值可以用 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、临时存储超限被驱逐(验证记录)

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权