Docker 资源管理进阶:cgroup v2、资源配额与优先级管理
深入讲解 Docker 容器资源管理的进阶知识:cgroup v2 与 v1 的差异及切换方法(systemd 统一层级管理)、CPU 资源配额的三种方式(--cpus/--cpu-period/--cpu-shares 优先级调度)、内存硬限制与软限制的区别(memory vs memory-reservation)、Block I/O 限速(--device-read-bps/--blkio-weight)、生产环境资源规划方法论,以及 cgroup v2 的新特性(PSI 压力感知调度、内存 oomkill 精确控制)。
容器的资源管理不只是“限制 CPU 和内存”这么简单。合理的资源配额可以让宿主机上的多个容器互不干扰;错误的配置会导致关键服务在业务高峰时被低优先级任务抢光 CPU,或者因为配额太低而频繁 OOM。本文深入讲解 Docker 资源管理的底层机制和生产最佳实践。
资源管理的底层:cgroup
Docker 的资源限制依赖 Linux cgroup(Control Groups)实现。cgroup 负责将进程组织成层级结构,并对每组设置资源配额。
宿主机
├── system.slice(系统服务)
│ └── docker.service(Docker daemon)
│ ├── container_A(CPU: 2核, 内存: 1GB)
│ ├── container_B(CPU: 0.5核, 内存: 256MB)
│ └── container_C(无限制)
└── user.slice(用户进程)
一、cgroup v1 vs cgroup v2
版本确认
# 检查系统使用的 cgroup 版本
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 ← v2(现代系统默认)
# tmpfs on /sys/fs/cgroup type tmpfs ← v1
# 或者
stat -f /sys/fs/cgroup | grep Type
# Type: cgroup2fs ← v2
# Docker 确认
docker info | grep -i cgroup
# Cgroup Driver: systemd ← 推荐(与 systemd 集成)
# Cgroup Version: 2
v1 vs v2 主要差异
特性 cgroup v1 cgroup v2
────────────────────────────────────────────────────────────────
层级结构 每个 subsystem 独立树 统一层级(单树)
CPU 控制 cpu, cpuacct 分开 cpu 统一
内存统计 memory subsystem memory.stat(更精确)
IO 控制 blkio subsystem io subsystem(更完整)
PSI 支持 无 有(压力感知调度)
内存 oomkill 控制 粗粒度 精确到进程
Rootless Docker 支持差 原生支持
切换建议:
RHEL 9 / Ubuntu 22.04+ 默认 cgroup v2,新部署直接用 v2
RHEL 7/8 默认 v1,升级时评估是否切换
切换到 cgroup v2(RHEL 8)
# 在内核参数中启用 cgroup v2
grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
# 重启生效
reboot
# 验证
cat /proc/cmdline | grep unified_cgroup
二、CPU 资源配额
2.1 三种 CPU 限制方式
# 方式1:--cpus(最直观,内部转换为 quota/period)
docker run -d --cpus=1.5 myapp
# 容器最多使用 1.5 个 CPU 核的算力
# 等价于:--cpu-quota=150000 --cpu-period=100000
# 方式2:--cpu-period + --cpu-quota(精细控制)
# period 默认 100ms(100000 微秒)
# quota 表示每个 period 内最多使用多少微秒的 CPU
docker run -d \
--cpu-period=100000 \ # 100ms 一个调度周期
--cpu-quota=50000 \ # 每 100ms 最多用 50ms CPU = 50% 的一个核
myapp
# 方式3:--cpu-shares(相对权重,只在 CPU 竞争时生效)
docker run -d --cpu-shares=512 myapp-low # 默认值 1024
docker run -d --cpu-shares=2048 myapp-high # 是 low 的 4 倍优先级
# 三种方式的区别
# --cpus:绝对上限,即使 CPU 空闲也不会超过该值
# --cpu-quota:和 --cpus 相同
# --cpu-shares:相对权重,CPU 空闲时不限制,只在竞争时按权重分配
2.2 CPU 配额验证
# 查看容器的 cgroup CPU 配额
CONTAINER_ID=$(docker inspect myapp --format '{{.Id}}')
# cgroup v2
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.max
# 150000 100000 ← quota=150000, period=100000 → 1.5 CPU
# cgroup v1
cat /sys/fs/cgroup/cpu/docker/${CONTAINER_ID}/cpu.cfs_quota_us
# 150000
cat /sys/fs/cgroup/cpu/docker/${CONTAINER_ID}/cpu.cfs_period_us
# 100000
# 查看 CPU shares
cat /sys/fs/cgroup/cpu/docker/${CONTAINER_ID}/cpu.shares
# 1024
2.3 CPU 绑核(cpuset)
# 将容器绑定到特定 CPU 核(避免跨核跳跃带来的 cache miss)
docker run -d --cpuset-cpus="0,1" myapp
# 查看系统 CPU 拓扑(确定哪些核属于同一物理核/NUMA 节点)
lscpu
# NUMA node0 CPU(s): 0-11
# NUMA node1 CPU(s): 12-23
# 高性能场景:同一 NUMA 节点的 CPU + 内存
docker run -d \
--cpuset-cpus="0-11" \
--cpuset-mems="0" \
myapp
# 验证绑核
docker exec myapp taskset -cp 1 # 查看 PID 1 的 CPU 亲和性
三、内存资源配额
3.1 硬限制 vs 软限制
# --memory(硬限制):超过立即 OOM Kill
docker run -d --memory=512m myapp
# --memory-reservation(软限制):低内存压力下的目标值
# 宿主机内存充足时,容器可超出 reservation
# 宿主机内存紧张时,内核尝试将容器内存压缩到 reservation
docker run -d \
--memory=1g \ # 硬限制:最多 1GB
--memory-reservation=512m \ # 软限制:期望用 512MB
myapp
# 两者组合含义:
# 正常情况:可使用 512MB ~ 1GB
# 内存紧张时:会被 reclaim 到 512MB
# 超过 1GB:OOM Kill
# --memory-swap(交换空间总量 = 内存 + swap)
docker run -d --memory=512m --memory-swap=512m myapp
# memory-swap = memory:禁止使用 swap(推荐生产设置)
# memory-swap = -1:允许无限 swap(危险!)
# memory-swap > memory:允许使用 (swap - memory) 大小的 swap
# --memory-swappiness(容器使用 swap 的倾向)
docker run -d --memory=512m --memory-swappiness=0 myapp
# 0:尽量不使用 swap(延迟敏感服务推荐)
# 60:默认值
3.2 内存统计(cgroup v2 改进)
# cgroup v2 的内存统计更准确
CONTAINER_ID=$(docker inspect myapp --format '{{.Id}}')
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.stat
# 关键字段
# anon: 匿名内存(进程堆栈、malloc)← 真实内存压力
# file: 文件缓存(可被回收)
# kernel_stack: 内核栈
# slab: 内核 slab 缓存
# Docker stats 显示的 MEM USAGE 包含 file cache
# 真实内存压力应看 anon(RSS)
docker exec myapp cat /sys/fs/cgroup/memory.stat | grep anon
3.3 OOMKill 控制(cgroup v2)
# cgroup v2 支持按 oom_score 精确控制哪个进程先被 OOM Kill
# 调高 OOM 优先级(值越高越先被 kill)
docker exec myapp bash -c "echo 100 > /proc/self/oom_score_adj"
# 调低 OOM 优先级(核心进程保护)
docker exec myapp bash -c "echo -900 > /proc/PID/oom_score_adj"
# -1000:永远不被 OOM Kill(谨慎使用)
四、I/O 资源限制
# 限制磁盘读写速率
docker run -d \
--device-read-bps /dev/sda:100mb \ # 读限速 100MB/s
--device-write-bps /dev/sda:50mb \ # 写限速 50MB/s
--device-read-iops /dev/sda:1000 \ # 读 IOPS 限制
--device-write-iops /dev/sda:500 \ # 写 IOPS 限制
myapp
# 注意:--device-* 只对指定设备生效
# 确认容器使用的是哪个块设备
docker exec myapp df -h / # 查看 / 挂载的设备
# blkio-weight(相对权重,类似 cpu-shares)
docker run -d --blkio-weight=200 myapp-low # 默认 500
docker run -d --blkio-weight=800 myapp-high # 高优先级
# 验证 I/O 限速
docker exec myapp dd if=/dev/zero of=/tmp/test bs=1M count=200 conv=fdatasync
# 应该看到写速度被限制在设置的值附近
五、PSI 压力感知调度(cgroup v2 新特性)
# PSI(Pressure Stall Information):量化 CPU/内存/IO 压力
# cgroup v2 独有特性
CONTAINER_ID=$(docker inspect myapp --format '{{.Id}}')
# 查看内存压力
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.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
# some:至少一个任务因内存等待的时间比例
# full:所有任务都在等待内存的时间比例
# avg10/avg60/avg300:10秒/1分钟/5分钟平均值
# 实际应用:基于 PSI 的资源调度
# kubelet / systemd-oomd 使用 PSI 来决定何时 evict 工作负载
# 比依赖内存使用率更精确(高使用率不一定有压力)
# 查看 IO 压力
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/io.pressure
# 查看 CPU 压力
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.pressure
六、生产资源规划方法论
确定合理的资源配额
# 第一步:无限制运行,观察实际资源使用
docker run -d --name profiling myapp
docker stats profiling --no-stream
# 观察至少24小时,记录:
# - CPU 正常值(p50):30%
# - CPU 峰值(p99):80%
# - 内存稳定值:256MB
# - 内存峰值:380MB
# 第二步:设置配额
# CPU 配额 = 峰值 × 1.2 安全余量(容忍短暂突发)
# 内存硬限制 = 峰值 × 1.5(足够的余量,避免频繁 OOM)
# 内存软限制 = 内存稳定值 × 1.2
docker run -d \
--cpus=1.0 \ # 峰值 0.8 × 1.2 ≈ 1 核
--memory=600m \ # 峰值 380MB × 1.5 ≈ 600MB
--memory-reservation=300m \ # 稳定值 256MB × 1.2 ≈ 300MB
--memory-swap=600m \ # 禁止 swap
myapp
宿主机资源规划
# 宿主机总 CPU = 容器 CPU 之和 × 超卖系数
# 推荐超卖系数:
# 无状态服务(Nginx/API):可超卖到 3-4x(峰值不同时)
# 有状态服务(数据库/缓存):不超卖,1:1
# 批处理任务:可超卖到 5-10x
# 宿主机总内存 = 容器内存之和 × 1.1(预留 10% 给系统)
# 内存不能超卖(超卖导致 OOM Kill 雪崩)
# 示例:32 核 64GB 宿主机规划
# 系统预留:2 核 4GB(给 Docker daemon、监控 agent 等)
# 可用:30 核 60GB
#
# 容器分配(CPU 4x 超卖,内存 1:1):
# 10 个 API 容器:各 1 核 2GB → 10 核(配额) 20GB
# 2 个数据库:各 4 核 8GB → 8 核 16GB
# 5 个 Worker:各 2 核 1GB → 10 核(配额) 5GB
# 总计:28 核配额 41GB(60GB 的 68%,合理)
daemon.json 资源默认值
// /etc/docker/daemon.json
{
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 65535,
"Soft": 65535
}
},
"default-shm-size": "64m",
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}
小结
Docker 资源管理的三个层次:硬限制(超出即 OOM Kill 或 CPU 节流)、软限制(压力下的目标值)、相对权重(竞争时的优先级)。
生产中最重要的原则是:内存必须设硬限制(防止内存泄漏吃光宿主机),CPU 可以超卖但要监控(通过 PSI 或 docker stats 观察实际压力),数据库和缓存类服务不能超卖(延迟敏感)。
cgroup v2 相比 v1 最重要的两个改进:PSI(压力感知)让你能精确感知容器的“真实压力”而不只是“使用量”;统一的内存核算让 OOM 决策更准确,不会误杀文件缓存占用大但实际压力不高的容器。
