Kubernetes CPU limits为何会导致延迟飙升?正确管理容器资源的方法
2026/9/8 4:23:49 网站建设 项目流程

在 Kubernetes 集群里排查性能问题,有一个非常反直觉的现象:某个服务的 CPU 使用率明明只有 500 毫核,配额给了 1 核,按理说余量充足,但接口延迟就是居高不下。这时候很多人的第一反应是去看 GC、看 SQL、看 Redis,折腾一圈之后才发现,真正的瓶颈藏在limits里。Kubernetes 社区有一篇文章的标题把这件事说得很直白:For the love of god, stop using CPU limits on Kubernetes。这个标题看起来像气话,但背后是一个讨论了很多年、依然被大量团队踩中的真实问题:CPU limits 不是普通的“资源上限提醒”,它会触发内核级的 CPU 节流,而节流对延迟敏感型服务的影响相当隐蔽。这篇文章会讲清楚 requests 和 limits 的本质区别,用一个最小实验演示 limits 为什么会拖慢服务,再给出生产环境里到底该不该设 limits 的判断方法。读完你会明白,正确的做法不是无脑去掉 limits,而是理解资源模型之后,把它放回它该在的位置。

1. 为什么社区一直劝你“停止使用 CPU limits”

先看一个高频场景。团队在 Kubernetes 上部署了一套 Java 网关,为了保证稳定性,给每个 Pod 设置了resources.limits.cpu: "1",同时requests.cpu: "500m"。在机器负载不高的时候,一切正常;一旦流量上来,CPU 总使用率还不到节点的一半,服务却开始出现大量超时。查看监控,Pod 的 CPU 使用率曲线带着明显的锯齿,每隔一段时间就被“压平”,而 GC 时间、线程池指标都很正常。

这不是业务代码的锅,而是 limits 触发了 CFS 配额限制。Kubernetes 把 limits 下发到底层容器运行时,最终会写到 Linux cgroup 的 CPU 配额上。cgroup 会严格限制容器在每个调度周期内最多使用的 CPU 时间,超过部分不是排队,而是直接冻结线程,等技术术语叫 throttle。对 Web 服务、API 网关这类对延迟敏感的进程来说,线程被冻结几十毫秒,反映到用户端可能就是一次 P99 超时。更麻烦的是,这种节流不会杀掉进程,不会报错,日志里看不出异常,让排查方向完全跑偏。

所以社区里喊出的所谓“停止使用 CPU limits”,针对的不是“资源管理”本身,而是“把 limits 当成万能保险丝”这种默认习惯。CPU 本来就是可压缩资源,超出 limits 不会导致进程被杀,只会让线程在配额耗尽时被暂停。如果请求量本身有长期峰值,节流会成为一个持续性的性能瓶颈;如果只是偶尔超过配额,节流则是间歇性的不确定性来源。对延迟敏感的业务,这种不确定性比 OOM 更难受,因为它无法通过重启恢复,也很难通过日志定位。

那是不是所有场景都应该去掉 limits?也不是。后面会讲到,批处理、离线任务、测试命名空间仍然需要 limits 来防止负载失控。真正的核心是搞清楚 requests、limits、QoS、cgroup 节流之间的关系,而不是把某个配置项一刀切地禁止。

2. 先把基础概念对齐:requests、limits、QoS

很多 Kubernetes 面试题都会问 requests 和 limits 的区别,但真实项目里能讲清楚的人并不多。如果只是背定义,很容易产生一个误解:requests 是给调度器看的,limits 是给运行时看的,两者互不影响。这个理解不算错,但漏掉了最重要的部分——它们在节点上落地时,走的是完全不同的机制。

2.1 requests 与 limits 分别管什么

requests 是容器对资源的“需求声明”。调度器为 Pod 选节点时,会把所有容器的 requests 加起来,判断节点剩余可分配资源是否够用。它还会转换成 cgroup 的 CPU 权重,也就是 CPU shares。当节点 CPU 充足时,这个权重几乎不会产生可感知的影响;当多个 Pod 竞争 CPU 时,内核按权重分配时间片。所以 requests 决定了“你需要多少”,以及“CPU 紧张时你能分到多少”。

limits 是容器对资源的“上限声明”。调度器不会把 limits 当作调度依据(Node 上的可分配资源不包含 limits),但它会转换成 cgroup 的 CPU 配额。Linux CFS 调度器按周期检查配额:假设一个周期是 100ms,limits 设为 1 核,那么容器在这个周期内最多只能使用 100ms 的 CPU 时间。无论容器里有多少个线程、多少个进程,共享的都是这个配额。一旦用完,剩余时间要么等待下一个周期,要么被强制暂停。

