cgroups v2 资源控制:统一层级、CPU/内存/IO 与 systemd 集成

深入 Linux cgroups v2 资源控制:统一层级与 v1 的差异、进程分组与 slice/scope/service 归属、控制器下放与 no-internal-process 规则、CPU 权重与配额、内存软硬上限与回收、IO 限速与延迟保护、PSI 压力指标,以及 systemd 委托机制与 cgtop 观测实践。

引言

**cgroups(control groups)是 Linux 内核的「资源账本」:它把进程分组,对每组施加 CPU、内存、IO、进程数等限制。cgroups v1 把每种资源做成独立的层级树,导致「同一个进程在不同树里的分组不一致」「挂载点碎片化」「内存与 IO 的记账对不上」等问题。cgroups v2 用一棵统一层级(unified hierarchy)**取代 v1,所有控制器挂在同一棵树上,并引入 PSI(Pressure Stall Information)、更清晰的内存水位与更好的委托(delegation)语义。如今 systemd、Docker、Kubernetes 都已默认使用 v2。

本文面向已经了解容器与资源限制基础的工程师,重点讲 v2 相对 v1 的变化、如何手动操作 cgroupfs、四大控制器(cpu/memory/io/pids)的关键参数、systemd 的委托机制,以及用 PSI 与 systemd-cgtop 做观测。

前置:namespace/cgroup 容器原理见 Linux 容器与隔离 ,其中 cgroup 部分只做了入门;CPU 调度见 Linux 进程调度与 CPU ,内存机制见 Linux 内存管理 。

1. cgroups v1 vs v2:为什么要统一层级

v1 的最大问题是层级爆炸:cpu、memory、blkio 各自一棵树,进程在每棵树里的位置独立,导致「同一个容器在 cpu 树里叫 A、在 memory 树里叫 B」,任何工具都难以还原「一个组的完整资源画像」。

v1:  /sys/fs/cgroup/cpu/       /sys/fs/cgroup/memory/    /sys/fs/cgroup/blkio/
       ├─ docker/abc            ├─ docker/abc             ├─ docker/abc
       └─ docker/def            └─ docker/def             └─ docker/def
     (三棵树,需各自维护,一致性全靠约定)

v2:  /sys/fs/cgroup/
       ├─ cgroup.controllers / cgroup.subtree_control
       └─ docker/abc/           ← 一个组,所有控制器都在这里
            ├─ cpu.max  cpu.weight
            ├─ memory.max  memory.high
            └─ io.max
维度v1v2
层级每控制器一棵树单棵统一树
控制器开关挂载即生效逐级 cgroup.subtree_control
内部进程允许有进程的组不能再开子组控制(no-internal-process)
压力指标无PSI(cpu.pressure 等)
内存水位仅 limitlow/high/max 三档 + swap 独立
委托弱强(Delegate=)
# 确认当前系统用的是 v1 还是 v2
stat -fc %T /sys/fs/cgroup
# 输出 cgroup2fs = v2;tmpfs = v1(混合模式看挂载)
mount | grep cgroup

心智:v2 的核心是「一棵树、一套开关、一个组包含所有资源」。理解这一点,后面所有参数的归属就都顺了。

2. 层级结构:slice / scope / service 与进程归属

在 systemd 系统上,cgroup 树由 systemd 自动维护,根下是几个切片(slice):

/sys/fs/cgroup/
├─ system.slice/          # 系统服务(sshd、nginx…)
│   ├─ sshd.service/
│   └─ nginx.service/
├─ user.slice/            # 用户会话
│   └─ user-1000.slice/
│       └─ session-3.scope/
├─ machine.slice/         # 虚拟机与容器(systemd-nspawn)
└─ init.scope/            # PID 1 自身
单元类型含义谁创建
.slice资源分组的目录管理员/systemd
.scope外部创建的进程(如会话、容器运行时)systemd 封装已存在进程
.servicesystemd 管理的服务进程systemd
systemd-cgls                 # 树状显示 cgroup 结构
systemd-cgls /system.slice   # 只看某切片
cat /proc/<pid>/cgroup       # 查某进程属于哪个组
# /proc/<pid>/cgroup 输出(v2 只有一行)
0::/system.slice/nginx.service

记忆:service 是「systemd 拉起的进程」,scope 是「别处创建、被 systemd 收编的进程」,slice 是「它们的分组目录」。容器进程通常落在 machine.slice 或自定义 slice 下。

3. 手动操作 cgroupfs:subtree_control 与 no-internal-process

v2 不能「挂载即用」,必须逐级把控制器下放给子组:父组的 cgroup.subtree_control 决定哪些控制器对子组可见。

# 1. 创建子组
sudo mkdir /sys/fs/cgroup/mygroup
# 2. 父组把 cpu/memory/io/pids 控制器下放给子组
echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 3. 现在子组里才出现对应控制文件
ls /sys/fs/cgroup/mygroup/
# 4. 把进程放进子组
echo <pid> | sudo tee /sys/fs/cgroup/mygroup/cgroup.procs

no-internal-process 规则是 v2 的一条硬约束:

