news 2026/9/7 6:35:14

Kubernetes CPU limits 引发延迟尖刺的底层原理与替代方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kubernetes CPU limits 引发延迟尖刺的底层原理与替代方案

先说结论:在 Kubernetes 里给 Pod 设置 CPU limits,是生产环境里最常见的“好心办坏事”之一。CPU limits 不会像内存 limits 那样直接把 Pod 杀掉,但它会让应用的延迟出现莫名其妙的尖刺,并且越是在高并发、突发流量下问题越明显。标题里这句 “For the love of god, stop using CPU limits on Kubernetes” 看起来情绪化,但它背后是社区里被反复验证过的生产事故经验,并不是一句口号。

很多 Kubernetes 入门指南和面试题只讲了 requests 和 limits 的“标准答案”:requests 用于调度,limits 用于限制上限。这个答案没有错,但它忽略了一个关键问题——CPU 的 limits 是通过内核 CFS 配额机制强制执行的。当应用撞上这个配额时,内核不会终止进程,而是直接让进程“停下来等下一段时间窗口”,这个过程就是 CPU Throttling(CPU 节流)。对在线业务来说,一次 100ms 级别的节流,就足以让 P99 延迟翻好几倍。

这篇文章不是让你完全不管资源上限,而是把 CPU limits 的底层机制、观测方法、取舍边界和替代方案一次讲清楚。读完你会知道:什么场景该去掉 CPU limits,什么场景必须保留,怎么用 Prometheus 验证 Pod 是否正在被节流,以及如何用更合理的手段保护集群资源。建议收藏备用,尤其是正在维护在线服务、经常排查延迟毛刺的读者。

1. 核心结论速览

维度CPU requestsCPU limits
作用阶段调度器选节点、节点资源预留Pod 运行时的内核级约束
底层机制调度器计算,软性保证cgroup CFS 配额,强制性节流
超额后果新 Pod 排队等待调度进程被内核节流,延迟升高
资源属性可压缩资源的“申请量”可压缩资源的“保险丝”
是否触发 OOM否,但会触发 CPU Throttling
对延迟影响可预测、可量化突发时不透明,难排查
云厂商计费部分平台按 requests 核算实例资源部分平台按 limits 核算,需看产品文档

这篇文章的主要判断是:在线延迟敏感型服务,默认不要设置 CPU limits,或者把 limits 设置成明显高于峰值的“保险丝”;离线批处理任务和 CI 类任务,可以保留 CPU limits 来保护邻居工作负载。

注意一点,CPU 在 Kubernetes 里属于可压缩资源(compressible resource)。意思是 CPU 可以被时间片切分,不会因为超额直接杀死容器,这是很多人觉得“CPU limits 比较安全”的原因。但正因为它是通过 cgroup 配额强行切时间片的,节流带来的损害是隐性的——CPU 使用率看起来不高,延迟却突然劣化,定位起来非常困难。

2. requests 和 limits 在 Kubernetes 里的定位完全不同

生产环境里经常看到resources配置写成下面这样:

apiVersion: v1 kind: Pod metadata: name: app-with-limits spec: containers: - name: app image: nginx resources: requests: cpu: 500m memory: 512Mi limits: cpu: "1" memory: 1Gi

这种写法看起来面面俱到,但 CPU 的 requests 和 limits 实际上是两套完全不同的机制。

requests 的作用范围是调度阶段。调度器在挑选节点时,会把节点上所有 Pod 的 requests 累加,用来判断新 Pod 能否放得下。也就是说,requests 决定了你这个 Pod 能不能被调度,以及节点会为它预留多少 CPU 份额。只要节点上还有空闲的 requests 额度,kubelet 就允许调度,即使节点实际已经跑得很满。

limits 的作用范围是运行阶段。kubelet 会把 limits 翻译成 cgroup 配置,限制这个容器所属的 cgroup 最多能使用的 CPU 时间。一旦达到配额,内核会启动节流机制。limits 对调度结果没有直接影响,它不会阻止节点超卖,也不会让调度器提前避开繁忙节点。这导致一个很尴尬的局面:你设置了 CPU limits,集群该超卖还是超卖,但你的应用却随时可能因为撞上配额被内核按住。

