一、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 v1 | cgroups 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=requests | cpu.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 最佳实践与避坑指南
关键原则:
- 生产环境优先使用 cgroups v2(kernel 5.8+),统一层级架构更简洁,PSI 提供更精准的压力指标
- request 与 limit 的差异:只设 limit 不设 request 会导致 CPU shares 极低(仅 2),在资源竞争时几乎无法获得 CPU 时间
- cpuset 与 numa 亲和:HPC 和数据库场景必须配置 cpuset 绑定核心 + NUMA 内存节点,避免跨 NUMA 访问
- IO 优先使用权重而非硬限:blkio.weight 的弹性分配比 throttling 更高效,硬限导致 I/O 峰值排队反而降低吞吐
- 关注 OOM 关键路径:高优先级服务设置 oom_score_adj=-1000 避免被 OOM Killer 击杀
- K8s 资源碎片化:合理配置 request/limit 比例,避免节点资源碎片导致 Pod 无法调度
常见陷阱速查
| 陷阱 | 后果 | 解决方案 |
|---|---|---|
| limit 远高于 node capacity | Pod 实际 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,让基础设施资源管理进入云原生的精细化时代。

发表评论 取消回复