当你执行 docker run --memory=512m --cpus=2 nginx 时,Docker 是怎么限制容器只能使用 512MB 内存和 2 个 CPU 的?答案是 Cgroup——Linux 内核的**控制组(Control Group)**机制。Cgroup 让你将进程分组,并对每个组施加资源限制、优先级分配和审计统计。
Cgroup 经历了从 v1 到 v2 的重大架构演进。Cgroup v1 的每个控制器有独立的层级树,导致进程可能属于不同控制器的不同 cgroup,管理混乱。Cgroup v2 采用统一层级设计——所有控制器共享同一棵 cgroup 树,一个进程只能属于一个 cgroup,所有控制器对同一组进程生效。
Cgroup 的历史可以追溯到 2007 年,Google 工程师 Paul Menage 和 Rohit Seth 向 Linux 内核提交了”Process Containers”补丁,后更名为 Control Groups(Cgroup)。Cgroup v1 的设计允许每个控制器(cpu、memory、blkio 等)拥有独立的层级树,进程可以同时属于不同控制器的不同 cgroup——这看似灵活,实则带来了管理混乱:一个进程在 cpu cgroup 中属于 /A,在 memory cgroup 中却属于 /B,运维人员难以追踪进程的资源归属。2016 年,内核开发者 Tejun Heo 主导了 Cgroup v2 的重构,采用统一层级设计——所有控制器共享同一棵 cgroup 树,一个进程只能属于一个 cgroup。这一”看似更不灵活”的设计,反而解决了 v1 的核心痛点。理解从 v1 到 v2 的演进动机,是理解 Cgroup v2 设计哲学的关键——v2 的每一个”限制”,都是对 v1 实际运维问题的回应。
前置知识
- Ch02 Linux Namespace 深入:Namespace 提供”视图隔离”,Cgroup 提供”资源限制”——两者是容器隔离的互补机制,理解它们的区别至关重要
- Linux 文件系统操作:Cgroup v2 通过
/sys/fs/cgroup/文件系统暴露接口,所有操作都是文件读写 - Linux 进程管理基础:进程树、信号、资源统计
Cgroup 和 Namespace 经常被混淆:Namespace 决定进程”能看到什么”,Cgroup 决定进程”能用多少”。
本章将深入 Cgroup v2 的架构设计、三大核心控制器(CPU/内存/IO)的实现原理,以及容器运行时如何使用 Cgroup。
一、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 写回) |
| 压力通知 | 无 | PSI(Pressure Stall Information) |
| 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 控制器的工作原理
Cgroup v2 的 IO 控制器基于比例-积分(PI)控制器实现限流:
- 每个 cgroup 维护一个 IO 预算(token bucket)
- 每次 IO 请求消耗预算
- 预算耗尽时,IO 请求被延迟(throttled)
- 下一个时间窗口预算恢复
# 查看 IO 限流统计cat io.stat# 8:0 rbytes=12345678 wbytes=87654321 rios=1234 wios=567# dbytes=0 dios=0# cost.wait=123456 cost.inflight=7890
# cost.wait: IO 限流导致的等待时间(纳秒)五、PSI:压力失速信息
5.1 PSI 原理
PSI(Pressure Stall Information)是 Cgroup v2 的重要特性,它量化了资源竞争的严重程度:
# 查看 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 MemoryMin int64 // memory.min CPUMax string // cpu.max (quota period) CPUWeight uint64 // cpu.weight PidsMax int64 // pids.max}
func ApplyCgroup(cgroupPath string, config *CgroupConfig) error { root := "/sys/fs/cgroup"
// 创建 cgroup 目录 path := filepath.Join(root, cgroupPath) if err := os.MkdirAll(path, 0755); err != nil { return err }
// 设置 memory.max if config.MemoryMax > 0 { if err := os.WriteFile( filepath.Join(path, "memory.max"), []byte(fmt.Sprintf("%d", config.MemoryMax)), 0644, ); err != nil { return err } }
// 设置 cpu.max if config.CPUMax != "" { if err := os.WriteFile( filepath.Join(path, "cpu.max"), []byte(config.CPUMax), 0644, ); err != nil { return err } }
// 将进程加入 cgroup if err := os.WriteFile( filepath.Join(path, "cgroup.procs"), []byte(fmt.Sprintf("%d", os.Getpid())), 0644, ); err != nil { return err }
return nil}七、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 的原理与实现的认知框架。
| 控制器 | 关键文件 | 功能 | Docker 参数 |
|---|---|---|---|
| CPU | cpu.max, cpu.weight | CPU 时间限制与权重 | —cpus, —cpu-shares |
| 内存 | memory.max, memory.min, memory.low | 内存限制与保护 | —memory, —memory-reservation |
| IO | io.max, io.weight | 块设备 IO 限制 | —device-write-bps |
| PID | pids.max | 进程数限制 | —pids-limit |
| cpuset | cpuset.cpus, cpuset.mems | CPU 亲和性 | —cpuset-cpus |
| PSI | cpu.pressure, memory.pressure, io.pressure | 压力监控 | 无 |
Cgroup v2 的统一层级设计大大简化了容器运行时的资源管理逻辑。runc 和 containerd 都已完整支持 Cgroup v2。如果你的系统还在使用 Cgroup v1,建议升级到 v2 以获得更好的 PSI 支持和 eBPF 扩展能力。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