从 QoS 等级来理解更清楚。Kubernetes 把 Pod 分成三类:

  • Guaranteed:所有容器 requests 和 limits 都设置且相等。
  • Burstable:至少有一个容器设置了 requests,且 requests 不等于 limits。
  • BestEffort:所有容器都没有设置 requests 和 limits。

CPU limits 一旦存在,就会参与 QoS 等级判断。你想通过设置 limits 来“保护集群”,结果可能只是把 Pod 变成了 Burstable,并没有真正解决资源隔离问题。真正的隔离和容量控制,应该交给 requests、命名空间配额、节点池和优先级机制,而不是靠单个 Pod 的 CPU limits。

3. CPU limits 为什么会引发严重节流:CFS 配额原理

要理解 CPU limits 的坑,需要先了解 Linux 内核的 CFS 带宽控制机制。cgroup v1 中有两个核心文件:cpu.cfs_period_uscpu.cfs_quota_us。Kubernetes 默认把 period 设置为 100000 微秒,也就是 100ms;quota 则由 CPU limits 换算而来。比如 limits 为 1 核时,quota 就是 100000 微秒,表示这个 cgroup 在每 100ms 周期内最多能使用 100ms 的 CPU 时间。

如果 cgroup 里的线程在这个周期内提前用完 quota,它们就会被标记为 throttled,暂停调度,直到下一个 100ms 周期开始。这就是 CPU Throttling 的本质:它不是排队,而是直接剥夺运行资格。

举个例子。某个 Java 服务配置了limits.cpu: 1,那么每个 100ms 周期里只有 100ms 的 CPU 预算。如果服务在周期开始的前 10ms 内突然爆发,多个线程同时被打满,瞬间就消耗掉 100ms 预算,那么剩下的 90ms 内整个进程只能等待,直到下一个周期重新分配预算。这种情况下,CPU 使用率的平均值可能连 0.5 核都不到,但延迟却可能频繁飙到几百毫秒。

这就是社区里反复强调的“激进节流”(aggressive throttling)现象。很多 JVM、Go、Node.js 服务都因此出现过 P99 延迟失控的问题——进程平均 CPU 使用率明明远低于 limits,但因为短时间窗口内的突发量超过配额,被内核反复节流。问题在于 CFS 配额是以 100ms 为窗口结算的,它根本不看你过去 10 秒的平均负载,只看当前窗口是不是超了。

从 cgroup v2 的角度看,对应的是cpu.max文件,格式是quota period,同样存在完全一致的节流逻辑。也就是说,不管集群用的是 cgroup v1 还是 v2,只要你设置了 CPU limits,就不可避免地引入这一层节流风险。CPU 的“可压缩”特性反而让这种损害更隐蔽:进程不会死,Kubernetes 也不会直接报错,只有延迟和吞吐数据会出现无法解释的波动。

4. 如何确认你的 Pod 正在被 CPU Throttle

很多团队直到线上延迟告警才想起 CPU limits 的问题。更稳妥的做法,是把节流指标纳入日常监控。cAdvisor 暴露了两组非常有用的指标:

  • container_cpu_cfs_periods_total:cgroup 结算的总周期数。
  • container_cpu_cfs_throttled_periods_total:其中触发节流的周期数。

用第二项除以第一项,就能得到最近一段时间内被节流的周期占比。下面的 PromQL 可以按 Pod 维度统计最近 5 分钟的节流比例:

sum by (namespace, pod) ( rate(container_cpu_cfs_throttled_periods_total{container!="", container!="POD"}[5m]) ) / sum by (namespace, pod) ( rate(container_cpu_cfs_periods_total{container!="", container!="POD"}[5m]) )

这个比例如果长期大于 0,说明 Pod 经常撞上 CPU limits。比例越高,节流越频繁。你也可以在 Grafana 里做一个单独的看板,观察每个工作负载的节流曲线,这比只看 CPU 使用率要有效得多。

如果没有完整的 Prometheus 环境,也可以先登进容器里看 cgroup 统计。cgroup v1 环境下执行:

kubectl exec -it <pod-name> -n <namespace> -- cat /sys/fs/cgroup/cpu/cpu.stat

输出会包含几个关键字段:nr_periods表示周期总数,nr_throttled表示被节流的周期数,throttled_time是累计节流时间,单位是纳秒。如果nr_throttled / nr_periods明显偏高,基本可以确认 CPU limits 在拖后腿。

