先给结论:你的场景(平时 QPS 低、整点冲 800 QPS 持续 3~5 秒、CPU 峰值 1.9 没到 limit 2)正是 cpu.burst 的主场,而不是调大 limit——因为你平时利用率低、攒下了"借时"额度,突发时正好花掉。调大 limit 只是把"突发"变成"常态高水位",整点全集群一起涨时会把节点打满,是更贵的解法。下面给一套可贴工单的排查 + 验证清单。
一、怎么算 throttling 占比(对齐 P99 时间线)
cgroup v2 的接口是 /sys/fs/cgroup/<容器路径>/cpu.stat(注意不是 cgroup v1 的 cpu.cfs_quota)。关键字段:
usage_usec:本 cgroup 累计消耗的 CPU 时间(微秒)nr_throttled:被节流的周期次数throttled_usec:累计被节流的时长(微秒)
占比公式(必须用两次采样的差值,绝对值没意义):
throttling_ratio = Δthrottled_usec / Δusage_usec
采样脚本(1Hz,落到带时间戳的日志,方便和 P99 曲线叠图):
CG=/sys/fs/cgroup/$(systemd-cgls --no-pager | awk '/your-pod/{print $NF}')
prev=0; prevt=$(date +%s.%N)
while true; do
u=$(awk '/usage_usec/{print $2}' $CG/cpu.stat)
t=$(awk '/throttled_usec/{print $2}' $CG/cpu.stat)
n=$(awk '/nr_throttled/{print $2}' $CG/cpu.stat)
now=$(date +%s.%N)
echo "$(date +%H:%M:%S) d_usage=$((u-prev)) d_throttled=$((t-prev)) ratio=$(awk "BEGIN{printf \"%.4f\", ($t-$prevt)*0; ($t-prevt)*0 + ($t-prevt)/(($u-prev)) }") nr=$n"
prev=$u; prevt=$now
sleep 1
done
(上面 ratio 那行 awk 写法略绕,实际里直接用 awk "BEGIN{printf \"%.4f\", ($t-prev)/($u-prev)}" 即可。)判据:整点窗口里 d_throttled 从平时的 ~0 跳到每周期几十万微秒、ratio 从 <1% 飙到 >10%,且时间戳和你 P99 毛刺窗口重合——就能在工单里坐实"是 CFS 节流不是 GC/下游"。
二、cpu.burst 的正确写法与验证
- 文件名是
cpu.burst(cgroup v2),单位微秒,内核上限 100000(即 100ms),写超过会被截断。设法:
echo 100000 > $CG/cpu.burst
- 语义(这是你设 5 倍还觉得没用的关键):burst 是"跨周期借时"——只有当你在之前的周期里没用满 quota、攒下了空闲额度,突发时才能花掉这份 credit。所以:
- 平时利用率低(你 CPU 峰值 1.9/2、日常更低)→ 攒额度快 → burst 有效 ✅
- 如果服务持续贴着 limit 跑,没有可借的额度,burst 一个微秒都不起作用 ❌ 你之前"设了 burst 但毛刺还在",大概率是整点那 3~5 秒里额度还没攒够、或者被同节点其他容器抢走。
- 验证是否真生效:开 burst 后再跑整点任务,看两点——①
nr_throttled的增速是否明显放缓;②d_throttled峰值是否从几十万微秒降到几万以下。若nr_throttled照涨,说明额度不够,要么把 quota 提到 2.5~3(给突发留真实余量),要么错峰整点任务(见下)。
三、limit vs burst 的判断标准(可直接抄)
| 场景特征 | 选哪个 | |---|---| | 平均利用率 < ~50% of limit,且突发 < 5~10 秒 | cpu.burst(100ms 上限内),最省 | | 突发持续 > 30 秒、或平均利用率 > 70% | 调大 limit(burst 攒不够额度) | | 全集群整点齐涨、节点会被打满 | 都不解决根因——错峰调度(把整点任务打散到不同分钟)+ 节点级 CPU 预留 |
副作用:burst 会削弱"公平性",同一节点上 burst 容器会挤占其他 Pod 的时间片,所以只给短时突发型服务开,别给常驻高负载服务开。
四、Go 侧"被 CFS 切走"的更早信号
比 GC 日志更早的判据是调度延迟(CFS 把线程切走时,runnable goroutine 会排队等待):
go tool trace <binary> # 跑整点窗口,看 "Go schedule" 和 "Thread blocked" 是否随 P99 同涨
或用 Go 1.21+ 的 runtime/metrics 直接拉指标(比 trace 轻):
import _ "runtime/metrics"
// 关注: /sched/latencies:avg (goroutine 排队延迟,纳秒)
// /cpu/.../usage:total 与 wall time 对比
当 /sched/latencies:avg 在整点窗口从 ~0 跳到毫秒级、而 GC 日志没动——就是线程被内核 CFS 切走的铁证,比"看 CPU 没到 limit"更直接。
五、给你的落地顺序(3 步)
- 先跑第一节的采样脚本,把
d_throttled时间线钉进工单(证据链); echo 100000 > cpu.burst,重放整点任务,看nr_throttled增速是否下降——有效就收工;- 若仍被节流,把 limit 提到 2.5 + 整点任务错峰 1~2 分钟,二者组合,别单靠拉高 limit。
一句话:你这个"平时闲、整点爆"的形态,是 cpu.burst 的教科书场景;先用 cpu.stat 差值坐实节流,再用 burst 验证,最后才谈 limit。