技术博客

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 精确控制)。

Dockercgroup资源管理性能容器配额生产运维

容器的资源管理不只是“限制 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 决策更准确,不会误杀文件缓存占用大但实际压力不高的容器。