HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 手撸容器系统

    • 完整手撸容器技术文档系列
    • 01-容器本质与基础概念
    • 02-Namespace隔离机制
    • 03-CGroup资源控制
    • 04-Capabilities与安全机制
    • 05-容器网络原理
    • 06-网络模式与实现
    • 07-CNI插件开发
    • 08-RootFS与文件系统隔离
    • 09-OverlayFS镜像分层
    • 10-命令行手撸容器
    • 11-Go实现最小容器
    • 12-Go实现完整容器
    • 13-容器生命周期管理
    • 14-调试技术与工具
    • 15-OCI规范与标准化
    • 16-进阶场景与优化
    • 常见问题与故障排查
    • 参考资料与延伸阅读

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 v1CGroup 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 和安全机制,这是容器安全的重要基础!

Prev
02-Namespace隔离机制
Next
04-Capabilities与安全机制