CPU limits 是 Kubernetes 里被讨论最多、也最容易踩坑的参数之一。它本意是限制容器能使用的 CPU 上限,防止某个应用把节点资源占满,但在实际生产环境里,这个“保护”机制经常变成应用性能突然下降、接口延迟飙高、服务被无辜重启的元凶。
如果你正在管理 Kubernetes 集群,或者负责应用容器化改造,这篇文章想和你认真聊一下:为什么很多有经验的团队会建议你“不要随便设置 CPU limits”,以及如果真的需要限制,应该怎么做、判断标准是什么、常见坑在哪里。
先说核心结论:CPU requests 一定要设置,而且要认真评估。CPU limits 能不加就不加,除非你有明确的、可量化的理由,并且充分理解 CFS 配额和内核节流机制。
很多人第一次看到这个观点会不理解:Kubernetes 官方文档里不是说得清清楚楚吗,requests 和 limits 是一对,limits 用来限制上限,防止某个容器把节点耗尽,为什么你们这些老油条又说不要用?这里面的关键不在“限制”本身,而在于 CPU 资源不同于内存,它的事件驱动和可压缩特性决定了 limits 的实现方式会带来非常隐蔽的性能问题。
1. 先把 CPU requests 和 CPU limits 的真实关系讲明白
1.1 两个参数不是同一个维度的东西
Kubernetes 里 CPU 相关的两个常用字段,一个是spec.containers.resources.requests.cpu,另一个是spec.containers.resources.limits.cpu。
很多刚接触 Kubernetes 的人会这样理解:requests 是最低保障,limits 是最高上限。容器至少能拿到多少,最多只能用多少。听起来像是一个范围区间,对吧?实际不是。
requests 的主要作用是调度。它告诉调度器:我这个容器需要多少 CPU,请给我找一台能满足这个条件的节点。调度完成后,requests 还会用于节点资源分配的计算、HPA 扩容判断、以及 kubelet 对节点压力驱逐的判断。它更像是一个资源预留承诺。
limits 的主要作用是约束。当容器实际使用 CPU 超过了 limits 规定的配额时,内核会通过 CFS(Completely Fair Scheduler,完全公平调度器)带宽控制机制对容器进程进行节流,也就是限制它继续使用 CPU。
这里面最微妙的地方在于:如果一个容器设置了 requests=500m、limits=1000m,它实际运行中最多能使用的 CPU 不是 500m,而是可以达到 1000m,但能否真的跑到 1000m,取决于节点上是否有多余的空闲 CPU。如果有空闲 CPU,它可以借用来突破 requests 限制;如果没有空闲 CPU,它的实际可用 CPU 会向 requests 靠拢。它最多不会超过 limits。
换句话说,limits 不是“最多能使用多少 CPU”的免费提示,而是一道强制刹车。当这个刹车触发时,容器中所有线程都会被暂停,直到下一个调度周期重新获得配额。这种暂停对用户态的进程来说,表现为 CPU 时间被莫名抽走,而应用本身并没有做任何错误操作。
1.2 空闲 CPU 为什么不能随便借给被 limit 的容器
一个节点上有 8 个 CPU 核心,跑了 3 个容器,CPU requests 一共只用了 3 核,那么节点上还有 5 核空闲。如果其中一个容器设置了 limits=1 核,它即使想利用空闲的核心,也无法越过 1 核的限制,超过之后就会被节流。而节点上的空闲 CPU 明明足够,其他容器也没有抢占资源,但这个容器依然会被强制放缓。
这就是矛盾所在:你设置 limits 的目的是防止某个容器拖垮节点,但实际发生的情况是,节点资源充足,没有任何容器受到威胁,你的容器却因为 limits 被内核强行压制了性能。
更难受的是,CPU 节流不会直接体现在 Kubernetes 事件里,不会出现在 Pod 的重启原因里,也不会让容器彻底退出。它只会让应用的响应时间变长、吞吐量下降、线上业务出现周期性的“卡顿”。这种问题极其隐蔽,排查起来也特别容易走弯路。
2. 为什么内核的 CFS 配额机制遇到多线程应用更容易出事
2.1 CFS 配额和 CFS 周期的概念
Linux 内核的 CFS 带宽控制工作原理可以简化成一句话:在每一个 period 周期内,一个进程组最多只能运行 quota 指定的 CPU 时间。
默认的 period 一般是 100ms。比如一个容器设置了 limits.cpu=2000m,那么内核给这个容器分配的 quota 大约是 200ms 的 CPU 时间,每 100ms 周期重置一次。也就是说,在这个 100ms 的时间窗口里,这个容器最多只能占用 2 个 CPU 核心的完整计算时间。
看起来没什么问题,对吧?关键坑在于:这是整个容器内所有线程共享的配额,不是每个线程单独一份。
如果你的应用是单线程程序,它在一个周期内最多能运行 100ms,如果超过配额,就被强制休眠,等下个周期。如果你的应用有 4 个线程,每个线程各自都想跑 100ms,那就超过了总配额,内核会让所有线程全部暂停,直到新的周期开始。
这种机制天然对多线程、高并发、突发型应用不友好。因为线程数量越多,在同一周期内累计消耗的 CPU 时间就越容易快速触顶。Linux 内核为了保证配额不超支,会在周期结束时统一“算总账”,算出来超了,就整体冻结,直到下个周期。
对一个 Java 应用来说,JVM 线程数量动辄几十个、上百个;对 Node.js 应用,也有 event loop 线程和线程池。只要高并发一上来,线程对 CPU 的需求呈突发性增长,CFS 配额很快被打满,容器直接被节流。这不是应用代码 bug,而是资源层的流速被切断了。
我在实际项目里见过这样的现象:某个服务平时响应时间 20ms,某天高峰期响应时间突然变成 800ms,去掉 limits 后响应时间恢复原样。代码没有任何变更,内存也没有问题,全部根因就是容器内线程触碰到了配额上限。
2.2 为什么 CPU 节流比内存 OOM 更隐蔽
内存超限时,内核会直接触发 OOM Killer,或者容器因为超过内存 limit 被 kubelet 杀死,现象是 Pod 重启、Exit Code 137。这种问题虽然也烦,但至少很直观,一看监控就能定位。
CPU 节流不会杀掉容器。它在内核时间片级别把进程暂停,然后恢复,不会产生任何错误日志,不会触发重启,不一定有事件。你只会看到应用指标里的延迟、吞吐量、CPU 用量出现周期性抖动。如果你平时没观察过 CPU throttling 相关指标,确实很难第一时间想到是 limits 在作怪。
尤其在使用 Kubernetes 的metrics-server或 HPA 时,HPA 扩容判断用的是 CPU usage 相对 requests 的百分比,而不是相对 limits。如果你把一个 Pod 的 requests 设得很低,比如 50m,但 limits 设成 2000m,服务在高峰期已经触顶 limits、被疯狂节流,HPA 可能完全无感,因为按 requests 计算出来的负载率已经超过了扩容阀值,导致集群里有一堆“假健康、真卡顿”的 Pod。
更讽刺的是,有些团队为了服务稳定,会把 limits 设置得比 requests 大很多,比如 requests=500m、limits=2000m,结果原本是想给服务留出 CPU 突发空间,实际却因为在突发时撞上了 CFS 配额而被强制节流。你预留的突发空间,并没有变成服务的缓冲能力,反而成为另一个限制线。
3. CPU limits 引发问题时的典型表现和排查思路
3.1 现象一:响应时间周期性上升,但 CPU 利用率并不高
一个常见场景是:服务端到端延迟每隔 100ms 就会有一个明显的峰值,这个周期约等于 CFS period。你的 Pod CPU 使用率可能只有 60%,看起来离 limits 还有距离,但实际上这个 60% 是整个周期内的平均值。在周期初始时,所有线程一起猛跑,很快就耗尽了配额,剩余时间全部被挂起;到了下个周期,又重新开始。所以平均 CPU 使用率并没有很高,但应用实际的“可用计算时间”已经被切成了碎片,导致任务被频繁暂停。
通过kubectl exec进容器,查看/sys/fs/cgroup/cpu/cpu.stat(不同内核版本路径略有区别,可能是cpu.stat)能看到nr_throttled(被节流的次数)和throttled_time(被节流的总时长)。如果throttled_time占比明显,基本可以确认节流正在发生。
比如你跑了 5 分钟,throttled_time达到了 30 秒,说明有 10% 的计算能力被白白切掉了。即使 CPU 使用率不高,应用也会觉得“CPU 不够用”。
3.2 现象二:Pod 频繁重启,但日志里没有明显异常
有些服务因为丢进了 limits 设置,线程调度被延迟,可能导致健康检查超时、锁等待超时、连接池耗尽等问题,然后被 kubelet 判定为不健康,触发重启。日志里可能看不到具体报错,只在 restart count 上看到在增长。
这种时候你要怀疑的往往不是应用能不能正常工作,而是它在特定资源模型下没法正常工作。我遇到过的一个真实案例是,某消息消费者应用设置了 limits.cpu=1000m,但是单条消息处理时会出现并发解压和批量发送,瞬时 CPU 需求很高,结果消费者经常出现心跳超时,被判定为离群实例,触发自动重启和重新平衡。移除 limits 后,稳定运行了几个星期都没有再出现重启。
3.3 排查顺序:先从这三个地方找直接证据
如果你收到了业务反馈说“服务变慢,但没有崩溃”,先不要急着改代码或加副本,按这个顺序查:
- 查 CPU 节流指标:
container_cpu_cfs_throttled_periods_total、container_cpu_cfs_throttled_seconds_total,这是最直接的判断依据。如果 throttled periods 占运行周期的比例高,说明 CPU limits 确实在限制进程运行。 - 查 Pod 实际 CPU 使用是否接近 limits:通过 Prometheus 的
container_cpu_usage_seconds_total计算容器实际使用的 CPU 量,和 limits 做对比。如果实际使用量经常接近或触顶 limits,那没问题;如果实际使用量离 limits 很远,但节流指标很高,说明应用线程出现了突发性的 CPU 竞争,比如在很短时间内把所有配额耗尽。 - 查节点的 CPU 分配压力和空闲资源:用
kubectl describe node查看节点的分配情况。如果当前节点的 CPU requests 总和远远低于节点总容量,说明节点完全不紧张,那节流就不是因为资源不够,而是 limits 本身在起作用。如果你发现节点 CPU requests 已经超卖严重,那说明集群整体资源规划有问题,移除 limits 后需要更谨慎。
4. 不设置 CPU limits,会不会真的把节点打爆
4.1 需要区分“资源竞争”和“资源黑洞”
很多人拒绝移除 limits,是有充分理由的:他们见过某一次事故,某个应用因为代码 bug 或者异常流量,把节点 CPU 直接吃满,导致同节点上的其他应用全部受到影响。这个担心是合理的。
CPU 不同于内存。内存是硬隔离,一个容器申请到的内存页不会被另一个容器占用;CPU 是共享的,如果不对容器做任何限制,完全依靠 Linux CFS 的默认调度策略来分配 CPU 时间。在多种进程竞争 CPU 时,CFS 会根据优先级和权重尽量均衡分配,但如果你不设置任何 limits,同时也不设置 requests,那么所有容器在同一层级下争抢 CPU,确实会出现某些容器饿死其他容器的情况。
这里的关键是:你真正需要的不是“限制上限”,而是“保障最低”和“合理划分优先级”。
4.2 只设置 requests 的情况下,CPU 如何竞争
如果你给每个容器都设置了合理的 requests,但故意不设置 limits,那么容器之间会怎样竞争?
节点会将总 CPU 时间分配给所有容器。如果所有容器的 CPU 使用都没有超过自己的 requests,那么每个容器都能按 requests 得到资源,相安无事。如果某个容器试图超过自己的 requests 去抢占额外 CPU,CFS 的权重机制会根据容器的 CPU shares(即 requests 值)来分配剩余空闲 CPU 时间。requests 高的容器在争抢空闲 CPU 时有更大权重。
具体来说,如果一个节点上有两个容器,容器 A requests=1000m,容器 B requests=500m,当两者都在满负荷运行时,容器 A 能拿到的 CPU 大约是容器 B 的两倍,而不是均分。这其实就是为容器输入了“优先级”的概念。
对运维来说,最担心的是某个容器因为绕过 limits 而彻底吃掉节点全部 CPU。这个情况在只设置 requests 时基本不会发生,因为 CFS 会在运行时按 requests 权重来分配时间。只有当你既没设 requests 也没设 limits 时,所有容器才处于同一默认权重下,容易出现某些异常容器在突发高 CPU 时抢占过多资源。所以必须设置 requests,这才是资源隔离和保障的基础。
4.3 低规格节点要不要额外保护:用 Burstable 和 Guaranteed 的 Qos 来理解
没有设置 limits 的 Pod 会被归入 Burstable 类(当 requests 和 limits 不一致且全部设置了时,也是 Burstable)。如果你为 Pod 设置了 requests 和 limits 且两个值相等,Pod 属于 Guaranteed 类。Guaranteed Pod 在节点资源紧张时,被驱逐的优先级最低。
如果集群节点资源不紧张,Burstable 类 Pod 可以通过突发占用来提高响应速度;如果节点资源紧张,kubelet 会先驱逐 BestEffort(没有任何 requests/limits)的 Pod,再考虑 Burstable。这个机制比硬性的 limits 更符合生产需求。
换个角度理解:limits 是把“最多能用多少”写进了内核,而 CFS 权重调度是把“该给你的保障份额”以权重方式写进内核。前者更像一刀切的刹车,后者更像灵活分配的比例。生产环境里我们更希望保障底线,同时允许服务在有富余资源时把速度提上去。
5. 如果一定要设置 CPU limits:什么时候、怎么设、设多少
5.1 什么场景下,设置 CPU limits 是合理的
确实有一些场景下设置 CPU limits 比不设更好。我总结了四个我会主动配置 limits 的场景:
场景一:应用内部依赖了静态线程池或工作线程。比如旧版 Java 应用使用Executors.newFixedThreadPool(N),线程数量固定,如果容器外没有线程数上限约束,容器内线程过多时会导致线程阻塞、上下文切换开销飙升。这类应用设置 limits 可以避免线程池被突然打满时 CPU 被闲置消耗掉,虽然这不是 Kubernetes 层面能根治的问题,但 limits 可以起到兜底作用。
场景二:调度器需要计算“最坏情况下”的资源占用。有些团队在集群做容量规划时,需要确保所有 Pod 的 limits 总和不超过节点容量,否则担心极端情况下触发内核 OOM 或者节点不可用。如果你确实经常遇到节点资源被超卖到极限,那么给关键业务设置严格的 limits 是主动行为,不是在背地里限制性能,而是在容量模型下有意为之。
场景三:多租户场景或对外提供资源配额。如果你的集群是给多个业务部门共用,每个部门申领资源时需要一个上限承诺,limits 就是承诺的载体。没有 limits 的话,某个部门的应用如果突增,会侵占其他部门应用的资源,虽然后续可以通过 Quota 和 LimitRange 来限制,但在请求时就带上 limits 更直观。
场景四:某些有状态中间件,如 Java 富客户端、Cassandra、Elasticsearch 等。这类组件对线程数和CPU依赖特别强,如果节点上 CPU 波动过大,它们反而容易出问题。给它们一个相对稳定的 CPU 上限比让它们随时抢占更高,因为抢占来的多 CPU 并不会稳定提升这些组件的性能,反而会导致 GC 线程抖动、JVM 频繁暂停等问题。
5.2 设置 limits 时,最重要的一个参数不是配额值,而是 period
如果确定要设置 CPU limits,请留意内核 CFS period 参数。Kubernetes 没有直接暴露 period 的配置字段,但可以通过 kubelet 启动参数--cpu-cfs-quota-period调整(一般默认是 100ms)。
如果你的应用是延迟敏感型、需要极低延迟,但突发计算量不大,可以把 period 调小,例如 20ms 或 10ms。这样配额更频繁地重置,线程更快获得新的 CPU 时间,节流带来的“停顿感”会减轻。但调小 period 会增加内核调度开销,要确认节点上容器数量是否很多。
如果调大 period,例如 500ms 或 1000ms,在周期内能积累更多 CPU 时间,对批处理类应用友好一些,但延迟会显著增加。生产环境一般不建议轻易改 period,因为它是节点级参数,会影响所有容器。改之前要做充足的对比测试,监控 CPU 节流率和响应时间变化。
5.3 建议的设置原则:把 limits 当作最后一道防线,不要把它当默认配置
我更推荐的原则是:先设 requests,不设 limits,运行观察一段时间。如果监控稳定,就不加 limits。如果确实遇到了资源抢占或容量规划问题,再加 limits,但要把值设在合理范围内,并确保 limits - requests 的差值不能是“拍脑袋”填出来的。
一个常见的比较稳妥的限制方式是:把 limits 设为 requests 的 1.5 到 2 倍。比如 requests=1 核,limits=2000m。这样应用能在短时间突发借助额外 CPU,而不会被硬性掐死。但要注意,这种配置下如果突发持续时间较长,最终还是会节流,所以要靠 HPA 或集群自动扩缩容来处理长期负载,而不是依赖 limit 内的突发空间。
limits 和 requests 一致时,Pod 属于 Guaranteed 类,稳定性最好,但突发能力为零。所以如果你想要突发能力,就接受 Pod 为 Burstable 类;如果你想要绝对的调度确定性和稳定的资源占用,就设成一致。
5.4 CPU 压测是唯一可靠的判断方式
我不太建议完全照着文档或者别人的经验来定 requests 和 limits,因为每个应用的 CPU 代码路径、线程模型、垃圾回收策略差异非常大。可靠的判断方式很简单:压测。
找一个和生产环境接近的 Pod 配置,压测三个版本:
- requests=500m,无 limits;
- requests=500m,limits=1000m;
- requests=500m,limits=2000m。
压测指标重点看四个:P99 延迟、吞吐量、CPU 节流率和线程阻塞情况。如果版本 2 和版本 1 相比,P99 延迟明显抬高,但吞吐量和 CPU 使用率又不高,说明节流对性能损伤很大,这个 limits 就不合适。如果版本 3 和版本 1 差别不大,且你的容量规划允许,可以保留一个相对宽的 limits 以便运维兜底。
注意,压测时要让负载有突发性,不要用恒定低负载去压,因为恒定低负载通常不会触顶配额。要模拟真实业务里的尖峰,比如 10 秒内并发翻倍、单条消息计算量特别大等。
6. 把 CPU limits 从现有环境里逐步移除的实操路径
6.1 第一步:用指标确认哪些 Pod 正在被节流
不要直接全部删除 limits,先收集证据。使用 Prometheus 查询 CPU 节流相关指标:
sum by (namespace, pod) ( rate(container_cpu_cfs_throttled_seconds_total[5m]) ) / namespace或者查看哪些 Pod 的节流时间占运行时间比例较高:
increase(container_cpu_cfs_throttled_periods_total[5m]) > 0通过 PromQL 你可以找出在某段时间内发生了节流的 Pod 列表。再结合container_cpu_usage_seconds_total看实际使用率和 limits 的接近程度。如果实际使用率远低于 limits,但节流次数很高,说明应用的 CPU 请求是突发型的,设置 limits 对它来说是纯粹的伤害。
6.2 第二步:按风险分组,先从小流量服务开始
移除 limits 这件事,最好按风险从低到高逐步推进。
优先级先动的是:
- 已经有独立监控、且节点容量充足的服务;
- 无状态服务,扩容快,能快速拉起新副本;
- 本来就依赖 HPA 自动扩容、且资源规划比较宽松的服务。
暂时不要动的:
- 共享节点上还跑着其他服务、且没有用 requests 做资源隔离的服务;
- 没有合理设置 requests 的服务,如果连 requests 都没有,移除 limits 后出事的概率很大;
- 集群节点 CPU 超卖严重、且没有一个配套的资源评估和排障流程的情况。
操作时建议通过 deployment yaml 移除 limits 字段,把resources.limits整个删掉,保留 requests。不要只把 limits 的值改大,因为一旦填了 limits,它仍然会成为节流阈值。
6.3 第三步:把自己的监控视图建好,设置快速回滚方案
没有 limits 之后,最重要的监控不是 CPU 使用率,而是节点的 CPU 压力。看这几个指标:
- 当前节点的 CPU requests 总和是否超过了节点总 CPU;
- 节点的 load average 是否长期高于核心数的 1.5 倍;
- 是否存在某几个 Pod 持续把 CPU 使用率拉到 70% 以上,同时其他 Pod 响应变慢。
如果发现移除 limits 后某个服务频繁抢占空闲 CPU,导致同节点其他服务性能下降,一般不是“移除 limits”这个动作的问题,而是你没有给其他服务设置合理的 requests。此时不要急着把 limits 加回去,先调整同类服务的 requests,把资源分配粒度理顺。
建议给每个服务配置一个资源变更的快速回滚方案,比如把 limits 的配置单独放在一条注释或环境变量里,或者用 Helm values 管理,避免在出现问题时临时手改 yaml 导致配置漂移。
6.4 第四步:把节点容量规划从“CPU limits 总和”改成“CPU requests 总和 + 突发余量”
传统做法里,很多团队在估算节点容量时,会看节点上所有 Pod 的 limits 总和不能超过节点 CPU 总量。这种做法给节点塞满了上限,导致资源利用率极低。
移除 limits 后,容量规划应该换成另一种思路:节点的 CPU requests 总和控制在节点 CPU 总量的 60% 到 80% 之间,预留 20% 到 40% 的突发放量空间,节点可以接受某些 Pod 在某些时刻借用空闲 CPU,但如果长期超过这个水位,就该扩容或缩容。
这样做以后,节点不需要再为每个 Pod 预留最高上限的空间,资源利用率能提升不少。很多团队说 Kubernetes 节省了机器成本,其实就是通过这种“有效的超卖”实现的。前提是把 requests 做准,否则超卖会变成吃空。
7. 关于 Linux 内核版本、Runtime 和云厂商托管集群的额外提醒
7.1 不是所有内核和 CRI 版本的 CPU 限制行为都完全一样
虽然 CFS 配额机制是 Linux 内核长期以来使用的通用机制,但不同版本对带宽控制的具体实现、默认参数、统计口径可能有细微差别。例如较新的内核版本在 cgroup v2 下,cpu.max和cpu.stat的路径就与 cgroup v1 不同。
如果你用的是云厂商的托管 Kubernetes 集群,例如托管版、Serverless 容器服务等,kubelet 参数和 cgroup 驱动可能由云厂商统一管理。你在节点层面能调整的东西很有限,但 Pod 层面的 limits 行为仍然成立。在使用容器运行时不指明 cgroup driver 的情况下,要特别留意 cgroup v1 和 v2 对 CPU 节流行为的差异,尤其是在 Pod 内读取cpu.stat时路径可能不同。
如果你在自建集群上操作,建议先确认内核版本和 cgroup 版本。如果用的是 cgroup v2,CPU 限制相关的位置在/sys/fs/cgroup/cpu.max,该文件里可以看到“quota period”两值;节流统计在/sys/fs/cgroup/cpu.stat,里面的nr_throttled和throttled_usec分别对应被节流的次数和时长。cgroup v1 的默认行为是采用 cpu.cfs_period_us 和 cpu.cfs_quota_us。
7.2 尽量用 Namespace 和 ResourceQuota 控制总资源,而不是靠 Pod limits
如果你担心的不是单个 Pod,而是某个命名空间下所有应用的总资源消耗,不要试图通过给每个 Pod 设置 limits 来限制总使用量。Namespace 级别的正确做法是声明ResourceQuota对象。
例如创建一个限制命名空间总 CPU requests 不超过 8 核的 ResourceQuota:
apiVersion: v1 kind: ResourceQuota metadata: name: cpu-quota namespace: production spec: hard: requests.cpu: "8" limits.cpu: "12"这样即使某些 Pod 不设置 limits,命名空间整体的资源消耗也会在配额管理范围内,不会无限扩张。ResourceQuota 常常和 LimitRange 配合使用,LimitRange 可以给默认的 requests 和 limits 赋值,但要注意这同样会引入 limits 的节流风险,所以默认值要设计得合理。
7.3 不要让 limits 成为唯一防线,也要看 CPU manager 策略
Kubernetes 还有CPUManager和TopologyManager这类静态分配能力。静态 CPU 管理策略可以让 Guaranteed 类 Pod 独占部分 CPU 核心,避免和其他容器共享核心。如果你的服务对延迟要求很高,而且是 Guaranteed 类,可以开启静态 CPU manager 策略。这时设置 limits 反而有意义,因为它是让 Pod 获得独占核心的条件之一:Pod 必须是 Guaranteed QoS,也就是 requests 和 limits 相等。
所以,如果你的应用足够关键、节点核心数足够多,可以考虑用“静态独占核心”的方式替代普通 CFS 共享调度。这样性能和隔离性都要比简单的 limits 好很多,但也意味着资源利用率会降低,不适合大规模普适使用。
8. 如果团队坚持要保留 CPU limits:给几个入门的实用技巧
8.1 用 Burstable 短突发而不是长时间满负载
有些团队出于安全考虑,最终决定保留 limits。如果确定要保留,我建议把服务的请求模型和 limits 值对应起来:
- 对于突发型流量服务,比如短期的脉冲、秒杀、编排任务,可以把 limits 设置成 requests 的 2 到 3 倍,留给突发空间;
- 对于长时间消耗型任务,比如批量计算、数据处理,limits 和 requests 保持一致,避免突发占用来回抖动,把负载压在一个相对稳定的水位;
- 对于延迟敏感型微服务,能用静态 CPU 独占最好,否则尽量不设 limits,靠 requests 和调度策略保障。
8.2 关注 throttled 指标之后,配合调整应用线程池
如果 CPU 节流无法避免地存在,那么从应用侧下手也是一个可行思路。比如 Java 应用在容器里会自动识别 CPU 核数,这个核数通常取自 cgroup 的 quota 值,而不是节点核心数。如果你在配置里手动指定了-XX:ActiveProcessorCount=N,可以手动设置为比 quota 略低的值,避免 JVM 创建过多线程。
但要注意,ActiveProcessorCount设小之后,JVM 线程池、GC 线程、ForkJoinPool 的并行度都会变小,需要压测验证。如果设置不对,P99 延迟和吞吐量都可能下降。这种手段解决的是“线程数过多导致配额快速耗尽”的问题,而 limits 带来的节流现象只是被缓解了,而不是根除。
8.3 用 HPA 配合,而不是用超大 limits 硬抗
如果你发现业务经常达到 limits 上限,说明这个服务容量已经不足,正确的做法不是调大 limits,而是配置 HPA 让它扩容。HPA 根据平均 CPU 使用率进行扩容,这个 CPU 使用率按 requests 计算,因此只要 requests 设置准确,HPA 会在负载升高时自动创建新副本。
设置 HPA 时,建议targetCPUUtilizationPercentage控制在 60% 到 80% 之间。太低了会导致扩容频繁,太高了可能扩容不够及时。如果 HPA 扩容之后,每个 Pod 的 CPU 使用率仍然接近或超过 limits,说明新增副本的生产速度仍不够,需要调整单副本的并发能力或业务处理逻辑,而不是继续堆资源。
9. 最后留几个我自己排查时会优先看的点
如果你正在被 CPU 性能问题折磨,又刚好接触过 Kubernetes,先别急着调代码,看看这几个地方:
- 有没有给 Pod 设置 CPU limits?如果设置了,立刻查一下
container_cpu_cfs_throttled_seconds_total。十次里有八次,问题就出在这里。 - requests 设置得是否合理?有些团队为了提升节点部署密度,把 requests 压得特别低,例如 10m、50m,这会导致调度器把你的 Pod 塞到特别繁忙的节点上。之后即使你给了 Pod 很大的 limits,也没有节点能提供这么多空闲 CPU。这类问题移除 limits 也解决不了,要先调整 requests。
- 容器的 CPU 使用率为什么在整百毫秒附近波动?如果是,几乎可以断定是 CFS quota 耗尽导致的周期性节流,因为默认 period 就是 100ms。
- HPA 扩容判断用的是 usage/requests,不是 usage/limits。如果你设置的 limits 比 requests 大得多,可能出现负载很高但 HPA 不扩容的情况,因为按 requests 算出来的使用率已经触顶,扩容却依然不够。此时需要同时观察 CPU usage 和 requests 的比例,以及 limits 是否成为瓶颈。
- 节点真的空闲吗?不要只看节点 CPU 使用率,很多节点 CPU 使用率不高,但 CPU requests 已经分配满了。这种情况下新调度到该节点的 Pod 虽然能运行,但会被 CFS 和其他 Pod 一起争抢时间片,表现和 limits 导致的节流很相似。通过
kubectl describe node能看到 allocatable CPU 和 requests 的分配情况。
说到底,CPU limits 本身不是一个邪恶的配置项,它是一个有明确语义的内核资源控制机制。问题在于很多团队把它当成默认参数,或者当成防止资源黑洞的唯一手段,结果是应用在资源充足时被白白限速,资源紧张时又没有真正解决问题。
如果你现在正管理着一个稳定的 Kubernetes 集群,我的核心建议是:把 requests 当作资源承诺来做,把 limits 当作特殊场景下的安全网来用,不要把安全网当作日常约束。让 CPU 调度回归到 CFS 的权重公平分配,比简单套一个配额限制要可靠得多。