2026-01-12

Linux cgroup:V1、V2、CPU 节流和 OOM 排障

从 cgroup V1/V2 差异出发,用 cpu.stat、memory.events、PSI 和 Kubernetes 资源配置定位 CPU 节流、内存回收与 OOM。

cgroup 给一组进程做资源记账、权重分配和上限控制。容器运行时、Kubernetes 和 systemd 都使用它,但生产排障不能只看 Pod 的 requests/limits:最终是否节流、回收或 OOM,要回到节点上的 cgroup 文件确认。

本文只讲排障需要的部分:确认 V1/V2、找到目标 cgroup、读懂 CPU 与内存计数器,再把结果映射回 Kubernetes 配置。

先确认 cgroup 版本

stat -fc %T /sys/fs/cgroup
  • cgroup2fs:cgroup V2,通常只有一个统一层级。
  • tmpfs:通常是 V1;继续检查各控制器挂载点。
mount -t cgroup,cgroup2
cat /proc/self/cgroup

不要看到 /sys/fs/cgroup 就假定是 V2。V1、V2 的文件名和部分语义不同,混用命令只会得到 No such file or directory 或错误结论。

V1 与 V2 的关键差异

项目 cgroup V1 cgroup V2
层级 控制器可以挂在不同树上 所有控制器共享统一层级
进程位置 CPU、内存等控制器可能处于不同路径 一个进程在统一树中只有一个位置
CPU 上限 cpu.cfs_quota_us + cpu.cfs_period_us cpu.max
CPU 权重 cpu.shares cpu.weight
内存上限 memory.limit_in_bytes memory.max
内存节流 memory.soft_limit_in_bytes,语义较弱 memory.high,超过后触发强制回收与节流
IO blkio.* io.*
压力指标 主要使用全局 PSI 支持每个 cgroup 的 cpu.pressure、memory.pressure、io.pressure

V2 还有 No Internal Process Constraint:对 domain controller 而言,启用子控制器的内部节点通常不能同时承载普通进程。委派和 threaded cgroup 有额外规则,不能简单理解成“任何进程都只能放叶子节点”。

不要直接改 systemd 或 kubelet 管理的 cgroup

生产节点上的 cgroup 通常由 systemd、容器运行时或 kubelet拥有。手工 echo 修改可能被管理器下一次同步覆盖,也可能破坏运行时假设。

需要做本地实验时,优先创建一个临时 systemd scope:

systemd-run --scope \
  -p CPUQuota=50% \
  -p MemoryHigh=250M \
  -p MemoryMax=300M \
  bash

cat /proc/$$/cgroup

只有获得明确委派的子树,才适合直接写 cgroup.subtree_control、cpu.max 或 memory.max。

CPU:区分权重和硬配额

cpu.weight 是竞争发生时的相对权重,不是 CPU 上限。cpu.max 才是 V2 的带宽上限,格式为:

$MAX $PERIOD

例如每 100ms 周期最多使用 50ms CPU 时间:

echo '50000 100000' > cpu.max

这等价于整个 cgroup 平均最多使用 0.5 个 CPU。cgroup 内有多个并行线程时,它们会一起消耗配额,因此可能在一个周期结束前耗尽额度,随后全部等待下一个周期。

排障先看 cpu.stat:

cat cpu.max
cat cpu.weight
cat cpu.stat

V2 常见字段:

  • usage_usec:累计 CPU 时间。
  • nr_periods:经历的配额周期数。
  • nr_throttled:至少发生一次节流的周期数。
  • throttled_usec:累计节流时间。

不要只看 nr_throttled 是否非零。观察一段与延迟异常相同的时间窗口,计算增量,再和请求延迟、运行线程数、GC 或批处理任务对齐。

cpu.max.burst 可允许有限突发,但它改变的是配额模型,不是免费容量。启用前必须验证邻居工作负载和节点总体竞争。

内存:memory.high、memory.max 和 OOM 不是同一件事

V2 内存排障至少读取:

cat memory.current
cat memory.high
cat memory.max
cat memory.events
cat memory.events.local
grep -E '^(anon|file|kernel|slab|sock|shmem) ' memory.stat
  • memory.current:当前记账到该 cgroup 的内存。
  • memory.high:超过后触发强制回收和节流;通常不会直接触发 OOM。
  • memory.max:硬上限。内核会先尝试回收,无法把使用量压回限制内时才进入 cgroup OOM。
  • memory.events:包含 high、max、oom、oom_kill 等计数,并可能包含子 cgroup 事件。
  • memory.events.local:只看当前 cgroup,避免父层级聚合干扰。

