文章

cgroup v2 详解

cgroup v2 详解

概述

cgroup(Control Group)是 Linux 内核实现资源隔离与限制的核心机制。Kubernetes 通过 cgroup 将 CPU、内存、I/O 等资源约束施加到 Pod 和容器上。cgroup v2 自 Linux 4.5 引入,v5.2 功能趋于完整,成为 K8s v1.31+ 的唯一选项

一句话:没有 cgroup,就没有 Pod QoS 的 Guaranteed / Burstable / BestEffort。

v1 vs v2:为什么必须迁移

架构层面的质变

cgroup v1:                               cgroup v2:
/sys/fs/cgroup/                          /sys/fs/cgroup/
  ├── cpu/                                 ├── cgroup.controllers
  │   └── kubepods/                        ├── cgroup.subtree_control
  │       └── pod-xxx/                     └── kubepods.slice/
  │           └── cpu.shares                   ├── cpu.weight
  ├── memory/                                 ├── memory.max
  │   └── kubepods/                           └── kubepods-burstable.slice/
  │       └── pod-xxx/                            └── pod-xxx/
  │           └── memory.limit_in_bytes               ├── cpu.weight
  ├── blkio/                                          └── memory.max
  │   └── kubepods/
  └── ...                                  所有控制器在同一棵树下
      每个子系统独立一棵树

v1 的本质问题:同一进程的 CPU 和内存约束分布在两棵独立的树上,无关联、无统一视图。v2 用一个统一层级解决了这个问题。

关键差异

维度cgroup v1cgroup v2
层级结构每个子系统独立树统一层级
进程归属一个进程可跨子树(混乱)一个进程只能在一个叶子节点
内存压力PSI (Pressure Stall Information)
OOM 控制OOM killer 按 cgroup 独立决策memory.oom.group 可杀整组进程
线程控制cgroup v1 自身不支持threaded 模式
委托模型无标准统一 delegation 模型

K8s 迁移时间线

K8s 版本cgroup v2 状态
v1.25GA,默认仍用 v1
v1.27默认 v1,但 v2 完全可用
v1.29kubelet 新增 --cgroup-driver=systemd 对 v2 的改进支持
v1.31cgroup v2 强制要求,不再支持 v1

v1 → v2 接口映射与语义改动

上面讲的是架构层面的质变,下面给出逐控制器的接口改名、语义变化、以及被彻底移除的控制器——这是从 v1 迁移时最容易踩坑、也最常被问到的”到底改了什么”。

控制器接口重命名对照