用表格看会更清楚:

维度requestslimits
主要作用调度器选节点的依据cgroup 对 CPU 使用的硬性上限
内核机制CPU shares 权重CFS quota 配额
CPU 充足时几乎无影响会持续限制,超限即节流
CPU 紧张时按权重参与竞争超限线程被冻结
是否导致进程被杀否,但会导致 latency 飙升

2.2 QoS 分级:Guaranteed、Burstable、BestEffort

根据 requests 和 limits 的组合方式,Pod 会被划分到三个 QoS 等级。这个等级直接影响节点资源不足时 Pod 的驱逐优先级。

QoS 等级设置方式特点
Guaranteedrequests 等于 limits,且内存、CPU 都设置资源保障最高,默认最不容易被驱逐
Burstable至少有一个容器设置了 requests,但 requests 不等于 limits最常见,如果 limits 大于 requests,就存在超用可能
BestEffort没有设置任何 requests 和 limits系统尽力而为,资源紧张时最先被驱逐

很多团队把 Pod 都配成 Guaranteed,理由是“这样最稳定”。但如果 CPU limits 设置得过低,Guaranteed 的代价就是每个周期都可能发生节流。QoS 保障的是“不被邻居抢走资源”,它并不能缓解“你自己给自己设了上限”。

2.3 CPU 是可压缩资源,不等于可以随便超用

Kubernetes 把资源分成可压缩和不可压缩两类。内存是不可压缩资源,超过 limits 的后果是 OOM Kill,进程直接死掉,重启后恢复,问题定位起来反而直接。CPU 是可压缩资源,超过 limits 时进程不会被杀,而是被内核节流。这种“看起来还活着,实际时不时被暂停”的状态,在监控图表上往往表现为使用率被削平、延迟升高,非常具有欺骗性。

还有一个常见误区是认为“单个容器只用 500m,limits 给 1000m,肯定够”。CFS 配额是全体进程共享的。如果一个 Java 应用触发了 Full GC,或某个线程池突发创建了大量线程,瞬时 CPU 需求超过配额,所有线程都会被一起暂停。也就是说,limits 并不只限制“超出的那部分”,它会在配额耗尽的瞬间冻结整个容器的执行流,包括那些本来只用了很小 CPU 的线程。

3. CPU limits 的副作用为什么这么隐蔽

如果你只是写了一个每秒钟请求量很低的内部服务,CPU limits 带来的影响可能很难察觉。但一旦服务对延迟敏感,或者行为存在周期性峰值,问题就会暴露出来。理解副作用,需要从 Linux CFS 配额原理说起。

3.1 CFS 配额与节流的原理

在 cgroup v2 环境下,容器对应的 cgroup 目录里有一个cpu.max文件,格式是$MAX $PERIOD。比如100000 100000表示每个 100ms 周期最多运行 100ms,也就是 1 核。如果改成50000 100000,表示每个周期最多 50ms,对应 0.5 核。

当 cgroup 内所有进程在一个周期内累计运行时间达到$MAX后,剩下未用完的时间片会被回收,进程进入 throttle 状态,直到下一个周期开始。这个机制本意是公平分配 CPU,但它没有感知业务优先级。对一个实时性要求较高的 Web 服务来说,线程在请求处理中途被冻结,等待重新调度,这就是延迟抖动的主要来源。

一个容易忽略的细节是:CFS quotas are accounted at the cgroup level, not per-thread level。也就是说,多线程应用的线程之间共享配额。即便你的应用把线程数开到 200,在配额耗尽时 200 个线程会被同时暂停,而不是每个线程单独分配一份 CPU 时间。

3.2 节流对延迟敏感服务的影响

延迟敏感型服务的典型特征是短请求、高并发、对 tail latency 敏感。这类服务平时 CPU 使用率低,但偶尔会有短促的峰值。如果 limits 设置得恰好低于峰值需求,就会在流量抖动时触发节流,制造大量 P99 超时。团队通常会先怀疑代码性能、依赖组件问题,花很长时间排查后才发现是资源配额造成的。

更麻烦的是,节流与 CPU 使用率不是同步的。假设 limits 是 2 核,应用实际平均使用 1 核,但其中有一个 50ms 的突发使用 3 核,配额就会超支。监控看到的平均使用率不高,看不出问题;只有在更细粒度的 CPU 时间维度上,才能发现节流计数在增长。

3.3 对 Java 这类多线程运行时的额外影响

