
那天下午的告警看起来一点都不吓人某个核心服务在 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: 250mlimits.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 sharesCPU 权重预约额度limits.cpu容器真正能使用的 CPU 时间上限CFS quotaCPU 配额执行上限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固定为100000100 毫秒单位微秒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 等于 limitsBurstable至少有一个容器设置了 requests但 requests 不等于 limits或者某些资源没有设置 limitsBestEffort任何容器都没有设置 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-columnsNAME:.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.cpuresources: requests: cpu: 500m memory: 512Mi # limits.cpu 已删除 limits: memory: 1Gi也可以用 JSON Patch 的方式kubectl patch deployment name -n namespace \ --typejson \ -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{modeidle}[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 填得过大。比如实际只用 200mrequests 写了 2 核。节点会因为“看起来满了”而拒绝新 Pod集群调度冗余被白白浪费。最后你想扩容都扩不进去。场景二requests 填得过小。实际峰值会用到 4 核requests 只写了 100m。调度器会把很多这样的小请求塞进同一台节点平时看起来利用率很低一旦流量高峰同时到来节点 CPU 直接被打爆所有 Pod 一起遭殃。所以去掉 limits 不是把责任推给内核而是把责任交给了 Requests 预算的准确性。6.2 把 Requests 变成团队可维护的预算长期来看比较可行的做法是把 Requests 当作“代码审查的一部分”。推荐一个简单的四步流程测量对每一个工作负载记录一周内 CPU 使用率的中位数、P95 和 P99设定requests 取中位数或略高于中位数的值保留一定的缓冲不要用峰值去做 requests验证发布后观察 HPA 行为确认扩容频率是否合理周期复核每季度重新审视一次 requests业务形态变化后及时调整VPAVertical 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 行为拉出来看一眼答案往往自然就出来了。技术上的很多争论到最后拼的其实不是立场而是你对底层机制的理解深度。