Kubernetes 里设了 CPU 限制,你的应用可能比想象中慢得多

Kubernetes 里设了 CPU 限制,你的应用可能比想象中慢得多

在 Kubernetes 集群中给容器设置 limits.cpu 是很多团队的标准操作。但一项基于真实集群的对照测试表明:CPU 限制不仅不能保护邻居 Pod,反而会让被限制的应用频繁冻结、尾延迟飙升、启动时间翻倍。

CPU 请求与限制是两回事

Kubernetes 中每个 Pod 可以声明两种 CPU 参数:

request(请求):告诉调度器「这个 Pod 至少需要多少算力」。调度器按 request 总和放置 Pod,确保节点容量不被超卖。 – limit(限制):一个硬性天花板。一旦 Pod 在某个时间窗口内用完配额,Linux 内核会直接冻结整个容器。

测试在同一台 4 核节点上部署了两个完全相同的 .NET 10 API 服务。两者都请求 250m(四分之一核)CPU,唯一的区别是一个设了 500m 的 limit,另一个不设。

内核如何执行限制

Linux 以 100 毫秒为周期执行 CPU 配额管理。500m 的 limit 意味着每个窗口只有 50 毫秒的实际 CPU 时间可用。预算耗尽后,内核冻结整个容器的所有线程——包括 HTTP 处理器、后台消费者和垃圾回收器。

一个运行 8 个繁忙线程的服务可以在约 12.5 毫秒内耗尽全部配额。这意味着每秒最多发生 10 次冻结

更隐蔽的是:如果只看 1 分钟平均 CPU 使用率,仪表盘看起来一切正常——平均值远低于 limit,但应用实际上整天都在被节流。正确的监控指标是 container_cpu_cfs_throttled_periods_total

实测数据:延迟与吞吐量

测试向两个服务发送相同负载:

慢请求变慢 2.4 倍:即使平均负载只需 320m(远低于 500m 的 limit),受限服务的慢请求延迟仍是不受限版本的 2.4 倍。 – 尖峰吸收能力归零:流量突增时,受限服务在 89% 的时间窗口内处于冻结状态;无限制服务则轻松吸收了 12 倍的突发流量,零降速。 – 启动时间缩短约一半:CPU 密集型的编译/初始化工作完成速度提升近一倍(具体倍数取决于服务启动阶段 I/O 占比)。

「吵闹邻居」真的存在吗?

最常见的担忧是:不加限制的 Pod 会抢光整台机器的资源。测试专门验证了这个场景——在同一节点上放了一个持续吃满 CPU 的「吵闹邻居」Pod:

– 无限制受害者的 P99 延迟变化不到 7 毫米,且从未触发节流。CPU request 本身就是保护伞——CFS 调度器按权重分配资源,空闲份额会被借走,工作一来立刻归还。 – 有 500m 限制的受害者反而被节流了 0.29% 的时间窗口,P99 延迟并未改善。

结论很明确:限制保护不了邻居,只能伤害自己。

为什么内存限制应该保留

CPU 和内存有本质区别:

  • CPU | 应用变慢,稍后补上 | 去掉 limit,保留 request
  • 内存 | 应用直接崩溃(OOM Kill) | 保留 limit

成本影响:多数集群预留了大量闲置算力

测试提供了一个说明性的成本模型:去掉不必要的 CPU 限制并重新调整 request 后,单个集群每年可节省数万美元的硬件开支。大多数集群的实际 CPU 利用率长期停留在个位数或低两位数,而限制让本可借用的空闲算力白白浪费。

正确的做法

1. 保留 CPU request:它是真正的资源保障,也是 CFS 权重的来源。 2. 去掉 CPU limit:除非使用 Guaranteed QoS + 静态 CPU Manager 绑核这一特定场景。 3. 先加 LimitRange:给未显式声明 request 的 Pod 设定默认值,一条清单即可生效。 4. 再补 ResourceQuota:按命名空间控制总 request 上限,守护成本而非稳定性。

重要提醒:不能用被节流状态下测到的 CPU 用量来设定新的 limit 值——那个数字反映的是 limit 允许了多少,而不是应用真正需要多少。

来源:GitHub 项目 k8s-cpu-limits-analyzed(inevolin)