cgroup v2 环境下执行:

kubectl exec -it <pod-name> -n <namespace> -- cat /sys/fs/cgroup/cpu.stat

输出里主要看nr_periodsnr_throttledthrottled_usec三个字段,含义和 v1 一致,只是节流时间单位变成了微秒。注意,不同发行版和容器运行时的 cgroup 路径可能不同,如果找不到文件,可以先在容器里执行mount | grep cgroup确认挂载版本。

只看 CPU 使用率是不够的。下面这条命令只能告诉你 Pod 当前用了多少 CPU,无法体现它是否被节流:

kubectl top pod -n <namespace>

所以建议的排查顺序是:先看kubectl top pod确认大体使用量,再看 cgroup 统计判断是否频繁节流,最后回到 Prometheus 看完整时间趋势。如果节流比例高、延迟毛刺又和节流时间点吻合,CPU limits 基本就是延迟劣化的元凶。

5. 什么场景应该保留 CPU limits,什么场景应该去掉

CPU limits 不是完全不能用,关键看工作负载类型。下面这张表可以作为初步判断依据:

工作负载类型典型场景CPU limits 建议原因
在线延迟敏感服务Web API、网关、在线推理、注册中心不建议设置,或设置明显高于峰值的余量节流会直接打爆 P99 延迟
离线批处理任务数据清洗、离线推理、日志分析可以设置 limits=requests 或 1.2~1.5 倍余量延迟不敏感,需要保护节点上的邻居任务
CI/CD 构建任务GitLab Runner、Jenkins Agent可以设置 limits=requests构建任务本身隔离性较弱,限制可以防止打满宿主机
无状态中间件Nginx、Redis、Kafka 客户端视业务优先级而定高频小包处理对调度延迟很敏感,需要先做节流观测
开发和测试环境个人学习集群、测试环境可以保留合理 limits防止误操作或异常代码打爆节点

在线服务的核心矛盾在于:你无法准确预测下一秒的突发量。即使平均 CPU 使用率只有 0.3 核,一次请求风暴就可能让应用在某个 100ms 窗口内冲到 2 核以上。如果这时候 limits 设置的是 1 核,应用就会被硬生生按下去,大量请求排队等待,延迟直接恶化。

对于批处理任务则相反,这类任务通常没有严格的延迟要求,允许延迟完成,但不允许影响同节点上的在线服务。此时把 limits 设置成略大于 requests,相当于给任务加了一个“软保险丝”,牺牲一些任务吞吐,换取节点稳定性,这笔账是合算的。

还有一类特殊情况:云厂商托管 Kubernetes 强制要求配置 limits,或者某些内部平台规范要求所有工作负载必须有 limits。这种情况下,可以把 CPU limits 设置成一个远高于峰值使用量的值,让它只充当监控和容量核算的下限,而不是日常运行的硬束缚。同时要在文档里注明,这个 limits 不是用于限制 CPU 的,只是平台合规要求。

6. 不用 CPU limits,怎么防止资源被单 Pod 拖垮

去掉 CPU limits 之后,很多人第一反应是担心失控。这个担心是对的,但解决方案不应该是盲目加 limits,而是把资源控制拆成多个层次。

首先是把 CPU requests 设置准确。requests 才是调度和容量规划的基石,调度器依据 requests 判断节点是否放得下新 Pod。如果 requests 设得过低,节点会被超卖,去掉 limits 后某个高负载服务确实可能把其他服务的 CPU 挤占掉。如果 requests 设得合理,即使没有 limits,调度器也会尽量把负载分散到多个节点。建议先用 VPA(Vertical Pod Autoscaler)或历史监控数据校准 requests 值,再考虑去不去 limits。

其次是使用命名空间级 ResourceQuota。ResourceQuota 可以限制某个命名空间内所有 Pod 的 requests 总和,防止单个团队无限申请资源。示例配置如下:

apiVersion: v1 kind: ResourceQuota metadata: name: team-a-quota spec: hard: requests.cpu: 20 requests.memory: 64Gi limits.cpu: 40 limits.memory: 128Gi pods: 200

这样即使某个服务的 Pod 不设置 CPU limits,团队整体的 CPU 请求量也受控,不会出现整个命名空间无限增长的问题。

