结论先行:K8s 的
resources.limits管的是「带宽」(每 100ms 周期里最多给多少 ms),而 CPU 频率(主频)是节点级 cpufreq governor 决定的,容器层面根本看不到、管不到。一个 4C limit 的延迟敏感服务,在共享节点上被 DVFS 把主频从 2.4GHz 拉到 800MHz 时,cpu.stat的 throttled 计数、container_cpu_usage_seconds_total、top的 %CPU 全绿,但 P99 会翻 3 倍。这是 cgroup 语义里最后一块「limit 覆盖不到」的角落,和 CPU Manager 独占核、CFS burst 都不是一回事。
一、现象:CPU 利用率只有 30%,P99 却从 18ms 抖到 55ms
环境:
- 节点:4 台 RK3588(4×Cortex-A76 @ 2.4GHz + 4×A55),16GB LPDDR4X,内核 6.1.55(cgroup v2),K3s v1.28.9+k3s1,containerd 1.7.13
- 业务:边缘网关,C++ 单进程(协议解析 + 转发),部署为 Deployment 副本数 4,
requests=1C / limit=2C,QoS=Burstable - 稳态:P50 12ms,P99 18ms,CPU 利用率 25~35%(
container_cpu_usage_seconds_total / container_spec_cpu_quota) - 抖动:每周 2~3 次,每次 30s~2min,P99 飙到 50~70ms,同期节点 CPU 利用率只有 40~55%(
node_cpu_seconds_total算出来),没有任何容器触发 throttled
监控全绿但延迟翻 3 倍,这是我们一开始最迷惑的点。查了:
container_cpu_cfs_throttled_periods_total:0(没有 CFS 节流)container_cpu_cfs_throttled_seconds_total:0container_memory_working_set_bytes:平稳node_load1:0.8,远低于 4 核- 网络
node_network_receive_bytes_total:平稳
二、原理:cpufreq governor 是「节点级」的,容器没有自己的 governor
Linux 的 CPU 频率调节(DVFS)走的是 /sys/devices/system/cpu/cpufreq/policy<N>/,粒度是「频率策略组」,不是「cgroup」。一个节点上所有 cgroup(包括所有容器)共享同一套 governor 和 min/max_freq。
RK3588 出厂默认是 schedutil governor(内核 4.11+),它的行为是:
- 调度器每次 enqueue 一个可运行任务时,调
cpufreq_driver->set_target告诉 cpufreq「这个 cgroup 的 util 是多少」; - cpufreq 根据当前所有 cgroup 的 util 加权,选一个满足所有 cgroup 的频率;
- 如果节点整体 util 低(比如只有 40%),cpufreq 会把频率降到 1.2GHz 甚至更低,以省电;
- 频率升上去有延迟(
transition_latency通常 50~300μs,但实际受 firmware 响应影响可能 ms 级),降下来很快。
关键机制:cpu.max 限的是「每 100ms 周期里这个 cgroup 能跑多少 ms」,不限制「跑的时候跑多快」。一个 cgroup 即使被允许跑满 2C,如果节点 cpufreq 把频率从 2.4GHz 拉到 800MHz,它的实际算力只有 1/3——但 container_cpu_usage_seconds_total 只累计「墙钟时间 × CPU 核数」,不除以频率,所以监控上完全看不出频率被拉低。
这就是「CPU 全绿但延迟翻 3 倍」的根因:监控采的是「用了多少 CPU 时间」,不是「每单位 CPU 时间干了多少活」。
三、具体做法:uclamp_min + performance governor 的两层解
3.1 短期:把节点 governor 切到 performance(治标)
# 查看所有 policy
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
# 输出:schedutil
# 切到 performance(锁最高频)
for p in /sys/devices/system/cpu/cpufreq/policy*; do
echo performance > $p/scaling_governor
done
# 验证
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
# 输出:performance
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq
# 输出:2400000(2.4GHz,稳定)
效果:P99 从 55ms 回落到 19ms,抖动消失。但代价是节点功耗从 8W 涨到 14W(RK3588 在 2.4GHz performance 下),边缘场景如果 24h 在线、电池供电,这个代价不可接受。
3.2 中期:uclamp_min 给延迟服务一个「频率下限」(治本)
内核 4.15 引入的 util_clamp(CONFIG_CPU_IDLE + CONFIG_ARM64_VA_BITS_48 相关),允许每个 cgroup 声明一个最低频率下限:
# 给某个 cgroup 设 uclamp_min(0-1024 范围,1024=最高频)
# 假设我们的网关 cgroup 路径是 /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/...
CGROUP=/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<UID>.slice/cri-containerd-<ID>.scope
# 设 uclamp_min = 800(≈78% 最高频,即 1.87GHz 下限)
echo 800 > $CGROUP/cpu.uclamp.min
# 验证
cat $CGROUP/cpu.uclamp.min
# 输出:800
效果:
- 节点空闲时 cpufreq 可以降到 800MHz(省电);
- 但只要这个 cgroup 里有可运行任务,频率就被钳在 ≥1.87GHz,P99 稳定在 22ms;
- 功耗从 14W 回落到 9W(空闲时降频,负载时抬频)。
注意:uclamp 是内核 4.15+ 才有的,RK3588 的 6.1 内核完全支持;但 x86 上很多服务器 BIOS 默认锁最高频,uclamp 基本不起作用(因为 firmware 不响应)。uclamp 主要在 ARM/边缘 SoC 上有意义。
3.3 K8s 里怎么落地:DaemonSet + privileged
cgroup 的 cpu.uclamp.min 不是 K8s 标准字段(截至 1.28.9,resources 只有 requests/limits,没有 uclamp 字段)。要落地只能走节点侧:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: cpufreq-keeper
namespace: kube-system
spec:
selector:
matchLabels:
app: cpufreq-keeper
template:
metadata:
labels:
app: cpufreq-keeper
spec:
priorityClassName: system-node-critical
containers:
- name: keeper
image: alpine:3.19
securityContext:
privileged: true
runAsUser: 0
command: ["/bin/sh", "-c"]
args:
- |
# 启动时切到 schedutil(省电)
for p in /sys/devices/system/cpu/cpufreq/policy*; do
echo schedutil > $p/scaling_governor
done
# 监听 cgroup 变化,给新起的延迟敏感容器设 uclamp_min
# 实际生产建议用 inotifywait 监听 kubepods.slice 目录
# 这里简化为循环扫描
while true; do
for c in /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/*/*/*; do
[ -f "$c/cpu.uclamp.min" ] && [ "$(cat $c/cpu.uclamp.min)" = "0" ] && \
grep -q "my-latency-service" "$c/cgroup.procs" 2>/dev/null && \
echo 800 > "$c/cpu.uclamp.min"
done
sleep 5
done
volumeMounts:
- name: cpufreq
mountPath: /sys/devices/system/cpu/cpufreq
- name: cgroup
mountPath: /sys/fs/cgroup
volumes:
- name: cpufreq
hostPath:
path: /sys/devices/system/cpu/cpufreq
- name: cgroup
hostPath:
path: /sys/fs/cgroup
生产建议:
- 用 label 选择器(
my-latency-service)而不是进程名匹配,K8s 原生 label 更稳; - inotify 替代 5s 轮询,容器起停时立即生效;
- 给 DaemonSet 加
nodeSelector只跑在 RK3588 节点上,x86 节点不需要。
四、踩过的坑(4 个反直觉点)
坑 1:cpu.max 和 uclamp 是正交的两个维度,别混着调。
我们一开始把 cpu.max 从 2C 抬到 4C,P99 只降了 15%——因为瓶颈不是带宽,是频率。cpu.max 管「能用多少核」,uclamp 管「用的时候跑多快」,两个旋钮要分开调。
坑 2:schedutil 的「抬频延迟」在 RK3588 上比 datasheet 标的大 5 倍。
datasheet 标 transition_latency=50μs,实测从 800MHz 升到 2.4GHz 要 250μs~1ms(firmware 握手慢)。这意味着短突发(<1ms)根本抬不上频率,服务会直接跑在低频上。uclamp_min 的价值就在这里——不让频率掉下来,而不是等它升上去。
坑 3:scaling_min_freq 和 uclamp_min 不是一回事。
scaling_min_freq 是「governor 允许的最低频率」(全局),uclamp_min 是「这个 cgroup 要求的最低频率」(cgroup 级)。前者是 governor 的约束,后者是 cgroup 的声明。如果 scaling_min_freq=800MHz 但 uclamp_min=800/1024≈1.87GHz,cgroup 的声明会覆盖全局的最低频——这是 uclamp 的设计意图。
坑 4:x86 服务器上 uclamp 基本无效。
我们一台 Intel Xeon 节点试了同样的配置,uclamp_min=800 写进去了,但 scaling_cur_freq 纹丝不动停在 2.2GHz(BIOS 锁频)。uclamp 只在 firmware 响应 DVFS 的 SoC(ARM/RISC-V)上有意义,x86 服务器基本用不到。
五、结论与可复用清单
核心结论:
cpu.max是带宽上限,不是频率保障——cgroup 的算力 = 核数 × 频率,K8s 只控了前者;- 边缘 SoC(ARM/RISC-V)上,DVFS 是延迟敏感服务最容易被忽视的延迟源,且监控全绿(因为
container_cpu_usage不除以频率); uclamp_min是内核 4.15+ 给的「cgroup 级频率下限」,是容器里唯一能声明频率诉求的机制,但 K8s 1.28.9 没有原生字段,要走节点侧 DaemonSet 落地。
可复用清单:
- [ ] 延迟敏感服务上 SoC 节点前,先
cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor确认 governor; - [ ] 监控加一条:
node_cpu_scaling_frequency(/proc/cpuinfo或cpupower frequency-info),和container_cpu_usage一起看; - [ ] 抖动排查 SOP:CPU 全绿 + P99 抖 → 先看
scaling_cur_freq是不是被 DVFS 拉低 → 再查uclamp_min有没有设; - [ ] ARM SoC 节点给延迟服务设
uclamp_min=800(78% 最高频),功耗代价约 +1W; - [ ] x86 服务器忽略 uclamp,走 CPU Manager static 独占核(见引用 1)。
边界与不适用:
- 本文场景是「共享节点 + 延迟敏感服务」,如果服务独占节点且 CPU 利用率 >70%,DVFS 通常不会降频(util 高会触发抬频),uclamp 收益很小;
- 如果服务是批处理(不关心 P99),保持
schedutil默认即可,uclamp 反而浪费功耗; - K8s 1.30+ 的
cpuManagerPolicy: static+node.kubernetes.io/compact-cpuset是另一条路(独占核),和 uclamp 不冲突,可以叠加用。
与相关工作的关系:
- 本文讲的是「频率」维度,和引用 1(CPU Manager 独占核)的「核」维度、引用 2(CFS quota)的「带宽」维度是三个正交旋钮,三者要分开调;
- 如果节点上多个容器都要高频率,uclamp 会「就高不就低」(取所有 cgroup 声明的最大值),这是内核语义,不是 bug。