一个 cgroup 若启用了某控制器(写进了 subtree_control),
就不能同时「自己直接持有进程」和「有子组用该控制器」——
即:有进程的组不能再往下开子组控制。

解法:把进程移到叶子组,中间组只做「分组容器」。
# 查看本组有哪些进程、有哪些子组
cat /sys/fs/cgroup/mygroup/cgroup.procs
cat /sys/fs/cgroup/mygroup/cgroup.controllers     # 可用控制器
cat /sys/fs/cgroup/mygroup/cgroup.subtree_control # 已下放的控制器

铁律:报错 EBUSY 多半撞上 no-internal-process。标准做法是「中间组不放进程、只放子组」,把实际进程全部放到叶子组。

4. CPU 控制:cpu.weight、cpu.max 与 PSI

v2 把 v1 的 cpu.shares 改名 cpu.weight(范围 1~10000,默认 100),把 cpu.cfs_quota_us/cfs_period_us 合并成 cpu.max:

# 相对权重:同层组按权重比例分 CPU
echo 200 | sudo tee /sys/fs/cgroup/mygroup/cpu.weight

# 绝对配额:每 100ms 周期内最多用 50ms CPU(= 0.5 核)
echo "50000 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max
# 完全不限:
echo "max 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max
参数语义v1 对应
cpu.weight相对权重cpu.shares(值域不同)
cpu.max配额 周期,绝对上限cpu.cfs_quota_us + cpu.cfs_period_us
cpu.weight.nice用 nice 值表达权重无
cpu.pressurePSI CPU 压力无(v2 新增)
# 查看 CPU 使用与 PSI
cat /sys/fs/cgroup/mygroup/cpu.stat
#   usage_usec / user_usec / system_usec / nr_throttled / throttled_usec
cat /sys/fs/cgroup/mygroup/cpu.pressure
#   some avg10=0.00 avg60=0.00 avg300=0.00 total=1234

心法:cpu.weight 是「不忙时随便用、忙时按比例分」,cpu.max 是「无论如何都别超过」。要「保底」用 weight,要「封顶」用 max,二者可叠加。

5. 内存控制:memory.max/high/low/swap 与回收

v2 的内存控制比 v1 精细,四档水位各司其职:

文件语义触发行为
memory.min硬保护下限低于此值内存永不被回收
memory.low软保护内存紧张时优先保护
memory.high软上限超过则节流并积极回收(不杀进程)
memory.max硬上限超过则触发 OOM(杀进程)
memory.swap.maxswap 上限限制可换出的量
# 设软上限 512M、硬上限 1G、禁 swap
echo 512M | sudo tee /sys/fs/cgroup/mygroup/memory.high
echo 1G   | sudo tee /sys/fs/cgroup/mygroup/memory.max
echo 0    | sudo tee /sys/fs/cgroup/mygroup/memory.swap.max

# 查看当前用量与事件
cat /sys/fs/cgroup/mygroup/memory.current
cat /sys/fs/cgroup/mygroup/memory.stat      # anon/file/kernel 细分
cat /sys/fs/cgroup/mygroup/memory.events    # high/max/oom 计数
回收顺序:memory.high 触发「软回收」→ 压不下去 → 逼近 memory.max → OOM killer
memory.events 里的 oom / oom_kill 计数是「这组被 OOM 了几次」的直接证据。

记忆:high 是「温柔提醒」,max 是「直接动手」。给业务设 high(让它自我回收)远比只设 max(一到就杀)体验好;memory.min 用来保护关键进程不被邻居挤掉。

6. IO 控制:io.max、io.weight 与 io.latency

v2 用 io.max 表达「设备级绝对限速」,用 io.weight 表达「相对带宽份额」,另有 io.latency 做「延迟目标保护」:

# 查设备号(major:minor)
lsblk -o NAME,MAJ:MIN
# 限制对 8:0(sda)读 100MB/s、写 50MB/s、2000 IOPS
echo "8:0 rbps=104857600 wbps=52428800 riops=2000" | sudo tee /sys/fs/cgroup/mygroup/io.max

# 相对权重(默认 100)
echo "8:0 200" | sudo tee /sys/fs/cgroup/mygroup/io.weight

# 延迟目标:若该组 IO 延迟超过 50ms,优先满足它
echo "8:0 target=50ms" | sudo tee /sys/fs/cgroup/mygroup/io.latency
参数单位作用
rbps / wbps字节/秒读/写带宽上限
riops / wiops次/秒读/写 IOPS 上限
io.weight1~10000相对带宽份额
io.latency时间延迟目标,低于它优先调度
cat /sys/fs/cgroup/mygroup/io.stat     # 各组实际 IO 统计

心法:限速用 io.max、公平用 io.weight、保延迟用 io.latency。注意 io.max 对 buffered I/O 的约束不如 O_DIRECT 精确,压测时要用 --direct=1 才能测出真实限制。

7. PIDs 与冻结:pids.max、cgroup.freeze

pids 控制器防止 fork 炸弹,cgroup.freeze 提供「暂停整组」的能力(比逐进程 SIGSTOP 更彻底):