然后是 PriorityClass。高优先级服务在资源紧张时可以先抢占低优先级 Pod 的资源。只是注意,PriorityClass 与 CPU quotas 不同,它是排队和抢占机制,可以在没有 limits 的情况下保护关键业务。节点层面的隔离也可以配合使用:把高 CPU 消耗的批处理任务放到独立的节点池,通过 nodeSelector、taints 和 tolerations 与在线服务分开,从物理层面避免相互影响。

最后是集群层面的自动扩缩容。如果集群整体 CPU 压力高,最直接的解决办法是扩容节点,而不是压制应用。Cluster Autoscaler 可以根据 Pod 的 requests 自动增加或减少节点,没有 limits 并不会导致集群无法扩容,调度器反而会因为 requests 更真实而做出更准确的容量决策。

需要特别提醒的是,不用 CPU limits 不代表不用内存 limits。内存是不可压缩资源,内存超限会触发 OOMKill,甚至可能导致节点进入不健康状态。所以内存的 requests 和 limits 该设还是要设,CPU 的问题要单独看待,不要因为取消 CPU limits 就顺手把内存 limits 也去掉。

7. 安全推进:移除 CPU limits 的落地步骤

如果你已经决定在线服务去掉 CPU limits,不要一次性全集群铺开,建议按下面的步骤推进。

第一步,先量化现状。确认 Prometheus 里已经采集了container_cpu_cfs_throttled_periods_totalcontainer_cpu_cfs_periods_total,记录当前各服务最近的节流比例和延迟指标。只有先把现状量化,改完之后才知道有没有效果。

第二步,挑选试点。选一个流量不大、延迟敏感度高的无状态服务,最好是一个可以快速灰度、快速回滚的部署。修改前先确认它的 CPU requests 覆盖了近期峰值,避免去掉 limits 后的资源变化导致调度问题。

第三步,灰度上线。新版本只去掉 CPU limits,保留 CPU requests,内存配置保持不变。发布后对比试点服务的节流比例、P99 延迟、错误率和 CPU 使用率。正常情况下你会发现节流比例降为 0 或明显下降,延迟毛刺减少。

第四步,扩大范围。试点稳定后,逐步推广到其他在线服务。批处理和 CI 任务可以继续保留 limits,也可以按需求设置 limits=requests。推广过程中如果节点出现 CPU 饥饿,先检查会影响资源调度的 requests 是否合理,优先调整 requests,而不是退回到加 limits 的做法。

第五步,建立长期监控和复盘机制。每次变更后保留 3 到 7 天的监控窗口,关注节流比例变化和节点压力。如果出现异常,直接回滚到上一个版本,不要在现场反复调整参数。

8. 常见问题与排查

问题现象可能原因排查方式解决方案
平均 CPU 使用率远低于 limits,但 P99 延迟很高CFS 配额内突发导致节流查看节流指标和 cgroup 统计去掉 CPU limits 或调高 limits
去掉 limits 后,某个服务的 CPU 把节点打满requests 设置过低导致超卖查看该服务 requests 和节点容量用 VPA 校准 requests,必要时节点隔离
容器没有被 OOMKill,但响应非常慢CPU throttling 导致线程暂停检查nr_throttled是否很高去掉 limits 并观察延迟恢复情况
设置了 limits=requests 后 Pod 调度失败集群剩余 requests 容量不足查看节点 allocatable 和已分配资源降低 requests 或扩容节点
Helm Chart 自动给所有 Pod 加上了 CPU limits模板默认配置或平台规范检查 Chart values 和渲染结果在 values 中覆盖limits.cpu
取消 limits 后节点整体 CPU 压力升高之前 limits 掩盖了超卖问题查看节点 CPU 和调度分布增加节点、设置 requests 和实际负载匹配
集群里存在大量 BestEffort Pod没有设置任何 requests/limits查看 QoS 分类给所有 Pod 设置 requests,避免无保障调度
修改 resources 后 Pod 重启只改了模板但未触发正常滚动更新查看 deployment rollout 状态触发标准滚动更新或重建

实际排查时,最容易被忽略的是 Helm 模板和平台网关自动注入的资源配置。很多服务表面上没有配置 limits,但渲染出来的部署文件里已经被平台加上了默认值。建议用下面的命令确认实际运行时的资源定义:

