前言"

容器原理篇说过 cgroup 是容器的三大支柱之一,这篇把它单独拿出来亲手玩透——不用 Docker,直接用内核的 cgroup 文件系统给任意进程上"资源紧箍咒"。

一、先看懂版本

stat -fc %T /sys/fs/cgroup/
# cgroup2fs   → cgroup v2(统一层级, RHEL9/新系统默认)
# tmpfs       → cgroup v1(多层级, 老系统)
版本 特点
v1 每种资源一个树(cpu/ memory/ …),各管各的
v2 统一一棵树,接口更简洁,进程归属唯一

本文以 v2 为主(趋势),v1 差异处标注。

二、v2 目录结构与核心文件

ls /sys/fs/cgroup/
# cgroup.controllers      本级可用的控制器
# cgroup.subtree_control  启用哪些控制器下放给子组
# cgroup.procs            组里的进程 PID 列表
# cpu.max / memory.max / io.max   ...各控制器文件

# 一层要往下传控制器才可用(父子约定)
cat /sys/fs/cgroup/cgroup.controllers
# cpuset cpu io memory hugetlb pids rdma misc

三、实战一:CPU 限制(0.5 核)

# 1. 建组(mkdir 即创建)
mkdir /sys/fs/cgroup/lab
#    若提示 no internal process: 先确保父级没有进程直接挂在组上

# 2. 配额: 每 100ms 调度周期内最多用 50ms CPU = 0.5 核
echo "50000 100000" > /sys/fs/cgroup/lab/cpu.max

# 3. 把一个死循环进程圈进去
sha1sum /dev/zero &
PID=$!
echo $PID > /sys/fs/cgroup/lab/cgroup.procs

# 4. 验证(另开终端)
top -p $PID
# %CPU ≈ 50.0  ← 被摁在半核, 稳如老狗

# v1 等价操作:
# echo 50000 > /sys/fs/cgroup/cpu/lab/cpu.cfs_quota_us
# echo 100000 > /sys/fs/cgroup/cpu/lab/cpu.cfs_period_us
# echo $PID > /sys/fs/cgroup/cpu/lab/tasks

原理:CFS 带宽控制——周期内用完配额,该组进程**被节流(throttle)**直到下个周期。观察节流:

cat /sys/fs/cgroup/lab/cpu.stat
# usage_usec 5123000
# nr_throttled 45         ← 被节流 45 次
# throttled_usec 3200000  ← 累计被冻结的时间

其他 CPU 手段:

# cpuset: 直接限定只能用哪几个核(绑核)
echo "2-3" > /sys/fs/cgroup/lab/cpuset.cpus
# weight: 相对份额(默认100, 类似 nice)
echo "50" > /sys/fs/cgroup/lab/cpu.weight

四、实战二:内存上限与 OOM

# 限额 100MB
echo "104857600" > /sys/fs/cgroup/lab/memory.max

# 圈进一个吃内存进程
python3 -c "
import time
buf = []
while True:
    buf.append(b'x' * 1024 * 1024)   # 每秒吃1MB
    time.sleep(0.1)
" &
echo $! > /sys/fs/cgroup/lab/cgroup.procs

# 观察
watch -n1 cat /sys/fs/cgroup/lab/memory.current
# 涨到 104857600 时 → 进程被 OOM Kill
dmesg | tail -2
# Memory cgroup out of memory: Killed process 23831 (python3)

内存的软硬两级:

echo "52428800" > /sys/fs/cgroup/lab/memory.high    # 软限: 超过就回收/减速
echo "104857600" > /sys/fs/cgroup/lab/memory.max    # 硬限: 超过就杀

监听 OOM 事件(编排系统的预警手段):

# memory.events 里是计数
cat /sys/fs/cgroup/lab/memory.events
# oom 1
# oom_kill 1            ← 真杀了
# 事件通知: 监听 memory.events 文件变化(inotify)

五、实战三:IO 限制(防止刷盘怪兽)

# 限写 /dev/vdb 到 10MB/s(先查设备号)
lsblk -o NAME,MAJ:MIN
# vdb 253:16

echo "253:16 rbps=10485760" > /sys/fs/cgroup/lab/io.max

# 圈进 dd 测试
dd if=/dev/zero of=/data/test.img bs=1M count=500 oflag=direct &
echo $! > /sys/fs/cgroup/lab/cgroup.procs
# dd: 10.4 MB/s  ← 被限速

六、pids:防 fork 炸弹

# 该组最多 100 个进程
echo 100 > /sys/fs/cgroup/lab/pids.max

# 验证
bash -c 'for i in $(seq 200); do sleep 100 & done' 2>&1 | tail -1
# bash: fork: 资源暂时不可用   ← 到顶了, 系统其他部分安然无恙

七、systemd 与 cgroup:日常的正确入口

生产环境不建议手搓 cgroup 文件——systemd 已经是标准管理入口:

# slice = cgroup 组的封装
systemd-cgls                          # 查看层级树
systemd-cgtop                         # 各组资源排行

# 给服务限额 = 写 unit(服务管理篇的老朋友)
[Service]
CPUQuota=50%                          # → cpu.max
MemoryMax=200M                        # → memory.max
TasksMax=100                          # → pids.max
IOWriteBandwidth=/data 10M            # → io.max

systemctl daemon-reload && systemctl restart myapp
# 验证 unit 落到了哪个 cgroup
cat /proc/$(systemctl show -p MainPID --value myapp)/cgroup
# 0::/system.slice/myapp.service
ls /sys/fs/cgroup/system.slice/myapp.service/

临时跑个受限进程(一行搞定):

systemd-run --scope -p CPUQuota=30% -p MemoryMax=100M \
  --uid=app ./heavy-job

八、回看容器/K8s 的限额

docker run --cpus=0.5 --memory=256m busybox sleep 99 &
# 它做的事(v2 机器上):
ls /sys/fs/cgroup/system.slice/docker-<id>.scope/
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/cpu.max     # 50000 100000
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max  # 268435456

# K8s 的 requests/limits:
#   limits.cpu: 500m   → 本质就是这个 cpu.max
#   limits.memory: 256Mi → memory.max, 超了= Pod OOMKilled(exit 137)
#   requests 内存      → 影响调度, 不设硬限

Docker/K8s 没有魔法,只是 cgroup 的编排器——你现在有了直接看穿和调试它们的能力:

# K8s 节点上找 Pod 的 cgroup
kubectl get pods -o wide                      # 找节点
crictl pods | grep <pod>                     # 拿 cgroup 路径
cat /sys/fs/cgroup/kubepods.slice/.../cpu.max

九、常见坑

现象 原因
mkdir 报 no internal process v2 要求中间组不能直接挂进程,移走 cgroup.procs
echo 报 no such file 控制器没在父级 subtree_control 里启用
限额不生效 进程 PID 写错组;子进程继承但组要确认
cpu.max 设 1000 100000 千分之一的核——配额/周期别写反
删不掉组 组里还有进程或子组,先迁移再 rmdir

小结

需求 文件(v2) systemd 等价
CPU 硬限 cpu.max CPUQuota=50%
绑核 cpuset.cpus AllowedCPUs=
内存硬限 memory.max MemoryMax=
内存软限 memory.high MemoryHigh=
IO 限速 io.max IOWriteBandwidth=
防炸 pids.max TasksMax=
观测 cpu.stat / memory.events systemd-cgtop

本文是「Linux 基础」系列第 10 篇。