Page Cache 会计入 cgroup 的 file 内存,但它通常可回收。看到 RSS 较低、memory.current 较高时,不要直接下结论;同时比较 anon、file、slab、shmem 和 memory.events。

需要把一组进程作为一个工作负载处理时,可以评估 memory.oom.group=1。它要求内核在 cgroup OOM 时尽量终止整个 workload,而不是只杀掉其中一个进程后留下不完整服务。

IO 与 PID:看控制器自己的事件

V2 使用 io.max 按设备配置读写带宽或 IOPS:

cat io.max
cat io.stat

设备由主次设备号标识,例如:

8:0 rbps=max wbps=10485760 riops=max wiops=max

Buffered IO 的回写时机、文件系统和底层设备都会影响观察结果。验收时同时看应用延迟、io.stat 和 io.pressure,不要只凭一条配置推断实际吞吐。

PID 控制器用于限制 cgroup 中的任务数量:

cat pids.current
cat pids.max
cat pids.events

达到 pids.max 后,新建进程或线程可能收到 EAGAIN。pids.events 中的 max 计数能证明限制确实被命中。

PSI:测量等待资源造成的停顿

系统级 PSI 位于:

cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

cgroup V2 还提供每组压力文件:

cat cpu.pressure
cat memory.pressure
cat io.pressure

输出示例:

some avg10=2.34 avg60=1.87 avg300=0.98 total=492817349
full avg10=0.41 avg60=0.20 avg300=0.09 total=91234567

some 表示至少有任务因资源压力停顿,full 表示所有非空闲任务同时停顿。不要套用固定的 avg10 > 5 告警阈值;先建立本服务基线,再用 SLO、队列长度和延迟验证阈值。

Kubernetes 如何映射到 cgroup

在常见 Linux 节点上:

  • CPU request 主要影响相对权重;CPU limit 通常映射为 CFS 带宽配额。
  • Memory limit 通常成为 cgroup 的硬内存上限。
  • Memory request 是否映射到 memory.min 或 memory.low,取决于 Kubernetes、运行时和 MemoryQoS 配置,不能一概而论。
  • initContainer、sidecar、Pod-level resources 和 QoS 分类都会影响最终层级,不能只看主容器。

先从 Kubernetes 侧收集证据:

kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>
kubectl get pod -n <ns> <pod> \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.containerID}{"\t"}{.lastState.terminated.reason}{"\n"}{end}'

再通过容器运行时取得容器在宿主机上的 PID,读取 /proc/<pid>/cgroup,映射到实际 cgroup 目录。不同 runtime 与 cgroup driver 的路径不同,不要在脚本里写死 kubepods.slice 层级。

两条生产 Runbook

CPU 延迟突增

  1. 记录异常窗口内 cpu.stat 的 nr_periods、nr_throttled、throttled_usec 增量。
  2. 同时检查 cpu.pressure、运行线程数和节点 CPU 饱和度。
  3. 区分硬配额节流与节点竞争:前者看 cpu.max 和节流计数,后者看权重、PSI 与节点负载。
  4. 调整 limit 前先确认并行度、GC、批处理和请求突发是否符合预期。

Pod OOMKilled

  1. 用 Pod status 和 Events 确认容器退出原因、时间和重启次数。
  2. 在对应 cgroup 中读取 memory.events.local,确认 oom、oom_kill 是否增加。
  3. 用 memory.stat 区分匿名内存、文件缓存、内核内存和共享内存。
  4. 检查父 cgroup 是否先命中上限;子容器未超限不代表 Pod 或上层 slice 没有限制。
  5. 最后再决定修复泄漏、调整缓存、降低并发,还是修改 requests/limits。

五分钟采集脚本

cg=${1:?usage: cgroup-snapshot.sh /sys/fs/cgroup/path}

for file in \
  cgroup.type cgroup.controllers cgroup.subtree_control \
  cpu.max cpu.weight cpu.stat cpu.pressure \
  memory.current memory.low memory.high memory.max \
  memory.events memory.events.local memory.pressure \
  io.stat io.pressure pids.current pids.max pids.events
do
  if [ -r "$cg/$file" ]; then
    printf '\n### %s\n' "$file"
    cat "$cg/$file"
  fi
done

脚本只读,不修改由 systemd 或 kubelet 管理的状态。将快照和同一时间段的应用指标、节点指标放在一起,才能判断限制是原因、结果,还是无关信号。

参考链接