Linux 基础体系 · 第 35/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux cgroups v2 深入:CPU、Memory、IO、PID、Pressure 和委派
所属系列:Linux 基础体系
所属模块:七、内核深入
标签:Linux、cgroups、容器、性能优化
1. cgroups v2 解决什么问题
cgroup(control group) 是 Linux 内核对一组进程进行资源核算和资源控制的机制。它可以限制、保护或统计:
- CPU 调度时间;
- 内存和 swap 使用;
- 块设备 I/O;
- 进程数量;
- 资源压力(Pressure Stall Information,PSI)。
cgroup 只负责“资源归属和资源控制”,不负责完整的进程视图隔离。例如:
- cgroups 可以限制一个服务最多使用多少 CPU;
- PID namespace 可以让进程看到不同的 PID 视图;
- mount namespace 可以隔离挂载点;
- network namespace 可以隔离网络设备和路由表。
容器通常同时使用 cgroups 和 namespaces。cgroups 解决“最多使用多少资源”,namespaces 解决“能看到哪些对象”。
cgroups v2 与 v1 的核心差异是:
- v2 使用一个统一层级(unified hierarchy);
- 一个 cgroup 目录同时承载所有启用的控制器;
- 控制器通过父 cgroup 的
cgroup.subtree_control向子树传播; - 资源控制更强调层级语义和委派安全;
- v2 不再使用 v1 中的
release_agent等机制; - 多数现代 systemd 发行版已经默认使用 v2。
检查当前系统:
stat -fc %T /sys/fs/cgroup
如果输出类似:
cgroup2fs
说明 /sys/fs/cgroup 是 cgroups v2。
进一步查看挂载和控制器:
mount | grep cgroup
cat /sys/fs/cgroup/cgroup.controllers
cat /sys/fs/cgroup/cgroup.subtree_control
典型输出可能是:
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
cpuset cpu io memory hugetlb pids rdma misc
cgroup.controllers 表示当前 cgroup 可以向子 cgroup 开放的控制器。
cgroup.subtree_control 表示当前 cgroup 已经向直接子 cgroup 开放的控制器。
这两个文件不是同一个概念:
- 父目录有
memory,不代表子目录已经能使用memory.max; - 只有父目录写入
+memory到cgroup.subtree_control后,子目录才会获得 memory 控制器接口。
2. cgroups v2 的层级模型
cgroup 层级是目录树。目录本身表示一个 cgroup,目录中的特殊文件表示控制器接口和状态。
例如:
/sys/fs/cgroup/
├── cgroup.controllers
├── cgroup.procs
├── system.slice/
│ ├── cgroup.procs
│ └── app.service/
│ ├── memory.max
│ └── cpu.max
└── demo/
└── worker/
进程通过写入 cgroup.procs 加入 cgroup:
echo "$PID" > /sys/fs/cgroup/demo/cgroup.procs
写入后,进程及其线程组归属于目标 cgroup。一个进程在同一时刻只能属于一个 cgroup,但不同控制器在 v2 中不再分别挂载到不同层级。
2.1 进程归属和线程归属
cgroup.procs 以进程组为单位移动进程。对多线程程序而言,写入一个线程组中的 PID,通常会移动整个线程组。
cgroup.threads 则允许在线程级别移动线程,但这需要满足 cgroup 的线程模式规则,普通服务管理一般不应随意使用它。
查看当前 cgroup:
cat /proc/self/cgroup
在 cgroups v2 中常见结果为:
0::/user.slice/user-1000.slice/session-2.scope
格式是:
hierarchy-ID::cgroup-path
v2 的 hierarchy ID 通常为 0,因为所有控制器位于统一层级。
2.2 控制器的层级传播
假设根 cgroup 中存在 cpu 和 memory 控制器:
cat /sys/fs/cgroup/cgroup.controllers
# cpu memory ...
先创建子 cgroup:
mkdir /sys/fs/cgroup/demo
此时 /sys/fs/cgroup/demo 未必有 cpu.max 或 memory.max。父 cgroup 需要显式开放控制器:
echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control
然后查看:
ls /sys/fs/cgroup/demo/cpu.max
ls /sys/fs/cgroup/demo/memory.max
控制器是由父级决定是否可用于直接子级的。子级也可以继续向孙级传播:
mkdir /sys/fs/cgroup/demo/worker
echo "+cpu +memory" > /sys/fs/cgroup/demo/cgroup.subtree_control
这形成了一个重要的因果关系:
父级 cgroup.subtree_control
│
▼
子级获得控制器接口
│
▼
子级可配置 cpu.max、memory.max 等
因此,写入 demo/cpu.max 失败,并不一定表示内核不支持 CPU 控制器,也可能只是 demo 的父级没有开启 +cpu。
3. 一个可验证的 cgroups v2 实验环境
下面的实验直接操作 cgroupfs,适合临时测试。生产环境中,如果系统由 systemd 管理,应该优先使用 systemd unit、scope 或 transient unit,而不是绕过 systemd 直接修改其目录。
假设当前 shell 具有 root 权限,并且系统确实使用 cgroups v2:
test "$(stat -fc %T /sys/fs/cgroup)" = cgroup2fs
创建测试层级:
CG=/sys/fs/cgroup/wr-demo
mkdir "$CG"
echo "+cpu +memory +pids" > /sys/fs/cgroup/cgroup.subtree_control
这里有两个风险:
- 根 cgroup 可能已经由 systemd 管理,直接改动可能与 systemd 的资源配置产生竞争;
- 某些控制器可能受内核配置、启动参数或虚拟化环境限制,写入不一定成功。
查看结果:
cat "$CG/cpu.max"
cat "$CG/memory.max"
cat "$CG/pids.max"
典型默认值可能是:
max 100000
max
max
max 表示不设置该项的有限上限;第二列 100000 是 CPU quota 的默认周期,单位为微秒。
清理前先确保没有进程仍在测试 cgroup 中:
rmdir "$CG"
如果目录非空,或者仍有子 cgroup、进程或内核对象关联,删除可能失败。不要在不了解进程归属的情况下直接删除生产 cgroup。
4. CPU:权重、配额和实际调度
CPU 控制器主要提供三类语义:
cpu.weight:竞争时的相对权重;cpu.max:一个周期内允许使用的 CPU 时间上限;cpu.stat:CPU 使用和限流统计。
4.1 cpu.weight:相对分配,不是百分比
查看和设置:
cat /sys/fs/cgroup/wr-demo/cpu.weight
echo 200 > /sys/fs/cgroup/wr-demo/cpu.weight
v2 中 cpu.weight 的常见有效范围是 1 到 10000,默认值通常为 100。它不是百分比,也不是“允许使用 200% CPU”。
假设同一个父 cgroup 下有两个可运行子 cgroup:
A.cpu.weight = 100
B.cpu.weight = 300
如果两者持续竞争同一个 CPU 资源,并且都没有 quota 限制,那么理想化分配比例为:
这只是竞争时的相对比例。如果机器有 8 个逻辑 CPU,而且只有 A 在运行,A 仍可能使用多个 CPU;如果 CPU 没有竞争,权重不会主动限制它。
因此,下列理解是错误的:
cpu.weight = 200 => 只能使用 2 个 CPU
cpu.weight = 50 => 只能使用 50% CPU
正确理解是:它影响同一资源层级中可运行任务之间的调度份额。
权重具有层级性。例如:
root
├── batch weight=100
│ ├── A weight=100
│ └── B weight=300
└── service weight=300
若 batch 和 service 同时竞争,第一层先按 100:300 分配;进入 batch 后,A 和 B 再按 100:300 分配。资源分配不是把所有叶子节点的权重简单相加。
4.2 cpu.max:硬配额
cpu.max 的格式是:
quota period
单位是微秒。设置:
echo "50000 100000" > /sys/fs/cgroup/wr-demo/cpu.max
含义是:
- 每个 100000 微秒,也就是 100 毫秒的周期;
- 最多使用 50000 微秒 CPU 时间;
- 在单核等效意义上相当于最多 50% 的一个 CPU;
- 配额是 cgroup 聚合值,而不是每个进程各自 50%。
如果 cgroup 中有 4 个线程,它们共享这 50000 微秒。四个线程同时运行时,可能很快耗尽配额,之后被限流到下一个周期。
取消 quota:
echo "max 100000" > /sys/fs/cgroup/wr-demo/cpu.max
如果 quota 大于 period,例如:
200000 100000
表示最多使用两个 CPU 的时间总量。它不保证一定能同时获得两个 CPU,因为实际并行度还受 CPU 数量、亲和性、调度和父级限制影响。
CPU 限流的过程是:
- cgroup 中的任务运行;
- 聚合使用时间达到当前周期的 quota;
- 内核将该 cgroup 标记为 throttled;
- 任务暂时不能继续获得 CPU;
- 周期结束后恢复可运行资格。
查看统计:
cat /sys/fs/cgroup/wr-demo/cpu.stat
可能得到:
usage_usec 812345
user_usec 700000
system_usec 112345
nr_periods 120
nr_throttled 45
throttled_usec 2300000
其中:
usage_usec:总 CPU 使用时间;user_usec:用户态时间;system_usec:内核态时间;nr_periods:经历的 quota 周期数;nr_throttled:发生限流的周期数;throttled_usec:累计被限流的时间。
nr_throttled 大不一定说明服务有问题;需要结合延迟、吞吐和 throttled_usec 判断。一个批处理任务被限流可能只是预期行为,而一个低延迟服务频繁被限流则可能导致尾延迟上升。
部分较新的内核还会提供 cpu.max.burst 等 burst 相关接口,但其可用性和语义依赖内核版本。部署脚本不应假定该文件永远存在,应先检查:
test -e /sys/fs/cgroup/wr-demo/cpu.max.burst
4.3 CPU 亲和性与 cgroup quota 不是一回事
CPU affinity 决定任务可以在哪些 CPU 上运行;cpu.max 决定 cgroup 总共能获得多少 CPU 时间。
例如:
- 允许任务运行在 CPU 0、1 上;
cpu.max=50000 100000。
这并不等于“固定占用 CPU 0 的一半和 CPU 1 的一半”。它只是限制整个 cgroup 每个周期获得的总 CPU 时间。
反过来,仅设置 cpu.max=200000 100000 也不等价于绑定两个固定 CPU。需要固定 CPU 时,应结合 cpuset 控制器和 cpuset.cpus,但 cpuset 受 NUMA、热插拔和父级 CPU 集合约束。
5. Memory:使用量、回收压力和 OOM
memory 控制器比“设置一个内存上限”复杂,因为 Linux 内存既包括匿名页,也包括文件页、内核内存、socket 等不同类型。
常见接口:
memory.current
memory.stat
memory.events
memory.min
memory.low
memory.high
memory.max
memory.oom.group
memory.swap.max
5.1 memory.current 和 memory.stat
cat /sys/fs/cgroup/wr-demo/memory.current
cat /sys/fs/cgroup/wr-demo/memory.stat
memory.current 给出当前 cgroup 的内存使用量,单位为字节。memory.stat 会拆分匿名内存、文件缓存、内核内存等类别。
不要把进程 RSS 简单相加后当成 cgroup 内存使用量。原因包括:
- 多个进程可能共享同一页;
- 文件页由 cgroup 记账,但其回收特性不同;
- 内核内存可能被纳入 cgroup 记账;
- 进程 RSS 与 cgroup 统计的边界并不完全相同。
因此,诊断容器内存时,应优先观察 memory.current、memory.stat 和 memory.events,再结合进程级指标。
5.2 memory.min 和 memory.low:保护,不是额度
memory.min 表示硬保护。父级发生内存回收时,低于该保护范围的内存通常不会被回收;如果多个子 cgroup 的 memory.min 总和超过父级可用内存,内核会按比例分配保护,而不是凭空创造内存。
memory.low 是最佳努力保护。内存紧张时,内核会尽量避免回收低于该值的内存,但在更严重的压力下仍可能回收。
例如:
echo 512M > /sys/fs/cgroup/wr-demo/memory.min
echo 1G > /sys/fs/cgroup/wr-demo/memory.low
这不表示 cgroup 必定拥有 1.5 GiB 内存,也不表示它可以使用 1 GiB。它表达的是回收优先级:
memory.min:更强的不可回收保护
memory.low:较弱的回收保护
若所有服务都设置过高的 memory.min,保护总量超过物理内存,系统可能把压力转移到未保护的 cgroup,甚至更快触发 OOM。
5.3 memory.high:回收和限速阈值
echo 1G > /sys/fs/cgroup/wr-demo/memory.high
当使用量超过 memory.high 时,内核会对该 cgroup 施加内存回收压力,必要时让相关任务在内存回收路径上花费更多时间。它不是立即杀进程的硬上限,但可能显著增加延迟。
这使得 memory.high 适合渐进式退化:
- 正常使用低于 high;
- 超过 high 后,任务受到回收压力;
- 应用可能变慢、分配阻塞;
- 如果仍继续增长,最终可能触及
memory.max。
但 memory.high 不是应用级限流器。它不能保证应用会平滑释放内存,也不能代替应用自身的缓存上限。
5.4 memory.max:硬上限和 OOM 路径
echo 2G > /sys/fs/cgroup/wr-demo/memory.max
当 cgroup 内存使用接近或超过 memory.max 时,内核会尝试回收。无法回收且分配仍然需要内存时,会在该 cgroup 内触发 cgroup OOM。
查看事件:
cat /sys/fs/cgroup/wr-demo/memory.events
可能输出:
low 0
high 12
max 3
oom 1
oom_kill 1
oom_group_kill 0
这些是累计计数:
low:触及 low 保护相关事件;high:触及 high 的事件;max:分配受memory.max影响的次数;oom:发生 OOM 条件的次数;oom_kill:因 cgroup OOM 杀死进程的次数;oom_group_kill:以组为单位杀死的次数。
设置:
echo 1 > /sys/fs/cgroup/wr-demo/memory.oom.group
表示发生 cgroup OOM 时,尽量把整个 cgroup 作为一个不可分割的任务组处理,而不是只杀死一个进程。它适合进程之间强耦合的服务,但会扩大故障影响面;一个辅助进程触发 OOM,也可能导致整个服务组退出。
取消内存上限:
echo max > /sys/fs/cgroup/wr-demo/memory.max
这里的 OOM 与全局 OOM 不完全相同。cgroup OOM 首先约束该 cgroup 的故障范围;如果系统整体也缺内存,仍可能出现全局回收和全局 OOM。
5.5 swap 是独立控制项
现代 cgroups v2 通常使用:
cat /sys/fs/cgroup/wr-demo/memory.swap.max
设置:
echo 0 > /sys/fs/cgroup/wr-demo/memory.swap.max
表示该 cgroup 不允许新增 swap 使用,但不一定自动把已经存在的 swap 使用量立刻换回内存。
如果设置:
echo 1G > /sys/fs/cgroup/wr-demo/memory.swap.max
它限制的是 swap 使用量,而不是“内存加 swap 的总和”。总资源边界需要同时考虑:
memory.max + memory.swap.max
实际可用接口还受系统是否启用 swap、内核版本和发行版配置影响。不要仅凭容器运行时的“内存限制”名称推断 swap 行为,应直接检查对应 cgroup 文件。
6. IO:设备、权重、带宽和延迟
cgroups v2 的 IO 控制器针对块设备资源。常见接口:
io.stat
io.weight
io.max
io.latency
首先确定设备的 major:minor:
lsblk -o NAME,MAJ:MIN,TYPE,MOUNTPOINTS
假设目标设备是 8:0,但这个值只是示例,不能直接复制到所有机器。
6.1 io.stat:实际记账
cat /sys/fs/cgroup/wr-demo/io.stat
典型格式类似:
8:0 rbytes=1048576 wbytes=2097152 rios=128 wios=256 dbytes=0 dios=0
这些字段表示该设备上的读写字节数和 I/O 操作数。不同内核版本还可能显示更多字段。
IO 统计并不等价于“应用调用了多少次 read(2)”:
- buffered I/O 可能先命中 page cache;
- 写入可能异步下刷;
- 多个进程的 I/O 可能合并;
- 文件系统、块层和设备队列会改变最终请求形态。
因此,应用层吞吐、page cache 行为和 io.stat 需要结合分析。
6.2 io.weight:竞争时的相对权重
设置:
echo "8:0 200" > /sys/fs/cgroup/wr-demo/io.weight
常见有效范围为 1 到 10000,默认通常为 100。它与 CPU weight 类似,是竞争时的相对权重,不是固定 MB/s。
如果两个 cgroup 在同一块设备上同时产生可调度的 I/O:
A.io.weight = 100
B.io.weight = 300
理想化情况下,设备调度资源大致按 1:3 分配。但实际结果还受到:
- I/O 调度器;
- 请求大小;
- 顺序或随机访问;
- 文件系统;
- 设备自身队列;
- 读写混合;
- SSD、NVMe 或网络块设备实现;
等因素影响。
6.3 io.max:按设备设置硬限制
格式通常为:
major:minor rbps=... wbps=... riops=... wiops=...
例如:
echo "8:0 rbps=10485760 wbps=5242880" \
> /sys/fs/cgroup/wr-demo/io.max
含义是:
- 对设备
8:0限制读取速率为 10 MiB/s; - 写入速率为 5 MiB/s;
- 未指定的
riops、wiops不改变原有设置。
也可以限制 IOPS:
echo "8:0 riops=100 wiops=50" > /sys/fs/cgroup/wr-demo/io.max
这里的风险是设备标识和挂载路径不是同一个概念。一个路径可能位于:
- LVM logical volume;
- device mapper;
- md RAID;
- overlayfs;
- 网络存储;
- 虚拟块设备。
限制上层设备不一定得到你期望的下层物理设备效果。应先确认应用 I/O 实际经过哪个块设备,并验证限制是否反映在应用延迟和 io.stat 中。
取消某一项限制:
echo "8:0 rbps=max wbps=max" > /sys/fs/cgroup/wr-demo/io.max
完整清除某设备设置的方式依赖当前文件内容和内核实现,生产脚本应先读取现值再修改,不要假设所有字段都可以省略。
6.4 io.latency 的边界
io.latency 用于表达目标延迟,例如:
echo "8:0 target=50000" > /sys/fs/cgroup/wr-demo/io.latency
50000 通常表示 50000 微秒,即 50 毫秒。
它不是“保证每次 I/O 都不超过 50 ms”的硬实时承诺,而是让内核在支持的块层和调度条件下,通过影响其他 cgroup 的调度来尽量保护目标 cgroup 的延迟。
因此:
- 底层设备已经饱和时,目标可能无法实现;
- 某些 I/O 调度器或设备路径可能不支持预期语义;
- 文件系统缓存和异步写回会使应用感知延迟与块设备完成延迟不同;
- 目标延迟设置过低可能把压力转移给其他 cgroup。
io.latency 属于依赖内核和设备调度能力的接口,使用前必须检查文件是否存在并做实际验证。
7. PID:限制进程数量,而不是限制 PID 数值
PID 控制器的主要接口:
pids.current
pids.max
pids.events
设置:
echo 100 > /sys/fs/cgroup/wr-demo/pids.max
表示该 cgroup 及其受控层级内最多允许创建 100 个任务。这里的“任务”在内核层面包含线程,因此一个创建大量线程的程序也可能耗尽 PID 配额。
查看:
cat /sys/fs/cgroup/wr-demo/pids.current
cat /sys/fs/cgroup/wr-demo/pids.events
可能输出:
max 2
pids.events 中的 max 表示因为达到 pids.max,创建任务失败的累计次数。
当进程调用 fork()、clone() 或创建线程时,内核会检查目标 cgroup 的 PID 配额。超过限制后,创建操作通常失败并返回 EAGAIN。这与内存 OOM 不同:
- PID 限制通常不会杀死已有进程;
- 它使后续创建失败;
- 应用可能把
EAGAIN错误误报成“系统 fork 失败”; - 线程池、子进程管理器和日志组件都可能受影响。
测试代码或命令可以观察这种故障,但不应在生产服务上随意设置过低值。设置为无限制:
echo max > /sys/fs/cgroup/wr-demo/pids.max
PID namespace 与 pids.max 也不同:
- PID namespace 改变进程看到的 PID 层级;
pids.max限制内核允许创建多少任务;- 容器可以拥有独立 PID namespace,但仍受宿主机 cgroup 的 PID 限制。
8. Pressure:资源压力不是资源使用率
PSI(Pressure Stall Information)描述的是任务因为资源不足而无法前进的时间比例。它回答的问题不是:
CPU 使用了多少?
而是:
有多少任务、在多长时间内因为 CPU 不够而无法运行?
cgroups v2 中常见文件:
cpu.pressure
memory.pressure
io.pressure
读取:
cat /sys/fs/cgroup/wr-demo/cpu.pressure
cat /sys/fs/cgroup/wr-demo/memory.pressure
cat /sys/fs/cgroup/wr-demo/io.pressure
典型输出:
some avg10=12.34 avg60=5.67 avg300=1.23 total=987654
full avg10=1.20 avg60=0.50 avg300=0.10 total=12345
8.1 some 和 full
some 表示在某段时间内,至少有一个相关任务处于资源 stall 状态。
full 表示在某段时间内,相关任务全部处于资源 stall 状态。对于 CPU,full 的含义尤其需要谨慎:它不是“CPU 利用率为 100%”,而是 cgroup 中没有任务能够有效推进,所有非空闲任务都在等待 CPU 或其他条件。
可以用集合表示:
- :cgroup 中当前有工作的任务集合;
- :时刻 处于该资源 stall 的任务集合。
则:
avg10、avg60、avg300 是指数衰减的时间窗口平均值,表示近似的 stall 时间占比,单位为百分比。它们不是简单的最近 10、60、300 秒算术平均。
total 是累计 stall 时间,单位为微秒。读取两次后可计算区间压力:
其中:
total_2-total_1是累计 stall 微秒增量;- 是两次采样间隔,单位为秒;
- 结果是该区间内的累计 stall 时间占墙上时间的比例。
但并发任务可能同时 stall,因此 PSI 的累计时间不应直接解释为“机器只剩下多少 CPU”。它是任务等待压力的聚合指标。
8.2 CPU PSI 与 CPU 使用率的反例
考虑两个 CPU 密集型任务运行在一台 2 CPU 机器上:
- 两个任务都持续运行;
- 两个 CPU 都被占满;
- 没有任务因为 CPU 调度而长时间等待。
此时 CPU 使用率接近 100%,但 CPU PSI 可能很低。
再考虑一台 2 CPU 机器上有 20 个可运行任务:
- CPU 使用率仍然接近 100%;
- 大量任务在运行队列等待;
- CPU
some会升高; - 如果所有任务在某些时刻都处于等待状态,
full也可能升高。
所以“CPU 使用率高”与“CPU 压力高”不是同义词。
8.3 Memory PSI 和 IO PSI
memory PSI 反映任务因内存回收、缺页处理或内存不足路径而无法推进的时间。它可能在内存还没有达到 memory.max 时就升高。
io PSI 反映任务等待 I/O 完成的时间。设备吞吐率并不直接决定 IO PSI:
- 大量顺序读可能吞吐很高但等待压力不大;
- 少量随机同步 I/O 可能吞吐很低但延迟很高,IO PSI 很高。
PSI 适合与控制器状态联合判断:
cpu.pressure + cpu.stat 的 throttled_usec
memory.pressure + memory.events 的 high/max/oom
io.pressure + io.stat 的 rbytes/wbytes/ios
例如:
- CPU PSI 高、
nr_throttled和throttled_usec同时增长:可能是 quota 过紧; - memory PSI 高、
memory.events的high增长:可能是 high 触发了回收压力; - IO PSI 高、
io.stat速率不高:可能是设备延迟、队列或同步 I/O 问题,而不只是带宽不足。
9. 完整示例:创建受限 worker 并验证故障路径
下面创建一个同时具备 CPU、内存和 PID 限制的 cgroup。假设控制器已经挂载且当前 shell 具备 root 权限。
CG=/sys/fs/cgroup/wr-demo
mkdir -p "$CG"
echo "+cpu +memory +pids" > /sys/fs/cgroup/cgroup.subtree_control
设置参数:
# 每 100 ms 最多使用 50 ms CPU 时间
echo "50000 100000" > "$CG/cpu.max"
# 内存硬上限 256 MiB
echo 256M > "$CG/memory.max"
# swap 上限 0;需要系统支持该接口
if [ -e "$CG/memory.swap.max" ]; then
echo 0 > "$CG/memory.swap.max"
fi
# 最多 64 个任务/线程
echo 64 > "$CG/pids.max"
# OOM 时尽量整体处理
echo 1 > "$CG/memory.oom.group"
启动一个进程并移动到 cgroup:
sleep 1000 &
PID=$!
echo "$PID" > "$CG/cgroup.procs"
cat "$CG/cgroup.procs"
为什么先启动再移动?因为示例需要一个已存在的进程作为测试对象。生产服务更适合:
- 先创建并配置 cgroup;
- 再启动服务,使其从一开始就在目标 cgroup 中;
- 或通过 systemd 直接创建 scope。
读取配置:
cat "$CG/cpu.max"
cat "$CG/memory.max"
cat "$CG/pids.max"
cat "$CG/memory.events"
如果写入 cgroup.procs 失败,常见原因包括:
- 当前用户没有权限;
- PID 已经退出;
- 目标 cgroup 不存在;
- 进程属于不允许跨越的线程模式;
- systemd 或其他管理器正在重排进程归属。
测试结束:
kill "$PID"
wait "$PID" 2>/dev/null || true
rmdir "$CG"
删除失败时,检查:
cat "$CG/cgroup.procs"
find "$CG" -mindepth 1 -maxdepth 1 -type d -print
10. cgroup 的状态、迁移和故障路径
cgroup 不是静态标签,而是内核持续维护的状态对象。常见状态文件包括:
cgroup.procs
cgroup.threads
cgroup.events
cgroup.stat
cgroup.events 可能包含:
populated 1
frozen 0
populated=1 表示该 cgroup 或其后代仍有任务。只有在没有子 cgroup、没有任务且没有其他引用时,删除才更可能成功。
进程迁移的基本步骤是:
- 写入目标 cgroup 的
cgroup.procs; - 内核检查权限和层级约束;
- 更新进程的 cgroup 归属;
- 后续资源记账和控制作用于新层级;
- 已经产生的统计通常不会自动“倒流”到新 cgroup。
这意味着迁移不是时间旅行。进程过去已经消耗的 CPU、内存和 I/O 不会因为移动而从旧统计中消失。
移动一个正在执行的服务还有竞态风险:
- 进程可能在移动前已经创建子进程;
- 服务管理器可能立即把进程移回原 cgroup;
- 新线程可能在不同时间加入;
- 资源限制可能在业务高峰中突然生效。
生产切换通常应通过服务管理器或容器运行时完成,以便定义清晰的生命周期。
11. 委派:让非 root 管理子 cgroup
委派(delegation) 是把某个 cgroup 子树的管理权交给另一个用户或服务,使其可以在该子树内创建 cgroup、移动自己的进程并设置被允许的控制参数。
委派不是简单地执行:
chown -R alice:alice /sys/fs/cgroup/project
这样做可能暴露超出预期的能力,甚至破坏宿主机资源管理。
11.1 委派需要同时考虑三个边界
文件系统权限边界
cgroupfs 中的目录和特殊文件遵循权限检查。被委派用户至少需要能够:
- 在委派目录下创建和删除子 cgroup;
- 将自己启动的进程写入目标
cgroup.procs; - 设置允许委派的控制文件。
通常不应把父级的 cgroup.procs 写权限交给不可信用户,因为移动进程的能力本身可能改变其他服务的资源归属。
控制器边界
控制器需要由父级通过 cgroup.subtree_control 开放。委派者只应获得明确允许的控制器,例如只允许 pids 和 memory,不一定允许 cpu、io 或 cpuset。
控制器配置必须满足层级约束:
父级已开放控制器
↓
子级获得控制接口
↓
委派者在子树内配置限制
如果父级未开放控制器,委派者不能通过修改子目录文件“凭空启用”它。
进程归属边界
cgroup v2 的设计要求避免一个 cgroup 一边直接承载业务进程,一边把域控制器资源分配给子 cgroup。这个规则通常称为 no internal process constraint。
直观地说:
错误或受限结构:
parent
├── 自己直接运行的进程
└── child 使用 cpu、memory 等域控制器
因为父级自己的进程会和 child 争夺同一层级资源,资源分配边界不清晰。更合理的结构是:
parent
├── service-a
├── service-b
└── delegated-subtree
├── worker-a
└── worker-b
父级只做资源分配,实际业务进程放入专门的子 cgroup。某些线程模式可以改变规则,但线程 cgroup 的创建和迁移约束更复杂,不应把它当作普通进程 cgroup 的替代品。
11.2 委派不等于获得 root 权限
cgroup 委派用户通常可以:
- 在被委派的子树中创建 cgroup;
- 管理自己启动的进程;
- 修改被开放的控制参数。
但这不意味着该用户可以:
- 修改父级或兄弟 cgroup;
- 移动任意系统进程;
- 改变整个系统的 CPU、内存或 I/O 上限;
- 绕过 namespace、LSM 或 capability 检查。
还要注意,cgroup namespace 只改变进程看到的 cgroup 路径视图,不自动授予 cgroupfs 写权限,也不自动提供资源管理能力。权限仍由文件系统权限、用户命名空间、capability 和内核检查共同决定。
11.3 一个委派的安全思路
可采用如下结构:
/sys/fs/cgroup/
└── delegated/
└── alice/
├── cgroup.procs
├── cgroup.subtree_control
├── memory.max
├── pids.max
└── jobs/
管理员负责:
- 创建
alice子树; - 只向其子树开放指定控制器;
- 设置父级硬上限;
- 以最小权限调整目录和必要文件;
- 不授予修改宿主级控制器和其他服务 cgroup 的能力。
委派者负责:
- 在自己的子树中创建任务 cgroup;
- 先设置限制,再启动或移动自己的进程;
- 监控
memory.events、pids.events和 PSI; - 不依赖删除 cgroup 来终止进程;
- 遇到权限或层级错误时检查父级传播状态,而不是反复重试写入。
生产系统通常更适合让 systemd、容器运行时或专门的资源管理器完成委派,因为它们能够同时处理服务生命周期、权限、日志和回收。
12. 与 systemd 和容器运行时的关系
在 systemd 系统中,常见 cgroup 层级包括:
system.slice
user.slice
user-UID.slice
session-*.scope
一个 systemd service 通常对应一个 cgroup。使用 systemd 创建临时 scope:
systemd-run --scope \
-p CPUQuota=50% \
-p MemoryMax=256M \
-p TasksMax=64 \
sleep 1000
这些参数最终会映射到 cgroups v2 的相关接口,但 systemd 可能同时设置其他属性。实际生效值应检查 unit 的 cgroup:
systemctl status <unit>
systemctl show <unit> -p ControlGroup
容器运行时也通常创建自己的 cgroup 层级。直接修改容器 cgroup 可能被运行时覆盖,或者与编排系统期望的资源状态不一致。尤其在 Kubernetes 等环境中,CPU request/limit、memory limit、QoS 类别和节点级 cgroup 层级之间存在多层映射,不能仅看容器内部一个 cpu.max 就推断整个调度结果。
13. 诊断:把控制参数和压力证据连起来
只看一个指标很容易误判。更可靠的诊断顺序是:
CPU
cat cpu.max
cat cpu.weight
cat cpu.stat
cat cpu.pressure
推导:
cpu.max受限且nr_throttled、throttled_usec增长:存在 quota 限流;cpu.max未受限但 PSI 高:更可能是兄弟 cgroup 竞争、CPU 数不足或调度延迟;- 权重低但无竞争:权重不会导致任务主动变慢;
- quota 很高但 cpuset 只有一个 CPU:实际并行度仍可能只有一个 CPU。
Memory
cat memory.current
cat memory.max
cat memory.high
cat memory.events
cat memory.pressure
cat memory.stat
推导:
memory.current接近memory.max且max、oom增长:硬上限正在影响分配;high增长而oom_kill不增长:可能是回收压力和延迟问题,而非已经杀进程;- memory PSI 高但
memory.max很宽松:可能是宿主机或父 cgroup 内存压力; - RSS 下降不明显但
memory.current变化:需要区分匿名页、文件页和内核记账。
IO
cat io.stat
cat io.max
cat io.weight
cat io.pressure
推导:
- IO PSI 高而吞吐不高:优先检查设备延迟、队列、同步访问和底层存储;
io.max生效时应用写入变慢,但 page cache 可能暂时掩盖限制;- 数据库直接 I/O 与普通 buffered I/O 的观察结果可能不同;
- 只限制某一设备,而实际数据位于另一设备时,配置不会产生预期效果。
PID
cat pids.current
cat pids.max
cat pids.events
推导:
pids.current接近pids.max:检查线程数、worker 数和子进程泄漏;pids.events的max增长:创建任务已经失败过;- 即使已有进程健康运行,后续
fork或线程创建也可能持续失败。
14. 常见失败表现及其原因
No such file or directory
常见原因:
- cgroup v1 而不是 v2;
- 控制器未由父级开放;
- 内核或发行版没有提供该可选接口;
- 拼写错误;
- 设备的 major:minor 不存在。
先检查:
stat -fc %T /sys/fs/cgroup
cat /sys/fs/cgroup/cgroup.controllers
cat /sys/fs/cgroup/cgroup.subtree_control
Invalid argument
常见原因:
cpu.max的 quota 大于允许范围或格式错误;memory.max小于当前无法回收的使用量;io.max的设备号或字段格式错误;cpuset设置超出父级允许的 CPU 集合;- 控制器不支持当前配置。
Device or resource busy
常见原因:
- 尝试启用控制器时违反层级约束;
- cgroup 仍有进程或子 cgroup;
- 正在使用不兼容的 threaded/domain 模式;
- systemd 或其他管理器持有并发状态。
权限错误
cgroupfs 不是普通配置目录。即使用户能读取 memory.current,也不表示能写入 memory.max。权限还可能受:
- rootless 用户命名空间;
- cgroup namespace;
- systemd 的委派策略;
- LSM;
- capability;
- 容器运行时限制;
影响。
“设置了 memory.max,但应用没有立即退出”
这是正常现象。memory.max 首先触发回收;只有无法继续满足分配时才进入 cgroup OOM。文件缓存可能被回收,应用也可能释放内存,因此设置上限不是定时器式的“到值立即 kill”。
“设置了 io.max,但应用刚开始仍然很快”
普通 buffered 写入可能先进入 page cache,应用的 write() 调用很快返回,真正的块设备写回发生在之后。需要观察一段时间,并结合 io.stat、设备指标和应用提交/落盘语义判断。需要持久化确认的应用还应区分 write()、fsync() 和实际设备完成延迟。
15. 生产取舍
cgroups 的参数应根据故障目标选择:
- 想让低优先级任务在竞争时少获得 CPU:使用
cpu.weight; - 想限制绝对 CPU 消耗:使用
cpu.max; - 想在内存紧张时优先保护关键服务:使用
memory.min或memory.low; - 想让服务逐渐退化而不是立刻 OOM:使用
memory.high; - 想建立明确的内存故障边界:使用
memory.max; - 想限制设备吞吐或 IOPS:使用
io.max; - 想在竞争时调整 I/O 份额:使用
io.weight; - 想防止 fork/线程爆炸:使用
pids.max; - 想判断资源是否已经影响业务进展:使用 PSI,而不是只看利用率。
这些控制项可以组合,但组合后因果关系会变复杂。例如:
cpu.max 太低
→ CPU throttling
→ CPU PSI 上升
→ 请求排队和尾延迟上升
或者:
memory.high 太低
→ 频繁回收
→ memory PSI 上升
→ 应用分配和访问延迟增加
→ 可能进一步触发 CPU 消耗
因此,限额不是越小越好。每次修改都应同时记录:
- 生效配置;
cpu.stat、memory.events、io.stat、pids.events;- 对应 PSI;
- 应用吞吐、错误率和尾延迟;
- 回滚方式。
回滚本身也要考虑状态:
echo "max 100000" > /sys/fs/cgroup/wr-demo/cpu.max
echo max > /sys/fs/cgroup/wr-demo/memory.max
echo max > /sys/fs/cgroup/wr-demo/pids.max
解除限制不会撤销已经发生的 OOM、I/O 排队或应用级超时;它只改变后续内核资源控制行为。若进程已经被 OOM 杀死,还需要由服务管理器重新启动。
cgroups v2 的核心不是几个限制文件,而是一套有层级、有记账、有故障路径的资源管理模型:
进程归属
→ 控制器层级
→ 资源分配或限制
→ 内核回收、调度、排队或拒绝
→ 统计事件与 PSI
→ 服务侧诊断和恢复
只有把配置、层级、权限和观测结果放在同一条因果链上,CPU、内存、I/O、PID 限制和资源压力指标才不会被误解为彼此独立的开关。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux sysctl 与内核参数:来源、持久化、风险、压测和回滚
- 下一篇:Linux namespaces 深入:PID、Mount、Network、User 和隔离组合
- 延伸:Linux cgroups 与 namespaces:资源控制、隔离和容器底层机制
- 延伸:Linux PSI 资源压力:CPU、内存、I/O Stall、告警和容量判断
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论