cgroup v1 与 v2 深度解析:Kubernetes 资源隔离从原理到迁移

Control Groups(cgroups)是 Linux 内核提供的进程资源管控机制,支持对 CPU、内存、磁盘 IO、网络带宽等资源进行分组限制。本文系统梳理 cgroup v1 与 v2 的核心差异、Kubernetes 集成方式、容器迁移注意事项,以及 eBPF 程序挂载到 cgroup 的使用方法。


一、cgroup 基础与 Kubernetes 集成

在 Kubernetes 中,resources.requestsresources.limits 对应 cgroup 的资源限制:

1
2
3
4
5
6
7
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: 500m
memory: 1Gi
  • 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
2
3
4
5
6
7
# 查看可用控制器
cat /sys/fs/cgroup/cgroup.controllers
# 输出: cpuset cpu io memory hugetlb pids rdma

# 启用 CPU 相关控制器
echo "+cpu" >> /sys/fs/cgroup/cgroup.subtree_control
echo "+cpuset" >> /sys/fs/cgroup/cgroup.subtree_control

六、cgroup 命名空间

基本原理

cgroup 命名空间(cgroupns)虚拟化 /proc/$PID/cgroup 文件和 cgroup 挂载点的视图,类似于 PID 命名空间对进程树的虚拟化。

启用 cgroup 命名空间后(v2 默认开启):

1
2
3
# 从宿主机查看容器进程
cat /proc/48685/cgroup
0::/kubepods.slice/kubepods-pod0e607a38_2e24_4a54_8658_6dc312143324.slice/cri-containerd-xxx.scope

未启用 cgroup 命名空间时(v1 默认):

1
2
3
cat /proc/298593/cgroup
12:pids:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod7f3aac27...
11:memory:/kubepods.slice/...

cgroupns 特性

  1. 根视图重映射:命名空间创建时,以创建进程所在的 cgroup 作为新命名空间的根(/),容器内进程看到的是自己的 cgroup 为根
  2. 资源隔离无关:cgroupns 只虚拟化层级视图,不直接提供资源隔离
  3. 生命周期:只要命名空间内有进程或 mount 引用,命名空间就存在
1
2
3
4
5
6
7
8
9
10
# 示例:在当前 cgroup 中创建新命名空间
unshare -C
cat /proc/self/cgroup
0::/ # 当前 cgroup 被映射为根

# 即使之后移动到其他 cgroup,cgroupns root 不变
mkdir sub_cgrp_1
echo 0 > sub_cgrp_1/cgroup.procs
cat /proc/self/cgroup
0::/sub_cgrp_1 # 相对于原始根显示新路径

七、从 v1 迁移到 v2

通用 Pod(无感知迁移)

对于普通业务容器,cgroup v1 → v2 的迁移对用户透明,无需修改。容器运行时(如 containerd)会处理 cgroup 文件系统的挂载差异。

需要关注迁移的场景:

  1. 容器内运行 systemd:systemd 会尝试挂载和管理 cgroup,行为因 v1/v2 和挂载权限而异
  2. 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
2
3
4
5
6
{
"destination": "/sys/fs/cgroup",
"type": "cgroup",
"source": "cgroup",
"options": ["nosuid", "noexec", "nodev", "relatime", "ro"]
}

容器内看到的 cgroup 根:

1
2
3
4
5
6
/sys/fs/cgroup/
├── cgroup.controllers
├── cgroup.max.descendants
├── cgroup.type
├── cpu.stat
└── ...

八、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
2
3
4
5
6
7
# Pod Spec
volumeMounts:
- mountPath: /sys
name: sys
volumes:
- emptyDir: {}
name: sys

挂载 /sys 后,容器运行时默认挂载 cgroup:

1
cgroup on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

systemd 和服务进程均位于容器自身的 cgroup 目录下:

1
2
3
4
5
6
7
# systemd 在 init.scope
PID 1: /sbin/init
cgroup: /kubepods.slice/.../cri-containerd-xxx.scope/init.scope

# 服务进程在 system.slice 下
PID 41: /lib/systemd/systemd-logind
cgroup: /kubepods.slice/.../system.slice/systemd-logind.service

九、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
2
3
4
SEC("cgroup_skb/egress")
int _hbm_out_cg(struct __sk_buff *skb) {
// 出站带宽管控逻辑
}

cilium/ebpf 示例

1
2
3
4
SEC("cgroup_skb/egress")
int count_egress_packets(struct __sk_buff *skb) {
// 出站包计数
}

加载和挂载步骤

1
2
3
4
5
6
7
8
9
# 挂载 BPF 文件系统
mount -t bpf none /sys/fs/bpf

# 加载 BPF 程序
bpftool prog load ./cgroup_skb.o /sys/fs/bpf/count

# 将程序挂载到 cgroup
bpftool cgroup attach /sys/fs/cgroup/kubepods.slice/... \
egress pinned /sys/fs/bpf/count

cgroup 文件系统访问监控

cgroup 挂载为虚拟文件系统(cgroupfs / cgroup2fs),访问 cgroup 等同于文件系统操作。可通过 kprobe 监控谁在读写 cgroup:

1
2
3
4
5
// 内核探针
SEC("kprobe/cgroup_file_open")
SEC("kprobe/cgroup_seqfile_show")
SEC("kprobe/cgroup_file_poll")
SEC("kprobe/cgroup_file_write")

NameToHandleAt 系统调用也可用于获取 cgroup 路径的文件句柄,无需 /proc 路径即可访问 cgroup 节点。


参考资料