概述

在云原生和容器化技术全面普及的今天,Linux cgroup(控制组)作为资源隔离和限制的内核基石,其重要性不言而喻。从 Docker 容器的内存限制到 Kubernetes Pod 的 CPU Requests/Limits,底层都依赖 cgroup 机制。然而,cgroup v1 的多层级架构、控制器行为不一致、线程模型混乱等历史包袱,让运维人员在生产环境中频频踩坑。

cgroup v2 作为对 v1 的彻底重构,采用单一层级树(unified hierarchy)架构,从根本上解决了 v1 的设计缺陷。自 Linux 4.5 引入以来,经过多个内核版本的迭代完善,cgroup v2 在 5.x 内核上已趋于成熟稳定。Ubuntu 22.04+、RHEL 9+、Debian 12+ 等主流发行版已默认使用 cgroup v2,Docker 和 Kubernetes 也已全面支持。

我将从 cgroup v2 的架构原理出发,深入讲解核心控制器的工作机制,结合 systemd 集成、Docker 容器限制、Kubernetes 场景等实战配置,最后覆盖 v1 到 v2 的迁移策略,帮助你在生产环境中驾驭这一关键技术。

cgroup v1 vs v2:为什么要重构

v1 的核心痛点

cgroup v1 在设计之初,每个控制器可以独立挂载在不同的层级树上。这带来了灵活性,但也埋下了大量隐患:

问题维度v1 表现影响
多层级架构每个控制器可挂载在独立的层级树进程在不同控制器中可属于不同 cgroup,管理视图割裂
线程模型进程的线程可分散到不同 cgroup资源计量混乱,难以准确归因
控制器间协调各控制器独立运作无法做跨资源的统一策略(如 CPU 和内存的联动)
委派安全子 cgroup 委派权限粗粒度容器逃逸风险,安全边界模糊
接口一致性不同控制器文件命名和语义不统一运维认知负担高,脚本维护困难

v2 的设计哲学

cgroup v2 的核心设计原则是单一层级树(unified hierarchy):整个系统只有一棵 cgroup 树,所有控制器挂载在同一棵树上。一个进程只属于一个 cgroup,该 cgroup 上可以同时启用 CPU、内存、IO 等多个控制器。

/sys/fs/cgroup/          ← 统一挂载点(cgroup2 文件系统)
├── cgroup.controllers   ← 全局可用控制器列表
├── cgroup.subtree_control ← 子级启用的控制器
├── cpu.weight            ← CPU 权重
├── memory.max            ← 内存上限
├── io.max                ← IO 上限
├── system.slice/         ← systemd 系统服务
│   ├── nginx.service/
│   │   ├── cpu.max       ← Nginx 的 CPU 限制
│   │   └── memory.max    ← Nginx 的内存限制
│   └── docker.service/
├── user.slice/           ← 用户会话
└── myapp/                ← 自定义 cgroup
    ├── cpu.max
    ├── memory.max
    └── cgroup.procs      ← 该 cgroup 中的进程列表

v1 与 v2 关键差异对比

