在 Linux 系统的进程资源管理体系中,Control Groups(cgroups)是最核心的机制之一。从 2007 年首次引入内核,到 2015 年 cgroups v2 合并入 4.5 内核,再到如今主流发行版全面切换 v2,cgroups 已经成为容器运行时、系统服务管理和多租户资源隔离的基石。本文将深入剖析 cgroups v2 的统一层级架构、核心控制器工作原理,并结合生产环境给出实用的资源限制配置方案。

一、Cgroups v1 的问题与 v2 的设计动机
要理解 cgroups v2 的设计,必须先明确 v1 的核心缺陷。cgroups v1 采用了多层级树(multiple hierarchies)的设计,每个控制器可以挂载到独立的层级上,这带来了几个严重问题:
- 控制器之间状态不一致:进程可以同时属于不同层级的不同 cgroup,导致 CPU 限制在层级 A、内存限制在层级 B,两者之间没有关联,无法实现统一的资源策略
- 管理复杂度爆炸:12 个控制器意味着最多 12 个层级树,运维人员需要分别管理每棵树的进程归属
- 命名混乱:v1 的 cgroup 路径在不同控制器下含义不同,进程的 /proc/
/cgroup 文件包含多行记录,每行对应一个层级 - 委托困难:容器运行时无法安全地将子 cgroup 委托给容器内部管理,因为不同层级的委托范围难以协调
cgroups v2 的核心设计决策是统一层级(unified hierarchy):所有控制器挂载在同一棵树上,一个进程在任意时刻只属于一个 cgroup。这个看似简单的改变,从根本上解决了上述所有问题。
二、Cgroups v2 统一层级架构详解
2.1 文件系统布局
cgroups v2 挂载在
1 | /sys/fs/cgroup |
(类型为 cgroup2),所有控制器共享这一个挂载点。可以通过以下命令确认系统是否运行在 v2 模式:
1
2
3
4
5
6
7
8
9
10
11 # 检查 cgroup 版本
mount | grep cgroup
# v2 输出: cgroup2 on /sys/fs/cgroup type cgroup2
# 查看当前进程的 cgroup
cat /proc/self/cgroup
# v2 输出: 0::/user.slice/user-0.slice/session-1.scope
# 查看支持的控制器
cat /sys/fs/cgroup/cgroup.controllers
# 输出: cpuset cpu io memory hugetlb pids rdma misc
v2 的 cgroup 核心接口文件包括:
| 文件 | 作用 | ||
|---|---|---|---|
|
当前 cgroup 启用的控制器(读) | ||
|
控制子 cgroup 可用控制器的开关(写) | ||
|
当前 cgroup 中的进程 PID 列表 | ||
|
当前 cgroup 中的线程 TID 列表(需开启 threaded 模式) | ||
|
cgroup 类型:domain / domain threaded / domain invalid / threaded | ||
|
子 cgroup 最大嵌套深度 | ||
|
子 cgroup 最大数量 | ||
|
统计信息:nr_descendants / nr_dying_descendants | ||
|
写入1可杀死该 cgroup 下所有进程(内核5.14+) |
2.2 控制器启用的委托机制
v2 的控制器启用采用自上而下委托模型。父 cgroup 通过
1 | cgroup.subtree_control |
将控制器委托给子 cgroup,子 cgroup 才能使用对应的资源控制文件。这是 v2 与 v1 最大的行为差异之一:
1
2
3
4
5
6
7
8
9 # 在根 cgroup 启用 cpu 和 memory 控制器向下传递
echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control
# 创建子 cgroup
mkdir /sys/fs/cgroup/myapp
# 确认子 cgroup 获得了控制器
cat /sys/fs/cgroup/myapp/cgroup.controllers
# 输出: cpu memory
关键规则:一个控制器只有在所有子 cgroup 都不依赖它时,才能从
1 | cgroup.subtree_control |
中移除。这保证了资源限制的一致性——不会出现子 cgroup 的限制突然失效的情况。
2.3 线程模式(Threaded Mode)
v2 默认是进程粒度管理,但某些场景(如按线程分配 CPU)需要线程级控制。v2 引入了 threaded 模式:
1
2
3
4
5
6 # 创建 threaded 子 cgroup
mkdir /sys/fs/cgroup/myapp/threads
echo "threaded" > /sys/fs/cgroup/myapp/threads/cgroup.type
# 将特定线程加入
echo 12345 > /sys/fs/cgroup/myapp/threads/cgroup.threads
注意:threaded cgroup 只能使用支持线程化的控制器(cpu、cpuset、pids),memory 等控制器不支持线程粒度。threaded cgroup 的资源统计会归入其最近的 domain 祖先。

