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"
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部