一、cgroups 架构总览:资源管控的基石

Control Groups(控制组)是 Linux 内核提供的一种机制,用于限制、记录和隔离进程组所使用的物理资源(CPU、内存、磁盘 I/O、网络等)。它是现代容器技术(Docker、Kubernetes、Podman)的底层基石。

1.1 核心设计思想

cgroups 将进程分组,并为每个组分配资源。其核心概念有三个:

  • Hierarchy(层级树):资源控制器的组织方式,每个 controller 挂在不同的 hierarchy 上
  • Control Group(控制组):进程的分组,每个组绑定一组资源限制参数
  • Subsystem/Controller(子系统/控制器):具体的资源控制模块(cpu、memory、blkio 等)

1.2 cgroups v1 vs v2

v1 是最广泛使用的版本,v2 是重新设计的统一层级架构:

特性cgroups v1cgroups v2
层级结构每个 controller 独立 hierarchy统一 hierarchy
进程归属同一进程可在不同 cgroup同一进程只能属于一个叶子 cgroup
资源竞争controller 间可能冲突统一协调,避免资源浪费
根组限制父 cgroup 也不能限制子组根组可对子组进行权重分配
压力通知无原生支持内置 PSI(Pressure Stall Information)
生产就绪广泛使用逐步推广(K8s 1.25+ 支持)

1.3 当前系统 cgroup 状态速查

# 查看已挂载的 cgroup 控制器
ls /sys/fs/cgroup/
mount | grep cgroup

# 查看进程所属 cgroup
cat /proc/self/cgroup

# 查看系统支持的所有控制器
cat /proc/cgroups

# 查看 cgroup v2 是否启用
ls /sys/fs/cgroup/cgroup.controllers

二、CPU 资源控制:CFS 调度器的带宽艺术

2.1 CPU 权重分配 —— cpu.shares

cpu.shares 定义了 cgroup 在 竞争 CPU 时间片 时的相对权重。它不是硬上限,而是比例分配:

# /sys/fs/cgroup/cpu/A/cpu.shares = 1024  (默认值)
# /sys/fs/cgroup/cpu/B/cpu.shares = 2048
# 当 CPU 资源饱和时,A 获得 1/3,B 获得 2/3

实战操作:

# 创建 cgroup
sudo mkdir -p /sys/fs/cgroup/cpu/limited_app
sudo mkdir -p /sys/fs/cgroup/cpu/favored_app

# 设置权重
echo 512 > /sys/fs/cgroup/cpu/limited_app/cpu.shares
echo 2048 > /sys/fs/cgroup/cpu/favored_app/cpu.shares

# 将进程移入 cgroup
echo $PID_LIMITED > /sys/fs/cgroup/cpu/limited_app/cgroup.procs
echo $PID_FAVORED > /sys/fs/cgroup/cpu/favored_app/cgroup.procs

2.2 CPU 硬限制 —— CFS Bandwidth Control

通过 cpu.cfs_period_us 和 cpu.cfs_quota_us 实现硬性的 CPU 带宽限制:

# 限制为 2 个核心的 50%(即 1 个核心)
echo 100000 > /sys/fs/cgroup/cpu/limited_app/cpu.cfs_period_us
echo 50000 > /sys/fs/cgroup/cpu/limited_app/cpu.cfs_quota_us

# 限制为 1 个核心的 80%(即 0.8 个核心)
# period = 100ms, quota = 80ms → 每个周期最多使用 80ms
echo 100000 > /sys/fs/cgroup/cpu/app/cpu.cfs_period_us
echo 80000  > /sys/fs/cgroup/cpu/app/cpu.cfs_quota_us

关键公式:CPU核心数 = cfs_quota_us / cfs_period_us

2.3 实时进程的 CPU 控制

实时进程(SCHED_FIFO/SCHED_RR)需要使用专属的 cpu.rt_period_us 和 cpu.rt_runtime_us:

# 保证实时任务最多占 95% CPU,留给常规任务 5%
echo 1000000 > /sys/fs/cgroup/cpu/machine/cpu.rt_period_us
echo 950000 > /sys/fs/cgroup/cpu/machine/cpu.rt_runtime_us

2.4 cpuset:核心绑定与 NUMA 亲和

cpuset controller 提供了最底层的核心绑定和内存节点控制:

# 将进程绑定到 CPU 核心 0-1 和 NUMA node 0
echo "0-1" > /sys/fs/cgroup/cpuset/rt_task/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/rt_task/cpuset.mems

# 确保独占核心(不与默认 cgroup 重叠)
echo "0-1" > /sys/fs/cgroup/cpuset/rt_task/cpuset.cpus.exclusive

