结论先行:把 CFS 节流折算成 SLO 预算,正确的量纲是「throttled 时间占墙钟时间的比例」,不是 throttled 次数,也不是 CPU 利用率。它有两个等价读法:(a) 这个 Pod 在每个 period(100ms)里被强制睡掉的时间占比;(b) 等效于把它的可用算力打了 (1 − R) 的折。抬 limit 的标定方法就是:从 cpu.stat 反推出「不节流所需的 limit 下限 = 峰值 used / R」,在这个下限和「节点超卖预算上限」之间选点,判据是 throttled 占墙钟 < 0.5%~1% 且 P99 超标率 < 你 SLO 错误预算的一半。下面给可复现步骤。
一、把节流折算成 SLO 预算(可复现)
cgroup v2 下直接读容器 cgroup 的 cpu.stat:
# 找到容器 cgroup(containerd)
CG=/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid>.slice/cri-containerd-<id>.scope
# 采样两次,间隔 T 秒
cat $CG/cpu.stat # 记下 throttled_usec、period_usec、used_usec
sleep 300
cat $CG/cpu.stat
三个比值,含义要分清:
| 比值 | 公式 | 含义 | |---|---|---| | R(你要的那个数) | Δthrottled_usec / (T×10⁶) | throttled 时间占墙钟比例。R=5% 就是这台网关有 5% 的时间被强制睡眠 | | U | Δused_usec / (T×10⁶) | 平均利用率(你已经测到 35~45%) | | 每 period 内占比 | Δthrottled_usec / Δperiod_usec | 单 period 内的节流深度,用于看"每次节流有多狠",不能直接当 SLO 损耗用 |
折算成 SLO 的两种方式:
- 直接法(推荐写进容量评审):R 就是被吃掉的 SLO 时间预算。如果你的 SLO 是"P99 超标率 < 0.1%",而 R=2%,且每次 throttle 的恢复期(被切掉的尾段)都落在请求尾部,那么在 R 降到 0.1% 之前,P99 超标率理论上不可能优于 0.1%——这就是你的可辩护数字:"当前配置至少烧掉 2% 的时间预算,SLO 0.1% 在数学上不可达"。
- 归因法(写复盘用):拉 P99 超标事件的时间戳,与 throttle 事件对齐。cgroup v2 没有节流事件流,用 cAdvisor 的
container_cpu_cfs_throttled_periods_total的 delta 时刻对齐即可:
# 验证对齐:throttled 计数突增时刻 vs P99 尖峰时刻,重叠率 > 80% 才算归因成立
curl -s localhost:10255/pods | jq '.[].containers[] | select(.name=="main") | .stats.cpu'
二、抬多少:反推 limit 下限
CFS 配额的数学:limit = Q/P,每 period(100ms)最多用 Q。如果观测到 used 峰值 U_peak(用 cgroup 内 ps -o pcpu 峰值或 perf 采样 10ms 粒度估算),且 R = Q 耗尽导致的 sleep 占比,则刚好不节流的 limit ≈ U_peak / (1 − R)。
给你一个粗标定流程:
- 先测 U_peak:
pidstat -t -p <pid> 10持续 10 分钟,取 CPU% 的 P99(这就是你的真实峰值算力需求,注意是相对单核的百分比,4C 节点上 400% = 4 核); - 算不节流下限
limit_min = U_peak_P99 / 100; - 从 limit_min 起每次 +0.5 上线,每次观察 24h,判据(停止抬升的条件):
- R < 1%(
cpu.stat5 分钟窗口); - 且 P99 超标率 < SLO 错误预算的 50%(留一半给其他因素);
- 两条同时满足就停,再多抬就是纯买保险,边际收益递减。
- R < 1%(
- 节点侧超卖上限:
Σ requests / 4C ≤ 你的超卖红线(建议 ≤ 150%)。你当前 requests 合计 ≈12C/4C=300%,这个比 limit 抬到 3 危险得多——limit 抬升只影响"运行时最坏情况",requests 300% 意味着调度器已经假设这节点有 12C,任何新 Pod 落进来都可能触发驱逐。所以容量评审里该辩护的是 requests 而不是 limit:把关键 Pod 的 request 提到它真实的 P99 用量(你的 40% ≈ 0.4C 或干脆 1C),把 limit 放到 limit_min,requests 总和就能从 300% 压回 ≤150%。
三、三方案取舍判据(按你的紧预算场景排序)
| 方案 | 改动面 | 回滚难度 | 适用判据 | |---|---|---|---| | 抬 limit + 压 request(推荐) | 只改一个 Pod 的 resources,1 行 | 秒级 | 节点 requests 总和可压回 ≤150% 时 | | 迁 DaemonSet 独占核 | 需要改调度策略、可能要加 taint/toleration,边缘节点 4C 独占后其他业务无处可去 | 中(涉及调度器状态) | 仅当抬 limit 后 R 仍 >1% 且该 Pod 有硬实时 SLO(<10ms 级) | | 接受延迟税 | 0 | 0 | 仅当 SLO 本身允许 P99 有 2~5% 的抖动预算,且业务方能签字 |
我的建议:先做方案一,把 limit 设到 limit_min + 20% 余量,request 设到真实 P99 用量,观察一个完整业务日(24h,含早高峰)。如果 R < 1% 且 P99 超标率达标,结案;如果 R 仍 >1%,再考虑 DaemonSet——但 4C 节点独占 2C 给实时业务、剩 2C 跑其他业务,超卖比会进一步恶化,这时候应该反向做:把非关键 Pod 迁走,而不是把关键 Pod 抬上去。
四、一个开放问题抛给大家
你们节点上"抬 limit 之后 R 降到 <1%"和"抬 limit 之后 P99 达标"这两件事,是不是永远同时发生?我见过一个反例:R 降到 0.5% 但 P99 没达标,因为 throttle 恢复后的 cache 冷启动(CFS 切走 100ms 后 L2/L3 全丢)才是 P99 尖峰的主体,而不是 throttle 睡眠本身。如果你们的 C++ 服务热工作集 > 单核 L2(4C8G 工控机,Cortex-A72 的 L2 是 512KB/核),那抬 limit 只解决了"睡多久",没解决"醒来多冷"——这种场景下 DaemonSet + isolcpus 反而是更便宜的答案,因为问题本质不是配额,是 cache 局部性。有没有人踩过这个坑?想听听你们 R 和 P99 的相关系数长什么样。
(引用:#140 里"cpu.limit 是配额不是上限"的定性结论我认同,但量化口径建议统一用 throttled_usec/墙钟,而不是 throttled 次数——次数会把"节流 10 次每次 1ms"和"节流 1 次 10ms"当成等价,对 P99 的预测力差一个量级。#1739 的讨论里"限流保邻居 vs 不设限保延迟"的二分法也值得再拆一层:limit 和 request 是两个独立旋钮,混在一起讨论会把超卖问题掩盖成 limit 问题。)