Java 的垃圾回收器、线程池、异步框架都依赖 CPU 时间。发生节流时,如果 GC 线程和业务线程被一起暂停,可能导致一系列连锁反应:GC 暂停时间变长、线程池任务堆积、健康检查响应变慢。Kubernetes 的存活探针一旦因为这种抖动连续失败,Pod 还会被反复重启,造成恶性循环。

这也是很多 Java 服务在物理机上运行正常,迁移到 Kubernetes 后反而“变慢”的原因之一。物理机没有 cgroup 配额约束,容器环境里配置不当的 limits 成了新的隐藏瓶颈。

3.4 limits 与资源配额、利润率的关系

还有一种情况,团队给所有 Pod 都设置了 limits,并不是因为业务需要,而是因为 Kubernetes 的ResourceQuota要求资源必须有上限。比如命名空间配置了 quotas,而配额按 limits 计算。在这个前提下,去掉 CPU limits 会导致 Pod 创建失败。

这种配置需要调整思路:ResourceQuota 是管理命名空间整体资源的手段,可以限制所有 Pod 的 requests 总大小;而 limits 是单个容器的资源上限。两者不是一回事。下面会专门讲生产环境怎么通过配额、LimitRange 来替代“给每个容器都写 limits”的做法。

4. 什么场景该用、什么场景不该用 CPU limits

社区争论“stop using CPU limits”多年,最终的正确结论绝不是“永远不设置”,而是“不要默认设置”。判断标准应该围绕业务特征展开:这个服务对延迟是否敏感?是否存在资源失控风险?是否能接受 Pod 的平均性能被压平?

场景建议原因
延迟敏感型在线服务(API、网关、RPC)优先不设 CPU limits,只设置精确 requests避免节流造成的尾部延迟抖动
离线批处理、数据流水线可以设置 limits,甚至设得较小任务对绝对完成时间不敏感,要防止占满节点
测试/开发命名空间设置 limits 兜底测试代码可能有 bug,需要隔离风险
共享集群的多租户环境必须设置 limits 或配额防止单个应用拖垮整个节点
无法快速扩容的在线服务先设置 limits 并持续监控,后续评估调整极端情况下保护节点稳定性优先

这不是一个简单的“设或不设”的选择,而是一个“看得懂代价”之后的决定。设置 limits,等于接受节流风险;不设置 limits,等于相信应用的资源使用是可控的。生产环境里更稳妥的做法,是用监控和自动扩缩容来管理在线服务的 CPU 使用,而不是把希望寄托在一个静态的 limits 上。

5. 环境准备与验证思路

下面做一个最小实验,目标是让节流现象“肉眼可见”。你只需要一个能运行的 Kubernetes 集群,minikube、kind 或者任何测试集群都可以。这里不限定具体版本和发行版,因为实验依赖的是 Linux 内核和容器运行时通用的 cgroup 行为。

需要准备的工具:

  • 一个可用的 Kubernetes 集群,且有权限创建 Deployment。
  • 本机安装kubectl,并已配置好 kubeconfig。
  • 一个方便执行压测命令的容器镜像。推荐包含yesbash的基础镜像,比如 busybox 或你正在用的业务镜像。
  • 如果想查看 cgroup 文件,需要能进入容器执行命令;如果容器内看不到 cgroup 路径,可以改用节点视角,但需要相应权限。

实验分为三个步骤:

  1. 创建一个设置了 CPU limits 的 Deployment,并压测触发节流。
  2. 在容器内或节点 cgroup 里观察配额文件和节流周期计数。
  3. 创建一个只设置 requests、不设置 limits 的 Deployment,对比同样压测下的表现。

这个设计能让每个现象都有对应的配置和观测方法,而不是停留在“理论上会节流”的层面。

6. 最小实验:观察 CPU 节流现象

6.1 创建带 CPU limits 的 Deployment

先创建一个带 limits 的 Deployment,requests 设为 500m,limits 设为 1 核。文件内容如下:

# 文件路径:deploy-with-limits.yaml apiVersion: apps/v1 kind: Deployment metadata: name: demo-with-limits labels: app: demo-with-limits spec: replicas: 1 selector: matchLabels: app: demo-with-limits template: metadata: labels: app: demo-with-limits spec: containers: - name: demo image: busybox:1.36 command: ["sleep", "3600"] resources: requests: cpu: "500m" memory: "128Mi" limits: cpu: "1" memory: "256Mi"

这个配置的含义是:调度器按 500m 判断节点资源是否足够;运行时 cgroup 配额按 1 核限制。创建后确认 Pod 正常运行:

