集群 CPU 平均只用了 8%,其余的钱花哪了:K8s request 和 limit 讲人话

一份覆盖两万多个生产集群的报告说,K8s 集群平均 CPU 利用率只有 8%,还在逐年下降。这不是运维不会省钱,而是 request 和 limit 这两个字段的含义被大多数人想错了:request 不是"最少用多少",是"调度时占多大的位";limit 也不是一条简单的上限,CPU 越线是被限速,内存越线是被枪毙。把这两件事讲清楚,"为什么大家都超配"和"为什么超配停不下来"就都有了答案,顺带把 QoS 三档、驱逐顺序、HPA 为什么按 request 算一起说了。


前几天翻到一份报告,Cast AI 每年出一次的 Kubernetes 优化现状,2026 年这版覆盖了两万多个生产集群。里面有个数字我盯了很久:平均 CPU 利用率 8%,去年是 10%。内存 20%,GPU 更惨,5%。另一个数字是”CPU 超配率”从 40% 涨到了 69%,也就是七成的集群,申请的 CPU 远远多于用掉的。

第一反应是:这么多公司的运维都不会省钱?后来想想不对。我自己写过的 Deployment 里,request 也是拍脑袋往大了填的,而且填的时候心里挺踏实。这个 8% 不是懒出来的,是 requestslimits 这两个字段的含义被大多数人想错了。想错的方向都一样,超配就是必然结果。

这篇把这两个字段到底在管什么讲清楚。看懂之后,“为什么大家都超配”和”为什么超配停不下来”这两个问题就都有了答案。

request 不是”最少用多少”,是”占多大的位”

先说 request。很多人把它理解成”这个容器至少需要这么多资源”,这个理解错得不多,但错在最关键的地方。

request 是给调度器看的。调度器给 Pod 挑节点时,只做一道减法:节点可分配的 CPU 减去已经落在这上面所有 Pod 的 request 之和,剩下的够不够放这个新 Pod 的 request。它不看任何一个 Pod 实际在用多少。一个 Pod 申请了 2 核,实际只跑 0.1 核,在调度器眼里它就是结结实实占着 2 核,这 2 核别人不能再申请。

所以 request 更像是占座。你在图书馆放了一个包,人去吃饭了,那个座位在管理员眼里就是有人的。

这就是 8% 的直接来源。一台 40 核的节点,上面 20 个 Pod 每个申请 2 核,调度器眼里这台机器已经满了,再来一个 Pod 就得加节点。但这 20 个 Pod 平均各用 0.2 核,加起来 4 核,利用率正好 10%。集群自动扩容器看到的是”调度器眼里满了”,于是继续加机器,账单继续涨,而机器的风扇都没转起来。

同一台节点的两种看法:调度器按 request 算已经占满,实际使用只有一小截

这里有两个容易混淆的数。分配率是 request 之和除以节点容量,报告里说的”超配”就是分配率远高于使用率。利用率是实际使用除以节点容量,就是那个 8%。两个数讲的是两个故事,一个是账面,一个是现实。很多集群账面上快满了,现实里空得能跑马。

request 还有第二个作用,发生在真正争抢的时候。节点上的 CPU 如果不够所有人一起用,内核按每个容器的 request 比例来分。申请 2 核的和申请 1 核的抢同一个核,前者拿到的是后者的两倍。平时不争抢,这个比例没有存在感;一旦争抢,request 就是你的话语权。所以 request 也不是随便填个小数就完事,它决定了高峰时谁先饿。

limit 也不是一条简单的上限

再说 limit。它的字面意思是”最多能用这么多”,但 CPU 和内存这两种资源撞到 limit 时的下场完全不同,这是很多线上事故的根子。

CPU 是可压缩的资源。容器超过 CPU limit 时,内核不会杀它,而是限速。具体机制是 CFS 带宽控制:每 100 毫秒一个周期,你的 limit 是 1 核,那这 100 毫秒里你最多用 100 毫秒的 CPU 时间。用完了,剩下的时间你的线程全部挂起,等下一个周期。表现出来是延迟毛刺:请求处理到一半被暂停几十毫秒,P99 忽然变差,但服务不死,CPU 图上甚至看不出用满了,因为它根本没机会用满。

内存是不可压缩的。内存拿到手就是拿到手了,内核没办法让你”慢一点用”。容器超过内存 limit 时,内核的 OOM killer 直接把进程杀掉,容器重启,事件里一行 OOMKilled。没有预警,没有降速,就是死。

CPU 与内存撞到 limit 的两种下场:一个被限速、一个被枪毙

这个差异直接推出一条经验:内存的 request 和 limit 设成一样,CPU 的 limit 放宽甚至不设

内存设成一样,是因为内存一旦超过 request 又没到 limit,这段”多用的”是借的。节点内存紧张时,kubelet 驱逐 Pod 的顺序是先看谁借得多。借的越多越先走。与其让它在某个不确定的时刻被驱逐,不如一开始就按峰值申请,申请多少用多少,谁也不欠。

CPU 放宽,是因为 CPU limit 带来的限速在绝大多数场景里只有坏处:机器明明有空闲的核,你的服务却被人为卡住。CPU 不设 limit 时容器可以用满节点上所有空闲的核,争抢时又按 request 分配,公平性并没有丢。真正需要 CPU limit 的是那些不能容忍邻居影响的场合,比如多租户平台,或者想要下面要讲的 Guaranteed 档位。

