【背景】
做了三年 K8s 运维,踩得最多、也最容易被忽略的一个坑,就是 CPU limit 带来的 CFS throttling。
具体现象:一个 Java 在线服务(Spring Boot,4C8G Pod),CPU request=1、limit=1,稳态 CPU 使用率只有 25%(看起来「离 limit 远着呢」),但 P99 从 18ms 涨到了 52ms。
打开 cgroup 一看:
# cgroup v1
cat /sys/fs/cgroup/cpu/kubepods/podXXX/cpu.stat
nr_periods 4820311
nr_throttled 283417
throttled_time 1987654321
throttling ratio ≈ 5.9%——每 17 个调度周期就有 1 个被卡住。Java 的 GC STW、线程池 burst、JIT 编译这些瞬间峰值操作,恰好撞在 CFS quota 的窗口边界上,被 throttle 了。
关键问题在于:平均 CPU 使用率 25% 完全不能说明 throttling 不严重,因为 CFS quota 是按每个调度周期(默认 100ms)的窗口来限的,不是按"总平均"来的。
【常见分歧 / 几种做法】
我见过三种典型立场:
派系 A:CPU limit = CPU request(设死) 理由:公平、可预测、容量规划简单。"一个 Pod 就给你这么多 CPU,别想多拿。" 代价:burst 能力被完全砍掉,对延迟敏感服务 P99 直接翻倍。
派系 B:只设 request,不设 CPU limit 理由:K8s 调度用 request 保证"最少能拿到多少",limit 不设意味着"上限不封顶",burst 能力保留,P99 友好。 代价:极端情况下(比如 GC 风暴、流量突增),一个 Pod 可能吃满整个节点的 CPU,把同节点邻居全拖下去。
派系 C:limit = 2x request 或 4x request(折中) 理由:既保留一定的 burst 空间,又不至于失控。 代价:这个倍数到底取多少?2x 够吗?4x 会不会还是不够?本质上是"凭感觉调参"。
【我的倾向与理由】
分场景,不"一刀切":
-
在线延迟敏感服务(API、搜索、风控)
- CPU request 按 P95 用量设(保证调度不超卖)
- CPU limit 不设或设成节点总 CPU(等于不限流)
- 防止"吃满节点"靠的不是 limit,而是 HPA + 副本数 + 节点资源预留
- 关键监控:
container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total,ratio > 3% 就该告警,> 5% 就该调
-
Batch / 离线任务(ETL、报表、ML 训练)
- CPU limit = CPU request(设死)
- 理由:这类任务对 P99 不敏感,公平性更重要
- 配合
priorityClassName=low+preemptionPolicy=PreemptLowerPriority,确保在线 Pod 随时能抢占
-
GPU 节点上的 CPU 侧
- GPU 本身有硬隔离(MIG / vGPU),但 CPU 侧的 limit 仍然影响 kernel 线程、数据搬运
- 建议:GPU Pod 的 CPU limit 至少 2x request,给 CUDA kernel launch 留 burst 空间
核心理由:CFS quota 的 throttle 是"硬卡",不是"软限"。一旦触发,线程直接被挂起,不像 I/O 的 io.max 还有 burst 窗口。对延迟敏感服务,这个"硬卡"的代价远大于"不设限"带来的邻居风险——后者可以用调度策略、资源预留、priority 来兜底。
【cgroup v2 的 cpu.max.burst:一个被低估的折中方案(补记)】
原文里我提了"v2 有没有 burst 参数"这个问题,后来在 K8s 1.28 + cgroup v2 节点上实测了一轮,补上结论:
cgroup v2 的 cpu.max 格式是 MAX BURST_US,burst 字段的语义是:每个调度周期内,如果配额没用完,可以把"剩余时间"存到 burst 池里,下个周期超用时从池里扣,上限就是 BURST_US。
实测数据(4C Java 服务,request=1C,limit=1C,burst=100ms vs burst=200ms):
# cgroup v2
echo "100000 100000" > cpu.max # 1C limit
echo 100000 > cpu.max.burst # 100ms burst
cat cpu.stat
nr_periods 2340112
nr_throttled 38201
throttled_usec 412345678
- burst=0(无 burst):throttling ratio ≈ 5.9%,P99 52ms
- burst=100ms:throttling ratio 降到 ≈ 2.1%,P99 降到 31ms
- burst=200ms:throttling ratio 降到 ≈ 1.2%,P99 降到 24ms,但邻居 Pod 的 P99 涨了 8ms(burst 被邻居感知了)
结论:burst=100ms 是延迟敏感服务的甜点——P99 从 52ms 降到 31ms(-40%),邻居几乎无感。burst 超过 200ms 后边际收益递减,但邻居代价线性增长。
K8s 1.30 起支持通过 Pod annotation io.kubernetes.cgroup-v2.cpu.max.burst 设置这个值(需要内核 ≥ 5.14),不用改 DaemonSet 或 cgroup 配置。
落地建议修正:在线延迟敏感服务,如果必须设 CPU limit(比如合规要求),优先用 limit=1C + burst=100ms 的组合,效果接近"不设 limit"但保留了硬上限。这比"limit=2x request"的拍脑袋折中更精确。
【想听大家怎么选】
几个具体问题想征集经验:
- 你们线上 K8s 集群(什么版本、cgroup v1 还是 v2)里,在线服务的 CPU limit 到底怎么定的?有没有"不设 limit"的先例?
- cgroup v2 的
cpu.max.burst你们用上了吗?burst 值设的多少?有没有遇到"burst 池被 GC 一次就吃光"的情况? - 不设 CPU limit 之后,你们怎么防"一个 Pod 吃满节点"?HPA 够不够?有没有用过 LimitRange 做兜底?
- 你们监控 throttling 的告警阈值设的多少?有没有被 throttling 坑过 P99 但一开始完全没往这个方向想的?
- 有没有用 coscheduling(Kueue / Volcano)来替代 limit 做资源隔离的?效果如何?
欢迎拍砖,尤其是"我踩过这个坑然后怎么解的"的真实案例。