4. CPU份额控制:cpu.shares / cpu.weight 的工作原理、相对权重与CPU时间分配、多容器争抢场景分析
CPU份额(Shares)是 Linux 内核提供的 CPU 时间相对权重分配机制,也是容器运行时(Docker/containerd)默认启用的 CPU 约束手段。与绑核(cpuset)的硬隔离不同,份额控制属于软限制——它不保证容器能获得多少 CPU,而是在 CPU 争抢发生时,按权重比例分配 CPU 时间。
4.1 核心概念:从 cpu.shares 到 cpu.weight
Linux 内核通过 CFS(Completely Fair Scheduler)调度组实现 CPU 份额控制。传统接口是 cpu.shares(CFS 配额模式),较新内核引入了 cpu.weight(CFS 带宽控制模式),两者本质相同,只是数值范围和语义略有差异。
| 参数 | 所属子系统 | 默认值 | 有效范围 | 说明 |
|---|---|---|---|---|
cpu.shares |
cpu,cpuacct | 1024 | 2 ~ 262144 | 传统接口,Docker 默认使用 |
cpu.weight |
cpu | 100 | 1 ~ 10000 | 新接口,更直观的权重值 |
映射关系:
cpu.weight = 1 + (cpu.shares - 2) * 9999 / 262142。实际使用中,Docker 的 --cpu-shares 参数直接写入 cpu.shares,而 Kubernetes 的 requests.cpu 会转换为 cpu.shares 值(1 核 = 1024)。
4.2 相对权重与 CPU 时间分配原理
CFS 调度器维护一个红黑树,每个调度实体(se)的虚拟运行时间(vruntime)作为键值。当多个容器(cgroup 组)共享同一颗 CPU 时,调度器按以下公式计算时间片分配:
容器 A 获得的 CPU 时间比例 = shares_A / (shares_A + shares_B + ... + shares_N)
关键特性:
- 仅在有争抢时生效:如果 CPU 空闲,容器可以突破份额限制使用全部 CPU。
- 按比例分配:两个容器分别设置 1024 和 2048,则争抢时获得 1:2 的 CPU 时间。
- 不保证上限:份额只决定相对比例,不限制使用量。
示例: 容器 A(shares=1024)、容器 B(shares=2048)、容器 C(shares=512)在 4 核机器上同时满载运行。当 CPU 完全争抢时,A 获得 1024/(1024+2048+512)=28.6%,B 获得 57.1%,C 获得 14.3%。若只有 A 和 B 运行,则 A 获得 33.3%,B 获得 66.7%。
4.3 多容器争抢场景分析
份额控制在实际生产环境中表现出多种行为模式,理解这些场景有助于正确配置容器 CPU 资源。
场景一:CPU 空闲 → 无争抢
- 现象:即使容器 shares 很低,也能使用全部 CPU 核心。
- 原因:CFS 只在就绪队列中有多个调度实体时才进行比例分配。
- 启示:份额不能替代
cpu.cfs_quota_us做硬限制。
场景二:CPU 满载 → 严格按比例分配
- 现象:所有容器 CPU 使用率之和接近 100% × 核心数。
- 原因:调度器严格按 shares 比例分配时间片。
- 启示:合理设置 shares 可保证关键业务容器获得更多 CPU。
场景三:部分容器空闲 → 剩余容器抢占空闲份额
- 现象:活跃容器获得超过其 shares 比例的 CPU。
- 原因:空闲容器不参与调度,其份额被其他容器按比例重新分配。
- 启示:份额是“软保证”,不是“硬上限”。
场景四:混合负载 → 突发与稳态并存
- 现象:批处理任务(高 CPU 突发)与在线服务(低延迟要求)共存。
- 风险:批处理容器可能抢占在线服务的 CPU 时间,导致延迟抖动。
- 对策:结合
cpu.cfs_quota_us限制突发上限,或使用 CPU 绑核隔离。
常见误区: 不要将 shares 理解为“预留 CPU 资源”。shares=2048 不意味着容器一定能获得 2 核 CPU,它只表示在争抢时获得其他容器两倍的时间。真正的预留需要配合 cpuset 或 quota 实现。
4.4 实践配置与验证
Docker 和 Kubernetes 对 CPU 份额的配置方式如下:
| 平台 | 配置方式 | 底层映射 |
|---|---|---|
| Docker | --cpu-shares 2048 |
写入 cpu.shares = 2048 |
| Kubernetes | requests.cpu: 2 |
写入 cpu.shares = 2048(1 核 = 1024) |
| containerd | Linux.Resources.CPU.Shares |
直接设置 cgroup v1 cpu.shares |
| cgroup v2 | cpu.weight |
自动转换,weight = shares / 10.24 |
验证命令(cgroup v1):
# 查看容器 A 的 CPU 份额
cat /sys/fs/cgroup/cpu/docker/<container_id>/cpu.shares
# 输出:1024
# 查看容器 B 的 CPU 份额
cat /sys/fs/cgroup/cpu/docker/<container_id>/cpu.shares
# 输出:2048
# 使用 stress 工具模拟满载,观察 CPU 分配
docker run -d --cpu-shares 1024 --name stress_a stress --cpu 4
docker run -d --cpu-shares 2048 --name stress_b stress --cpu 4
# 通过 top 或 htop 观察 CPU 使用率比例接近 1:2
4.5 份额控制的局限性
尽管份额控制是容器 CPU 管理的基础,但它存在以下不足:
- 无硬上限:无法防止单个容器耗尽所有 CPU。
- 延迟不可控:高优先级容器仍可能因调度延迟产生抖动。
- NUMA 不感知:份额分配不感知内存访问延迟,跨 NUMA 节点调度可能降低性能。
- 与 cpuset 冲突:同时使用 shares 和 cpuset 时,shares 只在绑核集合内生效。
最佳实践: 对于延迟敏感型应用(如 Redis、Nginx),建议使用
cpuset 绑核 + cpu.cfs_quota_us 硬限制的组合;对于批处理任务或非关键业务,使用 cpu.shares 做软隔离即可。Kubernetes 中 requests.cpu 对应 shares,limits.cpu 对应 quota,两者配合使用效果最佳。