Kubernetes CPU 资源管理:request、limit 与 CPU 限流的性能影响
节点 CPU 看起来很空,容器里的计算任务却明显变慢。把副本数加大之前,需要先回答一个问题:任务是在等物理 CPU,还是已经用完了这个容器在当前周期内允许使用的 CPU 时间?
在 zj 上的 kind 实验中,同一段消耗约 1 CPU 秒的计算,设置 100m CPU limit 后耗时约 10 秒;不设 limit 时约 1 秒。两者的 CPU request 都是 100m,区别只有是否配置执行配额。
request 和 limit 分别作用在哪里
CPU request 首先参与调度:scheduler 判断节点剩余的可分配请求量是否足以接纳 Pod。它不是给进程绑定一个物理核,也不意味着进程永远只能使用这个数量的 CPU。在 Linux 的共享 CPU 调度中,request 还会影响竞争时的相对权重。
CPU limit 则限制容器可消费的 CPU 时间。本文使用 cgroup v2,限制可以在容器的 /sys/fs/cgroup/cpu.max 中观察到。Kubernetes 对 request 和 limit 的职责划分见资源管理文档。
kubelet 把资源规约转换为 CRI 的 Linux 资源配置:request 用来计算 CPU shares,limit 用来计算 quota 与 period;运行时再把这些配置落实到 cgroup。使用 cgroup v2 时,共享权重表现为 cpu.weight,带宽上限表现为 cpu.max。这条转换路径见 v1.35.1 的 calculateLinuxResources。
真正暂停超额执行的是 Linux 内核。一个 cgroup 耗尽当前周期的 CPU 配额后,其中可运行的任务也需要等待配额补充;宿主机此刻即使有空闲核,也不会因此自动解除该 cgroup 的限制。下面读取 cpu.max 和 cpu.stat,就是为了把 Kubernetes 配置与实际执行约束对应起来。
用同一份计算比较两种配置
环境为 Linux 主机 zj、kind v0.33.0、Kubernetes v1.35.1。两个 Pod 使用同一个 Python 镜像,启动后保持空闲,再通过 kubectl exec 执行测量。为了避免以固定循环次数代替真实 CPU 工作量,循环以 time.process_time() 增加 1 秒作为结束条件,同时使用单调时钟记录实际耗时。
实验配置的差异如下:
| |
另一组仅保留 requests.cpu: 100m,不配置 limits.cpu。没有改变镜像、测量程序或计算目标。
测量前后还读取了 cpu.stat,计算计数器增量,避免把容器启动阶段的历史 throttling 算到本次任务里。实际结果如下:
| 观察项 | limit=100m | 不设 CPU limit |
|---|---|---|
| 进程 CPU 时间 | 1.000 秒 | 1.000 秒 |
| 实际耗时 | 10.000 秒 | 1.001 秒 |
cpu.max | 10000 100000 | max 100000 |
nr_periods 增量 | 100 | 0 |
nr_throttled 增量 | 100 | 0 |
两组进程消耗的 CPU 时间相同,实际耗时却相差约 9 秒;受限组还记录了 100 次 throttling。这组对照把额外等待与配额限制对应起来。
10000 100000 怎么读
cpu.max 的两个值分别是配额和周期,单位为微秒。本次 10000 100000 表示每个 100 毫秒周期最多消费 10 毫秒 CPU 时间,长期平均对应 0.1 CPU;max 表示当前这一层没有配置 CPU 带宽上限。字段定义见 Linux cgroup v2 文档。
这 10 毫秒是一个 cgroup 的总 CPU 时间预算,不是为每个线程各发 10 毫秒。多个线程并行执行时可能更快耗尽预算,然后一起等待后续周期。线程数增加,不一定让受相同配额限制的任务更快。
不设当前容器的 limit,也不表示拥有无限算力。它仍受宿主机容量、其他进程竞争,以及可能存在的上层 cgroup 限制。kind 的节点本身也是宿主机上的容器,所以这里的结果不能替代真实业务节点的容量测试。
配额为什么不是一颗速度更慢的 CPU
进程获得 CPU 时仍按实际处理器速度运行,预算用完后要等待后续可用预算。因此即使长期平均值相同,“短时间跑完后等待”和“均匀地慢速运行”对请求延迟也不同。
Linux 6.12 带宽控制文档解释了预算按批次分配到各 CPU 运行队列的实现:批量转移降低了多个 CPU 争用全局计账状态的开销,代价是计账粒度与开销之间需要取舍。未用完的本地份额还可能带来短时偏差,不能把每个采样窗口都当作精确的 10 毫秒开关。本次实验用单个计算任务验证 100m 配额,没有额外测量多线程延迟。
本次无 limit 对照约 1 秒完成,说明当时有足够可用 CPU,不能理解为无 limit 就保证独占一个核。
为什么 CPU 图表可能看起来不高
首先要看分母。容器使用 0.1 CPU,对一个有 12 个逻辑 CPU 的宿主机来说,占整机容量不到 1%;但对 limit=100m 的容器来说,已经用满了配置上限。两个图表并不矛盾。
其次要看时间窗口。一个任务先计算、再等待下游,或只在短时间内突发,较长窗口内的平均 CPU 可以很低。平均值无法显示每个配额周期是否发生等待。
因此,排查时应该把请求耗时、容器 CPU 使用量、配置的 limit 和 cpu.stat 的增量放在相同时间段内比较。只看到 nr_throttled 增加,还不能证明某次请求延迟完全由 throttling 导致;I/O、锁竞争、下游等待和 CPU 竞争也可能同时存在。
现场怎么确认
| |
先确认 limit 是否由工作负载直接配置,或由准入阶段补入。再确认应用的工作单位和并发度,使用同一负载比较配额变化前后的完成时间。如果放宽 limit 后 throttling 消失,但延迟仍然很高,就要继续找真正的等待位置。
是否保留 CPU limit,需要结合隔离要求和工作负载决定。这次实验支持“配额会拉长任务耗时”,不支持“所有服务都应删除 CPU limit”。多租户场景下,上限本身可能就是需要的隔离策略。
实验代码与使用说明见 k8sdev/deep-dive-cpu。