cgroup v1 与 v2 深度解析:Kubernetes 资源隔离从原理到迁移
Control Groups(cgroups)是 Linux 内核提供的进程资源管控机制,支持对 CPU、内存、磁盘 IO、网络带宽等资源进行分组限制。本文系统梳理 cgroup v1 与 v2 的核心差异、Kubernetes 集成方式、容器迁移注意事项,以及 eBPF 程序挂载到 cgroup 的使用方法。
一、cgroup 基础与 Kubernetes 集成
在 Kubernetes 中,resources.requests 和 resources.limits 对应 cgroup 的资源限制:
1 | resources: |
requests:Pod 期望独占的资源量(用于调度决策)limits:Pod 可使用的资源上限(通过 cgroup 强制执行)
二、cgroup v2 的改进
cgroup v2 是 Linux cgroup API 的最新版本,相比 v1 有以下关键改进:
- 单一统一层级:所有资源控制器共享一个 cgroupfs 树,消除 v1 多树带来的一致性问题
- 更安全的子树委托:容器可安全管理自己的子 cgroup,无需特权
- Pressure Stall Information(PSI):CPU、内存、IO 竞争时的量化指标,让用户准确感知资源压力而非靠经验猜测
- 增强的多资源联合管控:统一核算不同类型内存分配(网络内存、内核内存等);支持非即时资源变更(如页面缓存写回)的计量
v1 vs v2 内存接口对比
| v1 接口 | v2 接口 |
|---|---|
memory.kmem.limit_in_bytes |
memory.max |
memory.kmem.tcp.limit_in_bytes |
memory.swap.max |
memory.limit_in_bytes |
|
memory.memsw.limit_in_bytes |
三、三种 cgroup 模式
| 模式 | mount 详情 |
|---|---|
| v1 | tmpfs on /sys/fs/cgroup type tmpfs (ro,...)cgroup on /sys/fs/cgroup/cpu type cgroup (rw,...,cpu) |
| hybrid | cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,...)旧控制器仍挂载为 v1 |
| v2 | cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) |
四、层级差异
cgroup v1 多树结构
v1 为每个资源控制器(cpu、memory、pids 等)维护独立的层级树,导致进程在多棵树中都有对应节点,管理复杂且可能不一致。
cgroup v2 单一统一层级
v2 用一棵树统一管理所有控制器,每个节点同时控制多种资源,层级视图与进程树对齐。
cgroupfs 文件接口对比
| v1 | v2 |
|---|---|
cpu/cpu.cfs_quota_us: 100000 |
cpu.max: 100000 100000 |
cpu/cpu.cfs_period_us: 100000 |
|
cpu/cpu.shares: 512 |
cpu.weight: 20 |
cpu/cpu.stat: nr_periods 55 nr_throttled 0 throttled_time 0 |
cpu.stat: usage_usec 57710 user_usec 28855 system_usec ... |
memory/memory.limit_in_bytes: 2147483648 |
memory.max: 2147483648 |
memory/memory.usage_in_bytes: 6553600 |
memory.current: 1425408 |
pids/pids.max: 163839 |
pids.max: 163839 |
五、如何启用 cgroup v2
1. 节点级启用
编辑 /boot/grub/grub.cfg,在内核参数中添加:
1 | systemd.unified_cgroup_hierarchy=1 |
2. 开启控制器
1 | # 查看可用控制器 |
六、cgroup 命名空间
基本原理
cgroup 命名空间(cgroupns)虚拟化 /proc/$PID/cgroup 文件和 cgroup 挂载点的视图,类似于 PID 命名空间对进程树的虚拟化。
启用 cgroup 命名空间后(v2 默认开启):
1 | # 从宿主机查看容器进程 |
未启用 cgroup 命名空间时(v1 默认):
1 | cat /proc/298593/cgroup |
cgroupns 特性
- 根视图重映射:命名空间创建时,以创建进程所在的 cgroup 作为新命名空间的根(
/),容器内进程看到的是自己的 cgroup 为根 - 资源隔离无关:cgroupns 只虚拟化层级视图,不直接提供资源隔离
- 生命周期:只要命名空间内有进程或 mount 引用,命名空间就存在
1 | # 示例:在当前 cgroup 中创建新命名空间 |
七、从 v1 迁移到 v2
通用 Pod(无感知迁移)
对于普通业务容器,cgroup v1 → v2 的迁移对用户透明,无需修改。容器运行时(如 containerd)会处理 cgroup 文件系统的挂载差异。
需要关注迁移的场景:
- 容器内运行 systemd:systemd 会尝试挂载和管理 cgroup,行为因 v1/v2 和挂载权限而异
- Java/Node.js 应用:老版本 JDK/runtime 不识别 v2 接口,无法正确获取 CPU/内存限制
容器 cgroup 挂载权限矩阵
| Case | cgroup 版本 | 特权容器 | cgroupns 启用 | /sys/fs/cgroup 权限 | 容器内视图 |
|---|---|---|---|---|---|
| 1 | v1 | 是 | 否 | rw | 自身 cgroup 路径 |
| 2 | v1 | 否 | 否 | ro | 自身 cgroup 路径 |
| 3 | v2 | 是 | 否 | rw | 整个宿主机 cgroup 树 |
| 4 | v2 | 否 | 是 | ro | 自身 cgroup 路径 |
非特权容器(runc)在 v2 下通过 cgroupns 隔离视图,配置示例:
1 | { |
容器内看到的 cgroup 根:
1 | /sys/fs/cgroup/ |
八、Systemd 容器迁移
问题背景
部分需要在容器内运行 systemd 的 Pod(如 cassini 类存储 Pod),在挂载 /sys/fs/cgroup 为只读模式时,迁移到 cgroup v2 后可能启动失败。
原因:
- v1 下,即使宿主机 cgroup 以只读挂载,systemd 会在自己的 cgroup 路径内重新挂载子控制器,可以正常工作
- v2 下,systemd 尝试将自身移入
init.scope,并管理子 cgroup,需要对/sys/fs/cgroup有写权限
各场景兼容性矩阵
| Case | cgroup | 特权容器 | 挂载 /sys/fs/cgroup 读写 | systemd 能否启动 | 说明 |
|---|---|---|---|---|---|
| 1 | v1 | 是 | 否 | 是 | 容器仅看到自身 cgroup 路径;systemd 在容器内创建 kubepods.* 子树 |
| 2 | v1 | 否 | 否 | 否 | 无写权限,systemd 无法初始化 |
| 3 | v1 | 是 | 是 | 是 | 挂载宿主机完整 cgroup;systemd 在宿主机树下创建子 cgroup |
| 4 | v1 | 否 | 是 | 是 | 同上,非特权但可写 |
| 5 | v2 | 是 | 否 | 是 | 宿主机完整 cgroup 树挂载进容器(无 cgroupns),systemd 可操作 |
| 6 | v2 | 否 | 否 | 否 | 无写权限 |
| 7 | v2 | 是 | 是 | 是 | 宿主机完整 cgroup 树,读写,systemd 正常 |
| 8 | v2 | 否 | 是 | Ubuntu 20.04: 否;Ubuntu 22.04: 是 | 取决于 systemd 版本 |
Case 8 说明(非特权 + v2 + 挂载读写)
Ubuntu 20.04 自带 systemd 245,无法正确处理 cgroup 命名空间,导致启动失败。
解决方法:**挂载 /sys 而非 /sys/fs/cgroup**。
1 | # Pod Spec |
挂载 /sys 后,容器运行时默认挂载 cgroup:
1 | cgroup on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) |
systemd 和服务进程均位于容器自身的 cgroup 目录下:
1 | # systemd 在 init.scope |
九、Java / Node.js 应用兼容性
OpenJDK 容器感知
OpenJDK 通过读取 cgroupfs 检测容器资源限制,并据此设定 JVM 堆大小(通常为内存限制的 ½)。
测试环境(limits: cpu=2, memory=2Gi;requests: cpu=1, memory=1Gi):
| JDK 版本 | cgroup 版本 | BestEffort Pod | 非 BestEffort Pod |
|---|---|---|---|
| 11.0.13 | v1 | 可识别(Provider: cgroupv1,Effective CPU/Memory 正确) | 可识别 |
| 11.0.13 | v2 | No metrics available for this platform(无法识别) |
无法识别 |
- JDK 11.0.13 不支持 cgroup v2,需升级到 JDK 15+ 以获得 v2 支持
- 无法感知限制时,JVM 将使用宿主机全量资源计算默认堆大小,可能 OOM
Node.js 应用
| 版本 | cgroup 版本 | 表现 |
|---|---|---|
| 18.13 | v1 | total_heap_size 按容器限制计算 |
| 18.13 | v2 | total_heap_size 按容器限制计算(18.x 已支持 v2) |
Node.js 18.x 已原生支持 cgroup v2,无需特殊处理。
十、将 eBPF 程序挂载到 cgroup
cgroup 提供多个 eBPF attach 点,可用于网络流量监控、访问控制等:
常用 section 类型
SEC("cgroup-skb/ingress"):挂载到 cgroup 内进程的入站网络流量路径SEC("cgroup-skb/egress"):挂载到 cgroup 内进程的出站网络流量路径
内核示例(hbm_edt_kern.c)
1 | SEC("cgroup_skb/egress") |
cilium/ebpf 示例
1 | SEC("cgroup_skb/egress") |
加载和挂载步骤
1 | # 挂载 BPF 文件系统 |
cgroup 文件系统访问监控
cgroup 挂载为虚拟文件系统(cgroupfs / cgroup2fs),访问 cgroup 等同于文件系统操作。可通过 kprobe 监控谁在读写 cgroup:
1 | // 内核探针 |
NameToHandleAt 系统调用也可用于获取 cgroup 路径的文件句柄,无需 /proc 路径即可访问 cgroup 节点。