前言"
容器原理篇说过 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 篇。