根据 Robusta 的文章《For the Love of God, Stop Using CPU Limits on Kubernetes》以及近期内部发生的多起事故,本文档旨在深入分析在 Kubernetes (K8s) 环境中设置 CPU 限制 (CPU Limit) 的弊端,并结合具体事故案例,提供解决及预防此类问题的最佳实践。核心结论是,在绝大多数情况下,应停止使用 CPU 限制,并专注于精确设置 CPU 请求 (CPU Request),以避免性能瓶颈和不必要的生产事故。
CPU 与内存:可压缩与不可压缩资源的核心差异
理解 CPU 和内存资源在 K8s 中的本质区别,是做出正确资源配置决策的基础。
CPU 是可压缩资源 (Compressible Resource):CPU 的使用是基于时间片的。如果一个容器达到了其 CPU 限制,它会被"节流" (Throttled),即在一段时间内无法被调度,导致应用延迟。然而,这并不会"用尽"节点的 CPU 资源。当下一个时间片到来时,CPU 资源依然可用。文章中将其比作"可再生的水"------即使暂时被一个口渴的人多喝了,水源本身并不会枯竭。
内存是不可压缩资源 (Non-compressible Resource):内存一旦被分配给一个进程,就无法被系统在不终止该进程的情况下收回。如果一个容器试图使用超过其内存限制的内存,它会被 OOMKilled (Out of Memory Killed),直接导致应用崩溃。因此,为内存设置限制是保护节点稳定性的必要措施。
| 资源类型 | 特性 | 限制 (Limit) 的影响 | 请求 (Request) 的作用 |
|---|---|---|---|
| CPU | 可压缩 | 达到限制时,Pod 会被节流,导致性能下降和延迟增加。 | 保证 Pod 至少能获得的 CPU 资源,避免被其他 Pod 饿死。 |
| 内存 | 不可压缩 | 达到限制时,Pod 会被 OOMKilled,导致应用崩溃。 | 保证 Pod 至少能获得的内存资源,是 K8s 调度决策的依据。 |
为什么 CPU 限制弊大于利?
设置 CPU 限制的初衷是为了防止单个"失控"应用耗尽节点所有 CPU 资源,但这种做法在实践中往往导致更多问题。其根本原因在于 Linux 内核的 CFS (Completely Fair Scheduler) 调度器如何执行 CPU 限制。
当一个容器在单个调度周期内用尽其 CPU 配额时,它会被强制"睡眠",直到下一个周期才能再次被调度。这意味着,即使节点上还有大量空闲的 CPU 资源,这个被限制的容器也无法使用它们,从而导致应用性能无故下降。这就像文章中的比喻:一个人快渴死了,旁边明明还有水,但因为他今天的"限额"用完了,就只能眼睁睁地看着水而不能喝。
精确的 CPU 请求 (Request) 才是保证服务质量 (QoS) 的正确方式。K8s 调度器会确保节点上所有 Pod 的 CPU 请求之和不超过节点的总 CPU 容量。这意味着,只要设置了合理的 CPU 请求,每个 Pod 都能在需要时获得其所请求的 CPU 资源,从而避免了资源争抢导致的"饿死"现象。
💡 核心原则:CPU 是可再生资源,节点上未被使用的 CPU 是对资源的浪费。CPU 请求保障了最低资源供给,而 CPU 限制则人为地阻止了应用利用空闲资源,造成不必要的性能损耗。
内部事故分析:CPU 限制如何导致生产问题
近期发生的多起事故,其根本原因都与不合理的资源限制密切相关,尤其是 CPU 限制。虽然事故报告中未直接将"不合理的 CPU 限制"列为根因,但其表现出的运维特征(如 CPU 饱和、性能下降)与 CPU 节流的症状高度吻合。
| 事故编号 | 事故摘要 | 与 CPU 限制的关联分析 |
|---|---|---|
| INCI-1183 | Meta MySQL 数据库 CPU 使用率高 | 数据库是典型的对延迟敏感的应用。当查询负载突增时,如果 CPU 限制过低,MySQL 进程会因 CPU 节流而无法及时处理请求,导致查询变慢、连接堆积,最终表现为持续的高 CPU 使用率和性能下降。 |
| INCI-1387 | Loki querier 组件 CPU 达到 100% | Loki querier 在处理复杂查询时需要消耗大量 CPU。持续的 100% CPU 使用率是 CPU 节流的典型信号,意味着 querier 的潜力被限制住了,无法利用节点的空闲 CPU 来加速查询,导致用户查询超时和失败。 |
| INCI-1456 | Cilium agent OOM(内存限制过激) | 虽然这是内存限制问题,但它揭示了对关键系统组件(如网络插件)设置过于激进的资源限制所带来的风险。如果对 Cilium 同时设置了不当的 CPU 限制,同样会引发网络延迟、丢包等严重问题,影响整个集群的网络通信。 |
| INCI-1087 | Vector agent OOM(遥测管道中断) | 此事故再次印证了资源配置不当是导致系统故障的常见模式。无论是 CPU 还是内存,静态且过低的限制都无法适应工作负载的动态变化,最终导致关键的遥测管道中断,影响可观测性。 |
这些事故共同指向一个结论:静态的、尤其是不合理的低 CPU 限制,是潜伏在系统中的"定时炸弹"。当业务负载超过这个限制时,就会引发性能问题甚至服务中断。随着 GPU 节点的持续扩容,工作负载将进一步增大,若不及时修正资源配置,类似事故将更加频繁。
最佳实践与行动建议
为了从根本上解决此类问题,提升系统稳定性和性能,我们应采纳以下最佳实践:
1. 立即停止使用 CPU 限制
对于绝大多数应用,特别是那些对延迟敏感的服务(如数据库、API 网关、消息队列、网络插件等),应立即移除 resources.limits.cpu 配置。对于少数确实行为不端、可能无限循环消耗 CPU 的"糟糕应用",可以考虑保留限制作为最后的防护栏,但这种情况应属例外,而非惯例。
2. 精确设置 CPU 请求
将工作重心转移到设置准确的 resources.requests.cpu 上。CPU 请求应基于应用在正常负载下的实际使用情况来设定,并留有一定的余量以应对负载波动。可以利用监控数据(如 Prometheus)和自动化工具(如 Robusta 推荐的 KRR - Kubernetes Resource Recommender)来科学地推荐请求值。
3. 统一内存请求和限制
对于内存,最佳实践是始终将 resources.requests.memory 和 resources.limits.memory 设置为相同的值。这能为 Pod 提供稳定的 Guaranteed QoS 等级,避免因资源争抢而被驱逐,同时也能防止内存泄漏影响到节点上的其他应用。
| 资源 | 是否设置 Request | 是否设置 Limit | 备注 |
|---|---|---|---|
| CPU | ✅ 必须,且要精确 | ❌ 绝大多数情况下不设置 | 仅对已知行为异常的应用例外设置 |
| 内存 | ✅ 必须 | ✅ 必须,且与 Request 相同 | 防止 OOMKill 影响节点稳定性 |
行动计划
建议所有工程师立即对自己负责的服务进行审视,识别那些设置了较低 CPU 限制(如 250m、500m、1、2 等)的组件,评估移除这些限制并调整 CPU 请求的可行性,从而在下一次变更中消除潜在的事故隐患。
结论
遵循 K8s 的设计哲学,我们应当信任其调度器通过 CPU 请求来合理分配资源的能力,而不是通过强制的 CPU 限制来束缚应用的性能。通过移除不必要的 CPU 限制,并科学地设置 CPU 请求,我们不仅可以提升应用的性能和响应速度,还能显著减少因资源节流引发的生产事故,从而构建一个更具弹性和鲁棒性的系统。
参考资料
1\] Natan Yellin. (2022). *For the Love of God, Stop Using CPU Limits on Kubernetes (Updated)* . Robusta.dev. [https://home.robusta.dev/blog/stop-using-cpu-limits](https://home.robusta.dev/blog/stop-using-cpu-limits "https://home.robusta.dev/blog/stop-using-cpu-limits")