CPU limit 充足但 P99 仍抖 5 倍:nr_throttled 不涨时,「单次停顿时长分布」怎么采?有没有人最后定位到 cgroup v2 之外的内核侧静默杀手?
该问题的正文与回答。
背景
做了 15 年性能调优,最近在排查一个 K8s 1.29 集群(cgroup v2、内核 5.15.0-91、containerd 1.7)上的 Java 服务 P99 毛刺。
- 服务:JDK 17 + G1GC,单 Pod 单容器(无 sidecar),limit 4C / request 2C
- 稳态 CPU 使用:0.5–0.7C(远低于 limit,
cpu.stat里nr_throttled几乎不涨) - 现象:P99 每 2–3 分钟出现一次 400–700ms 尖峰,P50 稳定在 15ms;尖峰间隔不严格等间隔,但周期感明显
- 影响:SLO(P99 < 200ms, 99.9%)被击穿,告警频繁触发
已尝试的排查与结果
| 排查方向 | 操作 | 结果 |
|---------|------|------|
| GC | GC log + JFR | STW 最长 8ms,与 400ms+ 尖峰不对应 |
| CFS throttling | cpu.stat 的 nr_throttled / throttled_usec | 增量 <5/min,throttled_usec 增量可忽略 |
| cpufreq | cpupower frequency-info,已锁 performance governor | 主频稳定 2.4GHz,无降频 |
| THP | echo never > /sys/kernel/mm/transparent_hugepage/enabled | 已关闭,无改善 |
| IRQ 亲和性 | smp_affinity 绑定到非业务核 | 已做,无改善 |
| NUMA | numactl --cpunodebind --membind | 已做,无改善 |
| 内核定时器抖动 | cyclictest -p 99 -m -a <核> | 最大偏移 120μs,无法解释 400ms |
所有已知因素都排除了,毛刺依然在。
我目前的怀疑方向(想验证)
-
CFS 的 100ms period 粒度问题:
cpu.stat的nr_throttled和throttled_usec都是周期聚合值。一次 400ms 的停顿可能被拆成 4 个周期各 100ms 的 throttled,但nr_throttled只记了 4 次(而不是 1 次长停顿),throttled_usec记了 400ms(看起来"不严重")。真正决定 P99 的是单次停顿时长分布,而cpu.stat只给了聚合值,丢失了分布信息。 这是我最想验证的假设。 -
JDK G1 的 STW 阶段是否被 CFS 算进带宽:G1 的 mixed GC 在 STW 阶段所有线程停在 safepoint,但 GC 线程本身也在 cgroup 内,它的 CPU 时间是否计入
cpu.max的带宽计算?如果是,那么"看起来nr_throttled不涨"可能是因为 GC 线程的 CPU 时间被计入了,但业务线程的停顿时间没有被单独标记。 -
内核调度器的
deadline优先级:cgroup v2 的cpu.weight(默认 100)和cpu.max是两套独立机制。如果节点上其他 cgroup 的cpu.weight很高,CFS 的调度器可能在cpu.max不触发的情况下,仍然通过cpu.weight的权重分配让业务线程被"软抢占"——这种抢占不会体现在nr_throttled里,但会体现在 P99 里。
期望得到的答案形态
- 有没有人遇到过"limit 充足但 P99 仍抖"的场景? 最后定位到了哪个内核侧的"静默杀手"?用什么命令/指标抓到的?
- 单次停顿时长分布怎么采?
cpu.stat的聚合值不够用,你们是用 eBPF(比如perf trace钩cgroup_cputime_throttle)、还是perf sched、还是别的什么方法抓到的单次停顿分布? - G1 STW 和 CFS bandwidth 的耦合:有没有人验证过"GC 线程的 CPU 时间是否计入
cpu.max带宽"? cpu.weight的软抢占:有没有人观察到过cpu.max不触发但cpu.weight导致的调度延迟?
参考
- 社区已有 CFS throttling 的复盘([id=140]、[id=1481])和 cpufreq 漏调的排查([id=2390]),但都没有覆盖"limit 充足但毛刺依旧"这个矛盾场景。
- 我目前最缺的是单次停顿时长分布的采集方法,以及**
cpu.weight软抢占的可观测手段**。