三、内存资源控制:限制、交换与 OOM

3.1 内存硬限制

memory.limit_in_bytes:设置 cgroup 可使用的最大内存量,超出即触发 OOM Killer:

# 限制为 512MB
echo 536870912 > /sys/fs/cgroup/memory/webapp/memory.limit_in_bytes

# 查看当前使用量
cat /sys/fs/cgroup/memory/webapp/memory.usage_in_bytes

3.2 软限制 —— 内存压力时的引导

memory.soft_limit_in_bytes 不会杀死进程,但在内存紧张时,内核会优先回收超过软限制 cgroup 的页面:

# 硬限制:1GB,软限制:768MB
echo 1073741824 > /sys/fs/cgroup/memory/app/memory.limit_in_bytes
echo 805306368 > /sys/fs/cgroup/memory/app/memory.soft_limit_in_bytes

3.3 OOM 控制策略

通过 memory.oom_control 控制 Cgroup 级别的 OOM 行为:

# 关闭 cgroup 级别的 OOM Killer(进程由父 cgroup 管理)
echo 1 > /sys/fs/cgroup/memory/safe_app/memory.oom_control

# oom_control 返回值示例:
# oom_kill_disable 0
# under_oom 0
# oom_kill 0

3.4 内存统计与诊断

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

# 输出关键字段:
# cache      - 页缓存使用量
# rss        - 匿名映射和 shmem 的物理内存
# mapped_file - 映射文件的大小
# pgpgin/pgpgout - 换入换出次数
# swap       - swap 使用量
# active_anon/inactive_anon - LRU 链表中的匿名页

# 清空缓存强制回收
echo 3 > /sys/fs/cgroup/memory/app/memory.force_empty

3.5 内核内存限制(kmem)

限制内核为进程分配的内核对象内存(slab 分配器、网络栈缓冲区等):

# 限制内核内存为 100MB
echo 104857600 > /sys/fs/cgroup/memory/app/memory.kmem.limit_in_bytes

# 查看内核内存使用
cat /sys/fs/cgroup/memory/app/memory.kmem.usage_in_bytes

四、块设备 I/O 控制:磁盘带宽的精细管理

4.1 I/O 权重分配 —— blkio.weight

与 cpu.shares 类似,blkio.weight 在 cgroup 之间按比例分配 I/O 吞吐量,范围 100-1000(默认 500):

# 高优先级组获得更多 I/O 带宽
echo 1000 > /sys/fs/cgroup/blkio/db/blkio.weight
echo 100 > /sys/fs/cgroup/blkio/batch/blkio.weight
# db 与 batch 的 I/O 带宽比为 10:1

4.2 I/O 速率上限 —— throttling(throttle)

针对特定设备设置硬性的读写 IOPS 或带宽上限:

# sda:8:0, sdb:8:16
DEV="8:0"

# 限制读带宽 50MB/s → 52428800 bytes/s
echo "${DEV} 52428800" > /sys/fs/cgroup/blkio/slow/blkio.throttle.read_bps_device

# 限制写带宽 30MB/s
echo "${DEV} 31457280" > /sys/fs/cgroup/blkio/slow/blkio.throttle.write_bps_device

# 限制读 IOPS 1000
echo "${DEV} 1000" > /sys/fs/cgroup/blkio/slow/blkio.throttle.read_iops_device

# 限制写 IOPS 500
echo "${DEV} 500" > /sys/fs/cgroup/blkio/slow/blkio.throttle.write_iops_device

4.3 I/O 统计与监控

# 查看 I/O 统计
cat /sys/fs/cgroup/blkio/app/blkio.throttle.io_serviced
cat /sys/fs/cgroup/blkio/app/blkio.throttle.io_service_bytes

# 使用 iostat 辅助监控
iostat -d -x 1 /dev/sda

# 使用 iotop 查看实时进程 I/O
iotop -o -b -n 3

五、网络资源控制:带宽与优先级

5.1 网络分类标记 —— net_cls

net_cls 为 cgroup 中的进程发出的数据包打上 classid 标记,供 tc(Traffic Control)使用:

# 设置 classid(格式 0xAAAABBBB,AAA=主编号,BBB=次编号)
echo "0x100001" > /sys/fs/cgroup/net_cls/webapp/net_cls.classid

# 配置 tc 过滤规则(需结合 tc 命令)
tc qdisc add dev eth0 root handle 1: htb
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbps
tc filter add dev eth0 parent 1: protocol ip handle 1: cgroup

5.2 网络优先级 —— net_prio

