WEBKT

Cgroup v2 下 CPU 限制的新姿势:深度解析 cpu.max 与 v1 cfs_quota_us 的内核级差异与 CPU Burst

8 0 0 0

在容器化时代,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=100msquota=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 的 cpucpuacctcpuset 是三个相互独立的子系统(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

该文件包含两个值:quotaperiod

  • 默认值为 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 CPUAllocatorCPUBurst

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_us
cpu.cfs_period_us
cpu.max (合并单一文件)
配置原子性 否(多次 Write,存在中间态) (单次 Write,原子更新)
线程管理 线程可跨组,数据结构复杂锁竞争严重 强制进程树对齐,调度器开销大幅降低
突发流量应对 无能为力(硬限流,引发长尾延迟) 支持 CPU Burst(平滑突发,保障 P99)

如果你正在规划新集群的建设,或者正在对现有高并发服务进行性能调优,强烈建议将底层系统升级至支持 Cgroup v2 + CPU Burst 的版本。这一升级不仅是操作接口的简化,更是调度器底层逻辑的一次跃迁,是解决容器“异常限流”与“保障应用极致响应”的终极利器。

Linux内核探秘者 Cgroupv2CPU限流Linux内核调度

评论点评