与 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) |
Cgroups v1 vs v2
查看当前系统版本
|
核心差异
| 特性 | 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 v2 限制资源
Cgroups 的操作全部通过读写文件 完成--这就是第 01 篇提到的"一切皆文件"思想的典型应用.创建 cgroup = 创建目录,设置限制 = 写文件,加入进程 = 把 PID 写进文件.
CPU 限制
|
cpu.max 的格式
$MAX $PERIOD:在每个 $PERIOD 微秒的周期内,该 cgroup 最多使用 $MAX 微秒的 CPU 时间.
例如
"50000 100000"= 50%"100000 100000"= 100%(一个核)"200000 100000"= 200%(两个核)- 设为
"max 100000"表示不限制.
内存限制
|
| 文件 | 作用 | 超限后果 |
|---|---|---|
memory.max |
硬上限 | 触发 OOM Killer 杀进程 |
memory.high |
软上限 | 内核积极回收内存,进程被节流但不会被杀 |
memory.low |
最低保障 | 低于此值时内核尽量不回收该 cgroup 的内存 |
类比手机套餐:
memory.high像流量软限(超了降速但还能用)memory.max像硬限(超了直接断网).
IO 限制
|
PID 数量限制
|
fork 炸弹是什么?
一个恶意或失控的程序不断 fork 子进程,指数级增长直到耗尽系统资源.经典的 Bash fork 炸弹::(){ :|:& };:(千万不要运行).pids.max 就是容器的最后一道防线.
Docker 中的 Cgroups 应用
Docker 的 --cpus,--memory 等参数,底层就是创建 cgroup 并写入对应的限制文件:
|
|
核心理解
Docker 的资源限制参数没有任何魔法--它只是把你传入的值换算后写入 /sys/fs/cgroup/ 下的文件.你完全可以手动做同样的事.Docker 只是一层自动化封装.
深入理解
陷阱:容器内 free 看到的内存是宿主机的
如果一个容器的 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 感知的值.
cgroup "文件"的本质:不是文件,是 API
你可能会问:内核是不是实时去"读取"cpu.max 这个文件来做调度?
不是.这些"文件"根本不存在于磁盘上./sys/fs/cgroup/ 是虚拟文件系统,只是内核暴露给用户态的通信接口:
|
类比银行柜台:你填表(写文件)交给柜员(内核),柜员录入系统(内存数据结构).之后银行做业务查的是系统记录,不是翻你那张纸.你再来查询时(读文件),柜员从系统现查现报.
重启会丢失吗?
会. 但这不是问题--重建 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 版本 - 手动创建一个 cgroup,限制 CPU 为 30%,运行
dd if=/dev/zero of=/dev/null验证限制生效 - 用
docker run --memory="64m" ubuntu stress --vm 1 --vm-bytes 128M观察 OOM Killer 触发 - 运行一个 Docker 容器,找到其对应的 cgroup 目录,手动读取
cpu.max和memory.max文件 - 思考题:如果一个容器的
memory.max被设为 256MB,容器内用free命令看到的内存是多少?为什么?(提示:和 Namespace 有关)