三、核心控制器深度解析
3.1 CPU 控制器
v2 的 CPU 控制器与 v1 的 cpu + cpuacct 合并,使用全新的权重分配模型:
1
2
3
4
5 # CPU 权重设置(范围 1-10000,默认 100)
echo 200 > /sys/fs/cgroup/myapp/cpu.weight
echo 50 > /sys/fs/cgroup/otherapp/cpu.weight
# 含义:myapp 与 otherapp 争抢 CPU 时,myapp 获得 200/(200+50)=80% 的时间片
v2 还新增了
1 | cpu.max |
接口,直接替代 v1 的 cpu.cfs_quota_us + cpu.cfs_period_us:
1
2
3
4
5
6 # 限制最多使用 2 个 CPU 核
echo "max 100000" > /sys/fs/cgroup/myapp/cpu.max # 不限制(默认)
echo "200000 100000" > /sys/fs/cgroup/myapp/cpu.max # 每 100ms 周期可用 200ms = 2核
# 也可以用单值语法(周期默认 100000)
echo "200000" > /sys/fs/cgroup/myapp/cpu.max
1 | cpu.stat |
提供了详细的统计信息:
1
2
3
4
5
6
7
8 usage_usec 123456789 # CPU 使用总时间(微秒)
user_usec 98765432 # 用户态时间
system_usec 24691357 # 内核态时间
nr_periods 12345 # 经历的周期数
nr_throttled 678 # 被限流的周期数
throttled_usec 5678900 # 被限流的总时间
nr_burst 0 # 突发次数
burst_usec 0 # 突发总时间
3.2 Memory 控制器
Memory 控制器是生产环境中最常用的,v2 在接口设计上做了大量简化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 # 设置内存上限(字节)
echo "4G" > /sys/fs/cgroup/myapp/memory.max
# 设置内存+swap 上限
echo "6G" > /sys/fs/cgroup/myapp/memory.swap.max
# 设置内存低水位(低于此值不会被回收,内核尝试保护)
echo "3G" > /sys/fs/cgroup/myapp/memory.low
# 设置内存高水位(超过此值开始异步回收)
echo "3.5G" > /sys/fs/cgroup/myapp/memory.high
# OOM 组开关(子 cgroup 作为一个整体被 OOM kill)
echo 1 > /sys/fs/cgroup/myapp/memory.oom.group
v2 的内存保护层级(从强到弱):
-
1memory.min
:硬保护——内核绝不回收低于此值的内存,即使触发全局 OOM
-
1memory.low
:软保护——除非所有 cgroup 都无保护内存可回收,否则不回收
-
1memory.high
:限流阈值——超过此值后分配被限速(throttle),触发后台回收
-
1memory.max
:硬上限——超过此值触发 OOM kill 或分配失败
这个分级保护机制是 v2 的重大改进。在 v1 中只有 hard limit 和 soft limit,soft limit 的语义模糊,实际行为不可预测。v2 的 min/low/high/max 四级体系让资源保护策略更精细。
1
2
3
4
5
6
7
8
9
10
11 # 查看内存统计
cat /sys/fs/cgroup/myapp/memory.stat | head -20
# 输出示例:
anon 1073741824 # 匿名页(栈、堆、mmap)
file 536870912 # 文件缓存页
kernel_stack 8388608 # 内核栈
pagetables 16777216 # 页表
percpu 4194304 # per-CPU 数据
sock 8388608 # socket 缓冲区
shmem 0 # 共享内存
...
3.3 IO 控制器
v2 统一了 v1 的 blkio 控制器,并引入了基于权重的 IO 调度:
1
2
3
4
5
6
7
8
9
10
11
12 # 设置 IO 权重(范围 1-10000,默认 100)
echo 200 > /sys/fs/cgroup/myapp/io.weight
echo 50 > /sys/fs/cgroup/otherapp/io.weight
# 设置 IO 速率上限(字节/秒 或 IOPS)
echo "8:0 104857600" > /sys/fs/cgroup/myapp/io.max # /dev/sda 限速 100MB/s
echo "8:0 rbps=104857600 wbps=52428800 riops=5000 wiops=3000" > /sys/fs/cgroup/myapp/io.max
# 查看实际设备号
ls -l /dev/sda
# brw-rw---- 1 root disk 8, 0 ... /dev/sda
# 主设备号 8, 次设备号 0 → "8:0"
IO 控制器的统计信息非常详细:
1
2
3
4 cat /sys/fs/cgroup/myapp/io.stat
# 输出:
8:0 rbytes=1073741824 wbytes=536870912 rios=131072 wios=65536
dbytes=0 dios=0
3.4 PIDs 控制器
PIDs 控制器限制 cgroup 中的进程/线程数量,防止 fork 炸弹:
1
2
3
4
5
6 # 限制最大进程数
echo 1000 > /sys/fs/cgroup/myapp/pids.max
# 查看当前进程数
cat /sys/fs/cgroup/myapp/pids.current
# 输出: 42
3.5 Cpuset 控制器
Cpuset 在 v2 中仍然可用,用于将进程绑定到特定的 CPU 和 NUMA 节点:
1
2
3
4
5
6
7
8 # 绑定到 CPU 0-3
echo "0-3" > /sys/fs/cgroup/myapp/cpuset.cpus
# 绑定到 NUMA 节点 0
echo "0" > /sys/fs/cgroup/myapp/cpuset.mems
# 查看生效的 CPU
cat /sys/fs/cgroup/myapp/cpuset.cpus.effective

