概述
在云原生和容器化技术全面普及的今天,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 v1 | cgroup 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 的各控制器在不同内核版本中陆续引入。以下是主要控制器的最低内核版本要求:
| 控制器 | 功能 | 最低内核版本 |
|---|---|---|
cpu | CPU 带宽限制 | 4.15 |
cpuset | CPU 亲和性与 NUMA 节点 | 5.0 |
memory | 内存限制与统计 | 4.5 |
io | IO 带宽分配 | 4.5 |
pids | 进程数量限制 | 4.5 |
devices | 设备文件访问控制(BPF) | 4.15 |
rdma | RDMA 资源分配 | 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.5 | cpu.max = “150000 100000” | 1.5 核带宽限制 |
--cpu-shares=512 | cpu.weight = 50(近似映射) | 相对权重 |
--memory=1g | memory.max = 1073741824 | 内存硬限制 |
--memory-swap=1.5g | memory.swap.max = 536870912 | swap 限制 |
--pids-limit=200 | pids.max = 200 | 进程数限制 |
--device-write-bps /dev/sda:10mb | io.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_us | cpu.max | v2 格式为 “$max $period” |
cpu.cfs_period_us | cpu.max | 同上 |
cpu.shares | cpu.weight | 范围不同(v1: 2-262144, v2: 1-10000) |
memory.limit_in_bytes | memory.max | 直接使用 |
memory.memsw.limit_in_bytes | memory.swap.max | v2 独立控制 swap |
memory.soft_limit_in_bytes | memory.low | 语义类似 |
blkio.throttle.write_bps_device | io.max | 格式不同 |
pids.max | pids.max | 相同 |
cgroup.procs | cgroup.procs | 相同 |
tasks(线程级) | cgroup.threads | v2 线程化 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 性能开销
| 指标 | v1 | v2 | 差异 |
|---|---|---|---|
| 进程创建开销 | 基准 | +2-3% | v2 统一层级增加少量开销 |
| 内存统计精度 | cgroup 级 | 递归级 | v2 更精确 |
| IO 限制准确性 | 较差 | 更好 | v2 使用 io.max 更精确 |
| cgroup 操作延迟 | 基准 | -10-15% | v2 单层级减少锁竞争 |
| 多容器场景扩展性 | 线性退化 | 更优 | v2 减少层级深度 |
在绝大多数生产场景中,cgroup v2 的性能开销可以忽略不计,而其在管理便利性、安全性和统计精度上的优势远超微小的性能差异。
总结
cgroup v2 不仅仅是 v1 的版本升级,更是资源管理范式的重构。单一层级树架构从根本上解决了 v1 的多层级混乱问题,让资源限制、统计和管理变得清晰可预测。
核心要点回顾:
- 架构理解:v2 的统一层级树让进程只属于一个 cgroup,所有控制器在同一棵树上协作,消除了 v1 的视图割裂问题。
- 控制器使用:
cpu.max(带宽限制)、cpu.weight(权重分配)、memory.max(内存上限)、io.max(IO 限制)、pids.max(进程数限制)是生产环境最常用的五个控制文件。 - systemd 集成:现代 Linux 系统中,systemd 是 cgroup 的主要管理者。通过
systemctl set-property可以动态调整资源限制,无需重启服务。 - 容器场景:Docker 和 Kubernetes 已全面支持 cgroup v2。CPU requests 映射为
cpu.weight,limits 映射为cpu.max,内存的 requests/limits 映射为memory.low/memory.max。 - 迁移策略:迁移前做好评估清单检查(内核版本、systemd 版本、容器运行时、自定义脚本),通过 GRUB 参数切换,并更新 cgroup v1 路径引用。
- 生产监控:利用
memory.events、cpu.stat、io.stat和 PSI 压力指标构建完整的资源监控体系,基于 PSI 实现自动扩缩容决策。
在实践中,建议新部署的系统直接使用 cgroup v2,已有系统在评估兼容性后逐步迁移。cgroup v2 是云原生基础设施的重要基石,掌握它对于管理现代容器化工作负载至关重要。