特性cgroup v1cgroup v2
层级结构多层级,每控制器独立树单一统一层级树
挂载方式各控制器分别挂载统一挂载 cgroup2 文件系统
线程模型线程可跨 cgroup线程化控制器,同进程线程默认同 cgroup
控制器协调各控制器独立统一资源策略,跨控制器协调
委派安全粗粒度子树委派,nsdelegate 挂载选项
内存统计仅统计 cgroup 自身递归统计(memory_recursiveprot
进程放置进程可属于多个 cgroup进程只属于一个 cgroup
核心版本2.6.24+4.5+(完整功能 5.2+)

环境检查与启用

确认当前 cgroup 版本

# 查看 cgroup 文件系统挂载情况
mount | grep cgroup

# cgroup v2 输出示例:
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

# cgroup v1 输出示例(多行):
# tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
# cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
# cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
# cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

如果输出中只有 cgroup2,说明系统已使用 cgroup v2。如果看到多个 cgroup 挂载项(按控制器分别挂载),则是 v1 或 v1/v2 混合模式。

# 查看可用的控制器
cat /sys/fs/cgroup/cgroup.controllers

# 输出示例:
# cpuset cpu io memory hugetlb pids rdma misc

注意:/proc/cgroups 仅与 cgroup v1 兼容,不适用于 v2。检查 v2 控制器应使用上述 cgroup.controllers 文件。

检查内核版本支持

cgroup v2 的各控制器在不同内核版本中陆续引入。以下是主要控制器的最低内核版本要求:

控制器功能最低内核版本
cpuCPU 带宽限制4.15
cpusetCPU 亲和性与 NUMA 节点5.0
memory内存限制与统计4.5
ioIO 带宽分配4.5
pids进程数量限制4.5
devices设备文件访问控制(BPF)4.15
rdmaRDMA 资源分配4.11
hugetlb大页面使用限制5.6
misc混合资源控制5.13

在旧系统上启用 cgroup v2

对于支持但默认未启用 v2 的系统,可通过内核启动参数切换:

# GRUB 配置(编辑 /etc/default/grub)
# 在 GRUB_CMDLINE_LINUX 中添加:
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"

# 更新 GRUB 并重启
sudo update-grub    # Debian/Ubuntu
sudo grub2-mkconfig -o /boot/grub2/grub.cfg   # RHEL/CentOS
sudo reboot

# 重启后验证
mount | grep cgroup2

如果需要同时支持 v1 和 v2(兼容旧容器运行时),可以使用:

GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 systemd.legacy_systemd_cgroup_controller=yes"

核心控制器详解

CPU 控制器

CPU 控制器提供两种资源控制方式:权重分配(weight-based)和带宽限制(max-based)。

权重模式(cpu.weight)

权重模式按比例分配 CPU 时间片,类似 nice 值但更精确。范围 1-10000,默认 100。

# 创建 cgroup 并设置 CPU 权重
sudo mkdir /sys/fs/cgroup/myapp
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
echo 500 > /sys/fs/cgroup/myapp/cpu.weight   # 权重 500(默认 100 的 5 倍)

# 对比:v1 的 cpu.shares
# v1: echo 512 > /sys/fs/cgroup/cpu/myapp/cpu.shares
# v2: echo 500 > /sys/fs/cgroup/myapp/cpu.weight  (语义不同但效果类似)

权重模式的分配逻辑:

# 假设两个 cgroup,权重分别为 100 和 300
cgroup_A: cpu.weight = 100  → 获得 100/(100+300) = 25% 的 CPU
cgroup_B: cpu.weight = 300  → 获得 300/(100+300) = 75% 的 CPU

# 当 CPU 空闲时,两者都可以使用超过分配比例的 CPU
# 仅在 CPU 竞争时,权重才生效

带宽限制模式(cpu.max)

带宽限制设置硬性上限,格式为 $MAX $PERIOD

# 限制为单核 50% 的 CPU(每 100ms 周期内最多用 50ms)
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max

# 解释:max=50000us, period=100000us → 50000/100000 = 50% 单核

# 限制为 2 个完整核心
echo "200000 100000" > /sys/fs/cgroup/myapp/cpu.max

# 无限制(默认)
echo "max" > /sys/fs/cgroup/myapp/cpu.max

CPU 亲和性(cpuset)

cgroup v2 的 cpuset 控制器从内核 5.0 开始支持:

# 启用 cpuset 控制器
echo "+cpuset" > /sys/fs/cgroup/cgroup.subtree_control

# 限制进程只能运行在 CPU 0-3 上
echo "0-3" > /sys/fs/cgroup/myapp/cpuset.cpus

# 限制内存节点(NUMA 场景)
echo "0" > /sys/fs/cgroup/myapp/cpuset.mems

内存控制器

内存控制器是 cgroup v2 中最重要的控制器之一,提供内存限制、统计和 OOM 控制。

基本内存限制

# 限制最大内存使用为 1GB
echo 1073741824 > /sys/fs/cgroup/myapp/memory.max

# 限制最大内存为 1GB(人类可读格式,内核 5.9+)
echo "1G" > /sys/fs/cgroup/myapp/memory.max

# 限制 swap 使用(内核 5.8+)
echo 536870912 > /sys/fs/cgroup/myapp/memory.swap.max   # 512MB swap

# 查看当前内存使用
cat /sys/fs/cgroup/myapp/memory.current
# 输出:536870912

# 查看内存峰值
cat /sys/fs/cgroup/myapp/memory.peak
# 输出:805306368

# 查看详细内存统计
cat /sys/fs/cgroup/myapp/memory.stat

memory.stat 输出关键字段解读:

anon 536870912          # 匿名内存(堆、栈等)
file 268435456          # 文件缓存
kernel 67108864         # 内核内存
sock 33554432           # socket 缓冲区
shmem 16777216          # 共享内存
slab_reclaimable 8388608    # 可回收 slab
slab_unreclaimable 4194304  # 不可回收 slab
pgfault 1234567         # 页错误次数
pgmajfault 1234         # 主要页错误次数
oom_kill 0               # OOM kill 次数

内存递归保护

cgroup v2 引入了 memory_recursiveprot 挂载选项,子 cgroup 的内存保护会递归生效。配合 memory.low 实现优雅降级:

# 设置内存下限保护(在此范围内不被回收)
echo 536870912 > /sys/fs/cgroup/myapp/memory.low   # 512MB 受保护

# 设置内存上限
echo 1073741824 > /sys/fs/cgroup/myapp/memory.max  # 1GB 上限

# 当系统内存紧张时:
# - 低于 memory.low 的部分受保护,不会被优先回收
# - 超过 memory.low 但低于 memory.max 的部分可以被回收
# - 超过 memory.max 触发 OOM 或被 kill

OOM 控制

# 禁止 OOM killer 杀死该 cgroup 中的进程
echo 1 > /sys/fs/cgroup/myapp/memory.oom.group

# 当 memory.oom.group=1 时,cgroup 中任何进程触发 OOM
# 会导致整个 cgroup 的所有进程被 kill

# 查看 OOM 事件
cat /sys/fs/cgroup/myapp/memory.events
# 输出:
# oom 0
# oom_kill 0
# oom_group_kill 0

IO 控制器

IO 控制器允许对块设备的读写带宽和 IOPS 进行限制。

# 启用 IO 控制器
echo "+io" > /sys/fs/cgroup/cgroup.subtree_control

# 查看可用块设备
cat /sys/fs/cgroup/myapp/io.stat
# 输出示例:
# 8:0 rbytes=1234567 wbytes=2345678 rios=100 wios=200
# 8:16 rbytes=345678 wbytes=456789 rios=50 wios=60

# 限制 /dev/sda (8:0) 的写入带宽为 10MB/s
echo "8:0 rbps=max wbps=10485760" > /sys/fs/cgroup/myapp/io.max

# 限制读写 IOPS
echo "8:0 riops=max wiops=1000" > /sys/fs/cgroup/myapp/io.max

# 同时限制带宽和 IOPS
echo "8:0 rbps=10485760 wbps=10485760 riops=1000 wiops=1000" > /sys/fs/cgroup/myapp/io.max

获取设备号的方法:

# 获取 /dev/sda 的 major:minor
lsblk -o NAME,MAJ:MIN /dev/sda
# 输出:sda 8:0

# 或使用 stat 命令
stat -c '%t:%T' /dev/sda
# 输出:8:0(十六进制,需转换为十进制)

PID 控制器

PID 控制器限制 cgroup 中可运行的进程/线程数量,防止 fork 炸弹和资源泄漏:

# 限制最多 500 个进程
echo 500 > /sys/fs/cgroup/myapp/pids.max

# 查看当前进程数
cat /sys/fs/cgroup/myapp/pids.current
# 输出:42

# 当 pids.current 达到 pids.max 时,fork() 会失败并返回 EAGAIN

与 systemd 集成

在现代 Linux 系统中,systemd 是 cgroup v2 的主要管理者。systemd 在启动时自动挂载 cgroup2 文件系统,并为每个服务单元创建对应的 cgroup。

systemd 资源限制配置

# 创建带资源限制的 systemd 服务
sudo tee /etc/systemd/system/heavy-app.service << 'EOF'
[Unit]
Description=Heavy Application
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/heavy-app
# CPU 限制:最多使用 2 个核心
CPUQuota=200%
# CPU 权重(相对优先级)
CPUWeight=500
# 内存限制
MemoryMax=2G
MemoryLow=512M
# IO 权重
IOWeight=500
# 进程数限制
TasksMax=300
# 重启策略
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl start heavy-app

验证 systemd cgroup 配置

# 查看服务的 cgroup 资源使用情况
systemctl status heavy-app

# 输出中会包含 CGroup 信息:
# CGroup: /system.slice/heavy-app.service
#         ├─1234 /usr/bin/heavy-app

# 查看详细的 cgroup 控制器配置
systemctl show heavy-app | grep -E "CPUQuota|MemoryMax|MemoryLow|IOWeight|TasksMax"
# 输出:
# CPUQuotaPerSecUSec=2s
# MemoryMax=2147483648
# MemoryLow=536870912
# IOWeight=500
# TasksMax=300

# 实时监控 cgroup 资源
systemd-cgtop
# 输出示例:
# Control Group           Tasks   %CPU   Memory  Input/s Output/s
# /system.slice/heavy-app  1      45.0   1.2G    0B/s    10MB/s
# /system.slice/nginx      4       2.1   256M    0B/s    0B/s

systemctl 资源动态调整

systemd 允许在不重启服务的情况下动态修改 cgroup 限制:

# 临时增加内存限制
systemctl set-property heavy-app MemoryMax=4G

# 临时调整 CPU 配额
systemctl set-property heavy-app CPUQuota=300%

# 设置 IO 权重
systemctl set-property heavy-app IOWeight=800

# 如果要永久生效(写入配置文件)
systemctl set-property --runtime=false heavy-app MemoryMax=4G

线程化 cgroup

对于需要在同一进程内为不同线程设置不同 CPU 策略的场景,cgroup v2 提供了线程化 cgroup(Threaded cgroup):

# 创建线程化 cgroup
mkdir /sys/fs/cgroup/myapp/worker-threads
echo threaded > /sys/fs/cgroup/myapp/worker-threads/cgroup.type

# 将线程添加到线程化 cgroup
echo $TID > /sys/fs/cgroup/myapp/worker-threads/cgroup.threads

# 为不同线程组设置不同 CPU 权重
echo 200 > /sys/fs/cgroup/myapp/worker-threads/cpu.weight
echo 800 > /sys/fs/cgroup/myapp/main-threads/cpu.weight

手动 cgroup 管理实战

创建和管理自定义 cgroup

以下是一个完整的实战示例,演示如何手动创建 cgroup、配置资源限制、将进程加入和移出:

#!/bin/bash
# cgroup-v2-manage.sh — cgroup v2 手动管理示例

set -euo pipefail

CGROOT="/sys/fs/cgroup"
CGROUP_NAME="batch-job"

# 确保以 root 运行
if [ "$(id -u)" -ne 0 ]; then
    echo "请以 root 权限运行" >&2
    exit 1
fi

# 创建 cgroup
echo ">>> 创建 cgroup: $CGROUP_NAME"
mkdir -p "$CGROOT/$CGROUP_NAME"

# 启用控制器(在父级 subtree_control 中启用)
echo "+cpu +memory +io +pids" > "$CGROOT/cgroup.subtree_control"

# 设置 CPU 限制:单核 30%
echo "30000 100000" > "$CGROOT/$CGROUP_NAME/cpu.max"

# 设置内存限制:512MB
echo 536870912 > "$CGROOT/$CGROUP_NAME/memory.max"

# 设置 swap 限制:128MB
echo 134217728 > "$CGROOT/$CGROUP_NAME/memory.swap.max"

# 设置进程数限制:100
echo 100 > "$CGROOT/$CGROUP_NAME/pids.max"

# 设置 IO 限制:写入 5MB/s
DEVICE=$(stat -c '%t:%T' /dev/sda)
MAJOR=$((0x$(stat -c '%t' /dev/sda)))
MINOR=$((0x$(stat -c '%T' /dev/sda)))
echo "$MAJOR:$MINOR wbps=5242880" > "$CGROOT/$CGROUP_NAME/io.max"

echo ">>> 资源限制已设置:"
echo "    CPU:   30% 单核"
echo "    内存:  512MB"
echo "    Swap:  128MB"
echo "    进程:  100"
echo "    IO:    5MB/s 写入"

# 启动进程并加入 cgroup
echo ">>> 启动进程..."
python3 /opt/batch-job/main.py &
PID=$!

# 将进程加入 cgroup
echo $PID > "$CGROOT/$CGROUP_NAME/cgroup.procs"

echo ">>> 进程 PID=$PID 已加入 cgroup $CGROUP_NAME"
echo ">>> 监控资源使用(按 Ctrl+C 停止)..."

# 实时监控
while kill -0 "$PID" 2>/dev/null; do
    echo "--- $(date '+%H:%M:%S') ---"
    echo "CPU 使用: $(cat $CGROOT/$CGROUP_NAME/cpu.stat | grep 'usage_usec' | head -1)"
    echo "内存使用: $(cat $CGROOT/$CGROUP_NAME/memory.current) bytes"
    echo "进程数:   $(cat $CGROOT/$CGROUP_NAME/pids.current)"
    sleep 5
done

echo ">>> 进程已退出"
echo ">>> 内存峰值: $(cat $CGROOT/$CGROUP_NAME/memory.peak) bytes"
echo ">>> OOM 事件: $(cat $CGROOT/$CGROUP_NAME/memory.events | grep oom)"

# 清理 cgroup
rmdir "$CGROOT/$CGROUP_NAME"
echo ">>> cgroup 已清理"

CRIU 与 cgroup 检查点

在容器迁移和热升级场景中,CRIU(Checkpoint/Restore In Userspace)依赖 cgroup 来恢复进程的资源限制:

# 对运行中的进程做检查点
criu dump --tree $PID --images-dir /tmp/checkpoint

# 在另一台机器上恢复(cgroup 配置会自动重建)
criu restore --images-dir /tmp/checkpoint --cgroup-root /sys/fs/cgroup/myapp

Docker 与 Kubernetes 场景

Docker 使用 cgroup v2

Docker 从 20.10 版本开始支持 cgroup v2。确认 Docker 使用 v2:

# 检查 Docker 的 cgroup driver
docker info | grep -i cgroup

# cgroup v2 输出:
#  Cgroup Driver: cgroupfs
#  Cgroup Version: 2

# 或
#  Cgroup Driver: systemd
#  Cgroup Version: 2

运行带资源限制的容器:

# 启动容器并限制 CPU 和内存
docker run -d \
    --name myapp \
    --cpus="1.5" \
    --cpu-shares=512 \
    --memory="1g" \
    --memory-swap="1.5g" \
    --pids-limit=200 \
    --device-write-bps /dev/sda:10mb \
    myapp:latest

# 验证容器的 cgroup 配置
# Docker 在 v2 下创建的 cgroup 路径:
# /sys/fs/cgroup/system.slice/docker-<container-id>.scope/

CONTAINER_ID=$(docker inspect -f '{{.Id}}' myapp)
CG_PATH="/sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope"

echo "CPU 限制: $(cat $CG_PATH/cpu.max)"
echo "内存限制: $(cat $CG_PATH/memory.max)"
echo "内存使用: $(cat $CG_PATH/memory.current)"
echo "进程数:   $(cat $CG_PATH/pids.current)/$(cat $CG_PATH/pids.max)"

Docker 的 --cpus 参数在 cgroup v2 中的映射:

Docker 参数cgroup v2 文件说明
--cpus=1.5cpu.max = “150000 100000”1.5 核带宽限制
--cpu-shares=512cpu.weight = 50(近似映射)相对权重
--memory=1gmemory.max = 1073741824内存硬限制
--memory-swap=1.5gmemory.swap.max = 536870912swap 限制
--pids-limit=200pids.max = 200进程数限制
--device-write-bps /dev/sda:10mbio.max = “8:0 wbps=10485760”IO 带宽限制

Kubernetes 与 cgroup v2

Kubernetes 从 1.25 版本开始将 cgroup v2 作为稳定特性支持。kubelet 的 cgroup driver 配置:

# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1
kind: KubeletConfiguration
cgroupDriver: systemd   # 推荐使用 systemd(与 cgroup v2 配合最佳)
# 如果使用 cgroupfs,则 kubelet 直接操作 cgroup 文件系统

Kubernetes Pod 的资源请求和限制在 cgroup v2 中的体现:

# Pod 定义
apiVersion: v1
kind: Pod
metadata:
  name: resource-demo
spec:
  containers:
  - name: app
    image: myapp:latest
    resources:
      requests:
        cpu: "500m"     # 0.5 核请求
        memory: "512Mi"
      limits:
        cpu: "1000m"    # 1 核限制
        memory: "1Gi"

对应的 cgroup v2 路径和文件:

# Pod 的 cgroup 路径(使用 systemd cgroup driver)
# /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/
#   kubepods-pod<uid>.slice/
#     cri-containerd-<container-id>.scope/

# CPU requests → cpu.weight
# 500m request → cpu.weight ≈ 50(基于公式转换)

# CPU limits → cpu.max
# 1000m limit → cpu.max = "100000 100000"(1 核)

# Memory limits → memory.max
# 1Gi limit → memory.max = 1073741824

# Memory requests → memory.low
# 512Mi request → memory.low = 536870912

CPU requests 到 cpu.weight 的转换公式:

# Kubernetes 将 CPU request 转换为 cgroup v2 cpu.weight 的逻辑
def cpu_request_to_weight(cpu_millicores):
    # 转换为核数
    cpu_cores = cpu_millicores / 1000.0
    # weight 公式:weight = 1 + (cpu_cores - 1) * 99(近似)
    # 实际使用的是对数映射,确保小请求也有合理权重
    if cpu_cores <= 0:
        return 1
    # Kubernetes 的映射表
    weight = min(10000, max(1, int(100 * (cpu_cores))))
    return weight

# 示例
print(cpu_request_to_weight(100))   # 100m → 10
print(cpu_request_to_weight(500))    # 500m → 50
print(cpu_request_to_weight(1000))  # 1 核 → 100
print(cpu_request_to_weight(4000))  # 4 核 → 400

查看容器内 cgroup 视图

在 cgroup v2 下,容器内看到的 cgroup 视图更加统一:

# 进入容器
docker exec -it myapp bash

# 容器内查看 cgroup
cat /sys/fs/cgroup/cgroup.controllers
# 输出:cpu memory io pids

cat /sys/fs/cgroup/memory.max
# 输出:1073741824(即宿主机设置的 1GB 限制)

cat /sys/fs/cgroup/cpu.max
# 输出:150000 100000(即 1.5 核限制)

# cgroup v2 的统一视图让容器内进程
# 可以通过标准接口获取自身的资源限制

高级调优与生产实践

混合负载资源隔离策略

在生产环境中,不同类型的负载需要不同的资源隔离策略:

#!/bin/bash
# setup-mixed-workloads.sh — 混合负载 cgroup 配置

CGROOT="/sys/fs/cgroup"

# 创建工作负载分组
mkdir -p "$CGROOT"/{latency-sensitive,best-effort,batch}

# 启用所有需要的控制器
echo "+cpu +memory +io +pids" > "$CGROOT/cgroup.subtree_control"

# 1. 延迟敏感型应用(如 Web API)
#    高 CPU 权重,内存保护,低 IO 限制
echo 10000 > "$CGROOT/latency-sensitive/cpu.weight"     # 最高权重
echo 4294967296 > "$CGROOT/latency-sensitive/memory.max"  # 4GB 上限
echo 2147483648 > "$CGROOT/latency-sensitive/memory.low"   # 2GB 保护
echo 800 > "$CGROOT/latency-sensitive/io.weight"         # 高 IO 优先级

# 2. 尽力而为型应用(如后台任务)
#    中等 CPU 权重,适度内存
echo 200 > "$CGROOT/best-effort/cpu.weight"
echo 2147483648 > "$CGROOT/best-effort/memory.max"  # 2GB
echo 536870912 > "$CGROOT/best-effort/memory.low"   # 512MB 保护
echo 200 > "$CGROOT/best-effort/io.weight"

# 3. 批处理任务(如数据分析)
#    低 CPU 权重,大内存,IO 限制
echo 50 > "$CGROOT/batch/cpu.weight"              # 最低权重
echo 8589934592 > "$CGROOT/batch/memory.max"       # 8GB
echo 1073741824 > "$CGROOT/batch/memory.low"       # 1GB 保护
echo 50 > "$CGROOT/batch/io.weight"               # 低 IO 优先级

# 限制批处理任务的 IO 带宽(避免影响数据库性能)
DEV=$(stat -c '%t:%T' /dev/sda)
MAJOR=$((0x$(stat -c '%t' /dev/sda)))
MINOR=$((0x$(stat -c '%T' /dev/sda)))
echo "$MAJOR:$MINOR rbps=52428800 wbps=52428800" > "$CGROOT/batch/io.max"
# 限制读写各 50MB/s

echo ">>> 混合负载 cgroup 配置完成"
echo ">>> 延迟敏感: cpu.weight=10000, mem=4G(2G protected), io=800"
echo ">>> 尽力而为: cpu.weight=200,  mem=2G(512M protected), io=200"
echo ">>> 批处理:   cpu.weight=50,   mem=8G(1G protected),  io=50(50MB/s)"

cgroup v2 监控脚本

以下脚本用于持续监控 cgroup 资源使用,适合集成到 Prometheus exporter 或告警系统:

#!/usr/bin/env python3
"""cgroup v2 资源监控脚本 — 采集指定 cgroup 的资源使用数据"""

import os
import time
import json
from pathlib import Path

class CgroupV2Monitor:
    """监控 cgroup v2 资源使用情况"""

    CGROOT = Path("/sys/fs/cgroup")

    def __init__(self, cgroup_path: str):
        self.cgpath = self.CGROOT / cgroup_path.lstrip("/")
        if not self.cgpath.exists():
            raise FileNotFoundError(f"cgroup path not found: {self.cgpath}")

    def read_file(self, name: str) -> str:
        filepath = self.cgpath / name
        if not filepath.exists():
            return ""
        return filepath.read_text().strip()

    def get_cpu_stats(self) -> dict:
        """获取 CPU 使用统计"""
        stats = {}
        cpu_max = self.read_file("cpu.max")
        if cpu_max and cpu_max != "max":
            parts = cpu_max.split()
            stats["cpu_quota_us"] = int(parts[0])
            stats["cpu_period_us"] = int(parts[1])
            stats["cpu_limit_cores"] = int(parts[0]) / int(parts[1])
        else:
            stats["cpu_limit_cores"] = -1  # 无限制

        cpu_stat = self.read_file("cpu.stat")
        for line in cpu_stat.split("\n"):
            if line:
                key, val = line.split()
                stats[f"cpu_{key}"] = int(val)
        return stats

    def get_memory_stats(self) -> dict:
        """获取内存使用统计"""
        stats = {}
        stats["memory_current"] = int(self.read_file("memory.current") or 0)
        stats["memory_max"] = int(self.read_file("memory.max") or 0)
        stats["memory_peak"] = int(self.read_file("memory.peak") or 0)
        stats["memory_low"] = int(self.read_file("memory.low") or 0)
        stats["memory_swap_current"] = int(self.read_file("memory.swap.current") or 0)
        stats["memory_swap_max"] = int(self.read_file("memory.swap.max") or 0)

        mem_stat = self.read_file("memory.stat")
        for line in mem_stat.split("\n"):
            if line:
                key, val = line.split()
                stats[f"mem_{key}"] = int(val)

        events = self.read_file("memory.events")
        for line in events.split("\n"):
            if line:
                key, val = line.split()
                stats[f"mem_event_{key}"] = int(val)
        return stats

    def get_io_stats(self) -> dict:
        """获取 IO 使用统计"""
        stats = {}
        io_stat = self.read_file("io.stat")
        for line in io_stat.split("\n"):
            if not line:
                continue
            parts = line.split()
            dev = parts[0]  # major:minor
            for field in parts[1:]:
                key, val = field.split("=")
                stats[f"io_{dev}_{key}"] = int(val)
        return stats

    def get_pids_stats(self) -> dict:
        """获取进程数统计"""
        return {
            "pids_current": int(self.read_file("pids.current") or 0),
            "pids_max": int(self.read_file("pids.max") or 0),
        }

    def collect_all(self) -> dict:
        """采集所有资源数据"""
        return {
            "timestamp": int(time.time()),
            "cgroup": str(self.cgpath),
            "cpu": self.get_cpu_stats(),
            "memory": self.get_memory_stats(),
            "io": self.get_io_stats(),
            "pids": self.get_pids_stats(),
        }

if __name__ == "__main__":
    # 示例:监控系统.slice 下的 nginx 服务
    monitor = CgroupV2Monitor("system.slice/nginx.service")

    while True:
        data = monitor.collect_all()
        print(json.dumps(data, indent=2))

        # 计算内存使用率
        mem = data["memory"]
        if mem["memory_max"] > 0:
            usage_pct = mem["memory_current"] / mem["memory_max"] * 100
            print(f"\n内存使用率: {usage_pct:.1f}%")

        time.sleep(10)

内存压力感知与 PSI

cgroup v2 集成了 PSI(Pressure Stall Information),可以感知资源压力:

# 查看 cgroup 的 PSI 压力指标
cat /sys/fs/cgroup/myapp/psi/cpu.pressure
# 输出:
# some avg10=12.50 avg60=5.00 avg300=2.00 total=12345678
# full avg10=8.30 avg60=3.00 avg300=1.00 total=8765432

# some: 至少一个任务在等待 CPU
# full: 所有任务都在等待 CPU
# avg10/60/300: 10秒/60秒/300秒平均压力

cat /sys/fs/cgroup/myapp/psi/memory.pressure
cat /sys/fs/cgroup/myapp/psi/io.pressure

基于 PSI 的自动扩缩容逻辑:

#!/usr/bin/env python3
"""基于 PSI 压力指标的自动扩缩容决策器"""

import re
from pathlib import Path

class PSIMonitor:
    """读取 cgroup PSI 压力数据"""

    def __init__(self, cgroup_path: str):
        self.cgroot = Path("/sys/fs/cgroup") / cgroup_path.lstrip("/")

    def read_psi(self, resource: str) -> dict:
        """读取指定资源的 PSI 数据"""
        filepath = self.cgroot / f"psi/{resource}.pressure"
        if not filepath.exists():
            return {}

        content = filepath.read_text()
        result = {}
        for line in content.strip().split("\n"):
            match = re.match(
                r'(\w+)\s+avg10=(\S+)\s+avg60=(\S+)\s+avg300=(\S+)\s+total=(\d+)',
                line
            )
            if match:
                result[match.group(1)] = {
                    "avg10": float(match.group(2)),
                    "avg60": float(match.group(3)),
                    "avg300": float(match.group(4)),
                    "total": int(match.group(5)),
                }
        return result

    def should_scale_up(self) -> tuple:
        """判断是否需要扩容"""
        cpu_psi = self.read_psi("cpu")
        mem_psi = self.read_psi("memory")

        cpu_pressure = cpu_psi.get("some", {}).get("avg10", 0)
        mem_pressure = mem_psi.get("full", {}).get("avg10", 0)

        if cpu_pressure > 30:
            return True, f"CPU 压力过高 ({cpu_pressure:.1f}%),建议扩容"
        if mem_pressure > 20:
            return True, f"内存压力过高 ({mem_pressure:.1f}%),建议扩容"

        return False, "资源压力正常"

if __name__ == "__main__":
    monitor = PSIMonitor("myapp")

    scale_up, reason = monitor.should_scale_up()
    if scale_up:
        print(f"[ALERT] {reason}")
        # 这里可以触发 Kubernetes HPA 或自动扩容逻辑
    else:
        print(f"[OK] {reason}")

v1 到 v2 迁移指南

迁移评估清单

检查项检查方法通过标准
内核版本uname -r>= 5.2(完整功能)
systemd 版本systemctl --version>= 239
Docker 版本docker --version>= 20.10
Kubernetes 版本kubectl version>= 1.25(v2 GA)
自定义 cgroup 脚本审计代码不依赖 v1 多层级特性
监控工具检查工具文档支持 cgroup v2 路径
容器运行时docker info | grep cgroup支持 v2 driver
旧版应用读取 cgroup v1 路径的应用已更新或兼容 v2

迁移步骤

#!/bin/bash
# migrate-to-cgroup-v2.sh — cgroup v1 到 v2 迁移脚本

set -euo pipefail

echo "=== cgroup v1 → v2 迁移工具 ==="

# 1. 预检查
echo ""
echo "[1/5] 预检查..."

# 检查内核版本
KERNEL_VERSION=$(uname -r | cut -d. -f1-2)
KERNEL_MAJOR=$(echo $KERNEL_VERSION | cut -d. -f1)
KERNEL_MINOR=$(echo $KERNEL_VERSION | cut -d. -f2)
if [ "$KERNEL_MAJOR" -lt 5 ] || { [ "$KERNEL_MAJOR" -eq 5 ] && [ "$KERNEL_MINOR" -lt 2 ]; }; then
    echo "  [FAIL] 内核版本 $KERNEL_VERSION 过低,需要 >= 5.2"
    exit 1
fi
echo "  [OK] 内核版本: $KERNEL_VERSION"

# 检查 systemd 版本
SYSTEMD_VERSION=$(systemctl --version | head -1 | awk '{print $2}')
if [ "$SYSTEMD_VERSION" -lt 239 ]; then
    echo "  [FAIL] systemd 版本 $SYSTEMD_VERSION 过低,需要 >= 239"
    exit 1
fi
echo "  [OK] systemd 版本: $SYSTEMD_VERSION"

# 检查当前 cgroup 模式
CURRENT_MODE=$(mount | grep -c "^cgroup ")
if [ "$CURRENT_MODE" -eq 0 ]; then
    echo "  [INFO] 系统可能已在使用 cgroup v2"
    echo "  [OK] 无需迁移"
    exit 0
fi
echo "  [OK] 当前为 cgroup v1,开始迁移准备"

# 2. 检查容器运行时兼容性
echo ""
echo "[2/5] 检查容器运行时..."
if command -v docker &>/dev/null; then
    DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null || echo "0")
    DOCKER_MAJOR=$(echo $DOCKER_VERSION | cut -d. -f1)
    if [ "$DOCKER_MAJOR" -lt 20 ]; then
        echo "  [WARN] Docker $DOCKER_VERSION 需要升级到 >= 20.10"
        echo "  [INFO] 升级命令:curl -fsSL https://get.docker.com | sh"
    else
        echo "  [OK] Docker 版本: $DOCKER_VERSION"
    fi
