引言
**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
| 维度 | v1 | v2 |
|---|---|---|
| 层级 | 每控制器一棵树 | 单棵统一树 |
| 控制器开关 | 挂载即生效 | 逐级 cgroup.subtree_control |
| 内部进程 | 允许 | 有进程的组不能再开子组控制(no-internal-process) |
| 压力指标 | 无 | PSI(cpu.pressure 等) |
| 内存水位 | 仅 limit | low/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 封装已存在进程 |
.service | systemd 管理的服务进程 | 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.pressure | PSI 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.max | swap 上限 | 限制可换出的量 |
# 设软上限 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.weight | 1~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.events | populated/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 |
|---|---|
CPUWeight | cpu.weight |
CPUQuota | cpu.max |
MemoryHigh / MemoryMax | memory.high / memory.max |
IOWeight | io.weight |
TasksMax | pids.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_kill | OOM 杀进程次数 | >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 |
| 禁 swap | echo 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 容器与隔离 — namespace 与 cgroup 如何组成容器
- Linux 内存管理 — 回收、OOM 与 memory.high 的关系
- Linux 进程调度与 CPU — cpu.weight 与 CFS 的对应
- Linux systemd 服务管理 — 用 systemd 施加资源限制
- Docker 专题 — 容器运行时的 cgroup 用法
- Kubernetes 专题 — requests/limits 到 cgroup 的映射
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。