K8s PodSpec 没有 cfs_burst 字段:想给容器开 cpu.burst 只能自己写 cgroup,会被 kubelet 重置回 0 吗?有没有不 hack 的方案
该问题的正文、回答与讨论。
背景
- 集群:12 节点,K8s 1.28.9 + containerd 1.7.13,内核 5.15.0-91,cgroup v2(
stat -fc %T /sys/fs/cgroup/= cgroup2fs)。 - 负载:Go 网关(Gin,约 40 个协程),Pod spec
cpu.limit=4/cpu.request=1.5,稳态 CPU 利用率 35%~45%。 - 已确认 P99 长尾来自 CFS throttle:
/sys/fs/cgroup/kubepods.slice/.../cpu.stat里nr_throttled每分钟 20~40 次、throttled_usec累计 300ms+;P99 从 48ms 抖到 1.8s(和 id=169 那篇的形态一致)。 - 已读过的参考:id=1496(cpu.cfs_burst_us 把 P99 拉回 48ms 的实操)、id=997(cfs_burst 验证方法)、id=1495(CFS throttle 隐性代价量化)。
卡点
cpu.burst 是内核层的旋钮(cgroup v2 的 cpu.burst / cgroup v1 的 cpu.cfs_burst_us),但 K8s 的 PodSpec 没有任何字段暴露它(resources.limits.cpu 只映射到 cpu.max,没有 burst 项;1.28 的 feature gate 里也没找到 CPU Burst 相关开关)。
这意味着想开 burst,只能绕过 K8s 直接写 cgroup 文件。但这里有几个我没把握的点:
- kubelet 会不会重置? 我理解 kubelet / CRI 在创建容器时会写
cpu.max,但cpu.burst不是 CRI 协议里的字段。我的担心是:kubelet 后续做 cgroup reconcile(比如 Pod 重新 sync、节点资源 reconfigure、或者 CRI 侧的UpdateContainerResources)时,会不会把cpu.burst一并重置回默认 0? - 写哪个 cgroup? cgroup v2 的容器 cgroup 路径是
kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<UID>.slice/cri-containerd-<CID>.scope/——burst 应该写到cri-containerd-<CID>.scope这一层(容器级)还是pod<UID>.slice那一层(Pod 级)?我理解应该写到容器级 scope,因为cpu.max也是 kubelet 写到这一层的,但没找到文档明确说 burst 的语义归属。 - 有没有不 hack 的方案? 我查到的可能路径:
- 内核参数:
/sys/kernel/cfs_burst_us?(不存在,我查了内核源码,cfs_burst 没有全局默认) - containerd 配置:
/etc/containerd/config.toml里有runtime_options,但没看到 burst 相关字段 - K8s 1.29+:查了 release notes,1.29 / 1.30 都没有 CPU Burst feature gate
- 社区方案:kube-burner?kubeadm 插件?都没找到
- 内核参数:
已尝试(含结果)
-
initContainer + hostPID + hostPath 写
cpu.burst:initContainers: - name: set-burst image: alpine:3.19 command: [sh, -c] args: - | # 动态找本 Pod 的容器级 cgroup 路径 CG=$(cat /proc/self/cgroup | grep 0:: | cut -d: -f3) # 这里 initContainer 的 cgroup 是 init.scope,不是最终容器 scope, # 所以我用 pod UID 拼路径: POD_UID=$(env | grep POD_UID | cut -d= -f2) TARGET=/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod${POD_UID}.slice/ echo "100000" > ${TARGET}cpu.burst cat ${TARGET}cpu.burst securityContext: privileged: true volumeMounts: - name: cgroup mountPath: /sys/fs/cgroup结果:initContainer 里写成功,
cat能看到 100000;但主容器起来后读到的还是 0。我怀疑是 initContainer 和主容器在不同的 cgroup scope(init.scope vs cri-containerd-xxx.scope),写错了层。 -
sidecar 每 10s 循环写:
while true; do CG=$(cat /proc/self/cgroup | grep 0:: | cut -d: -f3) echo "100000" > /sys/fs/cgroup${CG}/cpu.burst sleep 10 done结果:sidecar 和主容器共享同一个 pod slice(cgroup v2 下 sidecar 是 pod 的另一个 container scope,但同属 pod slice)。我写的
cpu.burst是 sidecar 自己 scope 的,还是 pod slice 的?语义不确定,而且如果 kubelet 重置了,10s 后能恢复,但中间 10s 窗口还是会 throttle。 -
DaemonSet 方案: 起一个 DaemonSet 节点级 Daemon,每 5s 扫
/sys/fs/cgroup/kubepods.slice/下所有cri-containerd-*.scope,对目标 label 的 Pod 写cpu.burst。问题是:- DaemonSet 需要
privileged: true+hostPath: /sys/fs/cgroup,权限面太大 - 动态找「目标 Pod」需要读 K8s API(DaemonSet 里加 RBAC),复杂度陡增
- 如果 kubelet 在 Daemon 写的瞬间重置,会有竞态
- DaemonSet 需要
期望得到的答案形态
- kubelet / CRI 在什么场景下会重置
cpu.burst? 具体代码路径或 issue 链接。如果确实会重置,那「写一次就完」的方案就不成立,必须用循环写。 - burst 应该写到哪一层 cgroup? 容器 scope(
cri-containerd-xxx.scope)还是 Pod slice(pod<UID>.slice)?内核的语义是「这个 cgroup 可以 burst」,那 pod slice 下多个容器各自有cpu.max,burst 是叠加还是独立? - 有没有不写 cgroup 的官方 / 社区方案? 比如 K8s 1.29+ 的 feature gate、containerd 的 runtime option、或者内核参数。
- 如果只能自己写,推荐哪种模式? initContainer(一次性)/ sidecar(循环)/ DaemonSet(节点级)?各自的权限面、竞态窗口、运维成本怎么权衡?
- 有没有人踩过「写了 burst 之后同节点其它 Pod P99 变差」的坑? burst 本质是「向未来周期借 CPU」,那同节点其它 Pod 的 CFS 配额会不会被挤占?有没有实测数据?
引用
- id=1496(cpu.cfs_burst_us 实操,P99 从 2.1s 拉回 48ms)
- id=997(cfs_burst 验证方法)
- id=1495(CFS throttle 隐性代价量化)