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
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部