欢迎光临

Linux Cgroups v2 深度解析:统一层级架构、核心控制器与生产环境资源限制实战

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

Linux Cgroups v2 Architecture

一、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 核心接口文件包括:

文件 作用
1
cgroup.controllers
当前 cgroup 启用的控制器(读)
1
cgroup.subtree_control
控制子 cgroup 可用控制器的开关(写)
1
cgroup.procs
当前 cgroup 中的进程 PID 列表
1
cgroup.threads
当前 cgroup 中的线程 TID 列表(需开启 threaded 模式)
1
cgroup.type
cgroup 类型:domain / domain threaded / domain invalid / threaded
1
cgroup.max.depth
子 cgroup 最大嵌套深度
1
cgroup.max.descendants
子 cgroup 最大数量
1
cgroup.stat
统计信息:nr_descendants / nr_dying_descendants
1
cgroup.kill
写入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 祖先。

Linux Kernel Development

三、核心控制器深度解析

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 的内存保护层级(从强到弱):

  • 1
    memory.min

    :硬保护——内核绝不回收低于此值的内存,即使触发全局 OOM

  • 1
    memory.low

    :软保护——除非所有 cgroup 都无保护内存可回收,否则不回收

  • 1
    memory.high

    :限流阈值——超过此值后分配被限速(throttle),触发后台回收

  • 1
    memory.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

Server Infrastructure

四、生产环境资源限制实战

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 的重要新增功能,它量化了资源争抢的程度:

  • 1
    some

    :至少有一个任务在等待资源的时间占比

  • 1
    full

    :所有任务都在等待资源的时间占比(完全停滞)

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 并重新启动系统即可。

Data Center Operations

七、最佳实践总结

  • 始终通过 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 已经是每个运维工程师和后端开发者的必备技能。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Linux Cgroups v2 深度解析:统一层级架构、核心控制器与生产环境资源限制实战
分享到: 更多 (0)