kubectl get pod -n <namespace> <pod-name> -o yaml | grep -A 6 "resources:"

如果发现 limits 被自动注入,需要回到模板或集群准入控制器层面处理,而不是只改 Deployment。还要重点检查节点的资源分配情况,通过kubectl describe node <node-name>可以看到当前节点上所有 Pod 的 requests 和 limits 汇总,这是判断节点是否超卖、要不要调整 requests 的最直接手段。

9. 最佳实践总结

把上面的内容整理成一套可执行的规范,大概是这样几类。

在线服务统一采用“只设置 requests,不设置 CPU limits”的默认策略。前提是 requests 必须经过监控数据和 VPA 校准,覆盖近 1 到 2 周的业务峰值。对于必须设置 limits 的场景,把 limits 设置为峰值使用量的 1.5 到 2 倍以上,让它只充当保险丝。

内存配置与 CPU 分开管理。内存的 requests 和 limits 继续保留,防止 OOMKill 拖垮节点。不要因为 CPU 文章的建议就忽略内存限制的意义。

命名空间必须配置 ResourceQuota,限制团队级总请求量,而不是依赖单个 Pod 的 limits。给不同业务设置不同的 PriorityClass,保证高优业务在资源紧张时优先调度。

批处理任务和 CI 任务可以保留 limits=requests 的配置,延迟敏感度低的工作负载本来就是最合适的“受控对象”。它们被节流的代价远低于影响在线业务,所以该限制的时候还是要限制。

监控层面至少包含三块:CPU 使用率、CFS 节流比例、P99 延迟。节流比例建议做成单独的 Grafana 面板,与延迟指标联动观察。任何涉及 resources 的变更,都要有回滚方案和灰度窗口。

最后是合规和成本因素。云厂商的计费模式各有不同,有的按节点规格,有的按 requests,有的按 limits。生产环境调整 limits 之前,先确认现在的计费口径,避免去掉 limits 后账单出现意外变化,也避免因为计费需求而保留不必要的配置。

10. 写在最后

回到这个有点“情绪化”的标题:确实不应该轻易在 Kubernetes 里给在线服务设置 CPU limits。原因不在于 limits 是一个错误的 API,而在于它把“资源保护”这个复杂问题简化成了一个内核配额,转移了运维人员对 requests、优先级、节点隔离和容量规划这些真正关键环节的注意力。

在你的集群里,第一步不是急着删除所有 limits,而是先把节流观测做起来。用一个下午部署好指标采集和 Grafana 看板,找出节流最严重的几个服务,然后挑一个低风险服务做试点,对比去掉 limits 前后的延迟数据。当你亲眼看到 P99 恢复平稳、节流指标归零的时候,大概率会认同这个结论。

每个团队都应该有一套自己的资源策略,而不是照着模板配置 all-in-one 的 requests 和 limits。先做度量,再做决策,最后再规模化变更,这是最稳妥的路径。建议把这篇文章的观测命令和排查思路保存下来,等集群里再次出现“CPU 不高但延迟毛刺很多”的问题时,直接照着查一遍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 6:33:20

Java实现顺序表:从线性表到动态数组扩容的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:33:16

Docker新手实战:从安装避坑到MySQL与Redis部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:32:51

FlaUI与Winform联手:微信自动化操作实战指南

简介&#xff1a;面向希望用C# Winform实现微信自动化的开发者&#xff0c;以FlaUI为自动化核心的完整工程&#xff0c;系统展示了定时任务、自动回复、群聊机器人三大功能的落地思路&#xff0c;覆盖从消息监听、界面元素定位到逻辑判断与模拟发送的完整链路&#xff0c;适合有…

作者头像 李华
网站建设 2026/9/7 6:31:44

Nginx 1.7.11.3 Gryphon定制版实战:反向代理、负载均衡与部署排查

简介&#xff1a;这份压缩包是基于Nginx 1.7.11.3的Gryphon定制版本&#xff0c;专为媒体直播服务优化&#xff0c;与FFmpeg整合后可以支撑RTMP、HLS、DASH等流媒体协议&#xff0c;适合运维人员和流媒体开发者参考学习。包内共有126个文件&#xff0c;以C语言模块源码、头文件…

作者头像 李华