fi

# 3. 检查 Kubernetes 兼容性
echo ""
echo "[3/5] 检查 Kubernetes..."
if command -v kubectl &>/dev/null; then
    K8S_VERSION=$(kubectl version --short 2>/dev/null | grep Server | awk '{print $3}' | cut -d. -f1-2 || echo "0.0")
    K8S_MINOR=$(echo $K8S_VERSION | cut -d. -f2)
    if [ "$K8S_MINOR" -lt 25 ]; then
        echo "  [WARN] Kubernetes $K8S_VERSION 建议升级到 >= 1.25"
    else
        echo "  [OK] Kubernetes 版本: $K8S_VERSION"
    fi
fi

# 4. 检查自定义脚本
echo ""
echo "[4/5] 检查自定义 cgroup 脚本..."
echo "  [INFO] 搜索依赖 cgroup v1 路径的脚本..."
V1_SCRIPTS=$(grep -rl "/sys/fs/cgroup/cpu" /etc/init.d/ /usr/local/bin/ /opt/ 2>/dev/null || true)
if [ -n "$V1_SCRIPTS" ]; then
    echo "  [WARN] 发现以下脚本引用了 cgroup v1 路径:"
    echo "$V1_SCRIPTS" | while read script; do
        echo "    - $script"
    done
    echo "  [INFO] 需要将 v1 路径(如 /sys/fs/cgroup/cpu/xxx/cpu.cfs_quota_us)"
    echo "         更新为 v2 路径(如 /sys/fs/cgroup/xxx/cpu.max)"