kubectl apply -f deploy-with-limits.yaml kubectl get pods -l app=demo-with-limits -o wide

6.2 压测触发节流

进入容器,启动多个yes > /dev/null进程来制造 CPU 负载。yes命令会不断输出字符,输出重定向到/dev/null后,单进程可以打满一个逻辑核。

kubectl exec -it deploy/demo-with-limits -- sh # 在容器内执行 yes > /dev/null & yes > /dev/null & yes > /dev/null &

启动三个进程后,容器的需求大约为 3 核,而 limits 只有 1 核。此时如果查看容器的 CPU 使用率,会看到它被限制在大约 1 核附近,而不是真正的 3 核。这就是节流发生的直观证据。

6.3 观察 cgroup 配额和节流周期

在容器内查看 cgroup 文件,能够得到配额信息:

# cgroup v2 路径 cat /sys/fs/cgroup/cpu.max # 预期输出类似:100000 100000 (表示 1 核配额,周期 100ms) # 如果 limits 为 500m,则输出类似:50000 100000

同一路径下还有cpu.stat,可以看到节流相关的统计信息:

cat /sys/fs/cgroup/cpu.stat

输出中nr_throttled表示容器被节流的次数,throttled_usec表示累计被节流的时间。压测开始后,多观察几次,这两个值会不断增长。如果配置了 Prometheus,也可以通过指标查询节流情况,对应的指标名是:

rate(container_cpu_cfs_throttled_periods_total{namespace="default", pod=~"demo-with-limits.*"}[1m])

这个指标统计的是容器在每个周期里实际发生节流的周期数,是判断 limits 是否成为瓶颈最直接的依据。

6.4 对比去掉 limits 后的表现

创建一个只设置 requests、不设置 limits 的 Deployment:

# 文件路径:deploy-requests-only.yaml apiVersion: apps/v1 kind: Deployment metadata: name: demo-requests-only labels: app: demo-requests-only spec: replicas: 1 selector: matchLabels: app: demo-requests-only template: metadata: labels: app: demo-requests-only spec: containers: - name: demo image: busybox:1.36 command: ["sleep", "3600"] resources: requests: cpu: "500m" memory: "128Mi" limits: memory: "256Mi"

同样进入容器,执行三个yes > /dev/null &。此时没有 CPU 配额,容器可以充分使用节点上的空闲 CPU。如果你观察cpu.max,会看到类似max 100000的输出,表示没有上限;cpu.stat里也不再出现持续的节流增长。

这个对比实验并不复杂,但它把“limits 会改变容器的 CPU 行为”这件事从理论变成了可观测的事实。你的业务镜像可能比 busybox 复杂得多,但底层机制是完全一样的。

7. 生产环境里如何正确管理 CPU 资源

实验做完之后,很多人的第一反应是把所有 Deployment 的 CPU limits 都删掉。这个动作本身没有错,但没有配套措施就直接删,等于把一个可能出问题的机制换成了另一个可能出问题的机制。生产环境需要的是系统性调整,而不是一次性改动。

7.1 先定好 requests,用监控校准

requests 是整篇文章里真正需要认真对待的值。它不应该照抄 limits,也不应该随便填一个小数,而应该来自过去一段时间实际运行数据的统计。如果使用 Kubernetes HPA,可以根据业务指标自动调整副本数;requests 偏大,会导致资源浪费和调度失败;requests 偏小,节点过载时 CPU 争用会加剧。

推荐的做法是:选取业务高峰期的一周数据,统计 CPU 使用率的中位数或 P95,再乘以安全余量。不要直接把当前使用率的上限当作 requests,这样在性能优化、流量突增时没有缓冲空间。

7.2 用 ResourceQuota 和 LimitRange 兜底

如果担心“不设置 limits 之后,某个没写好代码的应用会打爆节点”,可以不在 Pod 级别加 limits,而在命名空间级别配置 ResourceQuota:

# 文件路径:namespace-quota.yaml apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: "20" requests.memory: 64Gi

这个配额限制的是命名空间内所有 Pod 的 requests 总量。它不会限制单个容器的 CPU 峰值,但能控制整体资源申请规模。再配合 LimitRange,可以约束每个容器的默认 requests:

# 文件路径:namespace-limitrange.yaml apiVersion: v1 kind: LimitRange metadata: name: default-requests namespace: dev spec: limits: - max: memory: 4Gi defaultRequest: cpu: 100m memory: 256Mi type: Container

这种组合的好处是:把“防止资源失控”的职责交给平台层,把“性能与延迟”的职责留给业务层。平台层保证不会有太多请求挤进命名空间,业务层则可以更灵活地管理单容器资源。

