4、CFS调度器原理:Linux完全公平调度器(CFS)在容器中的工作方式,CFS配额与周期

4.1 CFS调度器概述

Linux 完全公平调度器(Completely Fair Scheduler, CFS)自内核 2.6.23 起成为默认调度器,也是 Kubernetes 容器运行时(如 containerd、CRI-O)底层依赖的 CPU 分配引擎。CFS 的核心思想是:为每个可运行线程分配一个“虚拟运行时间”(vruntime),调度器总是选择 vruntime 最小的线程运行,从而在宏观上实现所有线程对 CPU 的公平共享。

在容器场景下,CFS 通过 CPU 组调度(CFS Group Scheduling) 和 带宽控制(CFS Bandwidth Control) 两个机制,将容器的 CPU 请求(requests)和限制(limits)转化为内核可执行的调度参数。

关键概念速览:
  • vruntime:线程实际运行时间的加权值,权重由线程优先级(nice 值)决定。
  • 调度周期(period):CFS 重新分配 CPU 时间的时间窗口,默认 100ms。
  • 配额(quota):在一个调度周期内,容器组(cgroup)允许运行的最大 CPU 时间。
  • CFS 组调度:将容器视为一个调度实体,在组内公平分配 CPU 时间。

4.2 CFS 在容器中的工作方式

Kubernetes 通过 cgroup v1 或 v2 的 cpu 子系统控制容器的 CPU 使用。当 Pod 指定了 resources.requests.cpu 和 resources.limits.cpu 后,kubelet 会将其转换为以下 CFS 参数写入 cgroup:

Kubernetes 资源字段 CFS 内核参数 说明
requests.cpu cpu.shares (cgroup v1)
cpu.weight (cgroup v2)
相对权重,决定 CPU 争用时的时间分配比例。例如 2 核 = 2048 shares。
limits.cpu cpu.cfs_period_us
cpu.cfs_quota_us
硬限制:在 cfs_period_us 微秒内,容器最多运行 cfs_quota_us 微秒。

当容器内的线程被调度时,CFS 会执行以下步骤:

  1. 入队:线程进入 CFS 红黑树,vruntime 初始化为当前 cgroup 的 vruntime。
  2. 选择:调度器从红黑树中取出 vruntime 最小的线程(即“最饥饿”的线程)。
  3. 运行:线程在 CPU 上执行,同时其 vruntime 按权重比例递增。
  4. 检查配额:在每个调度周期结束时,CFS 带宽控制器检查该 cgroup 的累计运行时间是否超过 quota。若超过,则将该 cgroup 内的所有线程节流(throttle),直到下一个周期开始。
  5. 重新入队:线程被抢占或时间片用完后,重新插入红黑树,等待下一次调度。
实践要点: 容器内的线程并非直接与物理 CPU 绑定,而是与 cgroup 的调度实体绑定。这意味着即使容器内只有一个线程,CFS 也会将其视为一个组调度实体,受 quota/period 限制。

4.3 CFS 配额与周期:核心参数详解

CFS 带宽控制通过两个关键参数实现 CPU 硬限制:

  • cpu.cfs_period_us:调度周期,单位微秒(μs)。默认值 100000(即 100ms)。取值范围通常为 1ms 到 1s。
  • cpu.cfs_quota_us:在一个周期内,该 cgroup 允许运行的总 CPU 时间,单位微秒。若设置为 -1,表示不限制。

容器的 CPU 限制核数计算公式为:

CPU 限制(核数) = cpu.cfs_quota_us / cpu.cfs_period_us

例如:quota = 200000, period = 100000 → 2 核
例如:quota = 50000, period = 100000 → 0.5 核

下表展示了常见 Kubernetes CPU 限制与 CFS 参数的对应关系:

K8s CPU limit cfs_period_us cfs_quota_us 等效物理核数 节流风险
100m 100000 10000 0.1 高(周期内仅 10ms 可用)
500m 100000 50000 0.5 中
1 100000 100000 1.0 低(满周期可用)
2 100000 200000 2.0 低(需双核并行)
4 100000 400000 4.0 低(需四核并行)

4.4 CFS 组调度与容器隔离