# 限制该组最多 512 个进程/线程
echo 512 | sudo tee /sys/fs/cgroup/mygroup/pids.max
cat /sys/fs/cgroup/mygroup/pids.current
cat /sys/fs/cgroup/mygroup/pids.events   # max 触发计数

# 冻结/解冻整组(v2 新增,冻结后组内所有进程停止运行)
echo 1 | sudo tee /sys/fs/cgroup/mygroup/cgroup.freeze
echo 0 | sudo tee /sys/fs/cgroup/mygroup/cgroup.freeze
cat /sys/fs/cgroup/mygroup/cgroup.events # populated / frozen 状态
文件用途
pids.max / pids.current进程数上限/当前值
cgroup.freeze冻结整组(写 1 冻结,0 解冻)
cgroup.kill一次性杀掉整组(v2 新增,最干净的清理)
cgroup.eventspopulated/frozen 事件

记忆:cgroup.kill 是容器/任务清理的「一键清场」——它保证组内所有进程被终止,避免传统「遍历 PID 逐个 kill」时漏掉新 fork 出来的子进程。

8. systemd 集成:Delegate 与资源切片

生产环境很少直接操作 cgroupfs,而是让 systemd 代管。给服务或切片设资源、把子树委托给某个服务自己管理:

# /etc/systemd/system/myapp.service
[Service]
CPUWeight=200
CPUQuota=50%                 # 等价 cpu.max 50%
MemoryHigh=512M
MemoryMax=1G
MemorySwapMax=0
IOWeight=200
TasksMax=512
Delegate=yes                 # 允许该服务管理自己的子 cgroup
# 用 slice 做「资源池」,让一组服务共享配额
# /etc/systemd/system/mypool.slice
[Slice]
CPUQuota=200%
MemoryMax=4G
# 把服务放进切片
systemctl set-property myapp.service Slice=mypool.slice
# 运行时临时改资源(写入 .d 覆盖文件,持久化)
systemctl set-property myapp.service MemoryMax=2G
systemd 指令对应 cgroup v2
CPUWeightcpu.weight
CPUQuotacpu.max
MemoryHigh / MemoryMaxmemory.high / memory.max
IOWeightio.weight
TasksMaxpids.max
Delegate=yes下放 subtree_control 给该服务

心法:Delegate=yes 是把「资源管理权」下放——Docker/containerd、systemd-nspawn、用户会话都靠它。开了委托的服务可以自己创建子组、设自己的限制,而不用每次找 systemd。

9. 观测:systemd-cgtop、PSI 与 cgroup.events

# 按 cgroup 排序的实时资源视图(类似 top,但按组聚合)
systemd-cgtop
systemd-cgtop -m --order=memory

# PSI:某组的 CPU/内存/IO 压力(some=至少一个任务受阻,full=全部受阻)
cat /sys/fs/cgroup/mygroup/memory.pressure
#   some avg10=1.23 avg60=0.45 avg300=0.12 total=98765
#   full avg10=0.00 ...
cat /sys/fs/cgroup/mygroup/io.pressure

PSI 的 avg10 表示「最近 10 秒内,该组有百分之几的时间处于资源受阻状态」——这是判断「限得太紧还是够用」的最直接指标。

# 系统级 PSI
cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io
# 事件计数
cat /sys/fs/cgroup/mygroup/cgroup.events
指标含义判读
cpu.pressure some有任务等 CPU 的时间占比高 = CPU 不够
memory.pressure full全组都卡在内存高 = 内存严重不足
io.pressure some有任务等 IO高 = IO 是瓶颈
memory.events oom_killOOM 杀进程次数>0 = 硬上限过低

铁律:调资源限制不能只看「有没有被杀」,要看 PSI。memory.high 设得过低会让 memory.pressure 长期偏高(持续回收导致卡顿),虽然没 OOM 但性能已经受损。

10. 速查表

需求做法
确认版本stat -fc %T /sys/fs/cgroup
下放控制器echo "+cpu +memory" > cgroup.subtree_control
CPU 权重echo 200 > cpu.weight
CPU 封顶echo "50000 100000" > cpu.max
内存软限echo 512M > memory.high
内存硬限echo 1G > memory.max
禁 swapecho 0 > memory.swap.max
IO 限速echo "8:0 rbps=... wbps=..." > io.max
进程数上限echo 512 > pids.max
冻结整组echo 1 > cgroup.freeze
杀整组echo 1 > cgroup.kill
观测systemd-cgtop / *.pressure
systemd 委托Delegate=yes

一句话记忆:cgroups v2 是「一棵统一树」——父组用 subtree_control 下放控制器、叶子组用 cpu.weight/cpu.max、memory.high/memory.max、io.max、pids.max 施加限制;PSI 告诉你「限得紧不紧」,systemd 用 Slice/Delegate 把这一切代管起来。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「linux」更多文章

  1. PAM 认证与权限提升:模块栈、密码策略与 sudo 深入
  2. NFS 与 CIFS 网络文件系统:服务端配置、挂载调优与安全
  3. LVM 与 RAID 存储管理:卷管理、快照与软阵列运维