Cgroups(Control Groups)是 Linux 内核提供的资源限制机制——控制进程组能用多少 CPU、内存、IO 等资源
与 Namespace 的关系
上一篇讲的 Namespace 解决的是"看到什么"(隔离视图),本篇讲的 Cgroups 解决的是"用多少"(资源限制)。两者结合就是容器的核心:一个既看不到外界、又不能无限消耗资源的进程。
类比:Namespace 像给每个租户一个独立的房间(看不到别人),Cgroups 则像给每个房间装了电表和水表——超出额度就断供,防止一个租户用光整栋楼的水电。
| 概念 | 说明 | 类比 |
|---|---|---|
| cgroup | 一组进程的集合,绑定了一组资源限制参数 | 一个"资源配额账户" |
| subsystem / 控制器 | 具体的资源控制器,如 cpu、memory、io 等 | 电表、水表、燃气表……每种资源一个计量器 |
| hierarchy(层级) | cgroup 的树形组织结构,子 cgroup 继承父级的限制 | 公司的预算层级——部门预算 ≤ 公司总预算 |
| task | cgroup 中的进程(由 PID 标识) | 账户里的"消费者" |
| 控制器 | 控制资源 | 容器场景 |
|---|---|---|
| cpu | CPU 时间片分配 | 限制容器 CPU 使用比例 |
| cpuset | 绑定特定 CPU 核心 | 将容器绑定到指定 CPU(减少调度开销) |
| memory | 内存使用上限 | 防止容器 OOM 影响宿主机 |
| io(v2)/ blkio(v1) | 块设备 IO 限制 | 限制容器磁盘读写速率 |
| pids | 进程数量限制 | 防止 fork 炸弹 |
| devices | 设备访问控制 | 控制容器能访问哪些设备 |
| freezer | 进程挂起 / 恢复 | 容器暂停功能(docker pause) |
# 判断系统使用 v1 还是 v2
stat -fc %T /sys/fs/cgroup/
# "cgroup2fs" → v2
# "tmpfs" → v1
# v2 下查看可用控制器
cat /sys/fs/cgroup/cgroup.controllers
# 输出示例: cpuset cpu io memory hugetlb pids rdma misc
| 特性 | Cgroups v1 | Cgroups v2 |
|---|---|---|
| 层级结构 | 每个控制器独立一棵树 | 统一层级——所有控制器在同一棵树上 |
| 挂载点 | /sys/fs/cgroup/cpu/、/sys/fs/cgroup/memory/……分散 |
/sys/fs/cgroup/ 一个入口 |
| 进程归属 | 一个进程可属于不同控制器层级的不同 cgroup | 一个进程只能属于一个 cgroup(叶子节点) |
| 线程控制 | 不支持 | 支持线程级别的资源控制 |
| 压力监控 | 无 | 支持 PSI(Pressure Stall Information) |
| 状态 | 逐步淘汰 | 推荐使用,主流发行版默认 |
v2 为什么要统一层级?
v1 中每个控制器独立一棵树,一个进程可能在 cpu 树的 A 组、在 memory 树的 B 组——管理混乱、容易冲突。v2 强制统一:一个进程只属于一个 cgroup 节点,该节点上可以同时启用多个控制器。简单、清晰、不矛盾。
Cgroups 的操作全部通过读写文件完成——这就是第 01 篇提到的"一切皆文件"思想的典型应用。创建 cgroup = 创建目录,设置限制 = 写文件,加入进程 = 把 PID 写进文件。
# 创建一个 cgroup(就是创建一个目录)
sudo mkdir /sys/fs/cgroup/my-container
# 启用 cpu 控制器
echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 设置 CPU 限制:每 100ms 周期内最多使用 20ms CPU(即 20%)
echo "20000 100000" | sudo tee /sys/fs/cgroup/my-container/cpu.max
# 格式: $MAX $PERIOD(单位:微秒)
# CPU 使用率 = MAX / PERIOD = 20000 / 100000 = 20%
# 将当前 shell 加入该 cgroup
echo $$ | sudo tee /sys/fs/cgroup/my-container/cgroup.procs
# 运行 CPU 密集型任务,观察被限制在 ~20%
dd if=/dev/zero of=/dev/null &
top # 查看 dd 的 CPU 使用率
# 清理
kill %1
echo $$ | sudo tee /sys/fs/cgroup/cgroup.procs # 移回根 cgroup
sudo rmdir /sys/fs/cgroup/my-container
cpu.max 的格式
$MAX $PERIOD:在每个 $PERIOD 微秒的周期内,该 cgroup 最多使用 $MAX 微秒的 CPU 时间。例如 "50000 100000" = 50%,"100000 100000" = 100%(一个核),"200000 100000" = 200%(两个核)。设为 "max 100000" 表示不限制。
sudo mkdir /sys/fs/cgroup/mem-test
echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 硬限制:最多使用 100MB,超出触发 OOM Killer
echo "104857600" | sudo tee /sys/fs/cgroup/mem-test/memory.max
# 软限制:超过 50MB 时内核优先回收该 cgroup 的内存
echo "52428800" | sudo tee /sys/fs/cgroup/mem-test/memory.high
# 查看当前内存使用量
cat /sys/fs/cgroup/mem-test/memory.current
# 查看详细内存统计
cat /sys/fs/cgroup/mem-test/memory.stat
| 文件 | 作用 | 超限后果 |
|---|---|---|
memory.max | 硬上限 | 触发 OOM Killer 杀进程 |
memory.high | 软上限 | 内核积极回收内存,进程被节流但不会被杀 |
memory.low | 最低保障 | 低于此值时内核尽量不回收该 cgroup 的内存 |
类比手机套餐:memory.high 像流量软限(超了降速但还能用),memory.max 像硬限(超了直接断网)。
sudo mkdir /sys/fs/cgroup/io-test
echo "+io" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 查看磁盘设备号(Major:Minor)
lsblk
# 假设主磁盘是 sda,设备号 8:0
# 限制该 cgroup 对 sda 的读写速率各为 1MB/s
echo "8:0 rbps=1048576 wbps=1048576" | sudo tee /sys/fs/cgroup/io-test/io.max
# rbps = read bytes per second
# wbps = write bytes per second
sudo mkdir /sys/fs/cgroup/pid-test
echo "+pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 限制最多 20 个进程(防止 fork 炸弹)
echo "20" | sudo tee /sys/fs/cgroup/pid-test/pids.max
# 查看当前进程数
cat /sys/fs/cgroup/pid-test/pids.current
fork 炸弹是什么?
一个恶意或失控的程序不断 fork 子进程,指数级增长直到耗尽系统资源。经典的 Bash fork 炸弹::(){ :|:& };:(千万不要运行)。pids.max 就是容器的最后一道防线。
Docker 的 --cpus、--memory 等参数,底层就是创建 cgroup 并写入对应的限制文件:
# Docker 运行容器时指定资源限制
docker run -d \
--name my-app \
--cpus="0.5" \
--memory="256m" \
--memory-swap="512m" \
--pids-limit=100 \
nginx
# --cpus="0.5" → cpu.max = "50000 100000"(50% 的一个核)
# --memory="256m" → memory.max = 268435456
# --pids-limit=100 → pids.max = 100
# 查看 Docker 为容器创建的 cgroup 配置
# 路径通常是:/sys/fs/cgroup/system.slice/docker-<container-id>.scope/
# 通过 docker inspect 查看资源限制
docker inspect my-app --format '{{.HostConfig.NanoCpus}}'
docker inspect my-app --format '{{.HostConfig.Memory}}'
# 直接读取 cgroup 文件验证
CONTAINER_ID=$(docker inspect my-app --format '{{.Id}}')
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.max
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
核心理解
Docker 的资源限制参数没有任何魔法——它只是把你传入的值换算后写入 /sys/fs/cgroup/ 下的文件。你完全可以手动做同样的事。Docker 只是一层自动化封装。
如果一个容器的 memory.max 被设为 256MB,容器内执行 free -h 看到的内存是多少?
答案:看到的是宿主机的全部内存(如 16GB),而不是 256MB。
原因:free 读取的是 /proc/meminfo,而 Linux 没有 Memory Namespace——/proc/meminfo 永远反映宿主机物理内存总量,不感知 Cgroups 的限制。
| 机制 | 解决的问题 | 影响 /proc/meminfo? |
|---|---|---|
| Namespace | 看到什么(视图) | 没有 Memory NS,不影响 |
| Cgroups | 用多少(配额) | 不影响,进程照样看到宿主机全量 |
真实的坑
早期 Java 应用在容器里读 /proc/meminfo 决定 JVM 堆大小,按宿主机 16GB 的 75% 分配了 12GB 堆——但容器配额只有 256MB,瞬间 OOM 被杀。JDK 10+ 才修复此问题(主动读取 cgroup 文件获取真实限额)。社区方案:LXCFS 可以劫持容器的 /proc/meminfo,让它返回 cgroup 感知的值。
你可能会问:内核是不是实时去"读取"cpu.max 这个文件来做调度?
不是。这些"文件"根本不存在于磁盘上。/sys/fs/cgroup/ 是虚拟文件系统,只是内核暴露给用户态的通信接口:
你的操作 内核内部
────────── ─────────
echo "20000 100000" > cpu.max
│
▼
VFS 层拦截写操作
│
▼
触发内核回调函数
cpu_max_write()
│
▼
值存入内核内存中的数据结构 ← 存在内存里,不在文件里
(task_group.cfs_bandwidth)
│
▼
CPU 调度器每次调度时
直接读内存中的数据结构做判断 ← 不是去"读文件"
类比银行柜台:你填表(写文件)交给柜员(内核),柜员录入系统(内存数据结构)。之后银行做业务查的是系统记录,不是翻你那张纸。你再来查询时(读文件),柜员从系统现查现报。
会。但这不是问题——重建 cgroup 是上层服务的职责:
| 场景 | 谁负责重建 cgroup? |
|---|---|
| Docker 容器 | dockerd 启动时重新创建 |
| systemd 服务 | systemd 为每个 .service 重建 |
| K8s Pod | kubelet 启动时重新配置 |
| 手动测试 | 丢了就丢了,本来就是临时的 |
设计哲学:内核只提供机制,不负责持久化。"重启后恢复到什么状态"是上层应用的职责——Docker 把容器配置存在 /var/lib/docker/,启动时重新"申报"给内核。你手动 echo 写的配置之所以会丢,是因为没有上层服务帮你记住并重放。
容器三件套到此两件就位
Namespace(第 02 篇)= 看到什么(视图隔离)
Cgroups(本篇)= 用多少(资源限制)
rootfs(第 04 篇 OverlayFS)= 文件从哪来(分层文件系统)
三者合一 = 容器。一个既看不到外界、又不能无限消耗资源、还有独立文件系统的进程。
| 要点 | 一句话概括 |
|---|---|
| Cgroups 本质 | 通过文件系统接口(/sys/fs/cgroup/)控制进程组的资源配额 |
| 操作方式 | 创建目录 = 建 cgroup,写文件 = 设限制,写 PID = 加入进程 |
| v1 vs v2 | v2 统一层级、一个入口、推荐使用;v1 分散混乱、逐步淘汰 |
| 四大限制 | CPU(cpu.max)/ 内存(memory.max/high)/ IO(io.max)/ PID 数(pids.max) |
| Docker 集成 | --cpus / --memory 等参数只是自动写 cgroup 文件,无魔法 |
stat -fc %T /sys/fs/cgroup/ 确认你系统的 Cgroups 版本dd if=/dev/zero of=/dev/null 验证限制生效docker run --memory="64m" ubuntu stress --vm 1 --vm-bytes 128M 观察 OOM Killer 触发cpu.max 和 memory.max 文件memory.max 被设为 256MB,容器内用 free 命令看到的内存是多少?为什么?(提示:和 Namespace 有关)