Java 服务在这里有个额外的坑。JVM 启动时会按它看到的 CPU 数决定 GC 线程数和 JIT 编译线程数。容器里的 JVM 看到的是 limit,如果 limit 设成 1 核,GC 就是单线程的;如果不设 limit,它看到的是节点的全部核数,起一堆线程,反而更容易触发限速。所以给 Java 服务不设 CPU limit 时,最好显式告诉 JVM 该用几个核。

request 和 limit 的关系,决定了你在队伍里的位置

把 request 和 limit 放在一起看,K8s 会给每个 Pod 定一个服务质量档位,叫 QoS class。它不用你填,是根据这两个字段的关系推出来的:

  • Guaranteed:每个容器的 CPU 和内存都设了 request 和 limit,而且两两相等。
  • Burstable:至少设了一个 request,但不满足上一条。
  • BestEffort:什么都没设。

这个档位管的是节点资源不够时谁先被赶下去。kubelet 发现节点内存快耗尽时,先驱逐 BestEffort 的 Pod,它们什么都没承诺,也什么都不欠;然后是 Burstable,按实际使用超出 request 的量排序,超得越多越先走;Guaranteed 排在最后,只有前两档全清空还不够时才轮到它们。

所以”要不要设 limit”这个问题的另一个答案是:看你想排在队伍的什么位置。一个核心服务,宁可多花点钱把 request 和 limit 设成相等,换一个 Guaranteed 的位置,比省下那点资源要划算。一个跑批的任务,放在 BestEffort 也没关系,被驱逐了重跑就是。

这里有个小细节:如果只设了 limit 没设 request,K8s 会自动把 request 设成和 limit 一样。所以”我只设了 limit”的 Pod,其实占的座位和 limit 一样大。这也是超配的来源之一:有人为了保险给了一个大 limit,request 跟着变大,调度器眼里这个 Pod 就是个大块头。

为什么大家都超配,而且停不下来

理解了上面三件事,超配的成因就很清楚了。

写 Deployment 的人怕两件事:怕内存不够被 OOM 杀掉,怕 CPU 不够被限速拖慢。这两件事的后果都是立刻能感知的事故。于是 request 往大了填,limit 再往大了填一档,图个安心。这一步在他的位置上是完全理性的。

但填大的代价,他看不见。调度器按填大的数字占座,节点账面很快满了,集群自动扩容器把”账面满了”当成真实需求,去云上再买一台。账单落在平台组或者财务那里,和填数字的人隔着两三个部门。

然后,没有任何环节会回头改这些数字。服务上线时填的 request 是按第一次压测的峰值猜的,半年过去业务量变了三次,配置文件一次没动过。Helm chart 里默认值都是往保守里给的,复制来复制去。

超配的自我强化回路:怕事故就多填,多填就多买,买的人看不见填的人

三个环节每一个都没有错,合起来就是七成的集群超配、利用率逐年下降。报告用了”结构性”这个词,我觉得很准。这不是某个人的问题,是信息断在了填数字和付钱之间。

HPA 和 VPA 为什么都盯着 request

昨天写 HPA 的算法时提过,HPA 按 CPU 利用率扩缩容时,那个百分比的分母是 request,不是 limit 也不是节点容量。把今天的内容套上去就明白了:request 是”账面上分给你的份额”,HPA 问的是”你把你的份额用了几成”。request 填大了,份额永远用不到几成,HPA 就永远不扩容,服务被打慢了它也无动于衷。

VPA 就是冲着这个问题来的:它观察容器的实际用量,反过来推荐甚至自动修改 request。本质上就是把”上线时拍脑袋填的数”换成”跑了一段时间以后实测的数”。Cast AI 这类工具做的事情也是这个方向,只是加上了节点层面的装箱。

一个例子:怎么把 request 填对

说一个通用的做法。一个 Java 服务,上线时 request 2 核 4G、limit 4 核 4G。跑了一个月,去监控里拉它的实际用量:CPU 的 P95 是 0.3 核,偶尔冲到 0.8 核;内存稳定在 2.5G,GC 之后回落到 2G,最高没超过 3G。

那么改法是:

  • CPU request 设 0.5 核。比 P95 高一点,留出争抢时的话语权。不用按峰值填,峰值可以借空闲的核。
  • CPU limit 去掉,配合 JVM 参数显式指定 CPU 数。
  • 内存 request 和 limit 都设 3.5G。按观察到的峰值加一点余量,两者相等,进 Burstable 里最安全的位置。想要 Guaranteed 就把 CPU 也设成相等。

这一个服务的账面占用从 2 核降到 0.5 核,一台节点能多放三倍的实例。没有改一行业务代码,只是把猜的数换成了量的数。

我的看法

request 和 limit 这两个字段,名字起得太像”需求”了,让人以为填的是”服务需要多少”。其实一个是你对调度器许的诺,一个是你对内核划的线。两者都不是需求,需求只有监控知道。

集群省钱这件事,我觉得顺序是先量再填,而不是先砍。砍是拿事故换钱,量是拿信息换钱。8% 这个数字之所以能逐年下降,就是因为量的人和填的人和付钱的人一直不是同一个人。把这三个人拉到同一张图前面,比任何优化工具都管用。

评论