runc与cgroup v2资源控制及运维故障诊断 - 第1节

1.1 Cgroup v2 设计哲学:从“修补”到“统一”

Cgroup v2 并非 v1 的简单增量升级,而是对 Linux 资源控制模型的一次根本性重构。v1 在十余年的演进中暴露出层级混乱、控制器冲突、安全模型薄弱等结构性问题。v2 的核心设计哲学可概括为:单一层级、无内部节点约束、自顶向下资源分配。

设计目标:消除 v1 中多个层级挂载导致的资源视图分裂,使 CPU、内存、I/O 等控制器在同一个树状结构中协同工作,为容器运行时提供一致、可审计的资源管控接口。

1.1.1 与 v1 的核心差异

对比维度 Cgroup v1 Cgroup v2
层级结构 多层级(每个控制器独立挂载一棵树) 单一统一层级(所有控制器共享一棵树)
线程粒度 支持线程级迁移(threaded) 仅支持进程级(threaded 模式受限)
内部节点 内部节点可包含进程 内部节点不能包含进程(cgroup.type 约束)
控制器启用 挂载时静态启用 通过 cgroup.subtree_control 动态委派
资源统计 分散在多个伪文件 统一在 *.events、*.stat 中
安全模型 无委派机制,需 root 支持无根委派(delegation)
运维启示:v2 的单一层级意味着 /sys/fs/cgroup 下只有一棵树。容器运行时(如 runc)不再需要为每个控制器分别挂载,只需在统一路径下创建子目录并写入 cgroup.subtree_control 即可委派控制器。

1.2 统一层级结构详解

v2 将整个系统资源控制组织为一棵以 /sys/fs/cgroup 为根的树。每个 cgroup 目录代表一个节点,节点之间通过父子关系形成层级。关键约束是:内部节点(有子 cgroup 的节点)不能直接包含进程,所有进程必须位于叶子节点。

Cgroup v2 统一层级结构 /sys/fs/cgroup (root) system.slice user.slice machine.slice docker-abc.scope kubepods libvirt-qemu container-1 container-2 container-3 关键约束: • 内部节点(如 system.slice、kubepods)不能包含进程,只能包含子 cgroup • 所有进程(容器 PID)必须位于叶子节点(如 container-1) • 控制器通过 cgroup.subtree_control 从父节点向子节点委派

1.2.1 无内部节点约束的工程意义

v2 强制“内部节点无进程”是为了消除资源统计的二义性。在 v1 中,父 cgroup 的资源使用量包含其所有子 cgroup 的用量,但父节点自身也可能运行进程,导致统计重复或遗漏。v2 的设计使得每个节点的资源统计只包含直接位于该节点的进程,子节点的用量独立核算。

常见误区:在 v2 中,如果尝试向一个已有子 cgroup 的节点写入 PID,系统会返回 EBUSY。runc 在创建容器时必须确保容器 cgroup 是叶子节点,否则初始化会失败。

1.3 控制器委派与 subtree_control

v2 引入 cgroup.subtree_control 伪文件作为控制器委派的唯一接口。父节点通过写入该文件,决定哪些控制器可以被子节点使用。这一机制取代了 v1 中通过挂载选项静态启用控制器的方式。

# 查看根节点已委派的控制器
cat /sys/fs/cgroup/cgroup.subtree_control
# 输出示例:cpu memory io pids

# 向子 cgroup 委派 cpu 和 memory 控制器
echo '+cpu +memory' > /sys/fs/cgroup/system.slice/cgroup.subtree_control

# 撤销委派
echo '-cpu' > /sys/fs/cgroup/system.slice/cgroup.subtree_control

1.3.1 控制器委派流程

  1. 根节点初始化:系统启动时,根 cgroup 的 subtree_control 由 init 系统(systemd)写入,默认启用 cpu、memory、io、pids。
  2. 中间节点委派:如 kubepods 向 kubepods/burstable 委派 cpu 和 memory。
  3. 叶子节点使用:容器 cgroup 只需读取父节点委派的控制器,直接写入对应的资源限制文件(如 memory.max、cpu.max)。
runc 实践:runc 在创建容器时,会先确保容器 cgroup 的父节点已委派所需控制器。如果父节点未委派 memory,runc 会尝试向父节点写入 +memory,若失败则返回错误。因此,SRE 在配置节点时应提前规划好控制器委派链。

1.4 关键伪文件映射

v2 将资源控制接口统一为 <controller>.<property> 格式,消除了 v1 中命名混乱的问题。以下是与容器运行时最相关的伪文件对照:

资源类型 v1 接口(示例) v2 接口 说明
内存上限 memory.limit_in_bytes memory.max 单位支持 K、M、G
内存软限制 memory.soft_limit_in_bytes memory.low 尽力保障,非硬限制
CPU 配额 cpu.cfs_quota_us + cpu.cfs_period_us cpu.max 格式 $quota $period
CPU 权重 cpu.shares cpu.weight 范围 [1, 10000]
I/O 带宽 blkio.throttle.read_bps_device io.max 按设备限速
进程数 pids.max(v1 后期引入) pids.max 兼容
v1 vs v2 接口命名对比 Cgroup v1(碎片化命名) memory.limit_in_bytes cpu.cfs_quota_us blkio.throttle.read_bps_device Cgroup v2(统一命名) memory.max cpu.max io.max v2 统一为 <controller>.<property> 格式,消除命名歧义

1.5 本章小结

  • 单一层级:v2 将所有控制器统一到一棵树,消除 v1 的多树分裂问题。
  • 无内部节点约束:内部节点不能包含进程,确保资源统计无歧义。
  • 动态委派:通过 cgroup.subtree_control 实现控制器委派,支持无根容器运行时。
  • 接口统一:伪文件命名规范化为 <controller>.<property>,降低运维复杂度。
  • runc 适配:容器运行时需遵循叶子节点约束和委派链检查,否则初始化失败。