else
    echo "  [OK] 未发现引用 v1 路径的脚本"
fi

# 5. 生成迁移指令
echo ""
echo "[5/5] 迁移指令..."
echo "  1. 编辑 GRUB 配置:"
echo "     sudo sed -i 's/GRUB_CMDLINE_LINUX=\"/GRUB_CMDLINE_LINUX=\"systemd.unified_cgroup_hierarchy=1 /' /etc/default/grub"
echo ""
echo "  2. 更新 GRUB:"
echo "     sudo update-grub    # Debian/Ubuntu"
echo "     sudo grub2-mkconfig -o /boot/grub2/grub.cfg   # RHEL/CentOS"
echo ""
echo "  3. 重启系统:"
echo "     sudo reboot"
echo ""
echo "  4. 重启后验证:"
echo "     mount | grep cgroup2"
echo "     cat /sys/fs/cgroup/cgroup.controllers"
echo ""
echo "  5. 更新 kubelet 配置(如使用 K8s):"
echo "     确保 cgroupDriver: systemd"
echo ""
echo "=== 迁移准备完成 ==="

cgroup v1 到 v2 接口映射

迁移过程中,需要将 v1 的接口文件替换为 v2 的等价文件:

v1 文件v2 文件说明
cpu.cfs_quota_uscpu.maxv2 格式为 “$max $period”
cpu.cfs_period_uscpu.max同上
cpu.sharescpu.weight范围不同(v1: 2-262144, v2: 1-10000)
memory.limit_in_bytesmemory.max直接使用
memory.memsw.limit_in_bytesmemory.swap.maxv2 独立控制 swap
memory.soft_limit_in_bytesmemory.low语义类似
blkio.throttle.write_bps_deviceio.max格式不同
pids.maxpids.max相同
cgroup.procscgroup.procs相同
tasks(线程级)cgroup.threadsv2 线程化 cgroup

