03-CGroup资源控制
学习目标
- 深入理解 CGroup v1 和 v2 的架构差异
- 掌握各种资源控制器的原理和使用方法
- 能够手动配置 CGroup 限制
- 理解 CGroup 在内核中的实现机制
- 掌握 CGroup 性能优化和故障排查
前置知识
- Linux 进程管理基础
- 文件系统操作
- 系统资源监控
- 内核基础概念
️ 一、CGroup 概述
1.1 什么是 CGroup?
CGroup (Control Groups) 是 Linux 内核提供的资源控制框架,用于限制、记录和隔离进程组的资源使用。
graph TD
A[Linux 内核] --> B[CGroup 框架]
B --> C[CPU 控制器]
B --> D[内存控制器]
B --> E[IO 控制器]
B --> F[进程数控制器]
C --> C1[限制 CPU 使用率]
D --> D1[限制内存使用量]
E --> E1[限制磁盘 IO]
F --> F1[限制进程数量]
G[进程组] --> H[CGroup 节点]
H --> I[资源限制]
H --> J[资源统计]
1.2 CGroup 核心概念
- CGroup:资源控制的节点,组织成树状结构
- Subsystem/Controller:资源控制器,如 cpu、memory、io
- Task:被控制的进程或线程
- Hierarchy:CGroup 的层次结构
二、CGroup v1 vs v2 架构对比
2.1 架构差异对比
特性 | CGroup v1 | CGroup v2 |
---|---|---|
层次结构 | 多层级,每个控制器独立 | 单层级,统一管理 |
挂载点 | 多个 /sys/fs/cgroup/xxx | 单个 /sys/fs/cgroup |
进程绑定 | 一个进程可在多个层级 | 每个进程只属于一个节点 |
接口统一性 | 不一致,各控制器独立 | 完全统一,简洁清晰 |
使用状态 | 旧系统 (CentOS 7) | 主流系统 (Ubuntu 22+) |
2.2 CGroup v1 架构图
graph TD
A[/sys/fs/cgroup] --> B[cpu]
A --> C[memory]
A --> D[blkio]
A --> E[pids]
B --> B1[cpu.shares]
B --> B2[cpu.cfs_period_us]
B --> B3[cpu.cfs_quota_us]
C --> C1[memory.limit_in_bytes]
C --> C2[memory.usage_in_bytes]
C --> C3[memory.oom_control]
D --> D1[blkio.weight]
D --> D2[blkio.throttle.read_bps_device]
D --> D3[blkio.throttle.write_bps_device]
E --> E1[pids.max]
E --> E2[pids.current]
2.3 CGroup v2 架构图
graph TD
A[/sys/fs/cgroup] --> B[统一接口]
B --> C[cpu.max]
B --> D[memory.max]
B --> E[io.max]
B --> F[pids.max]
B --> G[cgroup.procs]
B --> H[cgroup.subtree_control]
三、内核实现原理
3.1 核心数据结构
// 进程结构中的 cgroup 信息
struct task_struct {
// ... 其他字段
struct css_set *cgroups; // 指向 cgroup 子系统集合
// ... 其他字段
};
// cgroup 子系统集合
struct css_set {
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
struct list_head tasks;
// ... 其他字段
};
// cgroup 节点
struct cgroup {
struct cgroup *parent;
struct list_head children;
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
// ... 其他字段
};
3.2 资源控制流程
sequenceDiagram
participant P as 进程
participant K as 内核
participant C as CGroup
participant R as 资源控制器
P->>K: 请求资源 (CPU/内存/IO)
K->>C: 查找进程所属 CGroup
C->>R: 检查资源限制
R->>K: 返回限制结果
alt 资源充足
K->>P: 分配资源
else 资源不足
K->>P: 拒绝/限制资源
end
四、CPU 控制器详解
4.1 CGroup v2 CPU 控制
4.1.1 cpu.max 文件
# 格式: <quota> <period>
# quota: 每个周期内可用的 CPU 时间 (微秒)
# period: 周期长度 (微秒)
# 示例:限制为 20% CPU (每100ms可用20ms)
echo "20000 100000" > /sys/fs/cgroup/cpu.max
# 示例:限制为 2 个 CPU 核心
echo "200000 100000" > /sys/fs/cgroup/cpu.max
4.1.2 实战演示
# 1. 创建测试 CGroup
sudo mkdir /sys/fs/cgroup/test-cpu
cd /sys/fs/cgroup/test-cpu
# 2. 启用 CPU 控制器
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
# 3. 设置 CPU 限制 (50% CPU)
echo "50000 100000" > cpu.max
# 4. 运行 CPU 密集型任务
echo $$ > cgroup.procs
# 5. 启动压力测试
stress --cpu 4 &
# 6. 监控 CPU 使用率
htop
# 应该看到 CPU 使用率被限制在 50% 左右
4.2 CPU 控制器原理
graph TD
A[CFS 调度器] --> B[进程调度]
B --> C[检查 CGroup 限制]
C --> D{CPU 时间是否超限?}
D -->|是| E[暂停进程]
D -->|否| F[继续执行]
E --> G[等待下一个周期]
G --> B
五、内存控制器详解
5.1 CGroup v2 内存控制
5.1.1 主要控制文件
# 最大内存使用量
echo "512M" > /sys/fs/cgroup/memory.max
# 当前内存使用量
cat /sys/fs/cgroup/memory.current
# 内存统计信息
cat /sys/fs/cgroup/memory.stat
# OOM 事件
cat /sys/fs/cgroup/memory.events
5.1.2 实战演示
# 1. 创建测试 CGroup
sudo mkdir /sys/fs/cgroup/test-memory
cd /sys/fs/cgroup/test-memory
# 2. 启用内存控制器
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control
# 3. 设置内存限制 (128MB)
echo "128M" > memory.max
# 4. 运行内存密集型任务
echo $$ > cgroup.procs
# 5. 启动内存压力测试
stress --vm 1 --vm-bytes 200M &
# 6. 监控内存使用
watch -n 1 'cat memory.current memory.max'
# 当内存使用超过限制时,进程会被 OOM-kill
5.2 内存控制器原理
graph TD
A[进程申请内存] --> B[内存分配器]
B --> C[检查 CGroup 限制]
C --> D{内存是否超限?}
D -->|否| E[分配内存]
D -->|是| F[触发 OOM]
F --> G[选择进程杀死]
E --> H[更新统计信息]
G --> I[释放内存]
六、IO 控制器详解
6.1 CGroup v2 IO 控制
6.1.1 io.max 文件格式
# 格式: <major>:<minor> <limits>
# 限制读取带宽
echo "8:0 rbps=1048576" > /sys/fs/cgroup/io.max
# 限制写入带宽
echo "8:0 wbps=1048576" > /sys/fs/cgroup/io.max
# 限制 IOPS
echo "8:0 riops=100 wiops=100" > /sys/fs/cgroup/io.max
6.1.2 实战演示
# 1. 创建测试 CGroup
sudo mkdir /sys/fs/cgroup/test-io
cd /sys/fs/cgroup/test-io
# 2. 启用 IO 控制器
echo "+io" > /sys/fs/cgroup/cgroup.subtree_control
# 3. 设置 IO 限制 (1MB/s 读取)
echo "8:0 rbps=1048576" > io.max
# 4. 运行 IO 密集型任务
echo $$ > cgroup.procs
# 5. 启动 IO 压力测试
dd if=/dev/zero of=/tmp/testfile bs=1M count=1000 &
# 6. 监控 IO 使用率
iostat -x 1
七、进程数控制器详解
7.1 PIDs 控制器
# 设置最大进程数
echo "100" > /sys/fs/cgroup/pids.max
# 查看当前进程数
cat /sys/fs/cgroup/pids.current
# 查看进程列表
cat /sys/fs/cgroup/cgroup.procs
7.2 实战演示
# 1. 创建测试 CGroup
sudo mkdir /sys/fs/cgroup/test-pids
cd /sys/fs/cgroup/test-pids
# 2. 启用 PIDs 控制器
echo "+pids" > /sys/fs/cgroup/cgroup.subtree_control
# 3. 设置进程数限制 (10个进程)
echo "10" > pids.max
# 4. 运行进程
echo $$ > cgroup.procs
# 5. 尝试创建超过限制的进程
for i in {1..15}; do
sleep 1 &
echo "Created process $i"
done
# 6. 查看进程数
cat pids.current
# 应该显示 10 或更少
️ 八、综合实战:创建完整 CGroup 环境
8.1 命令行脚本
#!/bin/bash
# 创建完整的 CGroup 测试环境
echo "=== 创建 CGroup 测试环境 ==="
# 1. 创建测试 CGroup
CGROUP_PATH="/sys/fs/cgroup/test-container"
sudo mkdir -p $CGROUP_PATH
cd $CGROUP_PATH
# 2. 启用所有控制器
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control
# 3. 设置资源限制
echo "50000 100000" > cpu.max # 50% CPU
echo "256M" > memory.max # 256MB 内存
echo "8:0 rbps=1048576 wbps=1048576" > io.max # 1MB/s IO
echo "50" > pids.max # 50个进程
# 4. 显示配置
echo "=== CGroup 配置 ==="
echo "CPU 限制: $(cat cpu.max)"
echo "内存限制: $(cat memory.max)"
echo "IO 限制: $(cat io.max)"
echo "进程数限制: $(cat pids.max)"
# 5. 将当前进程加入 CGroup
echo $$ > cgroup.procs
echo "=== 当前进程已加入 CGroup ==="
echo "进程 ID: $$"
echo "当前 CPU 使用: $(cat cpu.stat | grep usage_usec)"
echo "当前内存使用: $(cat memory.current)"
echo "当前进程数: $(cat pids.current)"
echo "=== 开始压力测试 ==="
echo "启动 CPU 压力测试..."
stress --cpu 2 &
CPU_PID=$!
echo "启动内存压力测试..."
stress --vm 1 --vm-bytes 200M &
MEM_PID=$!
echo "启动 IO 压力测试..."
dd if=/dev/zero of=/tmp/iotest bs=1M count=500 &
IO_PID=$!
# 6. 监控资源使用
echo "=== 监控资源使用 ==="
for i in {1..10}; do
echo "--- 第 $i 次检查 ---"
echo "CPU 使用: $(cat cpu.stat | grep usage_usec | cut -d' ' -f2)"
echo "内存使用: $(cat memory.current)"
echo "进程数: $(cat pids.current)"
echo "IO 统计: $(cat io.stat | head -5)"
sleep 2
done
# 7. 清理
echo "=== 清理测试进程 ==="
kill $CPU_PID $MEM_PID $IO_PID 2>/dev/null
rm -f /tmp/iotest
echo "=== 测试完成 ==="
8.2 Go 代码实现
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"time"
)
type CGroupLimits struct {
CPUMax string // "50000 100000"
MemoryMax string // "256M"
IOMax string // "8:0 rbps=1048576 wbps=1048576"
PidsMax string // "50"
}
func createCGroup(name string, limits CGroupLimits) error {
cgroupPath := filepath.Join("/sys/fs/cgroup", name)
// 创建 CGroup 目录
if err := os.MkdirAll(cgroupPath, 0755); err != nil {
return fmt.Errorf("创建 CGroup 目录失败: %v", err)
}
// 启用控制器
controllers := []string{"+cpu", "+memory", "+io", "+pids"}
for _, controller := range controllers {
if err := os.WriteFile(
filepath.Join("/sys/fs/cgroup", "cgroup.subtree_control"),
[]byte(controller),
0644,
); err != nil {
return fmt.Errorf("启用控制器失败: %v", err)
}
}
// 设置资源限制
limitsMap := map[string]string{
"cpu.max": limits.CPUMax,
"memory.max": limits.MemoryMax,
"io.max": limits.IOMax,
"pids.max": limits.PidsMax,
}
for file, value := range limitsMap {
if err := os.WriteFile(
filepath.Join(cgroupPath, file),
[]byte(value),
0644,
); err != nil {
return fmt.Errorf("设置 %s 失败: %v", file, err)
}
}
return nil
}
func addProcessToCGroup(cgroupName string, pid int) error {
cgroupPath := filepath.Join("/sys/fs/cgroup", cgroupName)
return os.WriteFile(
filepath.Join(cgroupPath, "cgroup.procs"),
[]byte(strconv.Itoa(pid)),
0644,
)
}
func main() {
// 创建 CGroup
limits := CGroupLimits{
CPUMax: "50000 100000", // 50% CPU
MemoryMax: "256M", // 256MB 内存
IOMax: "8:0 rbps=1048576 wbps=1048576", // 1MB/s IO
PidsMax: "50", // 50个进程
}
if err := createCGroup("test-container", limits); err != nil {
fmt.Printf("创建 CGroup 失败: %v\n", err)
os.Exit(1)
}
// 将当前进程加入 CGroup
if err := addProcessToCGroup("test-container", os.Getpid()); err != nil {
fmt.Printf("加入 CGroup 失败: %v\n", err)
os.Exit(1)
}
fmt.Println("CGroup 创建成功,当前进程已加入")
fmt.Printf("进程 ID: %d\n", os.Getpid())
// 监控资源使用
for i := 0; i < 10; i++ {
fmt.Printf("--- 第 %d 次检查 ---\n", i+1)
// 读取 CPU 使用情况
if data, err := os.ReadFile("/sys/fs/cgroup/test-container/cpu.stat"); err == nil {
fmt.Printf("CPU 统计: %s", string(data))
}
// 读取内存使用情况
if data, err := os.ReadFile("/sys/fs/cgroup/test-container/memory.current"); err == nil {
fmt.Printf("内存使用: %s", string(data))
}
// 读取进程数
if data, err := os.ReadFile("/sys/fs/cgroup/test-container/pids.current"); err == nil {
fmt.Printf("进程数: %s", string(data))
}
time.Sleep(2 * time.Second)
}
}
九、调试技巧与故障排查
9.1 常用调试命令
# 查看 CGroup 层次结构
find /sys/fs/cgroup -name "*.max" -o -name "*.current" | head -10
# 查看进程所属的 CGroup
cat /proc/$$/cgroup
# 查看 CGroup 统计信息
cat /sys/fs/cgroup/cpu.stat
cat /sys/fs/cgroup/memory.stat
cat /sys/fs/cgroup/io.stat
# 查看 CGroup 事件
cat /sys/fs/cgroup/memory.events
cat /sys/fs/cgroup/io.events
9.2 常见问题排查
9.2.1 权限问题
# 问题:Permission denied
# 解决:确保以 root 权限运行
sudo su
# 或者给当前用户添加 cgroup 权限
sudo chown -R $USER:$USER /sys/fs/cgroup/test-*
9.2.2 控制器未启用
# 问题:No such file or directory
# 解决:启用相应的控制器
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control
9.2.3 资源限制不生效
# 检查进程是否在正确的 CGroup 中
cat /proc/$$/cgroup
# 检查 CGroup 配置
cat /sys/fs/cgroup/test-container/cpu.max
cat /sys/fs/cgroup/test-container/memory.max
9.3 性能监控
# 实时监控 CGroup 资源使用
watch -n 1 'echo "CPU: $(cat /sys/fs/cgroup/test-container/cpu.stat | grep usage_usec)"; echo "Memory: $(cat /sys/fs/cgroup/test-container/memory.current)"; echo "Processes: $(cat /sys/fs/cgroup/test-container/pids.current)"'
# 使用 htop 查看进程资源使用
htop -H
# 使用 iotop 查看 IO 使用
sudo iotop
十、验证检查清单
基础理解
- [ ] 理解 CGroup v1 和 v2 的架构差异
- [ ] 掌握各种资源控制器的作用
- [ ] 理解 CGroup 在内核中的实现原理
- [ ] 能够解释资源控制的工作流程
实践能力
- [ ] 能够手动创建和配置 CGroup
- [ ] 能够设置 CPU、内存、IO、进程数限制
- [ ] 能够将进程加入 CGroup 并验证效果
- [ ] 能够监控 CGroup 资源使用情况
调试技能
- [ ] 掌握 CGroup 调试工具的使用
- [ ] 能够排查常见的 CGroup 问题
- [ ] 能够进行 CGroup 性能优化
- [ ] 理解 CGroup 的最佳实践
相关链接
- 02-Namespace隔离机制 - 进程隔离技术
- 04-Capabilities与安全机制 - 安全控制技术
- 10-命令行手撸容器 - 综合实践应用
下一步:让我们学习 Capabilities 和安全机制,这是容器安全的重要基础!