功能cgroup v1 文件cgroup v2 文件备注
CPU 权重cpu.shares(2–262144,默认 1024)cpu.weight(1–10000,默认 100)二者非线性等价,见下方语义变化
CPU 带宽上限cpu.cfs_quota_us + cpu.cfs_period_uscpu.max = "$QUOTA $PERIOD"(微秒)"max 100000" = 不限制
CPU 实时带宽cpu.rt_runtime_us / cpu.rt_period_uscpu.rt.max = "$RUNTIME $PERIOD"v2 单独文件,与 cpu.max 解耦
CPU 统计cpuacct.usage / cpuacct.statcpu.statusage_usec/user_usec/system_usec单位改为微秒
内存硬限memory.limit_in_bytesmemory.max
内存软限memory.soft_limit_in_bytes(仅”建议”,内核不强制)memory.high(达到即节流分配、激进回收,但不 OOM)语义显著不同,见下
内存保护无对应memory.low(最佳保护)/ memory.min(硬保护,绝不回收)v1 无此能力
swap 上限memory.swap_limit_in_bytes + memory.memsw.limit_in_bytesmemory.swap.maxv2 拆开,无 memsw 概念
OOM 控制memory.oom_controloom_kill_disable=1 可抑制但风险大)memory.oom.group(=1 时杀整组进程)设计思路不同
内存统计memory.stat(字段少)memory.stat(字段更细,含 kernel/sock/slab 等)口径变化,影响 Java OOM
I/O 权重blkio.weightio.weight
I/O 带宽blkio.throttle.read_bps_deviceio.max = "$DEV $RBPS $WBPS $RIOPS $WIOPS"统一格式
I/O 统计blkio.*_service_bytesio.stat(每设备)
进程数pids.current / pids.max同名pids 控制器 v1/v2 基本一致
CPU/内存绑定cpuset.cpus / cpuset.mems同名;新增 cpuset.cpus.effective / cpuset.mems.effectivev2 多了 effective 视图
冻结freezer.state(独立 freezer 子系统)cgroup.freeze(=1 冻结整组)v2 统一到根层级
事件通知各子系统各自 notify_on_releasecgroup.events(统一,含 populated / frozenv2 统一事件接口

v2 彻底移除的控制器

v1 的某些子系统在 v2 中没有对应物,被更现代的机制替代:

v1 控制器现状替代方案
net_cls / net_prio移除eBPF(如 Cilium 基于 eBPF 做流量打标与优先级)
devices移除eBPF / LSM(systemd DeviceAllow 底层即用 BPF device filter)
perf_event移除独立挂载v2 统一层级下 perf 直接可用,无需单独挂子系统
debug移除

迁移影响:依赖 devices 控制器做容器设备白名单的老方案在 v2 下失效,必须改用 eBPF-based device filter(containerd/CRI-O 已默认走这条路)。

v2 才有的三个”硬规则 / 新能力”

  1. No Internal Process(NIP)约束:v2 规定——一个 cgroup 一旦在 cgroup.subtree_control 中启用了子控制器(即它有”域”子节点),就不能再直接包含进程,进程只能放在叶子节点。这消除了 v1 中”进程挂在中间节点导致统计混乱”的问题,也是 v2 统一视图能成立的前提。
  2. 统一委托模型(delegation)cgroup.procscgroup.subtree_control 可被 chown 给非 root 用户,实现安全的子树委托(rootless 容器、systemd user 实例都依赖它)。v1 无标准委托机制。
  3. threaded 模式:v2 允许以”线程”粒度(而非进程)做资源控制,cgroup.type 可设为 threaded,同一进程内的不同线程可落入不同 cgroup。v1 完全不支持。

语义变化要点(最容易踩坑)

  • memory.highmemory.soft_limit_in_bytes 的别名:v1 软限只是”建议”,内核不强制;v2 的 memory.high 是”硬节流线”,超过会主动延迟分配并激进回收——延迟敏感型负载可能因此变慢但不 OOM。
  • cpu.weightcpu.shares 非等价:v2 文档给出的近似换算为 weight ≈ 1 + (shares−1)×9999/262143(如 1024 shares ≈ 40 weight),与”默认 100”并非精确对应;K8s 内部另有自己的 request→weight 映射,不与 v1 完全对齐。迁移时不要假设两者线性平移。
  • swappiness 不再可调:v2 移除了 per-cgroup 的 memory.swappiness,无法针对单个容器单独调节,只能依赖全局 vm.swappinessmemory.high/memory.swap.max 组合。
  • 统计口径变化:v2 的 memory.stat 把 kernel 内存(sock、slab、kernel_stack)计入,Java 等基于 RSS 估算堆外内存的应用在 v2 下更容易触发 OOM(见下方”迁移常见坑”)。

五大控制器

cpu —— CPU 带宽与权重

两个独立的控制维度:权重(比例共享)和带宽(硬上限)。

文件含义示例
cpu.weight权重(默认 100),范围 [1, 10000]K8s requests.cpu 不可压缩资源通过 weight 映射
cpu.max$MAX $PERIOD,带宽限制(微秒)"20000 100000" = 0.2 核;"max 100000" = 不限制
cpu.stat使用统计(usage_usec, user_usec, system_usec)
cpu.pressurePSI 指标(some/full, avg10/avg60/avg300)

K8s 映射规则:

  • requests.cpucpu.weight(按比例换算:1 核请求 ≈ 1024 weight)
  • limits.cpucpu.max(直接设置带宽上限)
  • requests == limits 且为整数核 → Guaranteed Qos,触发 CPU Manager exclusive allocation
# 查看 Pod 的 CPU 约束
POD_CGROUP=$(cat /proc/$(pgrep -f "sleep" | head -1)/cgroup | awk -F: '{print $3}')
echo "CPU weight: $(cat /sys/fs/cgroup/$POD_CGROUP/cpu.weight)"
echo "CPU max:    $(cat /sys/fs/cgroup/$POD_CGROUP/cpu.max)"

memory —— 内存与 swap

文件含义
memory.max硬限制(字节),达到后触发 OOM
memory.high软限制,达到后节流(throttle allocation)但不 OOM,优先回收
memory.low最佳保护线,内存紧张时尽量不回收低于此线的内存
memory.min硬保护线,低于此线的内存绝不被回收
memory.current当前使用量
memory.swap.maxswap 上限(0 = 禁止 swap,max = 不限制)
memory.oom.group设为 1 时 OOM 杀掉整个 cgroup 的所有进程
memory.stat详细统计(anon, file, kernel_stack, slab, sock, …)
memory.pressurePSI 内存压力指标

K8s 映射:

  • limits.memorymemory.max
  • requests.memory → 影响 OOM 打分(oom_score_adj),不直接映射到 memory.low
# 查看 Pod 内存压力
POD_CGROUP="/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid>.slice"
cat $POD_CGROUP/memory.pressure
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

io —— 块设备 I/O 控制

文件含义
io.weightI/O 权重(默认 100),类似 cpu.weight
io.maxI/O 带宽硬限制:$DEV $RBPS $WBPS $RIOPS $WIOPS
io.stat每设备读写字节/操作统计
io.pressurePSI I/O 压力指标
# 限制某 cgroup 对 sda 的写带宽为 10MB/s
echo "8:0 rbps=10485760 wbps=10485760" > io.max

注意:io 控制器对 buffered I/O 的控制有限,仅直接影响 direct I/O。

pids —— 进程数限制

防止 fork bomb 或进程泄漏:

# 限制该 cgroup 最多 100 个进程
echo 100 > pids.max

K8s 通过 --pod-max-pids 使用(默认 -1 不限制)。

cpuset —— CPU/内存节点绑定

指定 cgroup 进程只能运行在哪些 CPU 和 NUMA 节点上:

文件含义
cpuset.cpus允许使用的 CPU 列表(如 0-3,8-11
cpuset.mems允许使用的 NUMA 内存节点(如 0
cpuset.cpus.effective实际生效的 CPU(受父节点限制)

这是 K8s CPU Manager static policy 的底层机制。

PSI —— 资源压力的新语言

PSI(Pressure Stall Information)量化了”有多少任务因为等不到资源而被阻塞”。它能区分 some(部分任务阻塞)和 full(所有任务阻塞),分别给出 10s/60s/300s 的平均值。

# 解读 PSI 指标
cat /sys/fs/cgroup/kubepods.slice/cpu.pressure
# some avg10=5.23 avg60=3.15 avg300=1.08 total=12345678
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# ↑ some=5.23 表示过去 10 秒平均有 5.23% 的时间有任务在等 CPU

# 使用 PSI 做主动 OOM(比直接 OOM 更平滑)
# 在 memory.pressure 的 some 指标超过阈值时主动驱逐低优先级 Pod

K8s 社区正在利用 PSI 做更智能的驱逐决策(替代粗暴的 memory.available < 100Mi)。

实践:从 v1 迁移到 v2

迁移前检查

# 1. 确认内核支持
grep cgroup /proc/filesystems
# 应有 nodev cgroup2

# 2. 确认当前运行模式
stat -fc %T /sys/fs/cgroup/
# cgroup2fs → v2
# tmpfs → v1

# 3. 检查 containerd/cri-o 配置
# containerd: /etc/containerd/config.toml
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
#   SystemdCgroup = true  ← 必须为 true

# 4. 检查 kubelet 启动参数
# --cgroup-driver=systemd  ← 推荐

迁移步骤

# step 1: 逐一 cordon + drain 节点
kubectl cordon node-1
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data

# step 2: 添加内核启动参数
# /etc/default/grub
GRUB_CMDLINE_LINUX="... systemd.unified_cgroup_hierarchy=1 cgroup_no_v1=all"
update-grub
reboot

# step 3: 验证
stat -fc %T /sys/fs/cgroup/  # cgroup2fs

# step 4: 恢复节点
kubectl uncordon node-1

迁移常见坑

问题原因解决
容器无法启动containerd/cri-o 未配置 SystemdCgroup=true检查 runtime 配置
kubectl top node 显示内存异常cgroup v1/v2 统计口径不同(total_inactive_file 处理)升级 kubelet ≥ v1.27
Prometheus cAdvisor 指标名变化v2 的路径和文件名不同升级 cAdvisor ≥ v0.47
GPU operator 异常NVIDIA GPU operator 旧版本不识别 v2升级到 ≥ v23.6
Java 应用 OOMv2 下 memory.stat 含 kernel 内存(如 sock),实际可用更少适当增大 limits,或降级内核

关联知识

参考资源

学习时间

阶段时间备注
架构理解2026-06-30完成:v1/v2 架构差异、五大控制器、PSI、迁移实践

状态: 🌱 学习中 下次复习日期: 2026-07-07