CPU shares 到 weight 的转换:

def cpu_shares_to_weight(shares: int) -> int:
    """将 cgroup v1 cpu.shares 转换为 v2 cpu.weight"""
    # v1 范围: 2-262144, 默认 1024
    # v2 范围: 1-10000, 默认 100
    # 近似线性映射
    weight = max(1, min(10000, int(shares / 1024 * 100)))
    return weight

# 示例
print(cpu_shares_to_weight(1024))  # 默认 → 100
print(cpu_shares_to_weight(512))   # 低优先级 → 50
print(cpu_shares_to_weight(2048))  # 高优先级 → 200
print(cpu_shares_to_weight(8192))  # 最高 → 800

常见问题与排查

问题1:控制器不可用

# 现象:写入 cgroup.subtree_control 时报错
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
# bash: echo: write error: Invalid argument

# 排查1:检查控制器是否在全局可用列表中
cat /sys/fs/cgroup/cgroup.controllers
# 如果 cpu 不在列表中,说明内核未启用该控制器

# 排查2:检查是否有进程在根 cgroup 中
cat /sys/fs/cgroup/cgroup.procs
# 根 cgroup 中有进程时,某些控制器无法启用
# 需要将进程迁移到子 cgroup

# 排查3:检查内核启动参数
cat /proc/cmdline | grep cgroup
# 确认没有 cgroup_no_v1 或 cgroup.disable 排除该控制器

