当你执行 docker run --memory=512m --cpus=2 nginx 时,Docker 是怎么限制容器只能使用 512MB 内存和 2 个 CPU 的?答案是 Cgroup,Linux 内核的**控制组(Control Group)**机制。Cgroup 让你将进程分组,并对每个组施加资源限制、优先级分配和审计统计。
Cgroup 经历了从 v1 到 v2 的重大架构演进。v1 的每个控制器有独立的层级树,进程可能同时属于不同控制器的不同 cgroup,管理混乱;v2 采用统一层级,所有控制器共享同一棵 cgroup 树,一个进程只属于一个 cgroup。
Cgroup 的历史可以追溯到 2006 年,Google 工程师 Paul Menage 和 Rohit Seth 向内核提交了“Process Containers”补丁,2007 年合入主线(随 2.6.24 于 2008 年初发布),后更名为 Control Groups。v1 允许每个控制器(cpu、memory、blkio 等)拥有独立层级树,进程可以同时属于不同控制器的不同 cgroup,这带来了管理混乱:进程在 cpu cgroup 属于 /A,在 memory cgroup 却属于 /B,资源归属难以追踪。2016 年 Tejun Heo 主导 v2 重构,采用统一层级,一个进程只属于一个 cgroup。这一“看似更不灵活”的设计反而解决了 v1 的核心痛点。
前置知识
Ch02 Linux Namespace 深入:Namespace 提供”视图隔离”,Cgroup 提供”资源限制”
Linux 文件系统操作:Cgroup v2 通过
/sys/fs/cgroup/文件系统暴露接口,所有操作都是文件读写Linux 进程管理基础:进程树、信号、资源统计
Cgroup 和 Namespace 经常被混淆:Namespace 决定进程”能看到什么”,Cgroup 决定进程”能用多少”。
下面看 Cgroup v2 的架构设计、CPU/内存/IO 三大控制器,以及容器运行时的使用方式。
一、Cgroup v2 架构
1.1 从 Cgroup v1 到 v2
1.2 Cgroup v1 vs v2 对比
| 维度 | Cgroup v1 | Cgroup v2 |
|---|---|---|
| 层级结构 | 每个控制器独立层级 | 统一层级 |
| 进程归属 | 可属于不同控制器的不同 cgroup | 只能属于一个 cgroup |
| 控制器挂载 | 各自挂载到 /sys/fs/cgroup/控制器名 | 统一挂载到 /sys/fs/cgroup/ |
| 内存控制器 | memory + memsw | memory(含 swap) |
| IO 控制器 | blkio | io(基于 cgroup 写回) |
| 压力通知 | 无 | per-cgroup PSI(系统级 PSI 4.20 起独立存在) |
| eBPF 扩展 | 有限 | 完整支持 |
| 内核版本 | 2.6.24+ | 4.5+(推荐 5.4+) |
1.3 Cgroup v2 的主要文件
# 查看 Cgroup v2 的根目录ls /sys/fs/cgroup/
# 核心文件cgroup.controllers # 当前 cgroup 可用的控制器cgroup.subtree_control # 子 cgroup 启用的控制器cgroup.procs # 属于当前 cgroup 的进程 PIDcgroup.type # cgroup 类型(domain/threaded)cgroup.max.depth # 最大嵌套深度cgroup.max.descendants # 最大后代数量cgroup.stat # 统计信息
# PSI(Pressure Stall Information)cpu.pressure # CPU 压力memory.pressure # 内存压力io.pressure # IO 压力二、CPU 控制器
2.1 cpu.max:硬限制
cpu.max 设置 CPU 时间的硬限制,格式为 quota period(微秒):
# 限制容器最多使用 2 个 CPU 核心# quota = 200000μs, period = 100000μsecho "200000 100000" > /sys/fs/cgroup/docker/container1/cpu.max
# 查看当前设置cat /sys/fs/cgroup/docker/container1/cpu.max# 200000 100000
# 不限制(默认)echo "max 100000" > /sys/fs/cgroup/docker/container1/cpu.maxCPU 配额计算公式:
可用 CPU 核数 = quota / period例如:200000 / 100000 = 2 核2.2 cpu.weight:软限制(权重)
cpu.weight 设置 CPU 时间的权重分配(1-10000,默认 100):
# 设置权重为 200(相对于默认 100,获得 2 倍 CPU 时间)echo "200" > /sys/fs/cgroup/docker/container1/cpu.weight
# 两个容器的 CPU 时间分配比例# container1 (weight=200) : container2 (weight=100) = 2:12.3 cpu.max vs cpu.weight
| 特性 | cpu.max | cpu.weight |
|---|---|---|
| 类型 | 硬限制 | 软限制(权重) |
| 超限行为 | 进程被限流(throttled) | 按权重分配空闲 CPU |
| 适用场景 | 严格限制 CPU 使用 | 相对优先级分配 |
| Docker 参数 | --cpus=2 | --cpu-shares=2048 |
| 空闲 CPU | 不可使用空闲 CPU | 可以使用空闲 CPU |
2.4 CPU 限流机制
2.5 cpuset:CPU 亲和性
# 限制容器只能使用 CPU 0 和 CPU 2echo "0,2" > /sys/fs/cgroup/docker/container1/cpuset.cpus
# 限制 NUMA 节点echo "0" > /sys/fs/cgroup/docker/container1/cpuset.mems
# Docker 等价参数docker run --cpuset-cpus=0,2 nginx三、内存控制器
3.1 memory.max:内存硬限制
# 限制容器最多使用 512MB 内存echo "536870912" > /sys/fs/cgroup/docker/container1/memory.max # 512 * 1024 * 1024
# 查看当前内存使用cat /sys/fs/cgroup/docker/container1/memory.current# 134217728 (128MB)
# 查看内存限制cat /sys/fs/cgroup/docker/container1/memory.max# 536870912
# 不限制echo "max" > /sys/fs/cgroup/docker/container1/memory.max3.2 memory.min / memory.low:内存保护
Cgroup v2 引入了内存保护机制,防止重要容器的内存被 OOM 回收:
| 文件 | 含义 | OOM 行为 |
|---|---|---|
memory.min | 最小内存保证(硬保护) | 低于此值绝不回收 |
memory.low | 最佳内存保证(软保护) | 低于此值尽量不回收 |
memory.max | 最大内存限制 | 超过此值触发 OOM |
# 设置内存保护echo "134217728" > memory.min # 保证至少 128MBecho "268435456" > memory.low # 尽量保留 256MBecho "536870912" > memory.max # 最多使用 512MB3.3 Swap 控制
# Cgroup v2 的 swap 控制# memory.swap.max = 最大 swap 使用量echo "268435456" > memory.swap.max # 最多 256MB swap
# 查看当前 swap 使用cat memory.swap.current
# 禁用 swap(Docker 默认)echo "0" > memory.swap.max
# Docker 等价参数docker run --memory=512m --memory-swap=1g nginx3.4 OOM 控制与处理
# OOM 控制组echo "1" > memory.oom.group # 整个 cgroup 作为 OOM 受害者
# OOM 事件通知(通过 cgroup.events)cat memory.events# oom 5 # OOM 事件计数# oom_kill 3 # OOM kill 计数# oom_group_kill 2 # 组 OOM kill 计数
# 内存压力统计cat memory.stat# anon 134217728 # 匿名页# file 67108864 # 文件缓存页# slab 33554432 # Slab 缓存# pgfault 12345 # 页错误# pgmajfault 67 # 主要页错误3.5 内存控制流程
四、IO 控制器
4.1 io.max:IO 硬限制
# 限制容器对 /dev/sda 的读写速率# 格式:major:minor rbps wbps riops wiopsecho "8:0 rbps=104857600 wbps=52428800 riops=1000 wiops=500" > io.max
# 查看当前 IO 限制cat io.max# 8:0 rbps=104857600 wbps=52428800 riops=1000 wiops=500
# 查看当前 IO 统计cat io.stat# 8:0 rbytes=12345678 wbytes=87654321 rios=1234 wios=567 dbytes=0 dios=04.2 io.weight:IO 权重
# 设置 IO 权重(1-10000,默认 100)echo "200" > io.weight
# 按设备设置权重echo "8:0 200" > io.weight4.3 io.max 与 io.cost:两种限流模型
Cgroup v2 的 IO 控制器提供两种限流模型,对应不同的使用场景:
| 模型 | 控制文件 | 限流方式 | 适用场景 |
|---|---|---|---|
| io.max | io.max | 绝对速率上限(BPS/IOPS) | 严格限制带宽,防止 IO 密集型容器抢占磁盘 |
| io.cost | io.cost.model + io.cost.qos | 基于权重的比例分配 | 多容器共享磁盘时的公平调度 |
io.max 模型是直接限速:设置 rbps=104857600,超过 100MB/s 的读请求就会被延迟,逻辑简单直接。
io.cost 模型更精细。它给每个 IO 操作计算一个”代价”(cost),根据 cgroup 的权重比例分配 IO 预算。代价不是简单的字节数,而是综合考虑了寻道时间、传输时间等磁盘物理特性:
# 启用 io.cost 模型(以 nvme0n1 为例)echo "8:0 rbps=0 wbps=0 riops=0 wiops=0" > io.max # 先清空 io.max 限制echo "ctrl=model nvme0n1" > io.cost.model # 指定设备使用 cost 模型echo "ctrl=qos nvme0n1 rpct=95 wpct=95" > io.cost.qos # 设置 QoS 参数
# 查看代价模型参数cat io.cost.model# ctrl=user nvme0n1 model=linear rbps=0 rseqiops=0 rrandiops=0# wbps=0 wseqiops=0 wrandiops=0
# 查看当前 IO 代价统计cat io.stat# 8:0 rbytes=12345678 wbytes=87654321 rios=1234 wios=567# cost.usage=98765 cost.wait=123456 cost.inflight=7890cost.usage 是该 cgroup 已消耗的 IO 代价,cost.wait 是因限流而等待的时间(纳秒),cost.inflight 是当前在飞的 IO 代价。
io.max 和 io.cost 不能同时对同一设备生效。io.max 的优先级更高:如果对某设备设置了 io.max,该设备就走 io.max 的直接限速逻辑,io.cost 权重分配不生效。要让 io.cost 生效,需要把 io.max 中对应设备的限制清零。
4.4 io.max 限流:时间片与令牌桶
io.max 的限流机制基于时间片(time slice)+ 令牌桶,而非简单的”用完就停”。内核实现(blk-throttle)为每个 cgroup 维护一个时间窗口(默认 100ms,即 DFL_THROTL_SLICE = HZ/10),在窗口内跟踪已派发的字节数和 IO 次数。
当一个 IO 请求到来时,限流逻辑分两步判断:
- BPS 检查:计算当前时间片内允许的字节数
bytes_allowed = bps_limit * jiffy_elapsed / HZ,如果bytes_disp + bio_size > bytes_allowed,则计算等待时间 - IOPS 检查:计算当前时间片内允许的 IO 数
io_allowed = iops_limit * jiffy_elapsed / HZ,如果io_disp + 1 > io_allowed,则计算等待时间
等待时间的计算方式是:根据已用配额和限制速率,推算出”还需要等多久才允许下一个 IO”,然后将请求挂入延迟队列,由定时器在到期后重新派发。
与 CPU 控制器的 CFS 限流对比:CFS 在 quota 用完后直接将进程挂起,等到下一个 period 才恢复,是”硬截断”模式。io.max 则更灵活,它不是等到时间片结束才重置配额,而是根据已用配额动态计算等待时间,让 IO 请求以更均匀的间隔派发,避免突发后长时间静默。
io.max 还支持配额结转(carryover):当配置在限流期间被修改时,内核会计算新旧配置下已等待的配额差值,将多等待的部分结转到新配置中,避免配置变更导致限流行为突变。
4.5 io.cost 限流:虚拟时间与代价模型
io.cost 模型的核心思想是虚拟时间(vtime)。它不直接限制速率,而是让每个 cgroup 的 IO 消耗在虚拟时间维度上按权重比例推进,从而实现比例分配。
4.5.1 代价计算:IO 不是等价的
io.cost 首先要解决的问题是:不同类型的 IO 对设备的”代价”不同。一个 4KB 随机读和一个 1MB 顺序写,占用的设备时间天差地别。io.cost 内置了 linear 代价模型,将每个 IO 的代价分为两部分:
- 基础代价:根据 IO 是顺序还是随机,赋予不同的基础开销(随机 IO 基础代价更高,因为涉及寻道/定位)
- 大小代价:按 IO 的字节数线性增加
IO_cost = base_cost(seq_or_rand) + size_coeff * io_size代价的单位是设备时间。如果一个 IO 的代价是 10ms,意味着设备处理这个 IO 大约需要 10ms。这个模型虽然简单,但足以区分随机小 IO 和顺序大 IO 的不同开销。
4.5.2 vtime 分配:权重如何变成 IO 预算
有了代价度量,下一步是按权重分配 IO 预算。io.cost 引入了**层级权重(hweight)**的概念。考虑下面的 cgroup 层级:
如果 B 空闲,只有 A0 和 A1 在发 IO,两者权重相等,各得 50%。如果 B 开始发 IO,B 得到 300/(100+300) = 75%,A0 和 A1 各分剩余的 12.5%。这个”展平后的权重比例”就是 hweight,所有活跃 cgroup 的 hweight 之和始终为 1。
关键机制:cgroup 的 vtime 流速与 hweight 成反比。如果 A0 的 hweight 是 12.5%,它的 vtime 流速是设备 vtime 的 1/8(100/12.5)。一个在设备上耗时 10ms 的 IO,在 A0 的 vtime 里算作 80ms。这意味着权重低的 cgroup 消耗 vtime 更快,更快用完预算,从而被限流。
每个 cgroup 跟踪自己已消耗的 vtime。当 cgroup 的 vtime 落后于设备当前 vtime 时,可以继续发 IO;当 cgroup 的 vtime 追上甚至超过设备 vtime 时,IO 被挂起,直到设备 vtime 推进到足够远的位置。
4.5.3 vrate 调整:自适应设备负载
代价模型不可能完美匹配所有设备的实际性能。设备内部有垃圾回收、IO 混合模式变化等因素,导致实际吞吐量波动。io.cost 通过**vrate(虚拟时间速率)**来动态适配。
vrate 是设备 vtime 相对于挂钟时间的流速。vrate = 100% 时,设备 vtime 与挂钟时间 1<1>1> 推进,所有 cgroup 加起来可以用满设备带宽。vrate = 75% 时,设备 vtime 只以 75% 的速率推进,所有 cgroup 加起来只能使用 75% 的设备带宽。
vrate 的调整基于两个信号:
- 请求队列等待(rq wait):当设备饱和时,硬件和软件队列填满,新 IO 必须等待请求槽位。这是最保守的饱和信号,默认启用
- 完成延迟 QoS:通过
io.cost.qos配置,当第 N 百分位的 IO 完成延迟超过阈值时,认为设备过载。这比 rq wait 更灵敏,可以更早检测到设备压力
4.5.4 工作保持:不浪费空闲容量
比例分配有一个天然问题:如果两个 cgroup 权重各 50%,但 A 只用了 10% 的设备能力,B 本可以用满剩余 90%,但按 50<50>50> 分配 B 只能拿到 50%,设备利用率只有 60%。这太浪费了。
io.cost 的**工作保持(work conservation)**机制解决了这个问题。它跟踪每个活跃 cgroup 的实际使用量,如果某个 cgroup 的使用量远低于其权重应得的份额,就将其多余权重临时让给其他 cgroup。同时设有”快速回收”机制:如果低使用量的 cgroup 突然需要更多 IO,它的权重会立即恢复,不会被长期惩罚。
4.6 Cgroup v2 IO 控制器 vs v1 blkio
Cgroup v1 的 IO 控制器叫 blkio,v2 改名为 io。不只是换了个名字,实现机制有本质区别:
| 维度 | Cgroup v1 blkio | Cgroup v2 io |
|---|---|---|
| 限流机制 | 基于 CFQ 调度器的权重分配 | io.max 时间片限速 + io.cost vtime 比例分配 |
| 硬限制 | 仅支持 throttle(BPS/IOPS),精度低 | io.max 精确限速,支持配额结转 |
| 权重分配 | 依赖 CFQ 调度器,对 NVMe/SSD 无效 | io.cost 基于代价模型 + vtime,不依赖特定调度器 |
| 写回集成 | 不感知页面缓存写回 | 与内核写回(writeback)机制集成 |
| 统计精度 | 只统计直接 IO | 统计包含缓冲写回(buffered writeback) |
| 自适应调节 | 无 | io.cost 通过 vrate 动态适配设备负载 |
v1 blkio 最根本的问题是依赖 CFQ 调度器。CFQ(Completely Fair Queuing)是机械硬盘时代的调度器,通过旋转和寻道优化来公平分配磁盘时间。CFQ 的权重分配逻辑是:根据 cgroup 的权重比例,轮流将磁盘时间分配给不同 cgroup 的 IO 请求。这对机械硬盘有效,因为寻道是主要开销,合理分配寻道时间就能实现公平。但现代 NVMe/SSD 没有机械寻道,IO 延迟极低且队列深度大,CFQ 的”轮流分配磁盘时间”逻辑失去了物理基础,权重控制在 SSD 上形同虚设。Linux 5.0 之后 CFQ 调度器已被移除,v1 blkio 的权重分配也就无处依附。
v2 的 io.cost 不依赖特定 IO 调度器。它的核心思路是:既然磁盘时间无法直接观测,那就通过代价模型估算每个 IO 的设备时间消耗,再用虚拟时间机制按权重分配。这个思路与 CPU 控制器的 CFS 有异曲同工之处:CFS 用虚拟运行时间(vruntime)按权重分配 CPU 时间,io.cost 用虚拟 IO 时间(vtime)按权重分配 IO 预算。两者都是”用虚拟时间做比例分配”,区别在于 CFS 的 vruntime 可以精确度量(CPU 时间就是挂钟时间),而 io.cost 的 vtime 需要靠代价模型估算,因此额外引入了 vrate 自适应机制来弥补模型误差。
另一个关键改进是写回集成。v1 blkio 只统计进程直接发起的 IO(direct IO),不统计内核的页面缓存写回。一个容器进程写入大量数据后,内核异步将脏页刷回磁盘,这部分 IO 不受 blkio 控制,容器可以绕过限制。v2 的 IO 控制器将写回 IO 归属到产生脏页的 cgroup,写回也受 io.max/io.cost 约束。
io.cost 模型在 Linux 5.4 引入,5.7 之后趋于稳定。5.4 之前的内核只能用 io.max 做硬限制,无法做基于权重的比例分配。生产环境建议 5.7+。
五、PSI:压力失速信息
5.1 PSI 原理
PSI(Pressure Stall Information)严格说是 4.20 引入的独立内核子系统,系统级 /proc/pressure/ 不依赖 cgroup;cgroup v2 在此基础上提供了 per-cgroup 的压力暴露(cpu.pressure/memory.pressure/io.pressure),用来量化单个 cgroup 内资源竞争的严重程度:
# 查看 CPU 压力cat /sys/fs/cgroup/docker/container1/cpu.pressure# some avg10=0.00 avg60=0.10 avg300=0.05 total=1234567# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# some: 至少一个任务等待资源# full: 所有任务都在等待资源(更严重)# avg10/60/300: 最近 10/60/300 秒的百分比5.2 PSI 在容器中的应用
# 监控容器的内存压力watch -n 1 "cat /sys/fs/cgroup/docker/container1/memory.pressure"
# 基于 PSI 触发告警# 当 avg10 > 50% 时,表示严重内存压力# 可能需要增加内存限制或优化应用| PSI 指标 | 含义 | 告警阈值 |
|---|---|---|
| some avg10 > 10% | 部分任务等待 | 关注 |
| some avg10 > 50% | 严重竞争 | 告警 |
| full avg10 > 0% | 所有任务等待 | 严重告警 |
六、容器运行时与 Cgroup
6.1 Docker 的 Cgroup 配置
# Docker 的 Cgroup 参数映射docker run \ --memory=512m \ # memory.max = 536870912 --memory-reservation=256m \ # memory.low = 268435456 --memory-swap=1g \ # memory.swap.max = 536870912 --cpus=2 \ # cpu.max = 200000 100000 --cpu-shares=2048 \ # cpu.weight = 2048 --cpuset-cpus=0,2 \ # cpuset.cpus = 0,2 --pids-limit=100 \ # pids.max = 100 --device-write-bps /dev/sda:50MB \ # io.max wbps nginx
# 查看容器的 Cgroup 路径docker inspect mycontainer --format '{{.CgroupPath}}'6.2 Kubernetes 的 Cgroup 配置
# Kubernetes Pod 的资源限制apiVersion: v1kind: Podspec: containers: - name: nginx resources: requests: cpu: "1" # cpu.weight 权重分配 memory: "512Mi" # memory.min 保证 limits: cpu: "2" # cpu.max 硬限制 memory: "1Gi" # memory.max 硬限制6.3 containerd 的 Cgroup 管理
// containerd 的 Cgroup 管理代码(简化)package cgroups
import ( "fmt" "os" "path/filepath")
type CgroupConfig struct { MemoryMax int64 // memory.max CPUMax string // cpu.max (quota period)}
func ApplyCgroup(cgroupPath string, config *CgroupConfig) error { // Cgroup v2 操作的本质:建目录 + 往各控制器文件写值 + 把进程写进 cgroup.procs path := filepath.Join("/sys/fs/cgroup", cgroupPath) if err := os.MkdirAll(path, 0755); err != nil { return err }
// 所有控制器的写入都是同一个模式:拼路径、写字节、0644 writeControl := func(name, value string) error { return os.WriteFile(filepath.Join(path, name), []byte(value), 0644) }
if config.MemoryMax > 0 { if err := writeControl("memory.max", fmt.Sprintf("%d", config.MemoryMax)); err != nil { return err } } if config.CPUMax != "" { if err := writeControl("cpu.max", config.CPUMax); err != nil { return err } } // 把当前进程加入 cgroup return writeControl("cgroup.procs", fmt.Sprintf("%d", os.Getpid()))}七、Cgroup 在容器运行时中的完整路径
从 Docker CLI 到内核,Cgroup 配置经过多层转换:
Cgroup 的内存限制包含页面缓存(file cache)。当容器读取大量文件时,页面缓存会占用 memory.current,可能触发限流或 OOM。如果应用需要大量文件 IO,考虑适当放宽内存限制或使用 memory.low 设置软保护。
八、eBPF 与 Cgroup
8.1 Cgroup eBPF 程序
Cgroup v2 支持附加 eBPF 程序,实现更灵活的控制逻辑:
// eBPF 程序:限制网络连接// 附加到 cgroup 的 BPF_CGROUP_INET_SOCK_CREATE hookSEC("cgroup/sock")int restrict_sockets(struct bpf_sock *ctx) { // 只允许 TCP 和 UDP if (ctx->protocol != IPPROTO_TCP && ctx->protocol != IPPROTO_UDP) { return 0; // 拒绝 } return 1; // 允许}8.2 常用的 Cgroup eBPF Hook
| Hook | 触发时机 | 用途 |
|---|---|---|
cgroup/sock | 创建 socket | 限制网络协议 |
cgroup/connect | 发起连接 | 限制出站连接 |
cgroup/sendmsg | 发送消息 | 限制目标地址 |
cgroup/recvmsg | 接收消息 | 限制来源地址 |
cgroup/post_bind | bind 之后 | 限制监听端口 |
cgroup/device | 设备访问 | 限制设备操作 |
九、动手实践
9.1 手动创建 Cgroup 并限制进程
#!/bin/bash# 手动创建 Cgroup v2 并限制进程
# 1. 创建 cgroupCGROUP_PATH="/sys/fs/cgroup/mycontainer"sudo mkdir -p $CGROUP_PATH
# 2. 启用控制器echo "+cpu +memory +io +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
# 3. 设置 CPU 限制(1 核)echo "100000 100000" | sudo tee $CGROUP_PATH/cpu.max
# 4. 设置内存限制(256MB)echo "268435456" | sudo tee $CGROUP_PATH/memory.max
# 5. 设置 PID 限制echo "100" | sudo tee $CGROUP_PATH/pids.max
# 6. 启动进程并加入 cgroupstress-ng --cpu 4 --timeout 60s &PID=$!echo $PID | sudo tee $CGROUP_PATH/cgroup.procs
# 7. 观察限流效果watch -n 1 "cat $CGROUP_PATH/cpu.stat"# nr_periods 10# nr_throttled 8 ← 被限流 8 次# throttled_usec 800000 ← 限流了 800ms
# 8. 清理sudo rmdir $CGROUP_PATH9.2 Cgroup 监控脚本
#!/bin/bash# 监控容器的 Cgroup 资源使用
CONTAINER=$1CGROUP=$(docker inspect -f '{{.CgroupPath}}' $CONTAINER 2>/dev/null)
if [ -z "$CGROUP" ]; then echo "Container not found" exit 1fi
CGROUP_FS="/sys/fs/cgroup${CGROUP}"
echo "=== CPU ==="echo "Limit: $(cat $CGROUP_FS/cpu.max)"echo "Usage: $(cat $CGROUP_FS/cpu.stat | grep usage_usec)"echo "Throttled: $(cat $CGROUP_FS/cpu.stat | grep throttled)"
echo ""echo "=== Memory ==="echo "Limit: $(cat $CGROUP_FS/memory.max)"echo "Current: $(cat $CGROUP_FS/memory.current)"echo "Swap: $(cat $CGROUP_FS/memory.swap.current) / $(cat $CGROUP_FS/memory.swap.max)"echo "OOM events: $(cat $CGROUP_FS/memory.events | grep oom)"
echo ""echo "=== IO ==="echo "Stats: $(cat $CGROUP_FS/io.stat)"echo "Pressure: $(cat $CGROUP_FS/io.pressure)"
echo ""echo "=== PSI ==="echo "CPU: $(cat $CGROUP_FS/cpu.pressure)"echo "Memory: $(cat $CGROUP_FS/memory.pressure)"附、实践:用 Cgroup v2 限制进程资源
本节用 Cgroup v2 的文件系统接口手工限制进程资源,观察限制效果。所有命令需要 root 权限。
附.1 确认 Cgroup v2 挂载
mount | grep cgroup2# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cat /sys/fs/cgroup/cgroup.controllers# cpuset cpu io memory hugetlb pids rdmaCgroup v2 挂载在 /sys/fs/cgroup/,所有控制器共享同一棵 cgroup 树。
附.2 创建自定义 cgroup 并启用控制器
# 创建 cgroup 目录mkdir -p /sys/fs/cgroup/my-demo
# 启用 CPU 和 memory 控制器(从根 cgroup 委派)echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control
# 确认控制器已启用cat /sys/fs/cgroup/my-demo/cgroup.controllers# cpuset cpu io memory hugetlb pids rdma附.3 设置内存限制
# 设置内存上限为 512MBecho "536870912" > /sys/fs/cgroup/my-demo/memory.max
# 确认限制生效cat /sys/fs/cgroup/my-demo/memory.max# 536870912附.4 设置 CPU 限制
# 限制 CPU 使用率为 50%(每 100000 微秒中最多使用 50000 微秒)echo "50000 100000" > /sys/fs/cgroup/my-demo/cpu.max
# 确认限制生效cat /sys/fs/cgroup/my-demo/cpu.max# 50000 100000cpu.max 的格式是 max quota:max 表示不限,50000 100000 表示每 100ms 周期内最多使用 50ms CPU 时间,即 50% CPU。
附.5 将进程移入 cgroup
# 启动一个消耗 CPU 的进程stress --cpu 1 --timeout 30 &
# 将进程 PID 写入 cgroupecho $! > /sys/fs/cgroup/my-demo/cgroup.procs
# 观察 CPU 使用率被限制在 50%top -bn1 | grep stress附.6 观察 PSI 压力指标
cat /sys/fs/cgroup/my-demo/cpu.pressure# some avg10=0.00 avg60=0.00 avg300=0.00 total=0# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
cat /sys/fs/cgroup/my-demo/memory.pressure# some avg10=0.00 avg60=0.00 avg300=0.00 total=0# full avg10=0.00 avg60=0.00 avg300=0.00 total=0PSI(Pressure Stall Information)是 Cgroup v2 的重要特性,some 表示”至少一个进程被延迟”,full 表示”所有进程被延迟”。当 CPU 或内存压力增大时,这些数值会上升,运维可以据此触发自动扩容。
实验结束后清理 cgroup:rmdir /sys/fs/cgroup/my-demo。如果 cgroup 中仍有进程,需要先将它们移回根 cgroup。
十、本章小结
上一章建立了 Linux Namespace 的原理与实现的认知框架。本章从 Cgroup v1 的多层级问题出发,拆解了 v2 统一层级的设计动机,以及 CPU/内存/IO 三大控制器的限流机制与保护策略。Namespace 回答”进程能看到什么”,Cgroup 回答”进程能用多少”,两者配合才构成完整的容器隔离。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






