3. cgroup v2 文件系统接口:/sys/fs/cgroup 目录结构、关键文件说明、读写操作规范
cgroup v2 通过内核虚拟文件系统暴露其层次结构与控制接口,挂载点固定为 /sys/fs/cgroup。该接口遵循“一切皆文件”的 Unix 哲学:目录代表控制组(cgroup),文件代表控制参数与状态。理解目录结构与关键文件的读写规范,是进行延迟水位观测与异常事件采集的基础。
3.1 目录结构:层次化组织与继承关系
cgroup v2 采用单根树(Unified Hierarchy)结构,所有控制器(Controller)共享同一棵树。根节点为 /sys/fs/cgroup,子节点通过创建子目录形成层级。每个子 cgroup 自动继承父 cgroup 的资源限制,但可以覆盖特定参数。
关键原则:在 cgroup v2 中,一个进程只能属于一个 cgroup,且所有控制器必须挂载在同一棵树上。这消除了 v1 中多棵树导致的资源统计混乱问题。
3.2 关键文件说明:控制参数与状态接口
每个 cgroup 目录下包含一组标准文件,用于资源限制、监控与配置。以下按功能分类列出最关键的接口文件:
| 文件名称 | 类型 | 功能描述 | 观测用途 |
|---|---|---|---|
cgroup.type |
只读 | 显示 cgroup 类型:domain(普通)、domain threaded(线程域)、threaded(线程) | 确认 cgroup 模式是否支持线程级隔离 |
cgroup.procs |
读写 | 列出/写入属于该 cgroup 的 PID 列表(线程组领导) | 追踪进程归属,排查资源泄漏 |
cgroup.threads |
读写 | 列出/写入属于该 cgroup 的线程 TID 列表 | 细粒度线程级观测 |
cpu.stat |
只读 | CPU 使用统计:usage_usec、user_usec、system_usec、nr_periods、nr_throttled、throttled_usec | 延迟水位核心指标:throttled_usec 反映 CPU 节流延迟 |
cpu.weight |
读写 | CPU 权重(1-10000),默认 100,用于相对带宽分配 | 调整 CPU 优先级,观测权重变化对延迟的影响 |
cpu.max |
读写 | CPU 带宽上限:格式 $MAX $PERIOD,如 50000 100000 表示 50% CPU |
设置硬限制,观测节流事件 |
memory.current |
只读 | 当前内存使用量(字节),包含匿名页、文件页、swap | 实时内存水位 |
memory.stat |
只读 | 详细内存统计:anon、file、kernel_stack、pgfault、pgmajfault、workingset_refault 等 | 异常事件观测:pgmajfault 突增预示内存压力 |
memory.events |
只读 | 内存关键事件计数:low、high、max、oom、oom_kill | 异常事件核心接口:oom_kill 计数直接反映 OOM 事件 |
memory.high |
读写 | 内存软限制,超过后触发 reclaim 和 throttle | 设置延迟敏感水位,观测 high 事件频率 |
memory.max |
读写 | 内存硬限制,超过后触发 OOM | 设置硬边界,防止内存泄漏影响宿主机 |
io.stat |
只读 | IO 统计:rbytes、wbytes、rios、wios、dbytes、dios、delay | 延迟水位指标:delay 字段反映 IO 节流延迟 |
io.max |
读写 | IO 带宽/IOPS 限制:格式 $MAJ:$MIN rbps=$VAL wbps=$VAL riops=$VAL wiops=$VAL |
设置 IO 上限,观测节流对延迟的影响 |
pids.current |
只读 | 当前 PID/TID 数量 | 检测 fork 炸弹或线程泄漏 |
pids.max |
读写 | PID 数量上限 | 限制进程/线程数,防止资源耗尽 |
观测最佳实践:对于延迟敏感工作负载,建议同时监控
cpu.stat 中的 nr_throttled 和 throttled_usec、memory.events 中的 high 和 max 事件、以及 io.stat 中的 delay 字段。这三个维度覆盖了 CPU、内存、IO 三大延迟来源。
3.3 读写操作规范:原子性与安全约束
cgroup 文件系统的读写操作遵循严格的规范,错误操作可能导致资源泄露或系统不稳定。
3.3.1 写入操作规范
- 单值写入:大多数控制文件(如
cpu.weight、memory.high)只接受单行写入,多余内容会被忽略。 - 多值写入:部分文件(如
cpu.max、io.max)需要按格式写入多个值,使用空格分隔。 - 原子性保证:对
cgroup.procs写入 PID 时,内核会原子性地将进程从原 cgroup 迁移到目标 cgroup。如果写入的 PID 不存在或已退出,操作静默失败(不返回错误)。 - 权限要求:写入控制参数需要 CAP_SYS_ADMIN 能力,或对目标 cgroup 目录具有写权限(通常由 cgroup 命名空间和 systemd 委托管理)。
# 正确写入示例:设置 CPU 带宽限制
echo "50000 100000" > /sys/fs/cgroup/kubepods/pod123/container-abc/cpu.max
# 正确写入示例:迁移进程到目标 cgroup
echo 12345 > /sys/fs/cgroup/system.slice/sshd.service/cgroup.procs
# 错误示例:写入格式错误
echo "50000" > /sys/fs/cgroup/.../cpu.max # 缺少 period 参数,写入失败
3.3.2 读取操作规范
- 批量读取:对于
*.stat和*.events文件,建议一次性读取全部内容,避免多次 open/close 带来的性能开销。 - 轮询间隔:对于延迟敏感观测,推荐轮询间隔为 100ms-1s。过短的轮询(<10ms)会增加内核锁竞争。
- 原子快照:读取
memory.stat等统计文件时,内核返回的是调用时刻的原子快照,但不同文件之间不存在事务一致性。
# 推荐的一次性读取方式
cat /sys/fs/cgroup/kubepods/pod123/container-abc/cpu.stat
# 输出示例:
# usage_usec 12345678
# user_usec 10000000
# system_usec 2345678
# nr_periods 1234
# nr_throttled 5
# throttled_usec 250000
3.3.3 常见避坑指南
| 操作 | 错误做法 | 正确做法 | 后果 |
|---|---|---|---|
| 创建 cgroup | 直接 mkdir 后不检查返回值 |
使用 mkdir -p 并检查目录是否创建成功 |
并发创建可能导致目录冲突 |
| 删除 cgroup | 直接 rmdir 子 cgroup |
先迁移所有进程到父 cgroup,再 rmdir |
非空目录删除失败,进程残留导致资源泄漏 |
写入 cgroup.procs |
写入线程 TID 而非 PID | 始终写入线程组领导 PID | 线程组内部分线程可能未被迁移 |
读取 memory.events |
只读取 oom 字段 |
同时读取 low、high、max、oom、oom_kill |
遗漏内存压力前兆信号 |
3.4 实战:快速定位延迟相关文件
当观测到应用延迟抖动时,按以下路径快速定位 cgroup 层面的根因:
- 确认进程归属:读取
/proc/$PID/cgroup获取进程所在 cgroup 路径。 - 检查 CPU 节流:读取对应 cgroup 的
cpu.stat,关注nr_throttled和throttled_usec是否持续增长。 - 检查内存压力:读取
memory.events,关注high和max事件计数是否在延迟时段内增加。 - 检查 IO 延迟:读取
io.stat,关注delay字段(单位:纳秒)。 - 检查 PID 限制:读取
pids.current和pids.max,确认是否接近上限导致 fork 失败。
注意:cgroup v2 文件系统接口是内核与用户态之间的无锁契约。不要在生产环境中使用
echo 配合重定向频繁写入控制文件,建议使用 cgset、systemd-run 或编程语言 SDK(如 Go 的 github.com/containerd/cgroups/v2)进行安全操作。