问题2:cgroup 目录无法删除

# 现象:rmdir 报 "Device or resource busy"
rmdir /sys/fs/cgroup/myapp
# rmdir: failed to remove '/sys/fs/cgroup/myapp': Device or resource busy

# 排查:检查是否有活跃进程
cat /sys/fs/cgroup/myapp/cgroup.procs
# 如果有 PID,需要先将进程移出

# 移出进程
for pid in $(cat /sys/fs/cgroup/myapp/cgroup.procs); do
    echo $pid > /sys/fs/cgroup/cgroup.procs  # 移到根 cgroup
done

# 检查子 cgroup
ls /sys/fs/cgroup/myapp/
# 需要先删除所有子 cgroup

# 检查是否启用了控制器但未禁用
cat /sys/fs/cgroup/myapp/cgroup.type
# 如果是 threaded,需要先改回 domain
echo "domain" > /sys/fs/cgroup/myapp/cgroup.type

问题3:Docker 容器内存限制不生效

# 现象:docker run --memory=1g 但容器内可用内存远超 1G

# 排查1:确认 Docker 使用 cgroup v2
docker info | grep "Cgroup Version"
# 应输出 2

# 排查2:检查 swap 限制
# 在 cgroup v2 中,memory.swap.max 控制的是 swap 使用量
# 而不是 memory+swap 总量(v1 的行为)
docker run --memory=1g --memory-swap=1g myapp
# 这会将 swap 限制设为 0(swap = memory-swap - memory = 0)

