
提起 Kubernetes 里的 CPU limits很多人的第一反应是不给 Pod 设 CPU 上限万一业务失控把整台节点吃满怎么办我过去也是这么想的而且集群里每个 Deployment 都会老老实实写上resources.limits.cpu。真正让我转变的是一次线上事故服务 CPU 使用率平均下来不到 60%节点也不是瓶颈可是接口 P99 从平时的 80ms 一路涨到 800ms重启后短暂恢复过几个小时又复发。最后定位到根因不是业务代码不是数据库而是我给容器设置的 CPU limits 触发了大量 CFS 节流throttling。所以这篇文章想把经验整理清楚CPU limits 到底做了什么、为什么会让服务变慢、什么场景真的需要它以及不设 limits 之后集群靠什么来保护。内容适合正在维护 Kubernetes 集群的运维、SRE以及负责服务容器化和容量规划的开发同学。1. 先看懂 requests 和 limits一个是“怎么分”一个是“怎么限”1.1 调度层面requests 决定 Pod 能落在哪台节点在 Kubernetes 里resources.requests和resources.limits是两套完全不同的机制但很多人会把它们混在一起理解。requests首先是调度依据。你在 Pod 里声明requests.cpu: 1kube-scheduler 找节点时会检查节点上已经分配的 requests 总和只有剩余可分配 CPU 大于等于 1 核这个节点才会被选中。也就是说requests 决定了 Pod 能不能调度上去也决定了节点会不会超卖。requests 另一个作用是在容器运行期映射到 cgroup 的权重。Linux cgroup v1 里对应cpu.sharescgroup v2 里对应cpu.weight。这里的权重不是“固定给你留 1 核”而是“节点 CPU 紧张时大家按权重分”。比如节点上有两个 Pod一个 request 2 核一个 request 1 核两者都在抢 CPU 时理论上资源按 2:1 分配。如果其中一个 Pod 很闲另一个 Pod 在节点 CPU 空闲时可以把没人用的核借过来跑。也就是说requests 不会硬性限制一个 Pod 只能用自己声称的份额。这个“借 CPU”的特性非常重要它意味着只设置 requests 的在线业务在突发流量时是有机会临时超出声明的。1.2 执行层面limits 是给 cgroup 加 CFS 配额limits.cpu则完全不同。Kubernetes 会把 CPU limits 转换成 cgroup 的 CFS quota。以 cgroup v1 为例核心是两个文件cpu.cfs_period_us配额周期默认通常是 100000 微秒也就是 100ms。cpu.cfs_quota_us在一个周期内这个控制组最多能使用多少 CPU 时间。如果你给容器设置limits.cpu: 2换算结果是 2 个核心在 100ms 内最多累积使用 200ms CPU 时间。内核会按这个周期不断检查用完了就把该 cgroup 里的线程冻结直到下一个周期重新分配预算。这与很多人脑中的“给容器留 2 个核的独立空间”完全不一样。CPU limits 不是在物理核上做隔离而是在时间账本上做“超额冻结”。配置方式实际起作用的位置行为特点只设置 requests调度器、cpu.shares / cpu.weight确定放置位置和争抢权重不休眠惩罚设置 limitscpu.cfs_quota_us / cpu.max周期内 CPU 时间硬上限超后直接节流requests 和 limits 都设置两者都生效调度按 requests运行期有硬顶还有一个容易踩的坑在 Kubernetes 里如果你只写了limits没写requestsrequests 会被默认成和 limits 一样。也就是说你以为自己“只限了 2 核没承诺 2 核”实际上调度、权重都按照 2 核算进去了。这会导致节点看起来被占掉很多资源但真实利用率很低。1.3 关键结论limits 不等于预留能力而是“超时会暂停”现在可以把结论先放在这里requests 是一种“软保证”让调度器知道你大概需要多少并在 CPU 争抢时按比例分配。limits 是一种“硬封顶”超过配额后线程会被挂起直到下一个周期。很多人在设计资源模型时把 requests 看成“启动预留”把 limits 看成“运行时保护”。但从内核执行机制看limits 更像是在 cgroup 层面实现了一个“用完了就先停机”的开关。这个开关在 CPU 密集型离线任务里问题不大但对在线延迟敏感服务可能是灾难。2. 为什么 CPU 没用满服务却明显变慢2.1 CFS 配额按周期结算突发流量最容易触发节流CPU 节流最反直觉的一点是并不需要平均 CPU 使用率超过 limits 才会被节流。CFS 带宽控制是按周期结算的。假设容器 limit 是 1 核那么在一个 100ms 周期里最多消费 100ms CPU 时间。如果应用有多个线程或者在某一个周期里请求突然增多线程可以在很短的真实时间内把整个周期的预算消耗完。预算消耗完之后容器里所有想要运行的任务都会被暂停直到下一个 100ms 周期重新开始。举个例子容器限制 1 核应用同时来了 4 个请求4 个线程分布在 4 个不同 CPU 核心上运行。如果每个线程跑了 30ms4 个线程加起来就是 120ms CPU 时间但 100ms 配额一共只有 100ms超过之后所有线程立刻被冻结最快也要等下一个周期才能恢复。从分钟级监控看CPU 使用率可能只有 50%因为其他周期很空闲。但在这 100ms 的微观窗口里线程已经被打断了。对接口来说表现为一次高延迟对消息处理来说表现为一批消息延迟。这就是为什么“监控图上 CPU 不高”和“服务不该变慢”之间没有必然关系。你看到的曲线是平滑后的平均值而节流发生在毫秒级的时间片里。2.2 多线程应用会把“平均限额”快速耗尽单线程应用相对不容易被节流因为它在一个周期内最多也就消耗一个核的时间。真正危险的是多线程应用尤其是线程池、异步框架、并发请求处理非常多的服务。Java 服务是一个典型场景。应用配置了核心线程池平时同一时刻只有少量请求CPU 占用不高。一旦流量抖动几十个线程同时处理任务这些线程分散在不同物理核上每一毫秒真实时间里可能累计消耗几十毫秒 CPU。CFS 只看这个 cgroup 的累计消耗不会看你“平均负载是不是很低”。于是预算很快见底线程被强制休眠。等下一个周期恢复后线程刚开始处理新的请求又来几个周期内预算又被打满。最终表现为CPU 使用率远低于 limit但线程执行被切得支离破碎耗时自然涨上去。流量越有突发性、线程越多、连接池越大这个问题越明显。相反一个严格控制并发度、任何时候只有固定数量线程在跑 CPU 的应用反而不容易出现这种隐蔽节流。2.3 Java、Go、Node 等运行时更容易看到抖动除了应用自身的多线程语言运行时本身也会产生短促 CPU 尖峰。Java 应用有 JIT 编译、GC、锁竞争、线程池调度。一次 Full GC 可能让 CPU 短暂飙升如果刚好撞上 CFS 配额不足GC 停顿会被进一步放大。Node.js 虽然主线程偏单线程但事件循环、异步 IO、Worker Thread 并发跑起来时同样可能在短窗口内消耗大量 CPU。Go 的 goroutine 调度对 CPU 数很敏感runtime 会按可使用的 CPU 数来调整并行度但运行时内部仍然可能因为某个时间段内的调度压力产生尖峰。这些运行时层面的短促消耗不是业务代码里能直接看到的。你看到的现象就是“同样的并发量之前能扛住设置完 CPU limits 后开始大量超时”。3. 先确认你的 Pod 到底有没有被节流3.1 看指标cfs_throttled 比 cfs_periods要验证是不是被 CPU limits 坑了不能只凭感觉。最直接的指标是 cgroup 里的 CFS 节流统计。在 Prometheus 里如果使用 kubelet 内置的 cAdvisor 指标会有这样的指标container_cpu_cfs_periods_totalCFS 周期总次数。container_cpu_cfs_throttled_periods_total发生节流的周期次数。container_cpu_cfs_throttled_seconds_total累计被节流的秒数。日常可以算这个比例rate(container_cpu_cfs_throttled_periods_total{container!POD,container!}[5m]) / rate(container_cpu_cfs_periods_total{container!POD,container!}[5m])这个值可以理解为“最近 5 分钟里有节流的周期占比”。低于 5% 通常可以接受如果超过 20%节流已经是一个非常明确的嫌疑对象如果到了 50% 以上在线服务大概率会出现明显的 P99 抖动。如果集群使用 cgroup v2也可以通过container_cpu_cfs_throttled_periods_total继续查cAdvisor 会汇总到同样的指标命名上。3.2 从应用侧看P99 上升但 CPU 不高多半是节流单看集群指标还不够要结合应用侧表现一起判断。我发现一套比较有效的观察顺序先看应用监控里的 P99、P95、超时率是否突然上升。再看容器 CPU 使用率此时往往不高甚至远低于 requests。再查 CFS throttled如果比例高基本可以锁定是 limits 导致。顺手看一下节点整体 CPU如果节点很空闲说明不是资源不够而是容器自身的“天花板”提前把线程压住了。这个组合特征非常典型应用不是真的缺资源但被一个微时间片层面的配额卡住了。有条件的还可以在 Pod 里直接读取 cgroup 信息验证。cgroup v1 环境cat /sys/fs/cgroup/cpu/cpu.stat输出里会给出nr_periods、nr_throttled和throttled_time三个字段。throttled_time是纳秒如果它持续增长说明节流正在发生。3.3 看现场线程调度和响应时间更能说明问题如果指标层面已经确认有节流再看线程现场会很明显。用top -H -p pid或jstack观察 Java 线程会看到线程大量处于RUNNABLE状态但整体吞吐很低。这其实是线程本来想跑内核却不给 CPU 预算于是任务一直排着队。我见过最典型的情况是容器限了 4 核但 Pod 里线程池有 16 个线程并发一上来线程分布到 16 个核上实际上每秒能拿到的时间却只有“4 核 × 运行时间”。线程池越大每个线程分到的 CPU 时间越碎等待越多。这个阶段不要先去调业务线程池大小。先去掉 CPU limits 验证如果 P99 明显回落再去考虑线程模型才是正确的排查顺序。4. 哪些场景是真的必须设置 CPU limits先说明白我不赞同“所有 Pod 一律不许设置 CPU limits”这种一刀切建议。生产环境不是只有在线接口服务也不是只有你一个人在用集群。CPU limits 在某些场景下仍然有存在价值。4.1 多租户和强隔离场景需要硬边界如果你的集群被多个团队共用或者你正在向外部团队提供容器平台能力那“某一个 Pod 长期占用空闲 CPU”可能引发其他团队投诉。比如节点上跑了一个资源模型比较激进的团队应用其他 Pod 相对空闲。按 cpu.shares 的分配机制应用之间会互相借用空闲 CPU。占用方可能突然把整台节点压满导致同节点上其他延迟敏感服务在抢 CPU 时受到明显干扰。这种场景下给高噪声风险的工作负载设置 CPU limits至少可以保证它永远不会吃超过预设上限。虽然 CFS 节流机制有副作用但相比“完全无上限导致租户互相伤害”强隔离的收益更大。4.2 CPU 密集型离线任务和批处理任务可以接受节流离线任务、批处理、数据同步、模型推理等负载对单次请求延迟不敏感更在意整体吞吐和资源可控性。这类任务通常有明确的 CPU 消耗上限设置 limits 反而合适任务本身是 CPU 密集的不存在“突发请求导致节流”的应用语义。throttle 后任务只是变慢不会造成用户可感知的超时。如果同一个节点还混跑在线服务离线任务的 CPU limits 可以防止它把节点 CPU 抢空。做法上我建议把这类工作负载放到独立的 ResourceQuota 或独立命名空间里管理避免“在线服务不许设 limits、离线任务必须设 limits”两种策略混在一起难以追踪。4.3 平台侧必须用 ResourceQuota 和 LimitRange 做容量边界很多公司不是由业务自己决定怎么设 limits而是平台在 K8s 控制面通过 ResourceQuota、LimitRange 做了统一约束。ResourceQuota 可以限制命名空间里所有 Pod 的 requests 和 limits 总量。比如这个团队最多只能申请 32 核 requests、64 核 limits。这保证了集群的整体容量有边界不会因为某个人声明了超大 requests 就把整个集群的资源占光。如果平台有强制要求每个 Deployment 必须出现 limits那也不是完全没救。你可以在合理的基线评估后把 limits 设置成一个远高于 requests 的“保险值”而不是当成精确的运行时约束。这样既满足了平台规范又不会让节流在日常流量下轻易发生。4.4 对延迟不敏感但必须保护整机安全的任务有些内部工具、监控采集器、日志采集器本身不要求低延迟但对稳定性有要求。如果代码里有死循环或异常重试会直接把节点 CPU 打满。这种情况下设置一个比较宽松的 CPU limits比如 requests 100m、limits 2 核可以防住最极端的失控情况。同时因为这类任务不会出现毫秒级突发流量被 CFS 节流影响业务的可能性很小。5. 不设 limits 后集群靠什么来保护既然 CPU limits 副作用明显那去掉之后集群不会失控吗我的答案分两层第一不要在没有任何容量体系和监控体系的情况下裸奔式去 limits第二Kubernetes 本身提供了比硬切断更合适的机制。5.1 用 requests 做准入和调度保证资源不超卖去掉 CPU limits 之后requests 依然是调度器和 kubelet 做资源核算的依据。Pod 申请多少 requests调度时就会扣掉多少可分配资源。也就是说只要你把 requests 写得真实节点不会出现“100 个 Pod 都说自己要 1 核实际上节点一共 8 核”的极端超卖情况。requests 的语义虽然没有硬性隔离但在调度层面已经限制了数量。这里的关键是requests 必须接近真实使用量不能为了省资源随意写低。我见过一个比较普遍的错误是 requests 写 100m但服务正常运行就要 4 核结果一个节点塞了几十个这样的 PodCPU 一紧张谁都跑不动。正确做法是让业务提供长期运行中 P75 或 P90 的 CPU 使用率再乘一个 1.2 到 1.5 的缓冲系数改成 requests 的基线。这不是精确公式但比拍脑袋强很多。5.2 用 QoS 分级处理节点压力下的驱逐顺序去掉 CPU limits 后Pod 的 QoS 类型可能会变化。这里要弄清楚只有 requests 和 limits 都设置且相等时Pod 才是 Guaranteed。只设置 requests、不设置 limitsPod 大概率是 Burstable。requests 和 limits 都没设置Pod 是 BestEffort。节点发生内存压力时kubelet 会先驱逐 BestEffort再驱逐 Burstable 中超出 requests 的 Pod最后才考虑 Guaranteed。所以如果你把 CPU limits 去掉了但内存层面仍然设置了合理 limits那么 QoS 能不能保住要看内存的 requests 和 limits 是否相等。我不建议为了拿到 Guaranteed QoS 而强行让 CPU requests 等于 limits。真正重要的是内存有保护CPU 让调度器按权重分。这个选择在绝大多数在线服务里都合理。5.3 用 HPA 处理负载伸缩而不是靠单 Pod 硬顶很多设置 CPU limits 的出发点是想“单 Pod 被限制住别影响节点”。这其实是靠单点硬扛的思路。Kubernetes 设计上更鼓励横向伸缩。去掉 CPU limits 后HPA 的意义会更大。HPA 可以基于 CPU 利用率自动扩缩副本例如apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70HPA 计算 CPU 利用率时依据的是 Pod 的 requests而不是实际可用的物理核。因此 requests 必须贴近真实负载。请求上来时HPA 会扩容多个副本分担压力而不是让单个 Pod 在 cgroup 配额里被反复冻结。5.4 用 LimitRange 和 ResourceQuota 约束规格边界如果担心团队里有人完全不写 requests平台侧可以借助 LimitRange 兜底。LimitRange 可以给没有声明 resources 的容器设置默认 requests 和 limits。例如apiVersion: v1 kind: LimitRange metadata: name: resource-profile spec: limits: - type: Container defaultRequest: cpu: 200m memory: 256Mi default: cpu: 4 memory: 1Gi这里给容器默认设置了 CPU limits 为 4 核。这是平台兜底逻辑不等于你要在每一个业务 Deployment 里手动写死 limits。真正的容量控制还可以放在 ResourceQuota 上apiVersion: v1 kind: ResourceQuota metadata: name: namespace-quota spec: hard: requests.cpu: 32 limits.cpu: 128 requests.memory: 64Gi limits.memory: 128GiResourceQuota 限制的是命名空间整体资源量。即使单个 Pod 不设 CPU limits它也不能突破整个命名空间的总请求量上限。平台想做精细化通常是“ResourceQuota 管总量 LimitRange 管默认值 业务自己管 requests”而不是把所有压力都压在单个容器的 limits 上。5.5 需要强 CPU 绑定时依赖 CPU 管理策略而不是 limits如果你真正的诉求是“这个 Pod 必须独占某些物理核心”那么 CFS quota 不是合适的工具。Kubernetes 提供了静态 CPU 管理策略可以让满足条件的 Guaranteed Pod 独占 CPU。但这要求 Pod 的 CPU requests 和 limits 是相等的而且必须是整数。所以这类高延迟敏感服务反而需要设置 CPU limits。它和你现在“为了防失控而设置 limits”不是一个目的。先想清楚自己是要“独占 CPU”还是“防止失控”。需要独占核心的服务走 CPU manager 的专用节点普通在线服务更依赖 requests 和 HPA。6. 实在绕不开 limits 时怎么设才不离谱有些团队因为历史原因、平台规范或者安全要求CPU limits 无法完全去掉。这时候可以把损失降到最低。6.1 limit 一定要明显高于 requests最忌讳的写法是requests.cpu: 1、limits.cpu: 1。这样 Pod 的 CPU 上限几乎等于正常使用基线任何突发都会触发节流。比较稳妥的做法是先看监控里的 CPU 使用分位数。如果服务日常 P95 是 1.2 核峰值能到 2 核requests 可以写 1.2 到 1.5 核limits 至少写到 3 到 4 核。limists 的作用只保留“防止失控吃满整机”而不是精确限制日常运行。limits 相对 requests 的倍数需要根据业务形态判断在线接口服务2 到 4 倍比较合理甚至更高。消息消费者1.5 到 3 倍。离线批处理任务1.2 到 2 倍因为它本来就在持续吃 CPU。未知负载类型先在测试环境观察两周不要直接上生产。这个倍数没有标准答案。原则是预留的冗余足够覆盖突发而不只是覆盖平均值。6.2 重点监测节流率和突发耗时设置了 limits 之后不要只看 CPU 使用率要把节流指标纳入日常监控。我建议至少配置两条规则- record: namespace_cpu_throttle_ratio expr: | sum by (namespace, pod, container) ( rate(container_cpu_cfs_throttled_periods_total[5m]) ) / sum by (namespace, pod, container) ( rate(container_cpu_cfs_periods_total[5m]) )然后用这条记录规则配置告警阈值可以先定 20%。如果连续 10 分钟超过这个阈值说明当前 limits 已经影响到业务运行了。真正做容量评估时要同时看三组数字Pod 实际 CPU 使用率。CFS throttled 比例。应用 P99 和超时率。三组放在一起看才能判断“是不是 limits 太少”还是“应用并发模型不合理”。6.3 降低周期影响先把应用自身并发控制做好有些团队为了不设过大的 limits选择限制业务并发度。这个方向是对的但要做在应用层不要在 K8s 层用 cgroup 配额硬切。在应用代码里我们可以用信号量、连接池、最大并发数来控制同时执行的任务数量。比如某个服务本来可以用 16 个线程处理请求但你已经算过单 Pod 最多稳定提供 4 核 CPU那就把线程池调到 8 个以下。这样 CPU 消耗自然收敛不再依赖 limits 去“踩刹车”。如果应用无法快速改造设置 limits 前也要考虑给 Pod 的spec.containers[].resources.limits.cpu设置一个冗余空间。为了一时安全把 limit 压到和 requests 一样不是保护是制造故障。6.4 按 Pod 和按 namespace 双重规划容量我见过很多服务单看资源使用不高但整个 namespace 的 requests 总量已经远超节点能力。去掉 CPU limits 后调度器只看 requests一旦某个 namespace 的 requests 总量没有控制住节点超卖率会变得不可控。所以就算单个业务不设 limits也要通过 ResourceQuota 限制 namespace 维度的总量。不能只做 Pod 层面的细粒度限制而完全不管团队整体申请了多少资源。容量规划要分两层单个 Pod 能涨多高整个 Namespace 能涨多高。7. 落地建议先怎么改后续怎么验证如果在你的集群里还不能立刻全量去掉 CPU limits我建议采用渐进式变更而不是某一天突然把线上所有 Deployment 的 limits 都删掉。7.1 从单条服务开始试点不要全集群一刀切第一步挑一个没有强依赖、可以让流量按百分比灰度切换的服务。先去掉它的limits.cpu但保留requests.cpu。YAML 里大概长这样resources: requests: cpu: 1 memory: 1Gi # limits: # cpu: 2 # memory: 2Gi注意当你移除 CPU limits 但保留内存 limits 时Pod 的 QoS 通常会变成 Burstable。服务如果一直有合理的内存 limits并且内存 requests 设得也够这在生产上通常是可以接受的。第二步对比变更前和变更后的节流指标。如果以前的 cfs_throttle_ratio 长期在 20% 以上去掉后应该明显降到接近 0。如果去掉后没有变化说明问题不在 limits需要继续往下排查应用代码或节点层。第三步观察一周包括 P99、错误率、Pod 重启次数、节点 CPU 稳态。只有这些没有劣化才扩大试点范围。7.2 观察周期、指标和告警配置不要只看平均值试点阶段不要只盯 CPU 的平均使用率。要用短窗口去观察1 分钟 CPU 峰值。CFS throttled 比例变化。容器线程调度是否有大量 runnable 等待。应用自身队列长度和超时分布。至少要积累三天以上包含流量高峰的数据。如果只在低峰期测试突发流量导致的节流可能根本不会被触发。很多人改完配置发现“没问题”是因为低峰期根本没有机会产生短时 CPU 尖峰。7.3 排查顺序与常见误区最后整理一套我自己比较常用的排查顺序遇到“CPU 不高但服务很慢”时按这个来先确认 Pod 是否在一直重启。如果是健康检查失败导致频繁杀掉重启不要先怀疑 CPU limits。再看 CFS throttled 指标。占比高优先怀疑 limits。看节点整体 CPU 和内存压力。节点都满了去掉 Pod limits 也没用要扩容或分散负载。看应用层日志、GC、线程池、数据库慢查询。这些也都会造成 CPU 不高但接口慢。最后看 requests 是否设置合理。requests 太高会导致调度不上去requests 太低会导致超卖严重。常见的误区有三个误区一CPU limits 能防 OOM。实际上 OOM 主要由 memory limits 控制CPU limits 和内存不直接相关。误区二CPU limits 等于预留 CPU。实际上它只是时间预算的硬顶而且副作用明显。误区三去掉 limits 后节点肯定会被打满。实际上只要 requests 真实再配合 HPA、ResourceQuota 和合理的调度策略节点是可管理的。回到开头那个线上事故最后我做的动作其实很朴素把核心在线服务的limits.cpu去掉保留合理 requests同时给集群加了基于 cfs_throttled 的监控。几小时后 P99 回到基线服务再也没有出现“资源看着够、响应莫名慢”的诡异现象。如果你所在的团队还处在“所有 Pod 必须设置 limits”的阶段我建议先不要说服所有人。先选一个服务做对照实验把节流率、P99、CPU 峰值做成报告用数据推动资源模型调整会更有效。Kubernetes 的资源管理从来不是“设得越严越好”而是要让调度、权重、伸缩各自干自己擅长的事。