Cgroup v2 下 CPU 限制的新姿势:深度解析 cpu.max 与 v1 cfs_quota_us 的内核级差异与 CPU Burst
在容器化时代,Kubernetes 用户经常面临一个诡异的性能难题:服务平均 CPU 利用率并不高(比如仅为 30%),但接口的 P99 延时却偶尔飙高,伴随着容器 CPU Throttling(限流)指标的激增。
这种“微观限流”现象,其根源在于 Linux Cgroup v1 的 CFS(Completely Fair Scheduler)带宽控制机制。随着 Cgroup v2 逐渐在主流 Linux 发行版及 Kubernetes(1.25+ 默认推荐)中普及,CPU 的资源限制迎来了一套全新的设计。
本文将从 Linux 内核源码与调度器原理出发,深度对比 Cgroup v1 的 cpu.cfs_quota_us / cpu.cfs_period_us 与 Cgroup v2 的 cpu.max,并剖析彻底治愈限流痛点的“新玩法”—— CPU Burst(CPU 突发负载)机制。
一、 Cgroup v1 的微观痛点:硬性的“割裂窗口”
在 Cgroup v1 中,我们通过两个文件来限制容器的 CPU 使用上限:
cpu.cfs_period_us:时间周期(通常默认是 100,000 微秒,即 100ms)。cpu.cfs_quota_us:在上述周期内,该控制组允许消耗的 CPU 总时间。
例如,若设置 period=100ms,quota=200ms,则代表该容器最多可以使用 2 个 CPU 核心的算力。
1. CFS 周期限流的缺陷
CFS 调度器使用了一种“攒代币”再消费的机制。每个周期开始时,内核分配给该 cgroup 相应的 quota。一旦其下属所有线程消耗光了这些 quota,该 cgroup 就会被加入到放电队列(throttled queue)中,直到下一个 100ms 周期到来,内核调用 reclaim_cfs_bandwidth() 重新发放 quota,这些线程才会被唤醒。
这种机制在面对**突发流量(Bursty Workload)**时,会产生极严重的性能瓶颈。
时间轴 (100ms 周期)
|-------------------|-------------------|
| ■■■■■■■■ (Throttled)| ■■■■■■■■ (Throttled)|
20ms 内耗尽 quota 20ms 内耗尽 quota
后 80ms 线程挂起 后 80ms 线程挂起
如上图所示,一个多线程应用(例如 Java Spring Boot 或 Go 微服务)在接收到请求时,多个线程会瞬间并发在极短时间(如 20ms)内榨干 100ms 周期内的所有 quota。在接下来的 80ms 内,所有线程被内核强制挂起。
这对于用户端而言,就表现为原本几毫秒就能响应的接口,耗时突然变成了 80ms+。
2. 内核锁竞争与多文件碎片化
在内核实现层面,Cgroup v1 的 cpu、cpuacct、cpuset 是三个相互独立的子系统(Subsystem)。
当编排工具(如 Kubelet)需要限制 CPU 时,必须分别往 /sys/fs/cgroup/cpu/... 和 /sys/fs/cgroup/cpuacct/... 写入数据。
由于多个子系统目录结构不统一,内核在维护这些 cgroup 树时,需要频繁地在多个不同的数据结构间同步状态,产生不必要的锁竞争(如 cfs_b->lock 竞争)。
二、 Cgroup v2 的统一优雅设计:cpu.max
Cgroup v2 彻底废弃了多挂载点的混乱设计,采用了统一层级拓扑(Unified Hierarchy)。所有的控制器(CPU、Memory、IO 等)都绑定在同一个进程树上。
1. 语法简化与原子性
在 Cgroup v2 中,v1 的两个 CPU 限制文件被合并为了一个单一的文件:cpu.max。
其内容格式极为直观:
$ cat /sys/fs/cgroup/system.slice/container.service/cpu.max
max 100000
该文件包含两个值:quota 和 period。
- 默认值为
max 100000,表示不设上限,周期为 100ms。 - 如果我们要限制该 cgroup 最多使用 2.5 核,只需将其改写为:
$ echo "250000 100000" > /sys/fs/cgroup/system.slice/container.service/cpu.max
内核改进:在 Cgroup v1 中,修改 quota 和 period 需要分别写入两个文件,这属于非原子操作。在配置高负载容器时,极易由于写入先后顺序导致短暂的配置失衡,甚至引发内核报错。而在 Cgroup v2 中,通过一次 write 系统调用传入两个参数,内核可以原子性(Atomic)地更新 tg->cfs_bandwidth 结构体,避免了瞬态异常。
2. 共享线程组管理优化
Cgroup v2 引入了更合理的 cgroup.type 线程管理模式。在 v1 中,由于可以把同一进程下的不同线程(TID)分发到不同的 CPU cgroup 中,导致内核在处理进程组资源汇报时,产生大量的垃圾回收开销。
Cgroup v2 默认强化了“进程粒度”的资源隔离。除非显式启用 threaded 模式,否则同一进程下的所有线程必须属于同一个 cgroup。这极大简化了 CFS 调度器在统计 cfs_rq(CFS 运行队列)运行时间时的开销,提升了 CPU 限制的精确度。
三、 终极玩法:Cgroup v2 下的 CPU Burst(内核级解法)
仅仅是合并文件和统一层级,并没有解决“20ms 耗尽 quota,剩余 80ms 被限流”的本质矛盾。为此,Linux 内核自 5.14 版本起(并在部分主流 Linux 发行版如 RHEL 8.x/9.x、Alibaba Cloud Linux 中被向后移植),在 Cgroup v2 下引入了 CPU Burst 机制。
1. 工作原理:带宽代币的“跨周期储蓄”
CPU Burst 允许一个 cgroup 将上一个周期未用完的 quota 累积起来,并在当前周期遇到突发流量时,临时超额使用。
在 Cgroup v2 中,除了 cpu.max,还多了一个新文件:cpu.max.burst。
cpu.max设定基础额度,例如100000 100000(1核)。cpu.max.burst设定允许的最大突发额度,例如50000(0.5核)。
周期 1 (空闲) 周期 2 (突发流量)
+-------------------+ +-----------------------+
| 仅消耗 20ms | | 基础额度 100ms |
| 剩余 80ms 存入 | | + 突发额度 50ms (取自周期1) |
| 缓冲区 (上限 50ms)| | |
+-------------------+ +-----------------------+
| 实际可用最大 150ms |
| 消除 Throttling! |
当周期 1 结束时,容器实际只运行了 20ms。在没有 Burst 机制时,剩下的 80ms quota 会直接作废。
而在 Cgroup v2 启用了 CPU Burst 后,这 80ms 里的最多 50ms(受 cpu.max.burst 限制)会被存入“缓冲区”。
到了周期 2,容器突然遭遇高并发,需要 140ms 的计算时间。此时,内核允许其消耗 100ms(基础额度)+ 40ms(从缓冲区借用)。容器顺利跑完所有任务,完全没有发生任何限流!
2. 内核源码级的数学公式
在 Linux 内核调度器源码 kernel/sched/fair.c 中,CFS 带宽控制的核心逻辑如下:
// 伪代码:内核计算 cgroup 可用带宽
void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b)
{
u64 now;
u64 overrun;
...
// 计算当前周期距离上次 refill 过了几个 period
overrun = n_periods;
// v1 经典逻辑:直接重置为 quota 设定值
// cfs_b->runtime = cfs_b->quota;
// v2 CPU Burst 逻辑:允许累加未消耗的部分,但不能超过最大限制 (quota + burst)
u64 max_buffer = cfs_b->quota + cfs_b->burst;
u64 new_runtime = cfs_b->runtime + overrun * cfs_b->quota;
if (new_runtime > max_buffer) {
cfs_b->runtime = max_buffer;
} else {
cfs_b->runtime = new_runtime;
}
}
通过这一重构,内核既保证了 cgroup 在长期统计学意义上的 CPU 占用率不会超过设定上限,又完美解决了微观时间片内因为瞬时毛刺而导致的被动限流。
四、 如何在 Kubernetes / 容器中开启新玩法?
要享受 Cgroup v2 与 CPU Burst 带来的红利,你需要满足以下基础环境要求:
- Linux 内核:>= 5.14(或使用支持该特性的企业级内核)
- 容器运行时:Containerd >= 1.6 或 Docker >= 20.10(需启用 Systemd Cgroup 驱动)
- Kubernetes:>= 1.25+ 默认支持 Cgroup v2。并且可以通过 Kubelet 的 Feature Gate 启用 CPU Burst。
1. 手动验证 Cgroup v2
在挂载了 v2 的系统上,cgroup 根路径通常在 /sys/fs/cgroup/,且该目录下没有 cpu,cpuacct,memory 等独立的子目录,只有一个统一的进程树。
# 查看是否为 cgroup v2
$ stat -f /sys/fs/cgroup
File: "/sys/fs/cgroup"
ID: 0 Namelen: 255 Type: cgroup2fs # 确认是 cgroup2fs
2. Kubernetes 下启用 CPU Burst
从 Kubernetes 1.22 开始,Alpha 阶段引入了对 CPU Burst 的支持;在 1.26 之后该特性已非常稳定。
你需要在 Kubelet 配置文件(通常是 /var/lib/kubelet/config.yaml)中开启 Feature Gate CPUAllocator 与 CPUBurst:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
CPUBurst: true
保存并重启 Kubelet。此后,当你在 Pod 中配置:
resources:
limits:
cpu: "2"
requests:
cpu: "1"
Kubelet 会自动根据算法计算出一个合理的 burst 值(通常为 limit 对应 quota 的一定比例,默认基于内核推荐公式计算),并写入到该 Pod 对应 cgroup 的 cpu.max.burst 文件中。
3. 生产环境收益实测
根据行业内(如阿里巴巴、字节跳动等)在大规模生产环境中的实践数据表明:
- 延迟指标改善:在启用 Cgroup v2 CPU Burst 后,高并发微服务(如 Spring Cloud、Node.js 异步应用)的 P99 延迟通常能降低 50% 到 80%。
- Throttling 归零:原先频繁出现的
container_cpu_cfs_throttled_periods_total增长趋势几乎抹平,运维团队不再需要为了消除限流警告而盲目地成倍放大容器的 CPU Limits,大幅节约了集群的整体物理资源。
五、 总结与选型建议
| 维度 | Cgroup v1 | Cgroup v2 |
|---|---|---|
| 配置文件 | cpu.cfs_quota_uscpu.cfs_period_us |
cpu.max (合并单一文件) |
| 配置原子性 | 否(多次 Write,存在中间态) | 是(单次 Write,原子更新) |
| 线程管理 | 线程可跨组,数据结构复杂锁竞争严重 | 强制进程树对齐,调度器开销大幅降低 |
| 突发流量应对 | 无能为力(硬限流,引发长尾延迟) | 支持 CPU Burst(平滑突发,保障 P99) |
如果你正在规划新集群的建设,或者正在对现有高并发服务进行性能调优,强烈建议将底层系统升级至支持 Cgroup v2 + CPU Burst 的版本。这一升级不仅是操作接口的简化,更是调度器底层逻辑的一次跃迁,是解决容器“异常限流”与“保障应用极致响应”的终极利器。