3. Linux CPU 调度器基础:CFS、调度域与 CPU 亲和性

在深入 containerd 的 CPU 管理策略之前,必须理解底层 Linux 内核的 CPU 调度机制。容器本质上是进程组,其 CPU 资源分配完全由内核调度器决定。本章聚焦于完全公平调度器(CFS)、调度域与调度组,以及 CPU 亲和性(affinity)这三个核心概念,它们是后续绑核调优的理论基石。

3.1 完全公平调度器(CFS)

CFS(Completely Fair Scheduler)自 Linux 2.6.23 起成为默认调度器,其设计哲学是“让每个进程都能公平地获得 CPU 时间”。CFS 摒弃了传统的时间片轮转,引入虚拟运行时间(vruntime)概念。

  • 虚拟运行时间(vruntime):每个进程的 vruntime 根据其实际运行时间和优先级加权计算。CFS 总是选择 vruntime 最小的进程来运行,确保所有进程在宏观上获得等比例的 CPU 时间。
  • 红黑树(Red-Black Tree):CFS 使用红黑树来组织就绪队列。vruntime 作为键值,树的最左节点即为下一个要运行的进程。插入和查找操作的时间复杂度为 O(log n),高效支持大量进程。
  • 调度周期与最小粒度:CFS 通过 sched_latency_ns(调度延迟)和 sched_min_granularity_ns(最小粒度)控制调度频率。默认调度延迟为 6ms,最小粒度为 0.75ms,确保交互式进程的响应性。
关键指标: CFS 的公平性并非平均,而是按权重(nice 值)分配。nice 值每增减 1,CPU 时间权重变化约 10%。容器场景下,cgroup 的 cpu.shares 参数直接映射到 CFS 权重。
容器关联: containerd 通过 cgroup v2 的 cpu.weight 接口控制容器的 CFS 权重。当多个容器争抢 CPU 时,CFS 根据权重比例分配时间片,而非固定时间片。

3.2 调度域与调度组

现代服务器通常采用 NUMA(非统一内存访问)架构,CPU 和内存被划分为多个节点。调度域(Scheduling Domain)是内核为管理 NUMA 拓扑而引入的层级结构,调度组(Scheduling Group)则是域内的基本调度单元。

层级 名称 说明 负载均衡范围
L0 SMT(超线程) 同一物理核上的多个逻辑核 核内均衡
L1 MC(多核) 同一 CPU 封装内的所有物理核 核间均衡
L2 NUMA 同一 NUMA 节点内的所有 CPU 节点内均衡
L3 NUMA(跨节点) 跨 NUMA 节点的所有 CPU 全局均衡

调度域的核心作用是实现负载均衡。内核定期在域内各调度组之间迁移进程,使各组负载趋于均衡。每个调度组包含一组 CPU,组内进程共享该组资源。容器绑核时,若未指定 NUMA 节点,调度器可能将容器进程迁移到远端节点,导致内存访问延迟增加。

性能陷阱: 在 NUMA 架构下,跨节点内存访问延迟比本地节点高 1.5~2 倍。容器绑核时,必须同时考虑 CPU 亲和性和内存分配策略(如 numactl --membind),否则绑核收益可能被跨节点内存访问抵消。

3.3 CPU 亲和性(Affinity)

CPU 亲和性决定进程可以在哪些 CPU 上运行。Linux 提供两种亲和性控制机制:软亲和性(通过调度器倾向性)和硬亲和性(通过 CPU 掩码强制绑定)。

  • 软亲和性(Soft Affinity):调度器倾向于将进程留在上次运行的 CPU 上,利用 CPU 缓存热度。这是默认行为,但调度器可能因负载均衡而迁移进程。
  • 硬亲和性(Hard Affinity):通过 sched_setaffinity() 系统调用设置 CPU 掩码(cpumask),强制进程只能在指定 CPU 上运行。容器绑核即采用此机制。

CPU 掩码通常以十六进制表示,例如 0x0F 表示 CPU 0~3。在 containerd 中,绑核通过 cpuset cgroup 控制器实现,其核心文件为 cpuset.cpus 和 cpuset.mems。

# 查看进程当前 CPU 亲和性
taskset -p <PID>

# 设置进程绑定到 CPU 0-3
taskset -p 0x0F <PID>

# 通过 cgroup v2 设置容器绑核
echo "0-3" > /sys/fs/cgroup/<container-path>/cpuset.cpus
echo "0" > /sys/fs/cgroup/<container-path>/cpuset.mems
绑核原则: 对于延迟敏感型容器(如 Redis、Nginx),建议绑定到同一 NUMA 节点内的物理核,避免跨节点访问。对于计算密集型容器,可绑定到连续物理核,减少核间缓存竞争。

3.4 核心概念关系图

下图展示了 CFS、调度域与 CPU 亲和性在容器调度中的协作关系:

Linux CPU 调度核心概念关系图 容器进程组 CFS 完全公平调度器 vruntime + 红黑树 调度域与调度组 NUMA 层级负载均衡 权重映射 负载均衡 CPU 亲和性 (Affinity) 硬绑定 / 软倾向 调度决策 cpuset cgroup cpuset.cpus / cpuset.mems 内核接口 物理 CPU 核 NUMA 节点拓扑 绑定 说明: 容器通过 cgroup 的 cpu.weight 向 CFS 声明权重,CFS 根据 vruntime 调度进程到 CPU。 调度域在 NUMA 层级间做负载均衡,而 CPU 亲和性通过 cpuset 强制绑定到指定物理核。

3.5 调度域拓扑示例

以下 SVG 展示了一个双路 NUMA 服务器的调度域层级结构,帮助理解容器绑核时的拓扑选择:

双路 NUMA 服务器调度域拓扑 NUMA Node 0 (CPU 0-7) 物理核 0-3 L1/L2 缓存共享 物理核 4-7 L1/L2 缓存共享 L3 缓存 (共享) 本地内存控制器 NUMA Node 1 (CPU 8-15) 物理核 8-11 L1/L2 缓存共享 物理核 12-15 L1/L2 缓存共享 L3 缓存 (共享) 本地内存控制器 QPI/UPI 互联 绑核建议:容器绑定到同一 NUMA 节点内的物理核(如 CPU 0-3),避免跨节点内存访问。containerd 的 cpuset 策略应优先选择同一节点。

3.6 关键参数速查表

参数/接口 所属层级 作用 容器场景示例
cpu.weight cgroup v2 设置容器 CFS 权重 echo 100 > cpu.weight
cpuset.cpus cgroup v2 强制绑定容器到指定 CPU echo "0-3" > cpuset.cpus
cpuset.mems cgroup v2 指定容器内存分配 NUMA 节点 echo "0" > cpuset.mems
sched_setaffinity() 系统调用 设置进程硬亲和性 taskset 工具底层实现
/proc/cpuinfo 内核接口 查看 CPU 拓扑信息 grep "physical id" /proc/cpuinfo
实践建议: 在 containerd 配置中,通过 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] 下的 SystemdCgroup = true 启用 cgroup v2,然后利用 Kubernetes 的 cpuManagerPolicy 或直接操作 cpuset 实现绑核。