那天下午的告警看起来一点都不吓人:某个核心服务在 Kubernetes 里跑得好好的,CPU 平均使用率不到 40%,内存也健康,Pod 从不重启,健康检查也正常。但它的 P99 延迟就是每隔几分钟毫无规律地跳到两秒,然后再落回去。
常规排查全部做过一遍:慢 SQL 没有、下游依赖超时没有、GC 明显异常没有、网络抖动也没有。直到有人翻出一个平时基本没人看的计数器,container_cpu_cfs_throttled_periods_total,才把怀疑对象锁定到 YAML 里的resources.limits.cpu上。
也是在那个阶段,我把社区里那篇标题非常直白的文章认真读了一遍——《For the love of god, stop using CPU limits on Kubernetes》。它记录了一次严重生产事故后的反思,结论乍一听很极端:在大量生产场景里,CPU limits 带来的麻烦,比它解决掉的麻烦更多。
这篇文章后来被引用很多次,也被批评过很多次。但我觉得它最有价值的不是“别用 limits”这个结论,而是它逼着你去想清楚一个核心问题:Kubernetes 的 CPU 限制到底是怎么在内核里实现的。想清楚了这一点,你自然会明白哪些场景该保留 limits,哪些场景去掉反而更安全。
1. 先从一次“CPU 看着没用满,服务却卡了”的现场说起
1.1 一个非常典型的故障现象
先把当时的现场还原一下。
服务一共有 6 个副本,每个副本的配置是:requests.cpu: 250m,limits.cpu: 2。从业务形态看,这是一个典型的突发型服务,平时流量不大,但每天有几次明显的访问高峰,处理请求时会短时间把 CPU 拉高。
从监控面板上看,服务平均 CPU 使用率只有 30% 左右。按常理推断,一个平均只用 0.5 核的服务,给它 2 核的硬上限,怎么都不该有性能问题。
但实际表现完全不是这样。延迟曲线呈现出非常规律的“锯齿状”,每隔一小段时间就出现一次离散的尖峰。最让人困惑的是,CPU 使用率指标一直很低,看起来根本不像资源瓶颈。
这里有一个非常容易踩的认知误区:CPU 使用率低,不代表容器没有在等 CPU。
Kubernetes 的 CPU limits 不是像内存那样“超了直接把你杀掉”,而是用一种更隐蔽的方式执行:当容器在一个极短的时间窗口内用完了配额,内核会把它“掐住”,让它在那段时间里什么都做不了。这个动作在大多数监控体系里几乎是隐形的,但它会直接变成业务延迟。
1.2 真相藏在 cgroup 的节流统计里
我当时用了一个很笨但有效的办法:进到出问题的 Pod 里,直接看 cgroup 的统计文件。
在 cgroup v1 环境里路径是:
cat /sys/fs/cgroup/cpu/cpu.stat在 cgroup v2 环境里路径变成了:
cat /sys/fs/cgroup/cpu.stat关键看两个字段:
nr_periods:内核一共执行了多少个限流检查周期nr_throttled:其中有多少个周期里容器被强制节流了
在故障现场,这两个数字的比值高得惊人。也就是说,容器每个限流周期里都有接近一半的时间是被内核“摁住”的。当你看到nr_throttled / nr_periods接近 50% 甚至更高时,哪怕kubectl top pod显示 CPU 只有几百毫核,业务延迟也会被拖垮。
这个现象才是题目里那句“stop using CPU limits”真正的出发点:CPU limits 带来的不是一个温柔的速度限制,而是一种非常粗颗粒、对监控不友好、对突发型负载极其不友好的离散节流机制。
2. 为什么 CPU limit 会造成“看不见的卡顿”
2.1 Requests 是预约,Limits 是硬顶
先看一张表。对于刚了解 Kubernetes 的人来说,requests和limits很容易混淆,面试也经常被问;但对一个要长期维护生产集群的人来说,不理解这两个字段背后的机制,迟早会在线上吃亏。
| 字段 | 作用 | 底层实现 | 一句话理解 |
|---|---|---|---|
requests.cpu | 调度器依据它决定 Pod 放在哪个节点;节点上所有 Pod 的 requests 之和不能超过节点可分配资源 | CFS shares(CPU 权重) | 预约额度 |
limits.cpu | 容器真正能使用的 CPU 时间上限 | CFS quota(CPU 配额) | 执行上限 |
requests是调度层的事。它负责回答“这个 Pod 能不能放进这台节点”。调度完成之后,运行时的 CPU 分配也受到 requests 的影响,但它的作用方式更像“权重”,而不是“天花板”。
limits是执行层的事。它负责回答“这个容器最多能用多少 CPU 时间”。它的实现方式不是简单地“超过 90% 就降速”,而是在一个固定时间窗口内累计 CPU 时间,一旦超过配额,就直接把容器线程挂起,直到下一个窗口开始。
理解这个区别,是理解整篇文章的前提。
2.2 CFS 配额:一个 100 毫秒的窗口
Kubernetes 的 CPU limits 背后用的是 Linux CFS(完全公平调度器)里的带宽控制机制。
默认情况下,Kubernetes 设置 CFS 的检查周期是 100 毫秒。如果你给容器设置了limits.cpu: 2,那内核会这样配置:
cpu.cfs_period_us固定为100000(100 毫秒,单位微秒)cpu.cfs_quota_us被设置为200000
含义是:每 100 毫秒真实时间里,这个容器累计最多只能使用 200 毫秒的 CPU 时间。
注意这里说的是“累计”。一个 2 核限制的容器,意味着两个 CPU 核心满载跑完整个 100 毫秒窗口,刚好用完配额。一旦超过,容器里超出配额的部分就会被强制冷却,直到下一个 100 毫秒窗口开启。
也就是说,CPU limits 更像是一个“发工资日当天必须花完,花不完就作废”的机制,而不是“账户里可以一直存钱”的机制。一个 CPU 密集的线程即使平时占有率很低,只要它和一个突发线程处在同一个窗口期内,就可能因为配额被提前吃完而一起被卡住。
2.3 为什么平均使用率不高,也可能被限流
很多人最大的困惑是:监控显示平均 CPU 使用率只有 0.5 核,limits 给的是 2 核,为什么会被限流?
问题出在“平均”上。
CFS 配额检查的不是 5 分钟平均,也不是 1 分钟平均,而是看每一个 100 毫秒窗口内累计使用了多少 CPU 时间。
假设容器设置了 2 核 limits。在某个 100 毫秒窗口内,业务突然来了一个突发请求,四个线程同时跑满 50 毫秒:
- 四个线程在 50 毫秒内消耗了 200 毫秒的 CPU 时间(4 × 50ms)
- 配额到 200ms 时,容器瞬间被节流
- 下半段窗口内,即使业务线程已经不再需要 CPU,一些本应在这 50 毫秒内处理完的工作也会被拖到下一个窗口
看这个例子,容器在 200 毫秒整体的平均使用率只有 1 核,远低于 2 核上限,但它已经被限流了整整 50 毫秒。
对于延迟敏感的业务,这 50 毫秒就是一次 P99 尖峰。而监控里看到的kubectl top却非常平静,因为它采样的粒度和 CFS 的检查粒度完全对不上。
2.4 CPU limits 和内存 limits 不是一种“物种”
这里要特别强调一个容易混淆的地方:很多人觉得“CPU 和内存都需要限制资源”,于是顺手把两个字段都写上,以为它们行为类似。实际上两者的执行机制完全不同。
内存 limits 是“硬性终结”。容器超过内存限制,内核会触发 OOM Killer,挑进程杀掉,Pod 大概率会因为探针失败或进程退出而重启。后果很直接,也容易发现。
CPU limits 是“软性节流”。容器超过 CPU 配额,不会死,不会重启,不会打印明显的错误,只是部分线程被暂时挂起。它既不直接报警,也不触发重启,但它会被翻译成业务上的超时、重试和用户体验劣化。
一个会主动杀死你的问题,反而好排查;一个悄悄让你变慢的问题,才是生产环境最折磨人的。
3. 不设 CPU limits 会不会失控:把 Requests 用好才是关键
3.1 Requests 才是真正的调度保证
很多人不敢去掉 limits,是担心容器会“把节点 CPU 吃干榨净”。
但这里需要理清一个事实:容器能不能“吃满”节点,和 limits 有没有关系,其实要看调度时预留了多少资源。
requests是调度器的硬约束。一个节点上所有 Pod 的requests.cpu之和不能超过节点的可分配 CPU。调度完成之后,当节点刚刚满足所有 Pod 的 requests 时,系统已经保证了每个 Pod 至少能用上它所预约的那部分 CPU。在 CPU 竞争激烈时,内核通过 CFS shares 按权重复分配 CPU,谁的 requests 大,谁在竞争时拿到的份额就高。
换句话说,真正支撑服务稳定性的是 requests,而不是 limits。limits 解决的是“容器不要超过某个值”,requests 解决的是“调度器要保证容器至少拿到这么多”。
另外还有一个很容易被忽略的点:HPA 的 CPU 扩容逻辑,是拿当前 CPU 使用量 / Pod 的 requests来计算利用率的。如果删了 limits 的同时也把 requests 删了,HPA 的 CPU 指标会直接失效。所以我的建议一直是:去掉 limits,但保留 requests,并且让 requests 尽量贴近真实用量。
3.2 “不加 limits”不等于“没有管控”
“不用 CPU limits”并不等于“裸奔”。这里要区分两个层次的控制:
第一层是工作负载层。容器去掉 limits 之后,靠 HPA 在负载升高时扩容,靠合理的 requests 保证调度预留,靠业务层重试和熔断来消化偶发抖动。
第二层是集群策略层。如果你担心某个命名空间里的 Pod 总量失控,可以用ResourceQuota限制该命名空间的 CPU request 总量;担心容器不写 requests 导致调度混乱,可以用LimitRange或准入控制来强制设置 requests。
这两层都和管理“某个容器单个窗口内最多用多少 CPU”不是一回事。它们解决的是更上层的容量治理问题,而不是给每个容器套上一个 400ms 的紧箍咒。
真正的管理边界应该放在“我允许这个命名空间最多申请多少 CPU”,而不是“你这个容器每个 100 毫秒窗口内最多用多少 CPU”。
3.3 去掉 limits 的副作用:QoS 会变
去掉 limits 不是没有代价。最直接的副作用是 Pod 的 QoS 等级会改变。
Kubernetes 给 Pod 划分了三个 QoS 等级:
- Guaranteed:所有容器都设置了 CPU 和内存的 requests 和 limits,并且每个资源的 requests 等于 limits
- Burstable:至少有一个容器设置了 requests,但 requests 不等于 limits,或者某些资源没有设置 limits
- BestEffort:任何容器都没有设置 requests 和 limits
当容器原本是requests.cpu == limits.cpu时,它是一个 Guaranteed Pod。如果你删掉了limits.cpu,它的 QoS 会变成 Burstable。
QoS 变化带来的影响主要体现在节点资源紧张时的驱逐优先级上。在内存压力下,节点会优先驱逐 BestEffort,然后是 Burstable,最后才是 Guaranteed。所以,把本来 Guaranteed 的服务改成 Burstable,理论上会略微增加它在极端情况下的驱逐风险。
实际影响取决于你的节点是否经常处于内存压力状态。如果节点的内存余量一直健康,这种影响几乎感受不到;但如果节点经常接近内存上限,你就需要重新评估。
另外,如果集群开启了 CPU Manager 的static策略,想要让容器绑定独占物理 CPU 核心,就必须是 Guaranteed QoS,也就是必须设置limits.cpu == requests.cpu且为整数。这是“必须保留 CPU limits”的一个典型例外场景。
3.4 什么场景比较适合先去 limits
基于上面的分析,下面几类服务通常比较适合先尝试去掉 CPU limits:
- 无状态、有多个副本、失败后能快速重建的服务
- 流量有明显的突发特征,平均使用率远低于 limits 设置值的服务
- 已经配置了 HPA,并且 requests 数值相对真实的服务
- 对偶发的几十毫秒延迟不敏感,有超时重试机制的业务
- 容器启动或冷启动阶段有短暂 CPU 高峰的场景
这类服务去掉 limits 之后,突发流量可以在短时间内使用更多节点 CPU,业务表现会更快,而不会被一个 100 毫秒的窗口卡住。
4. 什么时候真的不能去掉 CPU limits(先看清楚边界)
4.1 多租户集群、重点计费和强隔离场景
如果你的集群是多个团队共享的,或者你需要对每个团队、每个项目做资源计量和计费,那么 limits 往往是强需求。
原因很简单:requests 只解决调度预留,不解决“实际使用上限”。一个团队把 requests 写得很小,但实际业务突发时可能把节点 CPU 全部吃掉,影响同节点的其他团队。如果没有 per-container 的 hard cap,就很难对“按量计费”或“公平使用”做出明确承诺。
在这种场景下,cpu limits 更像是一个租户隔离的边界。它牺牲了一部分业务突发的灵活性,但换来的是更强的可预期性和可审计性。
4.2 服务等级承诺与已知稳态负载
还有一种场景:你的服务对外承诺了稳定的 CPU 能力,或者它的负载形态非常平稳,CPU 使用率长期贴着某个固定值波动。
比如一个常驻的模型推理服务,每个请求的 CPU 消耗基本可预测,流量也没有剧烈波动。这种服务设置requests.cpu == limits.cpu是有意义的:它告诉调度器“我需要独占这么多 CPU”,同时因为负载平稳,几乎不会触发 CFS 节流。
这里要留意,同样的写法放在突发型服务上会出问题,放在稳态服务上却可能很合适。问题从来不在 limits 本身,而在于你对自己的负载形态有没有准确的认知。
4.3 CPU 绑核(static CPU Manager)特例
前面提过,使用 CPU Manager 的static策略,需要 Pod 达到 Guaranteed QoS,也就是 requests 和 limits 都设置且相等。
这类场景里的 limits 不再是“限流”的含义,而是“绑核授权”。它和“给每个容器随手写一个 500m 的帽子”完全不同,因为此时容器已经被固定到专属物理核心上,CFS 节流的发生概率大大降低,业务的 CPU 时延也更可预测。
如果你正在跑低延迟服务,并且已经用上了绑核,那么请保留 limits。这个特例不能被“不用 CPU limits”这种口号覆盖掉。
4.4 如果平台策略强制要求每个 Pod 都必须有 limits
有些公司或 PaaS 平台要求所有工作负载都必须声明 limits,否则无法通过发布审批。这种策略本意是防止资源失控,但它会误伤大量突发型服务。
如果绕不开平台策略,我的建议是:把 limits 当成“安全网”来设置,而不是当成“精确速度限制”来设置。
例如一个服务真实的 CPU 需求是requests.cpu: 250m,你不需要给它设置limits.cpu: 1,而是可以设置limits.cpu: 4甚至更高。这样一来:
- 可以满足平台必须写 limits 的合规要求
- 正常情况下不会触发 CFS 节流
- 万一业务出现死循环或代码 bug 导致 CPU 异常飙升,仍然能兜住
这里有一个特别容易踩的坑:很多团队为了“规范”,喜欢给所有服务统一添加一个LimitRange,实现“没写 limits 就自动补一个默认值”。结果是,你从 Deployment 里删掉了limits.cpu,新 Pod 创建时又会被 LimitRange 静默补回去。改配置前一定要先检查命名空间里是否存在 LimitRange。
注意:清理 CPU limits 之前,先确认当前命名空间的 LimitRange 不会把旧的默认值重新注入到新 Pod 里,否则你改了半天,线上行为一点没变。
5. 想稳妥地移除 CPU limits,按这个顺序做
5.1 第一步:先盘点哪些工作负载在被打节流
不要凭感觉删配置。先把所有工作负载的节流情况量化出来。
在 Prometheus 里,可以用 cAdvisor 暴露的容器指标计算节流比例:
sum by (namespace, pod) ( rate(container_cpu_cfs_throttled_periods_total{container!="POD"}[5m]) ) / sum by (namespace, pod) ( rate(container_cpu_cfs_periods_total{container!="POD"}[5m]) )这个查询的结果表示“在过去 5 分钟里,每个 Pod 被节流的周期占比”。
通常可以按这个经验值判断:
- 小于 1%:低风险,可以继续观察
- 1% 到 5%:已经值得关注,尤其是 P99 有抖动时
- 大于 5%:说明负载已经频繁撞墙,需要认真评估
同时,盘点现有配置:
kubectl get deploy -n <namespace> -o custom-columns=NAME:.metadata.name,CPU_REQUEST:.spec.template.spec.containers[*].resources.requests.cpu,CPU_LIMIT:.spec.template.spec.containers[*].resources.limits.cpu注意这个命令在多容器 Deployment 下显示会比较混乱,但它足够用来做第一轮排查,找出“requests 和 limits 差距过大但使用率又不高”的嫌疑对象。
5.2 第二步:把服务分成三档
不要一上来就批量删。先把服务分成三档,按优先级推进。
| 分类 | 特征 | 处理建议 |
|---|---|---|
| 可以先试水 | 无状态、多副本、有 HPA、突发型负载、偶发抖动可接受 | 优先移除 CPU limits |
| 观察后再决定 | 核心 API,但有多副本和重试机制;延迟有一定容忍度 | 先用监控观察节流比例,再做决定 |
| 保守不动 | 单实例、强一致性、对外 SLA 严格、无监控覆盖、延迟极度敏感 | 保留 limits,或走绑核专用路线 |
这里最重要的判断标准是两个问题:如果它突然占满整台节点,会不会影响别人?如果它自己被卡一下,业务能不能重试?
两个都不满足的,优先级最高。
5.3 第三步:小步摘除,别一把梭
对于单容器的 Deployment,可以从 YAML 里直接删掉limits.cpu:
resources: requests: cpu: 500m memory: 512Mi # limits.cpu 已删除 limits: memory: 1Gi也可以用 JSON Patch 的方式:
kubectl patch deployment <name> -n <namespace> \ --type='json' \ -p='[{"op":"remove","path":"/spec/template/spec/containers/0/resources/limits/cpu"}]'但这里要特别提醒:
- 如果 Deployment 里有多个容器,
containers/0这种下标需要对应到具体容器,不要弄错 - 如果容器同时设置了 memory limits,请保留 memory limits,不要顺手一起删。内存和 CPU 的机制完全不同,CPU 可以节流后恢复,内存如果失控可能会引发节点级别的连锁反应
- 如果删完后发现 QoS 从 Guaranteed 变成 Burstable 导致了不可接受的影响,快速回滚的方式就是把这个字段加回去
- 修改后观察 1 到 2 周,重点看延迟、节流比例、节点 CPU 饱和度和 HPA 扩容行为
建议:每次只改一个服务,并且选择流量低谷时发布。不要同一个下午改完整个集群,否则一旦出问题,你无法定位是哪个改动引起的。
5.4 第四步:把告警从“容器级别”搬到“节点级别和业务级别”
去掉 limits 之后,原来那种“容器 CPU 被掐住”的告警自然就没了,但这不代表你可以关掉告警系统。你只是把控制手段从“容器内部硬限制”切换成了“外部观察和自动化响应”。
至少需要补齐这几类监控:
节点 CPU 饱和度告警:
100 - ( avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance) * 100 )当节点 CPU 长时间高水位时,即使没有 limits,也会因为节点整体繁忙导致所有 Pod 一起抖。这时需要的是扩容节点或疏散工作负载。
容器节流告警(针对仍然保留 limits 的少量工作负载):
sum by (namespace, pod) ( rate(container_cpu_cfs_throttled_periods_total{container!="POD"}[5m]) ) / sum by (namespace, pod) ( rate(container_cpu_cfs_periods_total{container!="POD"}[5m]) ) > 0.1业务延迟告警。这是最本质的指标。P99 延迟一旦上抬,告警就应该被触发,不管 CPU 指标是否正常。
6. 删掉一份 limits 容易,难的是把 Requests 预算当作长期工程来经营
6.1 Requests 不准确,去不去 limits 都会出事
去掉 limits 之后,真正决定集群稳定性的变成了 requests。如果 requests 只是随便填的数字,问题不会消失,只会换一个形状出现。
场景一:requests 填得过大。比如实际只用 200m,requests 写了 2 核。节点会因为“看起来满了”而拒绝新 Pod,集群调度冗余被白白浪费。最后你想扩容都扩不进去。
场景二:requests 填得过小。实际峰值会用到 4 核,requests 只写了 100m。调度器会把很多这样的小请求塞进同一台节点,平时看起来利用率很低,一旦流量高峰同时到来,节点 CPU 直接被打爆,所有 Pod 一起遭殃。
所以,去掉 limits 不是把责任推给内核,而是把责任交给了 Requests 预算的准确性。
6.2 把 Requests 变成团队可维护的预算
长期来看,比较可行的做法是把 Requests 当作“代码审查的一部分”。
推荐一个简单的四步流程:
- 测量:对每一个工作负载,记录一周内 CPU 使用率的中位数、P95 和 P99
- 设定:requests 取中位数或略高于中位数的值,保留一定的缓冲;不要用峰值去做 requests
- 验证:发布后观察 HPA 行为,确认扩容频率是否合理
- 周期复核:每季度重新审视一次 requests,业务形态变化后及时调整
VPA(Vertical Pod Autoscaler)的 recommendation 模式也可以作为辅助工具,它可以基于历史指标给出 requests 建议值。但建议把它作为参考,而不是直接自动应用,因为 VPA 的推荐结果有时会受短时峰值影响,导致 requests 被过度放大。
6.3 回到最初那个标题:它真正想说的不是“禁止”,而是“别盲从”
那句 “For the love of god, stop using CPU limits on Kubernetes” 之所以能引起共鸣,是因为太多团队把“每个容器必须写清 requests 和 limits”当成一条不可质疑的默认规则,却从没问过:限制到底是怎么执行的?对我的负载形态是否合适?
它真正想批评的不是 limits 这个功能,而是不假思索的默认策略:不管服务形态如何,一律给 limits 填一个看起来合理的数值,完了再也不看节流指标。
所以我的最终判断是:
- 突发型、有冗余、有 HPA 的服务:去掉 CPU limits,保留准确的 requests,更合理
- 稳态、低延迟、绑核或多租户强隔离的服务:保留 CPU limits,但要知道它在里内核里做了什么
- 无论哪一种,都要有监控、告警、回滚手段,而不是改完配置就完事
下次再有人争论“Kubernetes 到底该不该设置 CPU limits”,不用急着站队。先把每个服务的 CPU 时间曲线、CFS 节流比例、QoS 等级和 HPA 行为拉出来看一眼,答案往往自然就出来了。
技术上的很多争论,到最后拼的其实不是立场,而是你对底层机制的理解深度。