为不同 cgroup 的流量设置不同的优先级(0-7,数字越大优先级越高):

# 高优先级流量的 interface+prio
echo "eth0 7" > /sys/fs/cgroup/net_prio/critical/net_prio.ifpriomap
echo "eth0 2" > /sys/fs/cgroup/net_prio/bulk/net_prio.ifpriomap

# 配合 iptables 标记 DSCP 值实现端到端 QoS

六、systemd 层面的 cgroup 管理

systemd 原生支持通过单元文件配置 cgroup 资源限制,是生产环境的最佳实践:

6.1 服务单元的 CPU 和内存限制

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
ExecStart=/opt/myapp/bin/app

# CPU 限制:最多使用 1.5 个核心
CPUQuota=150%

# CPU 权重(默认 100)
CPUWeight=800

# 内存限制
MemoryMax=1G
MemoryHigh=800M    # 软限制,开始回收

# I/O 限制
IOWeight=500
IOPSReadMax=2000:/dev/sda

# 任务数限制(防 fork 炸弹)
TasksMax=500

[Install]
WantedBy=multi-user.target

6.2 slice 资源的层级管理

systemd slice 是预定义的 cgroup,可分为三种标准类型:

  • system.slice:系统服务(默认位置)
  • user.slice:用户会话
  • machine.slice:虚拟机/容器
# 创建自定义 slice
sudo systemctl set-property user-workload.slice CPUQuota=200% MemoryMax=2G

# 将服务放入指定 slice
[Service]
Slice=machine.slice

# 查看所有 slice 的资源使用情况
systemctl status *.slice

# 实时监控 cgroup 资源
systemd-cgtop

6.3 scope 与 service 的区别

  • Scope:用于管理一组外部创建的进程组(如 Docker 容器),由外部创建后注册到 systemd
  • Service:由 systemd 启动和管理的进程

七、Docker 的 cgroup 实践

Docker 通过 runc 容器运行时将每个容器的资源限制写入 cgroup:

7.1 容器资源限制参数

# CPU 限制
docker run -d --cpus="1.5" \          # 最多 1.5 核
  --cpu-shares=512 \                    # CPU 权重
  --cpuset-cpus="0-2" \                 # 核心绑定
  --cpu-quota=50000 \                   # CFS 配额
  --cpu-period=100000 \                 # CFS 周期
  
  # 内存限制
  --memory="512m" \                     # 硬限制
  --memory-reservation="256m" \         # 软限制
  --memory-swap="1g" \                  # swap 限制(=memory+swap)
  --oom-kill-disable \                  # 禁用 OOM Kill
  --kernel-memory="128m" \              # 内核内存限制
  
  # I/O 限制
  --device-read-bps /dev/sda:50mb \     # 读带宽上限
  --device-write-bps /dev/sda:30mb \    # 写带宽上限
  --device-read-iops /dev/sda:1000 \    # 读 IOPS 上限
  --device-write-iops /dev/sda:500 \    # 写 IOPS 上限
  --blkio-weight=500 \                  # I/O 权重
  
  --name webapp nginx:alpine

7.2 查看容器的 cgroup 配置

# 容器在宿主机上的 cgroup 路径(Docker)
cat /sys/fs/cgroup/cpu/docker/$CONTAINER_ID/cpu.cfs_quota_us
cat /sys/fs/cgroup/memory/docker/$CONTAINER_ID/memory.limit_in_bytes

# 更便捷的方式:通过 docker 统计
docker stats --no-stream $CONTAINER_ID

# 查看容器在 cgroup 树中的位置
docker inspect --format '{{.Id}}' $CONTAINER_ID

八、Kubernetes 中的 cgroup 映射

Kubernetes 的资源模型逐层映射到底层 cgroup:

resources.limits.cpu → cpu.cfs_quota_us / cpu.cfs_period_us
resources.requests.cpu → cpu.shares
resources.limits.memory → memory.limit_in_bytes
resources.requests.memory → memory.soft_limit_in_bytes

8.1 Pod 与 cgroup 的层级关系

Kubernetes 中的 cgroup 层级结构:

/sys/fs/cgroup/
├── kubepods.slice/                    # 根级别
│   ├── kubepods-burstable.slice/       # 有 limit/request 的 Pod
│   │   └── kubepods-burstable-pod${uid}.slice/
│   │       └── cri-containerd-${container_id}.scope/
│   ├── kubepods-besteffort.slice/      # 无资源的 Pod
│   │   └── kubepods-besteffort-pod${uid}.slice/
│   │       └── cri-containerd-${container_id}.scope/
│   └── ...

8.2 QoS 类别与 cgroup

