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%。
CPU 份额分配原理:CFS 调度组权重计算 容器 A shares=1024 容器 B shares=2048 容器 C shares=512 CFS 调度器(红黑树) vruntime 排序 → 选择最小 vruntime 调度实体 CPU 时间分配结果(争抢时) A: 28.6% | B: 57.1% | C: 14.3% 比例 = 1024 : 2048 : 512 = 1 : 2 : 0.5

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,两者配合使用效果最佳。