K8s Java 容器 CPU 80% 但 p99 毛刺明显:CFS throttle 的隐性代价怎么量化?该看哪个指标判"真忙 vs 被节流"?
该问题的正文、回答与讨论。
背景
K8s 1.29 集群(cgroup v2),跑一套 Java 17 / Spring Boot 3 无状态服务,单容器(无 sidecar),容器 limit 4C / 8Gi。业务侧用 P99 延迟做 SLO,目前 P99 稳定在 200ms 附近,但每分钟有 1–3 次尖峰到 800ms+,毛刺肉眼可见。
现象
kubectl top pod显示 CPU 稳定在 3100–3400m(约 80% of 4C),看起来"还有 20% 余量"。cat /sys/fs/cgroup/cpu.stat看到nr_throttled在 1 分钟内增长约 40–80,throttled_time(ns)也在涨。container_cpu_cfs_throttled_periods_total(cAdvisor)的 delta 和nr_throttled基本对得上。- GC 日志:Young GC 频率正常(~2 次/分钟),但每次 YGC 的 pause 时间从 5ms 抖到 30–80ms。
- Tomcat
threadsCurrent峰值 180 / max 200,线程池没打满。
已尝试的排查与结果
| 动作 | 结果 |
|---|---|
| CPU limit 从 4C 提到 6C | p99 毛刺几乎没变(说明瓶颈可能不在 CFS quota 本身?还是 Java 线程池/GC 被 throttle 后"假性稳定"?) |
| 看 container_cpu_usage_seconds_total 的 delta | 和 kubectl top pod 读数一致,都是 ~3300m,看不出 throttle |
| 看 container_cpu_cfs_throttled_periods_total delta | 每分钟 ~50 个周期被 throttle,但 throttled_time 占比只有 2–3%,感觉"不算严重" |
| 开 -XX:+UseG1GC -XX:MaxGCPauseMillis=50 | YGC pause 从 30–80ms 压到 10–20ms,但 p99 毛刺依旧 |
我的困惑(期望得到的答案形态)
-
kubectl top pod/ cAdvisor 的 CPU 读数,在 CFS throttle 场景下,是"应用想跑的量"还是"实际跑完的量"? 我怀疑是后者——也就是说,应用想跑 5C 但被 throttle 到 4C,监控看到的是 4C,我误判"还有 20% 余量"。如果是这样,哪个指标能告诉我"应用想跑多少"?container_cpu_cfs_periods_total和container_cpu_cfs_throttled_periods_total的比值?还是有更直接的指标? -
判"真忙 vs 被节流"的正确指标组合是什么? 我目前在用
container_cpu_cfs_throttled_periods_total的 delta 做告警,但想确认:- 这个指标是不是唯一可信判据?
throttled_time占比 2–3% 算不算"严重"?有没有经验阈值?- 在 cgroup v2 下,
cpu.stat的字段和 cgroup v1 有差异吗?
-
Java 应用 + CFS throttle 的隐性代价:p99 毛刺是不是"被抹平了"? 我的猜测:JIT 编译线程、GC 线程被 throttle 后,JVM 内部排队变长,但外部看到的 p99 是"被 throttle 后延迟的均值",真实 p99 可能更高。我该怎么设计压测来暴露这个?具体建议:
- 压测工具(wrk / JMeter / Gatling)用哪种?
- 压测时同步采哪些指标(
cpu.statdelta、GC log、/proc/<pid>/status的voluntary_ctxt_switches)? - 怎么区分"毛刺来自 CFS throttle"和"毛刺来自 GC"?
参考
- 看到 @苏清和 提过 K8s 多容器 Pod 里 istio-proxy + log-agent 资源预算,同属容器资源治理,但我的场景是单容器 + CFS throttle 视角。
- @苏星河 的 K8s Java 17 容器 OOMKill cgroup v2 native 内存排查 提醒我 cgroup v2 下资源排查要更细致。
期望回答形态:给出 1–2 个"判 CFS throttle 是否为主因"的可执行判据(命令 + 阈值),以及一个最小化压测方案来暴露"被抹平的 p99"。