7.3 真正需要保留 CPU limits 的少数情况

延迟敏感型在线服务不设 CPU limits,不代表所有工作负载都要这么做。批处理任务和数据流水线通常有固定预算,设置 limits 可以把资源消耗限制在可控范围内,避免任务之间相互干扰。测试环境更是如此,测试代码可能存在死循环或内存泄漏,limits 是安全兜底。

在共享集群中,如果多个团队共用一个节点池,而节点规格固定、无法快速扩容,CPU limits 仍然有它的价值。此时更像是一个运维取舍:接受部分延迟波动,换取整体稳定性。这种场景下,limits 应该写得有依据,比如根据压测结果确定合理的峰值,而不是随手填一个"2000m"

7.4 批量调整的逐步升级策略

生产环境批量去掉 limits 时,不要一次性修改几百个 Deployment。更稳妥的是按服务重要性分组,先选一个非核心、且有完善监控的服务做试点。观察对象包括:CPU 使用率、P99 延迟、错误率、Pod 重启次数、节点 CPU 使用率。

如果试点运行一周后,服务延迟没有劣化,节点资源没有失控,再逐步扩大到其他服务。每次变更都用标准发布流程走:代码评审、灰度发布、监控验证、回滚预案。如果某个服务在去掉 limits 后出现资源占用过高的问题,第一反应不是立刻加回 limits,而是分析它的资源使用曲线,调整 requests 或 HPA 指标。

8. 常见问题与排查思路

社区和实际项目中,围绕 CPU limits 的问题高度集中。下面整理几个最常见的场景,帮助快速定位。

问题现象可能原因排查方式解决方案
CPU 使用率不高,P99 延迟高limits 触发 CFS 节流查看container_cpu_cfs_throttled_periods_total是否增长调整或移除 CPU limits,优先保证延迟
Pod 内cat /sys/fs/cgroup/cpu.max看到大量配额cgroup v2 配额设置与 limits 一致计算 limits 对应周期值,与 YAML 对照按需调整 limits,或改用 requests-only
去掉 limits 后单个 Pod 占满节点 CPU应用本身存在资源失控风险检查应用线程数、死循环、流量突增配置 HPA、调整 requests,必要时在命名空间加配额
Pod 反复重启,但内存正常存活探针因节流连续失败查看事件和探针日志,确认节流时间窗口增加探针超时,同时评估 limits 是否合理
想给 Pod 设置 limits 但创建失败ResourceQuota 限制查看命名空间 quota 和 LimitRange调整配额策略,不要让业务配置被平台限制反向绑架
节点 CPU 跑满,但容器实际影响不明显多个容器竞争,shares 权重在起作用查看节点指标和各 Pod CPU 时间调整 requests,保证核心服务权重更高

这里特别说明一下“去掉 limits 会不会导致节点被打满”。即使所有在线服务都去掉 CPU limits,节点上仍然存在 CPU shares 竞争机制。当 CPU 资源不足时,内核按各容器 requests 对应的权重分配时间片。这个机制比静态 limits 更贴近真实资源需求,但前提是每个容器的 requests 设置得准确。如果 requests 全部默认填 500m,而实际需要的只有 50m,权重分配就会失真。

9. 总结与后续学习方向

这篇文章讨论的核心对象是 Kubernetes 中经常被误用的 CPU limits。它真正想传递的判断是:limits 在 Kubernetes 里不是一个无脑的保护措施,而是一个会改变容器运行时行为的硬性约束,尤其是在你对延迟敏感、对性能稳定性有要求的服务里,由 limits 引发的 CPU 节流会带来比资源超用更隐蔽的负面影响。正确的管理思路是先设置准确的 requests,用监控校准,再按业务场景决定是否保留 limits;在线核心服务优先考虑去掉 CPU limits,用 HPA 和命名空间配额代替硬编码上限;批处理、测试环境、共享集群则仍然保留 limits 做安全兜底。

如果这篇文章让你对 requests、limits、QoS、cgroup quota 之间的关系有了新的理解,下一步可以继续研究几个更深入的方向:cgroup v2 的cpu.max与带宽控制参数、Kubernetes 节点级的 CPU Manager 策略对 QoS 的影响、HPA 与 VPA 在 CPU 资源管理中的配合。把这些内容吃透,遇到 CPU 相关的性能问题时,你就会有比“改配置”更完整的判断能力。建议先拿一个非核心服务做实验,把第三节和第六节里的观测方法跑一遍,让节流数据说话,再决定你的集群里到底该怎么处理 CPU limits。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询