QoS 类别条件cgroup 软限制
Guaranteed所有容器 limits=requestscpu.shares ≥ 2(最低保证)
Burstable至少一个容器有 request/limit按 request 计算 shares
BestEffort无资源请求cpu.shares = 2(最低权重)

九、PSI - 压力停滞信息(cgroups v2)

PSI(Pressure Stall Information)是 cgroups v2 的内建特性,提供细粒度的资源压力指标:

# 查看 CPU 压力
cat /sys/fs/cgroup/mycpu.pressure
# 输出: avg10=2.35 avg60=1.08 avg300=0.53 total=245993286

# 查看内存压力
cat /sys/fs/cgroup/mymem.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

# 设置压力阈值通知(触发 epoll 事件)
echo "some avg10 50" > /sys/fs/cgroup/mycpu.pressure
echo "full avg10 30" > /sys/fs/cgroup/cgroup.pressure

指标解读:

  • some:至少一个任务在某资源上等待的时间占比
  • full:所有可运行任务同时在某资源上等待的时间占比(最严重的竞争状态)
  • avg10/avg60/avg300:10秒/60秒/300秒窗口的移动平均

十、生产级 cgroup 调优实战案例

10.1 案例一:数据库与批处理任务共置

在一台 8 核 32GB 的服务器上运行 PostgreSQL 和夜间数据批处理任务:

# /etc/systemd/system/postgresql.service
[Service]
Slice=db.slice
CPUQuota=500%           # 最多 5 核
CPUWeight=1000          # 高权重
MemoryMax=16G           # 硬限制
MemoryHigh=12G          # 软限制,提前回收
IOWeight=1000           # 最高 I/O 带宽
IOReadBandwidthMax=/dev/sda 200M
IOWriteBandwidthMax=/dev/sda 100M
StartupCPUShares=2048   # 启动时最高优先级

# /etc/systemd/system/batch.service
[Service]
Slice=batch.slice
CPUQuota=300%           # 最多 3 核
CPUWeight=100           # 低权重
MemoryMax=8G
MemoryHigh=6G
IOWeight=100            # 低 I/O 带宽
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 30M
AllowedCPUs=4-7         # 绑定到核心 4-7(与 DB 核心隔离)

# 通过 cpu.cfs_period_us 动态调整活跃时段
# 夜间 21:00-06:00 可放宽 batch CPU 配额
[Unit]
ConditionTime>=21:00
ConditionTime<06:00

10.2 案例二:Java 应用的精细内存控制

Java 进程的 JVM 堆 + 堆外内存需要特别关注:

# 通过 cgroup 确保 JVM 总内存(堆 + 元空间 + 线程栈 + 直接内存)安全
docker run -d \
  --name order-service \
  --memory="4g" \                    # 容器总内存限制
  --memory-swap="4g" \               # 禁用 swap
  --oom-kill-disable \                # 手动管理 OOM(通过 JVM 参数)
  --memory-swappiness=0 \            # 禁用匿名页 swap
  -XX:+UseContainerSupport \          # 识别 cgroup 限制
  -XX:MaxRAMPercentage=75.0 \         # JVM 栈利用容器内存的 75%
  -XX:+ExitOnOutOfMemoryError \       # OOM 时退出容器重启
  --tmpfs /tmp:size=256m \            # 限制临时文件
  myapp:latest

# 监控容器内存使用
docker stats --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" order-service

# 通过 cgroup 查看 JVM 线程内存
cat /sys/fs/cgroup/memory/docker/*order-service*/memory.stat

10.3 案例三:突发流量下的 CPU 弹性分配

电商大促期间,需要根据流量动态调整各服务的 CPU 配额:

#!/bin/bash
# dynamic_cpu_tune.sh - 根据负载动态调整 CPU 配额
# 在 Kubernetes 中对应 VPA 或 HPA,裸机部署时通过脚本实现

PROM_URL="http:9090"

while true; do
    # 获取 API 网关延迟
    LATENCY=$(curl -s "${PROM_URL}/api/v1/query?query=http_request_duration_seconds" \
              | jq -r '.data.result[0].value[1]')
    
    if (( $(echo "$LATENCY > 0.5" | bc -l) )); then
        # 延迟超过 500ms,扩容 API 服务的 CPU 配额
        echo 400000 > /sys/fs/cgroup/cpu/apigw/cpu.cfs_quota_us
        echo "[$(date)] API GW CPU expanded to 4 cores" >> /var/log/cgroup_tune.log
    elif (( $(echo "$LATENCY < 0.1" | bc -l) )); then
        # 延迟低,恢复默认配额
        echo 200000 > /sys/fs/cgroup/cpu/apigw/cpu.cfs_quota_us
    fi
    
    sleep 30