在 Kubernetes 节点上,每个 Pod 对应一个 cgroup 目录树。CFS 组调度确保:

  • Pod 级隔离:不同 Pod 的容器属于不同的 cgroup 组,CFS 在组间按 cpu.shares 分配 CPU。
  • 容器级隔离:Pod 内的多个容器共享 Pod 的 cgroup 配额,但各自有独立的线程调度实体。
  • QoS 优先级:Guaranteed Pod 的 shares 值较高,Burstable 次之,BestEffort 最低。CFS 在 CPU 争用时优先满足高 shares 的组。
常见误区: 设置 limits.cpu = 2 并不意味着容器独占 2 个物理核。CFS 仍然允许其他容器在空闲时使用这些核,只是当容器需要时,CFS 保证其在一个周期内最多获得 2 核的 CPU 时间。真正的核绑定需使用 cpu pinning(如 CPU Manager Policy)。

4.5 CFS 调度流程与配额检查机制

下图展示了 CFS 在容器环境下的完整调度流程,从线程就绪到配额节流:

CFS 容器调度与配额检查流程 线程就绪(可运行) CFS 红黑树选择 vruntime 最小线程 运行线程,更新 vruntime 周期结束?检查 cfs_quota_us 未超配额 继续运行,重置周期计时 超配额 线程节流(Throttled) 等待下一周期

4.6 配额与周期的性能影响

CFS 的 period 和 quota 设置直接影响容器的 CPU 利用率和响应延迟:

参数调整 优势 劣势 适用场景
减小 period(如 10ms) 降低节流延迟,提高响应速度 增加调度开销,CPU 利用率下降 延迟敏感型应用(如 Web 服务器、API 网关)
增大 period(如 500ms) 减少调度开销,提高吞吐 节流延迟增大,可能导致突发延迟尖刺 批处理任务、离线计算
quota 接近 period(如 1 核 = 100ms/100ms) 容器可连续运行,几乎无节流 CPU 争用时公平性下降 Guaranteed QoS Pod
quota 远小于 period(如 0.1 核 = 10ms/100ms) 严格限制 CPU 使用 频繁节流,上下文切换开销大 限制后台任务、sidecar 容器

4.7 查看 CFS 配额与节流状态

在生产环境中,可以通过以下命令检查容器的 CFS 参数和节流统计:

# 查看容器 cgroup 的 CFS 参数(cgroup v1)
cat /sys/fs/cgroup/cpu/kubepods/burstable/pod<pod-uid>/cpu.cfs_period_us
cat /sys/fs/cgroup/cpu/kubepods/burstable/pod<pod-uid>/cpu.cfs_quota_us

# 查看节流统计
cat /sys/fs/cgroup/cpu/kubepods/burstable/pod<pod-uid>/cpu.stat
# 输出示例:
# nr_periods 1000        # 总周期数
# nr_throttled 50        # 被节流的周期数
# throttled_time 1234567 # 被节流的总时间(微秒)
调优指标: 若 nr_throttled / nr_periods > 5%,说明容器频繁被节流,应考虑增大 CPU limit 或优化应用并发模型。若 throttled_time 持续增长,表明 CPU 限制过紧。

4.8 CFS 与 CPU Manager 的协同

当节点启用 CPU Manager(--cpu-manager-policy=static)时,Guaranteed Pod 的容器可以独占物理 CPU 核心。此时 CFS 的行为发生变化:

  • 独占核心:容器内的线程绑定到固定的物理核,CFS 不再在这些核上进行组间调度。
  • CFS 退化为组内调度:独占核上的 CFS 仅负责容器内多个线程之间的公平调度,不再受 quota/period 限制(quota 被设置为 -1)。
  • 性能优势:消除 CFS 节流抖动,适合对 CPU 延迟极度敏感的应用(如 DPDK、实时数据库)。
注意: 即使使用 CPU Manager,CFS 的 cpu.shares 仍然影响非独占核心的分配。混合使用独占核和共享核时,需仔细规划 Pod 的 QoS 等级和 CPU 请求。

4.9 本章小结

CFS 调度器是 Kubernetes CPU 资源管理的底层基石。理解其工作原理——特别是 quota 与 period 的配合机制、组调度隔离、以及节流产生的条件——是进行 CPU 容量配额调优的前提。在实际调优中,应结合应用的延迟敏感度和 CPU 利用率特征,合理选择 period 大小和 quota 比例,必要时借助 CPU Manager 实现核级隔离。