Linux cgroup v2 资源管控深度实战:从子系统拓扑到云原生隔离的完整指南
当 Kubernetes 在 1.20 版本中将 cgroup v2 设为 GA 状态时,Linux 资源管理的一个新时代正式拉开帷幕。相比 v1 那种各自为政的分散式设计,cgroup v2 以统一的层级树、递归资源控制和压力反馈机制,从根本上解决了长期以来 I/O 控制滞后、内存超售失控和多控制器冲突等棘手问题。本文将从内核实现、控制器语义、生产级调优以及与容器运行时的集成四个维度,对 cgroup v2 进行一次不留死角的拆解。
本文所有结论均基于 Linux 5.15 内核源码分析与实测,实验环境为 Kubernetes 1.28 containerd 1.7 Linux 6.2。
一、为什么 cgroup v1 走到了尽头
要理解 v2 的设计变革,必须先看清 v1 在生产环境中反复折磨 SRE 们的那些结构性缺陷。
1.1 层级树分裂:最根本的架构债务
cgroup v1 允许不同控制器(cpu、memory、blkio 等)将进程挂载到完全不同的层级树上。例如memory 控制器可以有一棵以 /sys/fs/cgroup/memory/teamA/app1 为挂载点的层级树,而 cpu 控制器另有 /sys/fs/cgroup/cpu/teamA/app2。这种设计导致:
- 一个进程在不同控制器中的位置彼此不可见,无法做全局资源预算
- 层级内部一致性内核不做强制保证,容易产生"子组总权重超过父组"的矛盾配置
- 用户态代理(如 OCI runtime spec)需要在不同控制器之间反复协调,代码复杂度呈指数上升
1.2 I/O 控制缺失:blkio 的半成品之痛
v1 的 blkio 控制器仅支持基于权重的比例分配(blkio.weight)和硬上限节流(blkio.throttle.read_bps_device),这两种方式均无法实现关键的低延迟目标:权重分配在并发度下降时自动失效(高权重组反而拿不到带宽),硬上限完全无视优先级,让延迟敏感的 OLTP 数据库与被压缩批处理任务平起平坐。此外,v1 blkio 的统计仅支持同步 I/O,对无处不在的带 Page Cache 的异步写回几乎完全失明。
1.3 内存事件反馈断裂:OOM 管理的盲区
v1 的 memory.oom_control 仅支持"杀死整个 cgroup"的二元决策。当一个多容器 Pod 的 sidecar(如 Envoy)突发内存占用导致 cgroup OOM 时,内核可能杀死同一 cgroup 中的业务容器——Kubernetes 不得不依赖复杂的 QOS class 分级和用户态 OOM Score 调整来规避。
二、cgroup v2 核心架构:统一的层级树
由 Facebook 工程师 Tejun Heo 主导的 cgroup v2 重设计(Linux 4.5 引入,5.0 趋于成熟),最核心的变化是强制所有单一控制器作用于同一棵层级树。
2.1 控制器统一与进程级附着
v2 的关键规则简单而严格:进程中只能出现在一个 cgroup 叶子节点中,所有控制器的资源限制统一从这个叶子节点向上递归继承并叠加计算。这意味着:
- 在
cgroup.controllers文件中声明本层启用的控制器集合 - 在
cgroup.subtree_control文件中配置向子树下放哪些控制能力 - 进程只能写入叶子 cgroup 的
cgroup.procs文件,禁止进程同时存在于多个兄弟 cgroup
2.2 树形结构的资源语义
v2 的递归语义极其清晰:子组从父组获取配额后在组内二次分配,父组的限制永远生效——无论子组如何竞争。例如一个 4 核 8G 的父组下有两个子组 app1、app2,权重分别为 300 和 100,那么当两者同时争抢 CPU 时,app1 获得 75%、app2 获得 25%。但这种比例分配只在争抢时生效——如果 app1 空闲,app2 可以临时占用空闲的 100% CPU,这一行为与 CFS 调度器的"空闲借用"语义完全一致,确保了资源利用率不浪费。
2.3 进程迁移的限制与代价
v2 下将进程从 /app/teamA 移动到 /app/teamB 时,该进程的 stat 数据(累计 CPU、内存峰值等)不会自动归属到目标组,而是在迁移瞬间归零。这是因为 v2 设计上假定 cgroup 层级反映的是组织架构,而非临时性的进程分组。因此 OCI runtime(runc、crun)在创建容器时会为每个 Pod 预建对应的 cgroup 目录结构,在容器生命周期内不做迁移——这与 v1 时代 Docker 频繁迁移进程到不同层级的做法有根本区别。
三、五大控制器:从接口到内核实现
v2 目前定义了 cpu、memory、io、pids、rdma、misc、hugetlb、perf_event 等控制器,本文重点讨论前四个在生产环境中最常用的。
3.1 cpu 控制器:权重与硬上限的双重语义
v2 cpu 控制器提供三种独立的限制维度:
- cpu.weight:等价 CFS 的 nice 值,仅在 CPU 争抢时按比例分配,范围 1-10000(默认 100)
- cpu.max:硬上限,格式
$MAX $PERIOD,例200000 100000表示每 100ms 最多用 200ms CPU 时间(即 2 核上限) - cpu.max.burst:允许超过 cpu.max 的累计额度(Linux 6.2新增 Kubernetes 1.28 支持),让非周期性疾病突发场景更灵活
与 v1 的 cpu.cfs_quota_us / cpu.cfs_period_us 对比,v2 的 cpu.max 同一文件同时承载配额与周期,更符合"单一原子值"的设计原则;burst 支持则直接回应了在线推理类工作负载"平时不定耗时突发"的流量模式。
3.2 memory 控制器:高水位、低水位与压力反馈的三轨制
v2 memory 控制器定义了 6 个关键字段,构成完整的分级保护体系:
- memory.min — 硬保证量,内核绝不回收此部分内存(Kubernetes Requests)
- memory.low — 尽力保证量,仅在系统全局内存压力下才回收(可选的低阈值)
- memory.high — 节流阈值,超出后异步回收 限速,但不会触发 OOM(推荐设为 limits 的 80-95%)
- memory.max — 硬上限,超出立刻触发 cgroup 级 reclaim,回收失败则触发 OOM(Kubernetes Limits)
- memory.swap.max — Swap 硬上限(0 完全禁用)
- memory.oom.group — 设为 1,整个 cgroup 在 OOM 之前先尝试全组回收后再单独杀进程(Kubernetes Burstable 的互补机制)
这一设计的精妙之处在于:memory.high 作为软限制让容器在超出预期用量时有一个缓冲减速段,不会像 v1 那样瞬间触发进程级 OOM kill。在 Kubernetes 中,kubelet 会将 requests.memory 映射为 memory.min(仅 Guaranteed QOS,Burstable 映射为 memory.low),limits.memory 映射为 memory.max,从而在 v1 中两个含糊不清的 limits/requests 概念升级为 v2 中多层精密的保护机制。
3.3 io 控制器:从带宽到 IOPS,从同步到异步
v2 io 控制器彻底重写了 blkio 逻辑,核心解决三个问题:
- 支持带 Page Cache 的异步 I/O 计量:v1 完全看不见 dirty page 的写回流量,v2 在
struct bio下发到底层设备时做 cgroup 标记,实现端到端 I/O 归因 - 基于设备的独立策略:每个设备可分别配置
rbps、wbps、riops、wiops的限制,格式如259:0 rbps=10485760 wbps=10485760 - 权重分配与上限节流的统一:
io.weight(设备无关,范围 1-10000)用于按权分配空闲带宽,io.max用于硬上限 - io.latency — 这是 v1 缺失的关键能力,允许为每个设备配置目标延迟(如
259:0 target=100要求 100μs 级别服务目标)。内核的 blk-iocost 控制器会自动统计设备的成本模型(R/W 延迟、吞吐曲线),在接近目标时主动节流,从而以接近理想的反馈控制保护延迟敏感的小 I/O 不被批量大 I/O 淹没
3.4 pids 控制器:防御 fork bomb 与资源耗尽
pids.max 是安全防御的最后防线:pids.current 实时查看当前进程数,超出 pids.max 后 clone/fork 调用会返回 EAGAIN,从内核态彻底阻断 fork bomb 扩散。Kubernetes 通过 Pod PID Limits 特性(1.20 beta)将每个 Pod 的 pids.max 设为 1024-4096,防止恶意容器耗尽主机进程 ID 空间。
四、压力反馈机制:PSI — 资源饱和度的新度量衡
cgroup v2 引入的 PSI(Pressure Stall Information)是近年来最重要的可观测性创新之一。它向上游揭示了传统 cpu.stat 和 memory.stat 统计无法传递的关键信号:不是因为配额用尽了,而是因为争抢导致排队了。
4.1 三级压力的分离量化
PSI 将"压力"再次分离为三种互不相关的场景:
- some:至少有一个任务正在因该资源争抢而停滞(some avg10=5.00 表示过去 10s 内平均有任务停滞 5% 时间)
- full:所有非空闲的任务全部停滞(full avg10=5.00 表示 10s 内全部任务同时停滞 5% 时间)
- total:累积停滞时间(微秒),用于长期趋势分析
语法格式示例:
# cat /sys/fs/cgroup/app/memory.pressure some avg10=0.00 avg60=0.05 avg300=0.02 total=32849 full avg10=0.00 avg60=0.01 avg300=0.00 total=2156
4.2 PSI 在生产级监控中的价值
传统 CPU 使用率 50% 可能只是一个数字,但 PSI 的 cpu.some avg10 如果持续大于 5 意味着有效算力已经被争抢吃掉了一半。Google 的 Borg 和蚂蚁集团的单机调度系统都将 PSI 作为真实负载的核心指标:
- cpu

发表评论 取消回复