# 排查3:检查内核参数
sysctl vm.swappiness
# 如果 swappiness=0,系统可能不使用 swap

# 排查4:确认容器内看到正确的限制
docker exec myapp cat /sys/fs/cgroup/memory.max
# 应输出 1073741824(1GB)

问题4:Kubernetes Pod CPU 限制不准确

# 现象:设置了 limits.cpu=1000m 但容器使用的 CPU 超过 1 核

# 排查1:检查 kubelet cgroup driver
cat /var/lib/kubelet/config.yaml | grep cgroupDriver
# 推荐使用 systemd

# 排查2:检查 Pod 的 cgroup
# 获取 Pod UID
POD_UID=$(kubectl get pod mypod -o jsonpath='{.metadata.uid}')
# 查看 cgroup
cat /sys/fs/cgroup/kubepods.slice/kubepods-pod${POD_UID//-/_}.slice/cpu.max

# 排查3:多核场景下的 CPU 限制
# cpu.max = "100000 100000" 表示每 100ms 可用 100ms
# 在多核机器上,容器可以在一个 100ms 周期内
# 在多个 CPU 核心上运行总共 100ms 的计算
# 但瞬时可能看到多个核同时高使用率
# 这是正常行为:cpu.max 限制的是总量,不是并发核数

性能基准对比

cgroup v1 vs v2 性能开销

指标v1v2差异
进程创建开销基准+2-3%v2 统一层级增加少量开销
内存统计精度cgroup 级递归级v2 更精确
IO 限制准确性较差更好v2 使用 io.max 更精确
cgroup 操作延迟基准-10-15%v2 单层级减少锁竞争
多容器场景扩展性线性退化更优v2 减少层级深度

在绝大多数生产场景中,cgroup v2 的性能开销可以忽略不计,而其在管理便利性、安全性和统计精度上的优势远超微小的性能差异。

总结

cgroup v2 不仅仅是 v1 的版本升级,更是资源管理范式的重构。单一层级树架构从根本上解决了 v1 的多层级混乱问题,让资源限制、统计和管理变得清晰可预测。

核心要点回顾:

  1. 架构理解:v2 的统一层级树让进程只属于一个 cgroup,所有控制器在同一棵树上协作,消除了 v1 的视图割裂问题。
  2. 控制器使用cpu.max(带宽限制)、cpu.weight(权重分配)、memory.max(内存上限)、io.max(IO 限制)、pids.max(进程数限制)是生产环境最常用的五个控制文件。
  3. systemd 集成:现代 Linux 系统中,systemd 是 cgroup 的主要管理者。通过 systemctl set-property 可以动态调整资源限制,无需重启服务。
  4. 容器场景:Docker 和 Kubernetes 已全面支持 cgroup v2。CPU requests 映射为 cpu.weight,limits 映射为 cpu.max,内存的 requests/limits 映射为 memory.low/memory.max
  5. 迁移策略:迁移前做好评估清单检查(内核版本、systemd 版本、容器运行时、自定义脚本),通过 GRUB 参数切换,并更新 cgroup v1 路径引用。
  6. 生产监控:利用 memory.eventscpu.statio.stat 和 PSI 压力指标构建完整的资源监控体系,基于 PSI 实现自动扩缩容决策。

在实践中,建议新部署的系统直接使用 cgroup v2,已有系统在评估兼容性后逐步迁移。cgroup v2 是云原生基础设施的重要基石,掌握它对于管理现代容器化工作负载至关重要。