done

十一、cgroup 调试与监控工具集

11.1 核心诊断命令速查

# 查看进程所属 cgroup
cat /proc/$PID/cgroup

# 查看所有 cgroup 中的进程
systemd-cgls

# 实时监视资源使用情况
systemd-cgtop

# 查看 CPU 配额实际值
cat /sys/fs/cgroup/cpu/cgroup/cpu.cfs_quota_us
cat /sys/fs/cgroup/cpu/cgroup/cpu.cfs_period_us

# 查看内存统计(含页缓存细分)
cat /sys/fs/cgroup/memory/cgroup/memory.stat

# 查看 I/O 使用量
cat /sys/fs/cgroup/blkio/cgroup/blkio.throttle.io_service_bytes

# 查看 cgroup v2 控制器
cat /sys/fs/cgroup/cgroup.subtree_control

11.2 使用 bpftrace 跟踪 cgroup 事件

# 跟踪进程迁移到不同 cgroup
bpftrace -e 'tracepoint:cgroup:cgroup_transfer_tasks {
    printf("task %s moved to cgroup %s at %d\n", comm, args->dst_path, nsecs);
}'

# 跟踪 OOM Killer 触发
bpftrace -e 'kprobe:oom_kill_process {
    printf("OOM: cgroup %s pid %d comm %s\n", comm, pid, arg0);
}'

# 跟踪 CFS 带宽配额耗尽
bpftrace -e 'tracepoint:sched:stat_runtime {
    printf("runtime: %dms throttled: %dms\n", args->runtime, args->runtime - args->time);
}'

11.3 Prometheus 监控集成

结合 cgroup_exporter 或 node_exporter 的 cgroup collector,实现容器资源的可观测:

# cgroup_exporter 提供的指标示例
container_cpu_cfs_throttled_seconds_total
container_cpu_cfs_periods_total
container_memory_usage_bytes
container_memory_working_set_bytes
container_memory_failcnt  # OOM 计数
container_blkio_device_usage_total_bytes

十二、cgroups 最佳实践与避坑指南

关键原则:

  1. 生产环境优先使用 cgroups v2(kernel 5.8+),统一层级架构更简洁,PSI 提供更精准的压力指标
  2. request 与 limit 的差异:只设 limit 不设 request 会导致 CPU shares 极低(仅 2),在资源竞争时几乎无法获得 CPU 时间
  3. cpuset 与 numa 亲和:HPC 和数据库场景必须配置 cpuset 绑定核心 + NUMA 内存节点,避免跨 NUMA 访问
  4. IO 优先使用权重而非硬限:blkio.weight 的弹性分配比 throttling 更高效,硬限导致 I/O 峰值排队反而降低吞吐
  5. 关注 OOM 关键路径:高优先级服务设置 oom_score_adj=-1000 避免被 OOM Killer 击杀
  6. K8s 资源碎片化:合理配置 request/limit 比例,避免节点资源碎片导致 Pod 无法调度

常见陷阱速查

陷阱后果解决方案
limit 远高于 node capacityPod 实际 CPU 被硬限但无感知设置合理 limit + 监控 throttle
memory limit = JVM Xmx容器额外内存超限触发 OOM留出堆外空间 20-30% 余量
cpuset 与默认 cgroup 重叠隔离形同虚设设置 exclusive 标志或缩小默认组核心
大量进程频繁创建/销毁cgroup.procs 写入延迟累积使用进程池或线程化模型
v1 blkio 仅限特定设备限制无效(需要正确主次设备号)先 ls -l /dev/sd* 确认设备号

十三、总结与演进趋势

cgroups 从 Linux 2.6.24(2008 年)引入至今,已从简单的资源计数器演进为容器生态的基石。未来发展方向:

  • cgroups v2 全面普及:统一架构逐步替代 v1,PSI 成为标准压力指标
  • RDMA 和 GPU 控制器:新兴硬件资源的 cgroup 化支持
  • 与 eBPF 深度融合:通过 BPF 程序在内核层直接修改 cgroup 行为
  • 虚拟化和机密计算:cgroup 扩展到虚拟化层(virtio-cgroup)

掌握 cgroups 不仅是理解容器技术的前提,更是 Linux 系统调优的必备技能。从 CPU 调度权重到内存 OOM 控制,从块设备 I/O 限速到网络 QoS 标记,cgroups 提供了完整的资源隔离方案,而 systemd 和容器编排框架则将这些能力封装为声明式的 API,让基础设施资源管理进入云原生的精细化时代。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部