线上服务延迟突然飙高,CPU 使用率却连 50% 都不到……这是很多开发者在 Kubernetes 上遇到过的最诡异问题之一。排查到最后,往往发现“凶手”不是代码、不是数据库、也不是网络,而是 yaml 里那两行看起来人畜无害的配置:limits.cpu: 500m。
这几年,“别再用 Kubernetes CPU limits”已经成了社区里一个很有争议的话题。一篇在技术圈流传很广的文章甚至直接用了一个相当情绪化的标题:For the love of god, stop using CPU limits on Kubernetes。很多第一次读到这句话的开发者第一反应是:不用 limits,那 Pod 会不会把节点 CPU 打满?会不会互相抢资源?
这篇文章想先给一个明确判断:CPU limits 不是资源保护的安全带,而更像一只会在关键时刻限制发动机功率的隐形手刹。它会让你的容器在节点 CPU 还有大量空闲时,被人为地“踩停”几百毫秒,从而导致 P99 延迟恶化。接下来,我会从 CFS 配额原理讲清楚它为什么会导致 CPU 节流,给出一套可复现的验证方式,最后谈谈工程上到底该怎么保护集群资源。
如果你正在为 Kubernetes 集群里的服务延迟不稳定、CPU 使用率与性能不成正比而头疼,这篇文章值得耐心读完。
1. 为什么说“CPU limits 是一个陷阱”
先看一个很典型的现状:大部分团队在 Kubernetes 里写资源参数时,习惯性地把 requests 和 limits 一起写,甚至只写 limits。原因也很朴素——怕 Pod 之间互相抢 CPU 影响稳定性,怕某个容器写死循环把整台节点打满,也有一部分原因是各种脚手架模板默认就生成了 limits。
但问题在于,很多人在设置 limits 时,并没有意识到 Kubernetes 对 CPU 和内存这两类资源的“强制手段”是完全不一样的。
- 内存是压缩性差的资源。容器超过内存 limits,最直接的后果是 OOM Kill,进程被杀死,这是硬性的、可见的、能被监控发现的。
- CPU 是可压缩资源。容器超过 CPU limits 后,并不会被杀掉,而是被“节流”:内核通过 CFS 配额夺走 CPU 执行时间,让它放慢速度。
听起来“放慢速度”没什么大不了?但真实世界的业务场景里,这个“放慢”往往不是均匀减速,而是集中在一个调度周期内被瞬间冻结,这会造成非常明显的响应时间毛刺。
更隐蔽的问题在于:当容器被节流时,应用自身的监控指标大概率是正常的。如果只看应用指标,CPU 使用率不高、内存不高、错误率也不高,就是延迟扛不住。于是代码、数据库、网络都被排查了一遍,最后才怀疑到 Kubernetes 资源层。
这就是为什么社区越来越多地给出一个有点极端、但在特定场景下成立的建议:默认不要设置 CPU limits。真正要设置的是 CPU requests——它才是调度器用来做资源分配和保障的核心。
2. 先从 requests 和 limits 的区别说起
2.1 CPU requests:调度与保障
CPU requests 是一个 Pod 中的容器声明“我最少需要多少 CPU 时间”的值。调度器(kube-scheduler)在选择节点时,会检查这个节点的可分配 CPU 是否满足所有已有 Pod 的 requests 之和与当前 Pod 的 requests 之和。只有满足,才会调度过去。也就是说,requests 决定了 Pod 被放在哪台机器上,也决定了这台机器会不会被过度承诺。
这里要强调一个容易混淆的点:requests 并不是说“请求这么多,就用这么多”。即使你不写 limits,Pod 在节点 CPU 空闲时依然可以跑满节点上的所有 CPU 核心。requests 的意义是:当其他 Pod 也需要 CPU 时,调度器会优先保证每个 Pod 至少能拿到它声明的资源。它是一份“调度契约”,不是“性能上限”。
2.2 CPU limits:quota 与强制上限
CPU limits 则不同。它声明的是“这个容器最多只能使用这么多 CPU”。Kubernetes 在容器运行时层面把它转换成 Linux cgroup 的 CFS 配额(Completely Fair Scheduler quota)。
CFS 配额的工作方式大体是这样的:内核为每个 cgroup 设置调度周期(cpu.cfs_period_us,通常为 100ms)和配额(cpu.cfs_quota_us)。如果 limits 是 500m,意味着容器在每个 100ms 周期内最多只能使用 50ms 的 CPU 时间。一旦用完,接下来要等到下一个周期才能继续获得 CPU。
这里容易产生的误解是:很多人以为 limits 设置 500m,就表示容器“最多只能吃到半颗核的 CPU”,是一种逐渐限制、平滑降速。但在 CFS quota 的实现里,配额是周期性累计消耗的。只要一个周期内累计 CPU 时间到顶,内核就会把整个 cgroup 内的线程全部暂停,直到周期结束。因此表现往往不是“变慢”,而是“间歇性停顿”。
2.3 QoS 等级是怎么来的
Pod 的 requests 和 limits 组合,决定了它的 QoS 等级:
- Guaranteed:requests 和 limits 相等且都设置了。
- Burstable:至少设置了一个请求或限制,且 limits >= requests。
- BestEffort:完全没设置 requests 和 limits。
Guaranteed 等级在节点资源争抢时的优先级更高,也更不容易被驱逐。但这不意味着你应该为了追求 QoS 等级而无脑设置 limits。QoS 带来的调度优先级提升,可以通过其他方式实现,比如精心规划 requests、控制混部密度。理解 QoS 等级的真正价值,在于知道节点过载时哪些 Pod 会被优先牺牲,而不是为了等级而设置 limits。
3. CPU 节流问题:症状、根因与传导链路
3.1 症状:看起来无害的“隐性问题”
CPU 节流最棘手的地方是它不会让进程崩溃,也不会直接报错。它的症状更多体现在业务层面:
- 单次请求延迟偶尔飙高几倍甚至十几倍;
- 同一时间节点 CPU 整体使用率并不高;
- 应用自身没有发现异常,GC、IO、线程池都正常;
- 压制一段时间后自动恢复,难以复现。
这类问题的特点是间歇性强、隐蔽性强。如果团队没有专门的 CFS 节流指标监控,往往要靠监控面板上的 latency 毛刺才能发现,而排查者很容易把注意力放到服务代码和依赖组件上。
3.2 根因:limits 设置过紧 + CFS quota 周期
节流的直接原因只有一个:容器在某个 CFS 周期内消耗的 CPU 时间达到了配额。但从根因上说,往往是几个因素叠加:
- limits 设置得与真实使用量太接近,甚至低于峰值需求;
- 应用是 Java、Go 这类对延迟敏感的服务,GC 或编译优化需要瞬时高 CPU;
- 业务有突发流量,突发的算力无法被满足;
- 多核容器在并发场景下的 CPU 消耗并不平均,某个瞬间全局累计消耗超过配额。
其中最容易踩坑的就是“按平均值设置 limits”。业务服务的 CPU 使用率天然有锯齿形波动,平均值 300m 不代表峰值只有 500m。一旦把 limits 卡在平均值的 1.5 倍左右,突发流量一到,几乎必然触发节流。
3.3 传导链路:从内核到业务延迟
当 cgroup 被 throttle 后,容器内的进程并不是均匀地变慢,而是线程被暂停。对同步请求模型来说,这就是请求被阻塞;对异步模型来说,这就是事件循环空了、队列积压、连接数上涨,最终可能从“一次请求慢”演变成“连接池耗尽”“网关超时”,甚至触发上游重试,进一步放大流量。
一条完整的故障链路往往是:
节点 CPU 有大量空闲 -> limits 限制了单个容器的瞬时使用 -> CFS 周期结束前线程被迫暂停 -> 请求延迟上涨 -> 上层服务超时重试 -> 压力进一步增大。
这也是为什么“CPU limits 导致的问题”经常被误判成“系统资源不够”。实际上节点资源很宽裕,只是单个容器被限住了瞬时算力。
4. 环境准备与监控前置
动手验证前,先准备好基础环境。以下操作在单节点或多节点 Kubernetes 集群上都成立,本文示例以常见的 kubeadm 或 kind 集群为例,Kubernetes 版本建议 1.23 以上。
4.1 环境清单
需要准备:
- 一个可用的 Kubernetes 集群;
- kubectl 能访问集群;
- metrics-server,用于
kubectl top查看资源使用; - 可选:Prometheus 和 Grafana,用于观察 CFS 节流指标。
安装 metrics-server 的方式取决于你的集群网络和版本,请参考官方 GitHub Releases 页面,获取与集群版本兼容的 components.yaml,然后执行 apply。安装完成后可以用下面命令确认:
kubectl top node kubectl top pod -A如果输出正常显示 CPU 和内存使用量,说明 metrics-server 工作正常。
4.2 监控指标:三个关键 CFS 指标
如果希望更精准定位问题,建议部署 Prometheus。Kubelet 内置的 cAdvisor 会暴露大量容器指标,其中与 CPU 节流直接相关的有三个:
- container_cpu_cfs_periods_total:CFS 调度周期总数;
- container_cpu_cfs_throttled_periods_total:发生节流的周期数;
- container_cpu_cfs_throttled_seconds_total:节流的总时长(秒)。
这三个指标是后续排查 CPU limits 问题最重要的依据。没有它们,你很难证明“延迟上涨是因为 CPU limits 节流”。
5. 完整示例:一次 CPU limits 引发的延迟事故复现
下面用一个尽量小的实验,复现“CPU limits 导致节流”的全过程。
5.1 创建带 CPU limits 的 Deployment
先创建一个命名空间,然后部署一个带 CPU requests 和 limits 的测试服务。这里用 nginx 模拟 Web 服务,把 CPU limits 设置得偏紧,制造节流。
文件路径:demo/cpu-limit-deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-throttle-demo namespace: demo spec: replicas: 1 selector: matchLabels: app: nginx-throttle-demo template: metadata: labels: app: nginx-throttle-demo spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 resources: requests: cpu: 100m limits: cpu: 300m应用这个配置:
kubectl create ns demo kubectl apply -f demo/cpu-limit-deployment.yaml查看 Pod 是否运行,并确认资源限制已经生效:
kubectl get pod -n demo -o wide kubectl get pod -n demo -o custom-columns=NAME:.metadata.name,CPU_LIMIT:.spec.containers[*].resources.limits.cpu,CPU_REQUEST:.spec.containers[*].resources.requests.cpu这里把 limits 设为 300m,requests 设为 100m。正常空载时 nginx 使用量很低,不太会触发节流;但只要压测并发一上来,CPU 消耗很容易突破 300m,从而产生明显的节流现象。
5.2 压测制造瞬时高负载
为了让 nginx 吃到更多 CPU,可以使用压测工具 ab 或 hey 对 Pod IP 或 Service IP 发起并发请求。为了网络链路简单,直接在集群内创建一个压测 Pod,压测时注意给 load-generator 也设置 requests,避免它在高负载时抢占被测试服务以外的节点资源:
kubectl run load-generator -n demo \ --image=busybox:1.36 \ --requests='cpu=100m' \ -- sh -c "while true; do wget -q -O /dev/null http://nginx-throttle-demo/demo/index.html; done"如果 wget 压测压力不够,也可以进入 nginx 容器内直接制造 CPU 消耗:
kubectl exec -it -n demo deploy/nginx-throttle-demo -- sh # 容器内执行,制造满核压力(演示环境专用) dd if=/dev/zero of=/dev/null &生产环境不要随便在业务容器里执行这类命令,这里只是为了让节流现象更快暴露。
5.3 查看 cgroup 节流数据
在 nginx 容器内直接查看 cgroup 的 CPU 统计。cgroup v1 路径为:
kubectl exec -it -n demo deploy/nginx-throttle-demo -- cat /sys/fs/cgroup/cpu/cpu.statcgroup v2 路径为:
kubectl exec -it -n demo deploy/nginx-throttle-demo -- cat /sys/fs/cgroup/cpu.stat输出中需要重点关注两个字段:
- nr_throttled:出现节流的周期数;
- throttled_time(v1)或 throttled_usec(v2):节流累计时间。
如果在压测前后,nr_throttled 快速上升,就说明容器被频繁暂停,业务延迟暴涨的根因基本可以锁定为 CPU limits。
5.4 用 Prometheus 指标辅助判断
如果集群内有 Prometheus,可以用下面的 PromQL 查看节流总时长:
sum(rate(container_cpu_cfs_throttled_seconds_total{namespace="demo"}[5m])) by (pod)也可以看“节流周期占比”:
sum(rate(container_cpu_cfs_throttled_periods_total{namespace="demo"}[5m])) by (pod) / sum(rate(container_cpu_cfs_periods_total{namespace="demo"}[5m])) by (pod)需要提醒:容器被节流并不等于一定发生了业务事故,但节流周期占比持续升高,或节流总时长与请求延迟尖峰高度相关时,就需要认真审视 limits 的合理性了。
6. 运行结果与如何判断成功
6.1 判断成功的标准
完成上述步骤后,判断“复现成功”的标准是:
- 压测期间,CPU limits 对应的 cgroup 出现明显节流;
- 压测期间,业务请求延迟出现尖刺;
- 移除 limits 后,同样压测下,延迟尖刺明显消失。
一个简易判断流程如下:
- 执行压测前先记录一次节流计数;
- 压测 1 分钟;
- 压测后再次查看节流计数,对比差值;
- 如果 nr_throttled 从 0 变成几十上百,说明节流非常频繁。
6.2 验证步骤
按顺序执行:
# 1. 查看压测前节流计数 kubectl exec -it -n demo deploy/nginx-throttle-demo -- cat /sys/fs/cgroup/cpu.stat # 2. 启动压测,持续 1 分钟 kubectl exec -it -n demo deploy/nginx-throttle-demo -- dd if=/dev/zero of=/dev/null & # 3. 压测结束后再次查看 kubectl exec -it -n demo deploy/nginx-throttle-demo -- cat /sys/fs/cgroup/cpu.stat在验证时,第一优先看的是container_cpu_cfs_throttled_seconds_total的增长,而不是容器内top显示的 CPU 使用率。因为容器内看到的是被节流后“剩余可用的资源”,往往看不出被限制的样子。
6.3 如果压测没有出现节流
如果压测期间延迟没有明显变化,也不要急着下结论。先确认压测工具是否真正触发了 CPU 峰值,再看 limits 和实际使用量的差距是否过大。如果 limits 本身就等于实际使用的 10 倍,节流自然不明显。可以把 limits 继续调低到 100m 或 150m,再重复压测,让现象更明显。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 服务 P99 延迟飙升,但 CPU 使用率不高 | 容器被 CFS 节流 | kubectl exec查看 cpu.stat,或 Prometheus 查 throttled 指标 | 放大或移除 CPU limits,改用 HPA 扩容 |
| 节点 CPU 明显空闲,Pod 却报超时 | 调度器按 limits 预留资源,节点可分配余量被耗尽 | kubectl describe node查看 Allocated resources | 只设置 requests,不设置 limits;清理无效限额 |
| 容器被反复 OOMKill | 内存超过 limits 或节点内存压力 | kubectl describe pod查看 Last State 是否 OOMKilled | 调整内存 requests/limits,检查内存泄漏 |
| 取消 limits 后 Pod CPU 使用率突增 | 之前被节流掩盖了真实需求 | 对比节流前后 CPU 曲线 | 根据新基线调整 requests,决定是否扩容节点 |
| 设置了 limits 但压测没有影响 | limits 远高于实际 CPU 用量 | 查看容器 CPU 使用量与 limits 的比值 | 若 limits 明显偏高,可以移除,避免调度资源浪费 |
这里尤其要关注“节点 CPU 空闲但服务超时”这一类问题。它最容易迷惑人,也最能体现 CPU limits 的副作用:limits 不仅会影响运行时的节流,还会让 kube-scheduler 把这个限制当成“已占用资源”计入节点分配量,造成节点看起来被占满、实际却很空闲的错觉。
8. CPU limits 到底什么时候该用
把“不要用 CPU limits”当成绝对教条,同样会踩坑。更准确的说法应该是:不要默认使用 CPU limits,除非有明确的非功能需求。
8.1 不建议使用 CPU limits 的场景
- 普通的无状态 Web 服务、微服务;
- 流量有波峰波谷、对延迟敏感的服务;
- 服务自身已经通过 HPA 做弹性伸缩;
- 团队刚开始接触 Kubernetes,对自己的业务资源画像不清楚时。
这些场景更合理的做法是:把 requests 设置成接近真实使用量的值,让调度器合理放置 Pod;用 HPA 在负载升高时扩容;用 VPA 辅助调整 requests。这样既不需要牺牲突发性能,也能保证基本的资源隔离。
8.2 建议使用 CPU limits 的场景
仍有几类场景适合设置 CPU limits:
- 批处理任务、短时明显的可压缩任务:防止某个离线任务把节点 CPU 占满,影响在线服务,效果比单纯的 requests 更强;
- 多租户场景中对单个工作负载有硬性隔离或计费上限;
- 需要稳定的 QoS 等级控制且团队对成本极度敏感;
- 平台团队在 ResourceQuota 层面强制所有 Namespace 的资源总量上限时,针对单个 Pod 的 limits 更像一道兜底的保险丝。
即使在上述场景,limits 也应留足余量。一个比较稳妥的经验是:limits 至少要比请求值或稳定峰值高出 30% 到 100%,并且必须配合节流监控,一旦发现节流比例过高,就要重新评估这个值。
8.3 一个必须设置的例外:内存 limits
本文讨论的是 CPU limits。对于内存,请务必单独评估:内存超过 limits 会导致 OOM Kill,进程会直接消失。对绝大多数需要稳定运行的服务来说,内存常常是需要限制的,但 CPU 则未必。很多团队把 CPU 和内存 limits 一起处理,其实两类资源的失败模型完全不同,应该分开设计。
9. 最佳实践与工程建议
9.1 用 requests 做资源画像,用 HPA 做弹性
不要凭感觉写 requests。可以先让 VPA 以 recommendation 模式跑一段时间,或者直接看监控系统中的历史分位数。建议把 requests 设置为 P95 或 P99 附近的 CPU 使用量,这样既能保证长时间在线服务的稳定性,又不会因为 requests 过大导致节点放不下太多 Pod。
HPA 配置示例,文件路径:demo/hpa.yaml
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx-hpa namespace: demo spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-throttle-demo minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60应用并验证:
kubectl apply -f demo/hpa.yaml kubectl get hpa -n demo当 Pod 平均 CPU 使用率超过 60% 时,HPA 会自动扩容;低于目标后逐步缩容。这样即使不设置 CPU limits,也能避免单实例把节点 CPU 打满的稳态风险。
9.2 用 ResourceQuota 控制总量,而不是控制单个 Pod 上限
担心某个团队把整个集群吃满?与其给每个 Pod 都加 limits,不如在 Namespace 级别设置 ResourceQuota:
apiVersion: v1 kind: ResourceQuota metadata: name: compute-quota namespace: demo spec: hard: requests.cpu: "4" requests.memory: 8Gi limits.cpu: "6" limits.memory: 12Gi也可以配合 LimitRange,在不允许设置 limits 的 Namespace 里做默认值约束。这样做的好处是把管理粒度从“逐个 Pod 控制”提升到“整个团队控制”,既避免了滥用 limits 造成的节流,又保留了资源上限管理能力。
9.3 建设节流监控和告警,分阶段迁移
如果集群里已经大量存在带 CPU limits 的工作负载,不要等出事故后才一次性全部移除。更稳妥的思路是分阶段迁移:
- 先用 Prometheus 把 container_cpu_cfs_throttled_seconds_total 接进来,找出节流最严重的 Pod;
- 观察一段时间,统计节流与业务延迟的相关性;
- 对节流严重但业务延迟没有受益的 workload,逐步放大或移除 CPU limits;
- 最后把新 workload 的模板改成“默认只设置 requests,不设置 limits”,并通过评审流程才允许额外添加 limits。
这一套流程虽然慢,但风险可控,也更容易让团队形成对资源参数的共同认知。
9.4 配置变更前做好验证和回滚
生产环境调整资源参数,本质上也是一次变更。建议先在一到两个低流量服务上实验,对比调整前后的 P99 延迟、错误率、节流指标;如果效果正向,再逐步扩大到全量。同时保留原 yaml 的版本记录,方便快速回滚。不要把“取消 limits”当成一次性的大动作,尽量分批小步试点。
10. 总结与后续学习方向
这篇文章围绕“要不要在 Kubernetes 中使用 CPU limits”展开,核心结论可以浓缩成几句话:
- CPU requests 是调度与保障的关键,宁可写准 requests,也不要滥用 limits;
- CPU limits 通过 CFS 配额实现,过度使用会导致容器节流,宏观表现是延迟毛刺、CPU 使用率与性能不匹配;
- 不要默认在每个服务上都设置 CPU limits,除非有批处理、多租户或硬隔离等明确需求;
- 内存 limits 的决策要独立考虑,因为它的失败模型是进程被杀,和 CPU 节流完全不同;
- 工程上更推荐用 requests + HPA + ResourceQuota 的组合来保护集群。
如果你现在正好在维护一个所有 Pod 都带有 CPU limits 的集群,下一步可以这样开始:先看监控里有没有节流指标,拉出节流最高的 Top 10 Pod,逐个确认它们的 limits 是否合理;如果合理,再评估是否放宽 30% 到 50%;如果不合理,大胆移除 CPU limits 进行灰度验证。这个过程会比“直接全部删除 limits”稳妥得多,也能让你真正理解这段资源参数背后的行为差异。
Kubernetes 的资源管理体系到远不止 requests 和 limits 这两个字段。往深了走,还可以继续研究 cgroup 与 CPU 调度、CPU Manager 的 static 策略、拓扑敏感调度、VPA 与 HPA 的配合、成本优化与容量规划。理解了操作系统层的实现之后,再看 Kubernetes 资源模型,会通透很多。