ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes CPU limits 陷阱:CFS 配额如何引发延迟毛刺与节流

Kubernetes CPU limits 陷阱:CFS 配额如何引发延迟毛刺与节流 线上服务延迟突然飙高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 并不是说“请求这么多就用这么多”。即使你不写 limitsPod 在节点 CPU 空闲时依然可以跑满节点上的所有 CPU 核心。requests 的意义是当其他 Pod 也需要 CPU 时调度器会优先保证每个 Pod 至少能拿到它声明的资源。它是一份“调度契约”不是“性能上限”。2.2 CPU limitsquota 与强制上限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 等级Guaranteedrequests 和 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_totalCFS 调度周期总数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.yamlapiVersion: 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-columnsNAME:.metadata.name,CPU_LIMIT:.spec.containers[*].resources.limits.cpu,CPU_REQUEST:.spec.containers[*].resources.requests.cpu这里把 limits 设为 300mrequests 设为 100m。正常空载时 nginx 使用量很低不太会触发节流但只要压测并发一上来CPU 消耗很容易突破 300m从而产生明显的节流现象。5.2 压测制造瞬时高负载为了让 nginx 吃到更多 CPU可以使用压测工具 ab 或 hey 对 Pod IP 或 Service IP 发起并发请求。为了网络链路简单直接在集群内创建一个压测 Pod压测时注意给 load-generator 也设置 requests避免它在高负载时抢占被测试服务以外的节点资源kubectl run load-generator -n demo \ --imagebusybox:1.36 \ --requestscpu100m \ -- 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_timev1或 throttled_usecv2节流累计时间。如果在压测前后nr_throttled 快速上升就说明容器被频繁暂停业务延迟暴涨的根因基本可以锁定为 CPU limits。5.4 用 Prometheus 指标辅助判断如果集群内有 Prometheus可以用下面的 PromQL 查看节流总时长sum(rate(container_cpu_cfs_throttled_seconds_total{namespacedemo}[5m])) by (pod)也可以看“节流周期占比”sum(rate(container_cpu_cfs_throttled_periods_total{namespacedemo}[5m])) by (pod) / sum(rate(container_cpu_cfs_periods_total{namespacedemo}[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.yamlapiVersion: 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 级别设置 ResourceQuotaapiVersion: 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也不要滥用 limitsCPU 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 资源模型会通透很多。
返回列表