四、生产环境资源限制实战
4.1 为 Web 服务配置资源隔离
以下是一个典型的 Nginx + Node.js 应用的 cgroups v2 配置方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 #!/bin/bash
# setup-app-cgroups.sh — 为生产应用配置 cgroups v2 资源隔离
CGROUP_ROOT=/sys/fs/cgroup
# 1. 确保控制器已启用
echo "+cpu +memory +io +pids" > $CGROUP_ROOT/cgroup.subtree_control
# 2. 创建应用 cgroup 树
mkdir -p $CGROUP_ROOT/prod/nginx
mkdir -p $CGROUP_ROOT/prod/nodeapp
# 委托控制器到子级
echo "+cpu +memory +io +pids" > $CGROUP_ROOT/prod/cgroup.subtree_control
# 3. Nginx:2核 CPU、2G 内存、100MB/s IO
echo 200 > $CGROUP_ROOT/prod/nginx/cpu.weight
echo "200000" > $CGROUP_ROOT/prod/nginx/cpu.max # 2核
echo "2G" > $CGROUP_ROOT/prod/nginx/memory.max
echo "2.5G" > $CGROUP_ROOT/prod/nginx/memory.swap.max
echo "1.5G" > $CGROUP_ROOT/prod/nginx/memory.low # 保护 1.5G
echo 500 > $CGROUP_ROOT/prod/nginx/pids.max
# 4. Node.js 应用:4核 CPU、4G 内存
echo 400 > $CGROUP_ROOT/prod/nodeapp/cpu.weight
echo "400000" > $CGROUP_ROOT/prod/nodeapp/cpu.max # 4核
echo "4G" > $CGROUP_ROOT/prod/nodeapp/memory.max
echo "6G" > $CGROUP_ROOT/prod/nodeapp/memory.swap.max
echo "3G" > $CGROUP_ROOT/prod/nodeapp/memory.low
echo 1000 > $CGROUP_ROOT/prod/nodeapp/pids.max
# 5. 将进程移入 cgroup
NGINX_PID=$(pgrep -f "nginx: master" | head -1)
echo $NGINX_PID > $CGROUP_ROOT/prod/nginx/cgroup.procs
4.2 Systemd 集成(推荐方式)
在现代 Linux 系统上,systemd 是 cgroups v2 的主要管理者。通过 slice + service 单元可以声明式配置资源限制:
1
2
3
4
5
6
7
8
9
10 # /etc/systemd/system/prod.slice
[Unit]
Description=Production Workloads Slice
[Slice]
CPUWeight=200
MemoryHigh=8G
MemoryMax=10G
MemorySwapMax=12G
IOWeight=200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 # /etc/systemd/system/nodeapp.service
[Unit]
Description=Node.js Application
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/app/server.js
Slice=prod.slice
# 资源限制
CPUWeight=400
CPUQuota=400% # 4核
MemoryHigh=4G
MemoryMax=4G
MemorySwapMax=6G
MemoryLow=3G
IOWeight=300
TasksMax=1000
# 安全加固
ProtectSystem=strict
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
1
2
3
4
5
6
7
8 # 应用配置
systemctl daemon-reload
systemctl enable nodeapp.service
systemctl start nodeapp.service
# 验证 cgroup 配置
systemd-cgtop # 实时查看 cgroup 资源使用
systemctl show nodeapp -p CGroup # 查看服务 cgroup 路径
4.3 容器运行时的 cgroups v2 配置
Docker 和 containerd 已全面支持 cgroups v2。以下是一些关键配置:
1
2
3
4
5
6
7
8
9
10
11 # Docker run 使用 cgroups v2 资源限制
docker run -d \
--name myapp \
--cpus=4 \
--cpu-shares=512 \
--memory=4g \
--memory-swap=6g \
--memory-reservation=3g \
--pids-limit=1000 \
--device-write-bps /dev/sda:100MB \
myapp:latest
值得注意的是,cgroups v2 下 Docker 的内存行为有一些变化:
1 | memory.swap.max |
是独立于
1 | memory.max |
的,不再像 v1 那样 swap limit = memory + swap – memory。v2 中
1 | memory.swap.max |
直接表示可用的 swap 大小。
五、监控与调试
5.1 实时监控工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 # systemd-cgtop — 实时查看 cgroup 资源使用
systemd-cgtop
# 输出示例:
# Control Group Tasks %CPU Memory Input/s Output/s
# / 345 12.3 4.2G 0B 0B
# /prod 120 8.5 3.1G 0B 0B
# /prod/nodeapp 45 6.2 2.4G 0B 0B
# /prod/nginx 15 0.3 128M 0B 0B
# 查看特定 cgroup 的内存压力
cat /sys/fs/cgroup/prod/nodeapp/memory.pressure
# some avg10=2.34 avg60=1.89 avg300=1.12
# full avg10=0.00 avg60=0.00 avg300=0.00
# 查看 CPU 压力
cat /sys/fs/cgroup/prod/nodeapp/cpu.pressure
# some avg10=5.67 avg60=3.45 avg300=2.01
5.2 PSI(Pressure Stall Information)
PSI 是 cgroups v2 的重要新增功能,它量化了资源争抢的程度:
-
1some
:至少有一个任务在等待资源的时间占比
-
1full
:所有任务都在等待资源的时间占比(完全停滞)
PSI 可用于构建自动扩缩容策略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 #!/bin/bash
# psi-monitor.sh — 基于 PSI 的自动告警脚本
CGROUP=/sys/fs/cgroup/prod/nodeapp
ALERT_THRESHOLD=10.0 # 10% 压力阈值
while true; do
# 读取 10 秒平均内存压力
MEM_PRESSURE=$(awk '/^some/ {print $2}' $CGROUP/memory.pressure | \
sed 's/avg10=//')
if (( $(echo "$MEM_PRESSURE > $ALERT_THRESHOLD" | bc -l) )); then
echo "[$(date)] ALERT: Memory pressure at ${MEM_PRESSURE}%"
# 触发告警或扩容逻辑
fi
sleep 30
done
5.3 压力测试与验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 # CPU 压力测试
stress-ng --cpu 4 --timeout 60s --metrics-brief &
# 观察 cgroup CPU 限流
cat /sys/fs/cgroup/prod/nodeapp/cpu.stat | grep throttled
# 内存压力测试
stress-ng --vm 2 --vm-bytes 2G --timeout 60s &
# 观察内存回收和 OOM 行为
dmesg -w | grep -i oom
# IO 压力测试
fio --name=test --filename=/tmp/testfile --size=1G \
--rw=randwrite --bs=4k --iodepth=64 --numjobs=4 &
# 观察 IO 限流
cat /sys/fs/cgroup/prod/nodeapp/io.stat
六、v1 到 v2 迁移常见问题
6.1 兼容性检查
1
2
3
4
5
6
7
8
9
10 # 检查当前 cgroup 版本
stat -fc %T /sys/fs/cgroup
# cgroup2_fs → v2
# tmpfs → v1(或混合模式)
# 检查内核是否支持 v2(需要 4.5+,推荐 5.8+)
uname -r
# 检查 systemd 版本(需要 232+,推荐 244+)
systemctl --version
6.2 常见迁移问题
- Docker 兼容性:Docker 20.10+ 支持 cgroups v2,但旧版本需要升级。在 v2 下
1--memory-swappiness
参数已废弃
- 自定义 cgroup 路径:v1 的 /sys/fs/cgroup/cpu/、/sys/fs/cgroup/memory/ 等路径不再存在,所有操作都在 /sys/fs/cgroup/ 下
- 控制器名称变化:v1 的 blkio → v2 的 io;v1 的 cpu+cpuacct → v2 的 cpu
- systemd 自动管理:在 v2 模式下不要手动操作 /sys/fs/cgroup/,通过 systemd 单元文件配置更安全可靠
- 混合模式问题:某些发行版可能同时挂载 v1 和 v2(hybrid 模式),建议完全切换到 v2 以避免混淆
6.3 强制启用 cgroups v2
对于仍在使用 v1 或混合模式的系统,可以通过内核参数强制启用 v2。编辑 GRUB 配置文件
1 | /etc/default/grub |
,添加内核参数
1 | systemd.unified_cgroup_hierarchy=1 |
,然后更新 GRUB 并重新启动系统即可。

七、最佳实践总结
- 始终通过 systemd 管理资源限制:手动写 cgroup 文件容易出错,systemd 单元文件是声明式、可版本化、可审计的
- 善用 memory.low 和 memory.high:不要只设 memory.max 硬上限,memory.high 的限流机制能在 OOM 之前给出缓冲
- 使用 PSI 而非纯使用率做告警:使用率 80% 不代表有压力(可能刚好在稳定运行),PSI 直接衡量争抢程度
- 为关键服务设置 memory.min:保证数据库、缓存等关键服务的内存不被回收
- 设置 pids.max 防 fork 炸弹:即使信任的服务也可能因为 bug 导致进程泄漏
- 监控 throttled 指标:CPU throttled 次数持续增长说明需要提高配额或优化代码
- 分层设置资源限制:slice → service 多级设置,上层保证总体不超限,下层精细分配
cgroups v2 不只是 v1 的接口升级,而是一次架构上的根本改进。统一层级让资源策略变得可预测,PSI 让资源争抢可观测,分级内存保护让关键服务更安全。随着 Kubernetes 1.25+ 默认使用 cgroups v2,以及各大发行版全面切换,深入理解 v2 已经是每个运维工程师和后端开发者的必备技能。
汤不热吧