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 中多棵树导致的资源统计混乱问题。
cgroup v2 目录层次结构图 /sys/fs/cgroup (根) system.slice/ user.slice/ kubepods/ sshd.service/ docker-xxx.scope/ pod123.slice/ pod456.slice/ container-abc/ container-def/ 说明: • 每个目录代表一个 cgroup,自动继承父级资源限制。 • 容器运行时(如 Docker/containerd)通常在 kubepods/ 下创建 pod 和 container 层级。

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 遗漏内存压力前兆信号
cgroup v2 文件读写操作流程图 用户态操作 写入控制文件 内核校验权限与格式 更新 cgroup 状态 读取统计文件 内核生成原子快照 返回统计结果 关键约束: 写入操作需要 CAP_SYS_ADMIN;读取操作无权限限制(默认 644 权限)。

3.4 实战:快速定位延迟相关文件

当观测到应用延迟抖动时,按以下路径快速定位 cgroup 层面的根因:

  1. 确认进程归属:读取 /proc/$PID/cgroup 获取进程所在 cgroup 路径。
  2. 检查 CPU 节流:读取对应 cgroup 的 cpu.stat,关注 nr_throttled 和 throttled_usec 是否持续增长。
  3. 检查内存压力:读取 memory.events,关注 high 和 max 事件计数是否在延迟时段内增加。
  4. 检查 IO 延迟:读取 io.stat,关注 delay 字段(单位:纳秒)。
  5. 检查 PID 限制:读取 pids.current 和 pids.max,确认是否接近上限导致 fork 失败。
注意:cgroup v2 文件系统接口是内核与用户态之间的无锁契约。不要在生产环境中使用 echo 配合重定向频繁写入控制文件,建议使用 cgset、systemd-run 或编程语言 SDK(如 Go 的 github.com/containerd/cgroups/v2)进行安全操作。