Linux cgroup v2 深度剖析:云原生时代的资源隔离核心机制
引言
在现代容器化和云原生架构中,Linux Control Groups(cgroup)是实现资源隔离与限制的内核基石。从 Docker 到 Kubernetes,从 systemd 到OCI运行时,cgroup 无处不在。2016 年,Linux 4.5 正式发布 cgroup v2,标志着资源管理从 "可用" 到 "一致、统一、可预测" 的范式转变。
尽管 cgroup v2 已发布近十年,许多系统开发者对其架构设计理念、与 v1 的本质差异、以及在生产环境中的最佳实践仍缺乏系统认知。本文将深入剖析 cgroup v2 的统一层级设计、资源控制模型、eBPF 集成机制,以及在容器编排系统中的真实部署与调优策略。
一、cgroup 演进:从 v1 的混乱到 v2 的统一
1.1 cgroup v1 的架构局限性
cgroup v1 采用多子系统(multi-hierarchy)架构,每种资源控制器(cpu、memory、blkio、net_cls 等)独立挂载为独立的 cgroup 层级树。这带来了三个根本性问题:
跨子系统不一致:当进程被分配到不同控制器的 cgroup 时,层级路径可能冲突。例如 memory 控制器将进程放入 /foo,cpu 控制器希望放入 /bar,v1 无法保证一致性。
管理复杂度膨胀:N 个容器 × M 个控制器 = N×M 个 cgroup 目录需要维护,运维负担沉重。
缺乏资源优先级统一抽象:v1 中 weight 在不同控制器语义不同,无法构成全局资源分配视图。
1.2 cgroup v2 的统一层级设计
cgroup v2 的核心设计哲学是 "单一层级树"(single hierarchy):
- 所有资源控制器挂载到同一棵 cgroup 树中
- 子 cgroup 的约束条件从父 cgroup继承并叠加
- 线程粒度控制(thread mode)支持更精细的线程组管理
- 统一的资源分配语义:权重(weight)全局一致
这种设计的直接结果是:在 cgroup v2 中,描述一个容器的资源边界只需挂载一个 cgroup 路径,而非 v1 中的多个独立路径。这极大地简化了容器运行时的实现复杂度。
二、cgroup v2 资源控制全景
2.1 CPU 资源控制
cgroup v2 的 CPU 控制彻底抛弃了 v1 中 cpu.shares + cpu.cfs_period_us + cpu.cfs_quota_us 的分裂模型,引入了统一的 cpu.weight 和 cpu.max 双参数模型。
cpu.weight(有效范围 1-10000,默认 100):
采用 Weighted Fair Queuing(WFQ)语义,当 CPU 资源发生竞争时,各 cgroup 按照 weight 比例分配 CPU 时间。两个 weight=100 的 cgroup 竞争时各得 50%,一个 weight=100 与 weight=200 竞争时分别获得 33.3% 和 66.7%。
cpu.max:格式 $MAX $PERIOD,硬上限。例如 50000 100000 表示在 100ms 周期内最多使用 50ms 核时间(即 0.5 核上限)。设置 max 表示无限制。
cpu.pressure:PSI(Pressure Stall Information)接口,记录 CPU 资源争抢导致的调度延迟,这是 v2 的重要创新,让 "隐式资源饥饿" 变得可观测。
2.2 内存资源控制
cgroup v2 的内存控制由三个核心接口组成:
memory.min:硬保护阈值。cgroup 内内存用量即使超出该值也不会被内核回收。适用于保障关键工作负载的最低内存需求。
memory.low:尽力保护(best-effort protection)。内核尽量不回收该 cgroup 的内存,但在系统内存紧张时允许突破。适用于给予一定保障同时不阻止系统在极端情况下回收。
memory.max:硬上限。进程内存使用触碰此阈值时触发 cgroup OOM 杀死器,只杀死该 cgroup 内进程(而非系统全局 OOM Killer)。
memory.high:软上限。超出后内核积极回收,产生内存压力但不杀死进程。配合 PSI 可以在超配场景下实现自适应调节。
除了内存本身,v2 统一了内存.stat 统计输出,包含 anonymous、file、kernel_stack、sock、shmem、file_mapped、file_dirty、file_writeback 等细分项,为内存分析提供了精确的归因能力。
2.3 I/O 资源控制
cgroup v2 将 blkio 控制器统一为 io.weight 和 io.max 双接口:
io.weight(1-10000,默认 100):WFQ 权重模型,在发生 I/O 拥塞时按权重分配带宽和 IOPS。
io.max:按设备粒度设置硬限制,格式为 $MAJ:$MIN rbps=$READ_BPS wbps=$WRITE_BPS riops=$READ_IOPS wiops=$WRITE_IOPS。例如 8:0 rbps=10485760 wbps=5242880 限制 sda 读取 10MB/s、写入 5MB/s。
io.bfq.weight:如果使用 BFQ I/O 调度器,可以设置 per-cgroup 的 BFQ 权重(1-1000,默认 100)。
io.latency:v2 新增特性,允许为每个 cgroup 设置 I/O 延迟目标。例如 8:0 target=100000 表示目标 I/O 延迟不超过 100ms,内核自动调节 I/O 队列延迟以满足此目标。
2.4 网络资源控制
cgroup v2 本身不直接实现网络带宽限制(那是 tc/netem 的职责),而是通过 cgroup-attached eBPF 程序 实现:
bpf_cgroup_storage:per-cgroup 的 BPF 映射存储,用于在 BPF 程序中维护 per-cgroup 的网络状态。
cgroup sysctl:cgroup/sysctl 允许通过 write 系统调用访问 cgroup 内进程可访问的特定 sysctl 参数。
网络带宽限制的标准方案是使用 tc + cgroup filter 或 CNI 插件在 veth 上配置 HTB/TBF 队列规则。
2.5 PID 控制器
pids.max:限制 cgroup 内的最大进程/线程数。这是防御 fork bomb 式 DoS 攻击的直接手段。pids.current 实时报告当前数量。
三、cgroup v2 与 eBPF 的深度集成
3.1 cgroup-based eBPF 程序附加
cgroup v2 为 eBPF 提供了一个天然的策略挂载点。由于 v2 的单一层级保证,eBPF 程序可以安全地附加到 cgroup 节点上,对所有后代进程生效。
主要的 cgroup eBPF 程序类型:
- BPF_PROG_TYPE_CGROUP_SKB:在数据包进出 cgroup 时触发,用于网络策略(过滤/修改/重定向)
- BPF_PROG_TYPE_CGROUP_SOCK:在 socket 创建时触发,控制 socket 创建权限
- BPF_PROG_TYPE_CGROUP_SOCK_ADDR:在 bind/connect 时触发,控制地址绑定
- BPF_PROG_TYPE_CGROUP_SOCKOPT:在 getsockopt/setsockopt 时触发
- BPF_PROG_TYPE_CGROUP_DEVICE:控制设备访问权限(替代 device cgroup v1)
- BPF_PROG_TYPE_CGROUP_SYSCTL:在 sysctl 访问时触发
- BPF_PROG_TYPE_CGROUP_SOCK_CREATE:v6.7+ 新特性,更早期的 socket 创建拦截
3.2 bpf_lsmod_64 与 cgroup BPF 生命周期
cgroup BPF 程序使用 bpf(BPF_LINK_CREATE) 创建 BPF link 来绑定到 cgroup。与传统的 netlink 挂载相比,BPF link 的优势:
- 生命周期随 link fd 自动管理,close fd 即解除绑定
- 支持
PIN到 bpffs 持久化,跨进程共享 - 支持 F Stones 风格的 BPF 热升级,无需重启即可更新策略
3.3 实战:使用 cgroup BPF 限制 socket 绑定
通过编写一个 BPF_PROG_TYPE_CGROUP_SOCK 程序,可以在 socket 层级实现 "容器只能绑定到特定端口段" 的策略,替代传统的 Linux capabilities 机制:
// 当容器内进程调用 bind 时,BPF 程序会被触发
// 通过 bpf_sk_storage 检查是否允许绑定目标端口
SEC("cgroup/sock")
int restrict_bind(struct bpf_sock *sk) {
if (sk->src_port < 8000 || sk->src_port > 9000)
return 0; // 拒绝
return 1; // 允许
}
四、统一层级树的线程模式(Thread Mode)
4.1 domain vs threaded
cgroup v2 引入了 "thread mode"(线程模式),解决了 v1 无法精细管理进程内线程组的问题。v2 将 cgroup 分为两种模式:
domain cgroup:普通的 cgroup,节点可以被分配进程,资源约束汇总到父节点。
threaded cgroup:叶子节点下的线程组,作为 sub-cgroup 实现对同一进程内不同线程组的独立资源控制。threaded cgroup 不拥有自己的 CPU/内存使用统计汇总,而是通过 "threaded domain" 向上传递。
4.2 启用与操作
通过写入 cgroup.type 为 threaded 激活线程模式,此时该 cgroup 变为 threaded domain,子 cgroup 自动成为 threaded cgroup,可被写入线程 ID(TID)实现线程级调度控制。
典型应用场景:Web 服务器主进程分配在一个 cgroup 中,工作线程组按照优先级分别放入 threaded sub-cgroup,实现不同工作线程的差异化 CPU 权重分配。
五、PSI:压力阻塞信息——可观测性的飞跃
Pressure Stall Information(PSI)是 cgroup v2 的资源争抢可观测性基础设施。PSI 将 "资源不足导致任务停滞" 这一过去难以量化的指标,转换为精确的时间百分比。
5.1 三个资源维度
cpu.pressure:记录 CPU 调度等待(cpu.some/full)。cpu.only 在 v2 中不存在(因为 CPU 不存在 "完全无法运行" 的场景)。
memory.pressure:memory.some 表示至少一个任务因内存停滞;memory.full 表示所有任务同时停滞。
io.pressure:io.some 表示至少一个任务因 I/O 停滞;io.full 表示所有任务同时停滞。
5.2 数据格式
PSI 接口输出包含 avg10/avg60/avg300(过去 10秒/60秒/300秒 的累计停滞百分比)和 total(自 cgroup 创建以来的累计停滞微秒数):
some avg10=15.20 avg60=8.30 avg300=3.10 total=125890000
full avg10=10.10 avg60=4.20 avg300=1.50 total=98000000
avg10=15.20 意味着过去 10 分钟内,有 15.2% 的时间至少有一个任务因资源停滞。avg60/avg300 平滑曲线可用于判断是瞬时尖刺还是持续压力。
5.3 在生产中的使用模式
基于 PSI 的自动弹性系统:
- avg10 > 30% for 1分钟 → 触发告警
- avg60 > 15% for 5分钟 → 自动减载
- avg60 > 40% for 3分钟 → 自动驱逐(evict)
相比基于 usage 的监控,PSI 直接反映用户体验——即使 memory.usage 未达到 max,PSI 的 full 停滞也意味着应用已经变慢。这种从 "资源用量" 到 "用户体验" 的可观测性升级,是 PSI 最大的工程价值。
六、从 v1 迁移到 v2:生产实践
6.1 systemd 与 cgroup v2
systemd v233+ 已原生支持 cgroup v2,并通过 unified_cgroup_hierarchy=1 内核参数切换:
# 在 /etc/default/grub 中添加
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"
grub-mkconfig -o /boot/grub/grub.cfg
systemd 在启动时自动将进程组织到 /sys/fs/cgroup 的 v2 层级中,每个 service unit 创建对应的 $service.service cgroup,并通过 CPUQuota=、MemoryMax= 等指令映射到 cgroup v2 接口。
6.2 Kubernetes 与 cgroup v2
Kubernetes 1.28+ 默认支持 cgroup v2。Kubelet 通过 --cgroup-driver=systemd 将 Pod 资源请求映射为 cgroup v2 配置:
requests.cpu→ 通过 cpu.weight 实现(基于 (requests/1000)*10000 的映射)limits.cpu→ 通过 cpu.max 实现(硬上限)requests.memory→ memory.min + memory.low 组合(Burstable QoS)limits.memory→ memory.max(Guaranteed QoS)
注意 cgroup v2 中 Kubernetes 的 CPU requests 不再通过 CFS quota 硬限制,而是通过 weight 实现软保障,这使得节点 CPU 利用率可以更高(超卖策略更灵活)。
6.3 容器运行时实现
containerd 和 CRI-O 通过 cri-o.conf / config.toml 中的 cgroupv2 = true 启用。runc 和 crun 在创建容器时自动写入对应的 cgroup 控制器参数。
runsc(gVisor)也有自己的 v2 适配机制,通过在用户态模拟 cgroup 文件系统实现兼容。
七、cgroup v2 高级特性与内核新进展
7.1 cgroup.events 与 cgroup.events.stream
当 cgroup 内没有进程且没有子 cgroup 时,cgroup.events 文件报告 populated 0。v2 新增了 inotify 机制,当 cgroup 层级变化(populated 状态变化、压力阈值突破)时触发事件通知,让容器运行时无需轮询即可感知容器生命周期。
7.2 cgroup.freeze(Linux 5.2+)
写入 1 到 cgroup.freeze 可冻结整个 cgroup 内所有进程(类似 atomic SIGSTOP),写入 0 解冻。这是实现容器检查点/恢复(Checkpoint/Restore)和实时迁移(Live Migration)的关键基础。Kubernetes 当前不使用 freeze 但 CRIU 等工具深度依赖此接口。
7.3 Memory QoS(Linux 5.19+)
v2 新增 memory.reclaim 接口,允许主动触发内存回收:写入 1g 会让内核从该 cgroup(及子 cgroup)中回收 1GB 内存。对于长生命周期服务(如 Redis 缓存服务器),可在内存压力到达前主动回收冷内存页,避免触发内核紧急回收的延迟抖动。
7.4 PSI 支持基于 cgroup 的层级聚合(Linux 6.x)
新内核允许 PSI 统计在层级上聚合。系统管理员可以设置 threshold 文件 (memory.pressure 的 trigger 写入) 实现自定义压力事件通知——当 PSI 超过阈值时触发 eventfd 通知,无需轮询 /proc/PID/status。
7.5 嵌套资源控制(Nested Resource Control)
cgroup v2 的域模型天然支持嵌套控制:在 Kubernetes Pod cgroup 内创建容器 sub-cgroup,容器 sub-cgroup 内创建进程级 threaded sub-cgroup,每个层级独立设置约束。资源分配的顺序遵循 "父级优先" 原则——子级不能超过父级限制,这保证了资源超卖场景下的边界安全。
八、性能调优与最佳实践
8.1 CPU 密集型负载
对延迟敏感的 CPU 密集型负载,使用 cpu.max 硬限制而非 cpu.weight 软权重。配合 cpu.pressure 监控确认是否存在调度延迟。如果 PSI 的 avg10 > 5%,应考虑提升 cpu.max 配额。
对于 NUMA 敏感型负载,cgroup v2 需配合 cpuset 控制器(cpuset.cpus/cpuset.mems)绑定 NUMA 节点,避免跨 NUMA 访问导致的内存带宽瓶颈。
8.2 内存超配与 OOM 防护
在容器节点中,memory.max 设置应为 Pod 真实需求的 100-120%,过高的超配会导致 cgroup OOM 时杀死关键进程。建议策略:
- Burstable Pod:memory.min = requests, memory.max = limits × 1.2
- BestEffort Pod:不设 memory.min, memory.max = nodes 可用内存 / Pod 数
- DaemonSet 关键组件(node-exporter, kube-proxy):设置 memory.min 防止被意外驱逐
8.3 I/O 隔离
多容器共享同一块磁盘时,io.max 是公平性保障的最后防线。对于数据库类延迟敏感型负载,优先使用 io.latency 替代 io.max——io.latency 允许突发,只惩罚持续超限的 I/O 模式。
存储 SPDK/io_uring 等新 I/O 引擎绕过 VFS 层时,cgroup v2 的 io 控制器可能无法统计此类 I/O。此时需在内核启动时禁用 io 统计或使用 BPF 自定义统计。
8.4 cgroup 数量优化
对于运行大量短期任务的作业系统(如 CI/CD),过深的 cgroup 层级和过多叶子节点会影响调度器遍历性能。建议:
- 层级深度控制在 4 层以内
- 定期清理空 cgroup(无进程的后代)
- 使用 delegating cgroup 模式(设置 cgroup.controllers 委托给子 cgroup 管理)
九、深入内核实现:cgroup v2 的数据结构
9.1 css_set 与 cgrp_cset_link
每个进程通过 css_set(cgroup subsystem state set)关联到一组 cgroup。v2 中所有子系统共享一个 css_set,而 v1 中不同子系统可以有独立的 css_set(v1 的根源问题)。
9.2 cgroup 核心数据结构关系
cgroup 树中核心结构为 cgroup 结构体,包含:
- cgrp_css:各子系统状态(mem_cgroup、blkcg 等)指针数组
- cgrp_flags:标记 cgroup 状态(frozen、populated 等)
- sibling/children:双向链表构成树结构
- ancestors:父节点资源聚合数组(leaf 到 root 的资源分布)
9.3 mem_cgroup 内部
mem_cgroup 是 cgroup v2 内存控制的核心结构体。关键字段有:
- memory:页计数器(page counter),记录当前用量
- swap:swap 用量统计
- soft_limit:软限制阈值
- swappiness:可覆盖全局 vm.swappiness 的 cgroup 级交换偏好
- oom_lock:OOM 互斥锁,确保同一时间只有一个 OOM 决策运行
当 memory 计数器接近 max 时,内核触发 DIRECT RECLAIM(直接内存回收),回收路径会唤醒 kswapd 或直接扫描匿名页/LRU 文件页。如果回收仍然失败,触发 cgroup OOM。
十、总结与展望
cgroup v2 通过统一层级树、PSI 可观测性、eBPF 集成接口三大支柱,构建了一个架构清晰、语义一致、运行时可编程的资源管理子系统。相比 v1 的碎片化,v2 成为了云原生基础设施不可或缺的内核基础组件。
从发展趋势看,cgroup v2 正在与 io_uring、eBPF、调度器(EEVDF)、以及内存管理(MGLRU)深度协同,构建 "以内核为中心的系统可编程架构"。对于系统工程师而言,深入理解 cgroup v2 已不再是加分项,而是从事容器化、云原生系统开发的必备基础能力。
参考资料
- Linux Kernel Documentation: Documentation/admin-guide/cgroup-v2.rst
- systemd.resource-control(5) man page
- Kernel Recipes 2017: "cgroup v2" by Tejun Heo
- Kubernetes Official Docs: "cgroup v2"
- LWN.net series: "cgroup v2: a long-awaited rewrite"

发表评论 取消回复