Linux 内核 sched_domain 深度工程实战:从 CPU 拓扑感知到全栈负载均衡调优
本文聚焦 Linux 内核调度器中 `sched_domain` 子系统的核心机制,从 CPU 拓扑发现到多级调度域构建,再到负载均衡算法的工程实现,最终给出在 AI 推理、DPDK 和实时系统三大典型场景下的生产级调优方案。
一、为什么需要 sched_domain
现代服务器 CPU 拓扑异常复杂:多路 NUMA 节点、超大 LLC 共享域、混合架构(P-core/E-core / big.LITTLE)、不同层级缓存拓扑。调度器如果全量遍历所有 CPU 选择任务放置目标,在 256 核甚至 512 核机器上将成为灾难。
sched_domain 是 Linux 内核为建模这种层级化 CPU 拓扑而引入的核心数据结构,它将 CPU 组织成树状层次(DIE → MC → NUMA),每层只关注局部拓扑关系,避免全局遍历。
// kernel/sched/sched.h
struct sched_domain {
/* 由子域连接的域组 */
struct sched_domain *parent; /* 上层调度域 */
struct sched_domain *child; /* 下层调度域 */
struct sched_group *groups; /* 域内的调度组链表 */
unsigned long min_interval; /* 最小平衡间隔 */
unsigned long max_interval; /* 最大平衡间隔 */
unsigned int busy_factor; /* 繁忙因子 */
unsigned int imbalance_pct; /* 不平衡阈值 */
unsigned int nr_balance_failed; /* 平衡失败计数 */
unsigned int cache_nice_tries; /* 缓存亲和尝试次数 */
int flags; /* 调度域特性标志 */
int level; /* 调度域层级 */
/* 关键统计 */
unsigned long last_decay_max_lb_cost;
unsigned int nr_migrate_deferred;
u64 max_newidle_lb_cost; /* 空闲平衡最大开销 */
};
二、CPU 拓扑发现与调度域构建
2.1 ACPI 与设备树
内核在启动阶段通过 ACPI MADT 表(x86)或设备树(ARM64)收集 CPU topology,填充 cpu_topology[] 数组。以 ARM64 为例:
// arch/arm64/kernel/cpu_topology.c
const struct cpu_topology cpu_topology[NR_CPUS];
static void fetch_cpu_topology(int cpu)
{
// 从 PPTT (Processor Properties Topology Table) 解析
thread_id = cpuid & 0xff;
core_id = (cpuid >> 8) & 0xff;
socket_id = (cpuid >> 16) & 0xff;
cluster_id = (cpuid >> 24) & 0xff;
}
拓扑信息最终决定 sched_domain 的层次结构。Linux 定义了 5 种顶层标志:
| 标志 | 含义 | 典型场景 |
|---|---|---|
| `SD_SHARE_CPUCAP` | 共享 CPU 计算能力 | 同构多核 |
| `SD_SHARE_PKG_RESOURCES` | 共享封装级资源(LLC/缓存) | 同 Socket 内 |
| `SD_NUMA` | NUMA 节点边界 | 跨 Socket |
| `SD_ASYM_CPUCAP` | CPU 能力不对称 | 混合架构 P/E core |
| `SD_ASYM_PACKING` | 不对称打包调度 | 异构首选高性能核 |
2.2 调度域层次结构构建流程
// kernel/sched/topology.c
static struct sched_domain *
sd_init(struct sched_domain_topology_level *tl, const struct cpumask *cpu_map)
{
struct sched_domain *sd = sd_alloc(tl->level, cpu_map);
// 根据拓扑标志设置调度域参数
sd->flags = tl->sd_flags;
sd->level = tl->level;
// 计算平衡间隔(与域内 CPU 数成正比)
sd->min_interval = sd_decay(tl->sd_flags) ? HZ/10 : 1;
sd->max_interval = assume_immobile ? HZ/2 : HZ;
// 不平衡阈值(防止抖动性迁移)
sd->imbalance_pct = 125; // 默认 125%
return sd;
}
以一台 2 路 AMD EPYC 9754(128C/256T × 2)为例,内核构建的 sched_domain 层次:
Level 3: NUMA (2 个域,每个 Socket 一个)
└── Level 2: DIE (每个 Socket 内 CCD 域)
└── Level 1: MC (每个 CCX/CCD,L3 共享域)
└── Level 0: DIE (最底层,物理核心域)
三、核心负载均衡算法
3.1 periodic load_balance
每个 CPU 在 tick 到达时,从最底层调度域向上逐级检查平衡条件:
// kernel/sched/fair.c: 10000+
static void run_rebalance_domains(struct softirq_action *h)
{
struct rq *this_rq = this_rq();
int cpu = smp_processor_id();
unsigned long interval;
struct sched_domain *sd;
// 从最低层调度域向上遍历 rcu 域
for_each_domain(cpu, sd) {
interval = sd->balance_interval;
if (time_after_eq(jiffies, sd->last_balance + interval)) {
if (load_balance(cpu, this_rq, sd, CPU_IDLE)) {
/* 找到可迁移的任务 */
break;
}
sd->last_balance = jiffies;
}
}
}
关键 load_balance() 核心逻辑(简化版):
static int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_idle_type idle)
{
struct sched_group *group, *busiest;
unsigned long imbalance;
/* 1. 查找域内最繁忙的调度组 */
update_sd_lb_stats(sd, &local_group, &preferred_busiest,
&todo_llc, &stats);
group = sched_group_first(sd);
busiest = group;
for_each_sd_groups(sd, group) {
if (sg_plain_overflow(group, busiest))
busiest = group;
}
/* 2. 计算不平衡量 */
imbalance = min(
max((avg_load - this_rq->avg_load) >> 1, 0),
sd->imbalance_pct * busiest_load / 100
);
/* 3. 选择迁移任务(优先迁移非 cache-hot 任务) */
list_for_each_entry_reverse(p, &cfs_tasks, se.group_node) {
if (!task_hot(p, sd) && can_migrate_task(p, this_cpu, busiest_cpu)) {
detach_task(p, busiest_rq);
attach_task(this_rq, p);
return 1;
}
}
return 0;
}
3.2 newidle_balance(空闲均衡)
当 CPU 即将进入 idle 状态时,执行比周期性均衡更具侵略性的均衡:
static int newidle_balance(struct rq *this_rq, struct rq_flags *rf)
{
int pulled_task = 0;
struct sched_domain *sd;
rcu_read_lock();
for_each_domain(cpu, sd) {
if (sd->flags & SD_BALANCE_NEWIDLE) {
if (pull_newidle_task(cpu, this_rq, sd)) {
pulled_task++;
/* 只在新空闲层拉取一个任务 */
break;
}
}
}
rcu_read_unlock();
return pulled_task;
}
工程提示: newidle_balance 的开销随域数量线性增长。在大 NUMA 系统上,频繁的新空闲均衡会导致尾延迟抖动。可以通过 kernel.sched_min_granularity_ns 和 sysctl_sched_cfs_bandwidth_slice_us 间接控制均衡频率。
3.3 nohz 均衡与housekeeper
在 CONFIG_NO_HZ_FULL 场景下,运行任务的 CPU 关闭 tick,需要专门的 housekeeper CPU 分担负载均衡工作:
static void nohz_balancer_kick(struct rq *rq)
{
// 当某 CPU 负载超过阈值时被唤醒的 housekeeper
if (!housekeeper_cpu(cpu)) return;
// 扫描 nohz 域内的 CPU 负载情况
// 必要时主动唤醒 idle CPU 参与均衡
rcu_read_lock();
for_each_cpu_and(cpu, nohz.idle_cpus_mask, sched_domain_span(sd)) {
if (cpu != smp_processor_id())
resched_cpu(cpu);
}
rcu_read_unlock();
}
四、混合架构调度与 SD_ASYM_PACKING
Intel 12+ 代酷睿(Alder Lake/Raptor Lake)和 ARM big.LITTLE 的普及催生了不对称调度策略。
4.1 ITMT / Intel Turbo Max Technology
Intel 硬件 ITMT(Intel Turbo Max Technology)通过 MSR(MSR_HWP_CAPABLES)暴露每个核心的"性能优先级",内核据此构建 asym_cap_data:
// arch/x86/kernel/itmt.c
static void __itmt_sched_core_priority(int cpu)
{
// 读取 ITMT 优先级,更新 sched_asym_cpucapacity 排名
asym_cap_data[cpu].prio = msr_prios[cpu];
// HIGHEST_PRIO 的 CPU 具有 SD_ASYM_PACKING 标志
if (asym_cap_data[cpu].prio >= HIGHEST_PRIO)
sched_set_itmt_support(cpu);
}
4.2 ARM64 asym_cpucapacity
ARM64 架构在 cpu_capacity 框架下,选择最大 capacity 的核心作为"大核",在小核空闲时任务优先分配到大核:
// kernel/sched/fair.c
static void update_cpu_capacity(struct sched_domain *sd, int cpu)
{
unsigned long capacity = scale_cpu_capacity(cpu);
struct sched_group *sg = sd->groups;
// 找到域内最大容量
sg->sgc->capacity = max(sg->sgc->capacity, capacity);
sg->sgc->min_capacity = min(sg->sgc->capacity, capacity);
sg->sgc->max_capacity = max(sg->sgc->capacity, capacity);
}
五、NUMA 均衡:打破内存访问瓶颈
5.1 numa_faults 驱动的任务迁移
Linux 4.x 开始引入 Auto NUMA Balancing(通过 kernel.numa_balancing sysctl 控制),依赖于 PMU(性能监控单元)的缺页采样:
// kernel/sched/fair.c: numa 均衡核心
static void task_numa_work(struct callback_head *work)
{
struct task_struct *p = current;
unsigned long migrate, next_scan;
// 扫描任务访问的页面,统计每个 NUMA 节点的缺页率
// 当在某节点的缺页率超过阈值时触发任务迁移
if (numa_faults_accessed(p, preferred_nid) > threshold) {
// 任务迁移 + 页面迁移
migrate_misplaced_pages(p, preferred_nid);
migrate_task_to(p, preferred_nid);
}
}
5.2 生产环境关键参数调优
# 启用 Auto NUMA Balancing(默认开启)
sysctl -w kernel.numa_balancing=1
# NUMA 均衡扫描延迟(毫秒)- 越大开销越低,但响应越慢
sysctl -w kernel.numa_balancing_scan_delay_ms=1000
# 最小扫描周期(毫秒)
sysctl -w kernel.numa_balancing_scan_period_max_ms=60000
六、工程实战:三大典型场景调优
场景一:DPDK / 高频交易——绑核与隔离
# GRUB 参数配置:隔离核心 2~7,关闭 tick
isolcpus=2,3,4,5,6,7 nohz_full=2,3,4,5,6,7 rcu_nocbs=2,3,4,5,6,7
# 效果:核心 2~7 不被调度器分配常规任务
# 配合 taskset 或 cgroup cpuset 进一步隔离
taskset -c 2 ./dpdk_app
深度优化:在 sched_domain 层面,可以通过 sched_setaffinity 让调度器在剩余域内均衡,而隔离核心完全不参与均衡循环,避免均衡开销影响关键路径。
场景二:AI 推理服务(vLLM / TensorRT-LLM)
# Kubernetes Pod 配置
resources:
limits:
cpu: "8"
memory: "8Gi"
nvidia.com/gpu: "1"
---
annotations:
cpu-load-balancing.crio.io: "disable" # 禁用 CFS 负载均衡
cpu-quota.crio.io: "disable"
内核层推荐配置:
# 增大调度域不平衡阈值,避免不必要的跨 NUMA 迁移
sysctl -w kernel.sched_domain.cpu0.domain0.imbalance_pct=110
# 减小 NUMA 均衡频率
sysctl -w kernel.numa_balancing_scan_period_min_ms=100
# 增加 cfs 最小粒度,提升延迟确定性
sysctl -w kernel.sched_min_granularity_ns=10000000
场景三:实时音频/视频处理
# 策略:SCHED_FIFO + 核心亲和 + 关闭均衡
schedtool -F -p 99 -e ./realtime_encoder
taskset -c 4-7 schedtool -F -p 99 -e ./realtime_encoder
# 关闭该核心域的负载均衡
echo 0 > /sys/devices/system/cpu/cpu4/sched_domain/domain*/busy_factor
七、关键调试接口
7.1 /proc/sys/kernel/sched_domain/
# 查看调度域层次和参数
cat /sys/devices/system/cpu/cpu0/sched_domain/domain*/name
# 输出: MC → DIE → NUMA
# 查看具体参数
cat /sys/devices/system/cpu/cpu0/sched_domain/domain1/min_interval
cat /sys/devices/system/cpu/cpu0/sched_domain/domain1/busy_factor
7.2 perf sched(调度器性能分析)
# 记录调度事件
perf sched record -a sleep 10
# 生成调度热力图
perf sched map
# 分析调度延迟
perf sched latency --sort max
# 输出示例:
# Task | Runtime ms | Switches | Average delay ms | Max delay ms |
# ----------------------------------------------|----------------|---------|-----------------|
# vllm:worker-0 | 9850.2 | 42 | 0.30 | 12.50 |
# python:enqueue | 12.8 | 380 | 1.05 | 8.92 |
7.3 ftrace 跟踪调度域均衡
# 跟踪负载均衡事件
echo 1 > /sys/kernel/debug/tracing/events/sched_sched_load_balance/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -50
八、内核演进趋势
Linux 内核在调度域领域持续演进:
- Linux 6.1+: 引入
kernel.sched_deadline_period_max_us替代旧常数,完善了 CFS 与 Deadline 的域级隔离 - Linux 6.6+: 针对 Intel Hybrid (P-core/E-core) 架构,引入 ITMT v3 与调度器深度协同
- Linux 6.9+: 新增
SD_ASYM_PACKING_FULL标志,进一步细化不对称调度策略
最值得关注的是 EEVDF(Earliest Eligible Virtual Deadline First)调度器(Linux 6.6 后默认)对调度域行为的影响:EEVDF 消除了 CFS 运行时间片机制的部分特性,但保留了 sched_domain 的多层均衡框架。
九、总结
sched_domain 是 Linux 内核在多核异构时代最成功的设计抽象之一。它将复杂的全局拓扑问题分解为层次化的局部均衡问题,通过"域-组"模型在 O(depth) 级别完成决策,而非全局 O(n) 搜索。
在实际工程中,sched_domain 的性能取决于三个核心参数:
1. imbalance_pct:越低越激进的均衡策略,越高越保守
2. min_interval:限制均衡频率,避免抖动
3. cache_nice_tries:缓存亲和尝试次数,影响 cache-aware 迁移决策
设计原则是:能用 isolcpus 或 cgroup cpuset 分流,就不要让调度器硬猜。在核心绑定的极端场景下,仍需理解调度域的行为来避免"遮蔽"导致的任务饥饿。
---
参考资料
- Linux 内核源码
kernel/sched/fair.c·kernel/sched/topology.c - Understanding the Linux Kernel, 3rd Edition, Chapter 7
- kernel-doc:
Documentation/scheduler/sched-domains.rst

发表评论 取消回复