简介

在多核 SMP/NUMA 架构服务器、工业嵌入式 Linux 集群、高并发云主机场景中,CPU 负载不均衡是导致系统吞吐下降、调度时延抖动、任务响应延迟的核心诱因。例如某 8 核服务器中,1 个核心满载运行,其余 7 个核心空闲,不仅浪费硬件算力,还会使高负载核心上下文切换频繁、缓存命中率暴跌,直接影响业务稳定性。

Linux 内核作为主流服务器与嵌入式系统核心,自 2.6 版本起引入调度域(sched_domain)调度组(sched_group) 层级化负载均衡架构,核心由load_balance函数驱动,通过find_busiest_group定位最忙调度组、find_busiest_queue锁定最忙 CPU,最终通过move_tasks完成任务跨核迁移,实现 CPU 负载动态均衡。

该机制是 Linux 内核调度子系统的核心能力,支撑着从手机嵌入式芯片到大型 NUMA 服务器的全场景多核调度。对于内核研发工程师、系统性能调优专家、嵌入式 Linux 开发者而言,吃透负载均衡核心流程、find_busiest_group负载计算逻辑、任务detach/attach迁移细节,是解决多核负载不均、优化调度时延、定制化调度策略的必备技能。本文从核心概念、环境搭建、源码逐行剖析、实操验证到问题排查,全链路拆解负载均衡底层实现,可直接用于内核源码研读、性能调优报告撰写、工程项目方案落地。

一、核心概念与术语解析

1.1 调度域(sched_domain)

内核为多核 CPU 构建的层级化负载均衡管理单元,按硬件拓扑分层(从底层到顶层):

  • 基础域(Core Domain):单个 CPU 核心,调度组仅含 1 个 CPU,负责核心内负载均衡;
  • 中层域(DIE/NUMA Domain):同一物理 CPU 插槽 / NUMA 节点内的核心,共享 L3 缓存,调度组含多个 CPU;
  • 顶层域(NUMA/System Domain):整个系统所有 CPU 核心,跨 NUMA 节点,调度组含所有 CPU。

核心结构体(kernel/sched/sched.h):

struct sched_domain {
    struct sched_domain *parent;  /* 父调度域 */
    struct sched_domain *child;   /* 子调度域 */
    struct sched_group *groups;   /* 所属调度组链表 */
    unsigned long imbalance_pct;  /* 不均衡阈值(默认125%) */
    unsigned int balance_interval;/* 均衡检查间隔(ms) */
    /* 负载统计、调度标志等成员省略 */
};

1.2 调度组(sched_group)

调度域内的CPU 分组单元,代表一组负载均衡的 CPU 集合,核心成员:

struct sched_group {
    struct sched_group *next;     /* 同域下一个调度组 */
    cpumask_t cpumask;            /* 组内CPU掩码 */
    unsigned long cpu_power;      /* 组CPU算力总和(核心数) */
    /* 负载统计、带宽控制等成员省略 */
};

1.3 运行队列(rq)

每个 CPU 私有任务就绪队列,承载该 CPU 上所有就绪态 CFS/RT/DL 任务,核心成员:

struct rq {
    struct task_struct *curr;     /* 当前运行任务 */
    struct rb_root cfs_tasks;     /* CFS任务红黑树 */
    unsigned int nr_running;      /* 就绪任务总数 */
    unsigned long shturl.cc/f;    /* 队列总负载权重(PELT算法计算) */
    /* 迁移线程、锁、负载统计等成员省略 */
};

1.4 核心负载均衡术语

  • 负载(Load):由 PELT(Per-Entity Load Tracking)算法计算,反映 CPU 队列任务的时间累积占用率,单位为SCHED_LOAD_SCALE(默认 1024);
  • 不均衡阈值(imbalance_pct):默认 125%,即当最忙组负载超过平均负载 125% 时,触发任务迁移;
  • 拉模式(Pull):负载均衡核心逻辑 ——空闲 / 低负载 CPU 主动从高负载 CPU 拉取任务,而非高负载 CPU 推送,减少锁竞争;
  • detach/attach:任务迁移核心操作 ——detach_task将任务从源 CPU 队列摘除,attach_task将任务挂载到目标 CPU 队列。

1.5 负载均衡触发时机

  1. 周期性触发scheduler_tick定时器(10ms)触发,调用trigger_load_balance,检查是否到达均衡间隔,发起SCHED_SOFTIRQ软中断;
  2. 事件触发:CPU 进入空闲态(NEWLY_IDLE)、任务唤醒失败、CPU 上下线时,主动触发负载均衡。

二、环境准备

2.1 软硬件环境要求

环境类型 版本 / 配置要求
操作系统 Ubuntu 20.04 / 22.04 64 位(SMP/NUMA 架构)
内核版本 Linux 5.15、6.1、6.6(长期支持版,负载均衡逻辑一致)
硬件配置 4 核及以上 x86_64 CPU(支持 SMP),8G + 内存,NUMA 架构服务器更佳
编译工具 gcc 9.4+、make、libncurses-dev、bison、flex、libelf-dev
调试工具 gdb、kgdb、perf、trace-cmd、ftrace、numactl

2.2 内核源码获取与编译配置

1. 安装依赖工具
# 更新软件源并安装编译依赖
sudo apt update && sudo apt install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev trace-cmd perf
2. 下载 Linux 6.1 LTS 源码
# 下载源码包
wget shturl.cc/ptwrcO13yRietmk9NvZOIFDRjt1pkCHERJRebaeVdCGgzqrWxEu
# 解压
tar -xf linux-6.shturl.c
cd linux-6.1
3. 开启负载均衡与调试选项
# 复制当前内核配置
cp -v /boot/config-$(uname -r) .config
# 打开配置界面
make menuconfig

必须开启以下核心配置:

CONFIG_SCHED_SMP=y          # 启用SMP调度
CONFIG_SCHED_DEBUG=y         # 调度器调试
CONFIG_FTRACE=y              # 函数跟踪
CONFIG_DEBUG_KERNEL=y        # 内核调试
CONFIG_NUMA=y                # 启用NUMA(可选,NUMA架构必选)
4. 编译安装内核
# 编译(-j后接CPU核心数,加速编译)
make -j$(nproc)
# 安装模块
sudo make modules_install
# 安装内核
sudo make install
# 更新grub引导
sudo update-grub

重启系统,选择新编译内核进入,验证内核版本:

uname -r
# 输出:6.1.0-xxx-generic

2.3 源码定位

负载均衡核心源码路径:

kernel/sched/fair.c    // load_balance、find_busiest_group、move_tasks实现
kernel/sched/sched.h    // sched_domain、sched_group、rq结构体定义
kernel/sched/core.c     // 负载均衡触发、软中断处理

三、应用场景

Linux 负载均衡机制是多核系统性能的核心保障,广泛应用于高并发服务器、工业控制、嵌入式集群等场景。在Web 服务器集群中,大量 HTTP 请求生成的 CFS 任务可通过负载均衡均匀分布到各 CPU 核心,避免单核心过载导致的响应超时;工业机器人控制系统中,运动规划、传感器数据处理、故障检测等实时任务,借助负载均衡在多核间动态调度,保障控制指令的低时延执行;NUMA 架构数据库服务器(如 MySQL、PostgreSQL)通过层级化调度域,优先在同 NUMA 节点内迁移任务,减少跨节点内存访问延迟,提升缓存命中率;此外,容器化云主机5G 基站基带处理嵌入式 Linux 集群等场景,均依赖负载均衡算法优化多核资源利用率,避免算力浪费,保障系统高吞吐与低时延。

四、实际案例与源码深度剖析

4.1 负载均衡总流程:load_balance 函数

load_balance是负载均衡的核心入口函数,运行在软中断上下文,核心流程分为 3 步:找最忙组→找最忙 CPU→迁移任务

4.1.1 load_balance 核心源码(kernel/sched/fair.c)
static int load_balance(int this_cpu, struct rq *this_rq,
                        struct sched_domain *sd, enum cpu_idle_type idle,
                        int *balance)
{
    struct sched_group *group;    // 最忙调度组
    struct rq *busiest;           // 最忙运行队列
    unsigned long imbalance;      // 负载不平衡量
    int sd_idle = 0;
    cpumask_t cpus = CPU_MASK_ALL;
    int nr_moved = 0;              // 实际迁移任务数

    schedstat_inc(sd, lb_cnt[idle]);

redo:
    // 1. 调用find_busiest_group:查找当前调度域内最忙调度组
    group = find_busiest_group(sd, this_cpu, &imbalance, idle,
                                &sd_idle, &cpus, balance);
    if (!group) {
        // 无最忙组,负载均衡完成
        schedstat_inc(sd, lb_nobusyg[idle]);
        goto out_balanced;
    }

    // 2. 调用find_busiest_queue:在最忙组内查找最忙CPU队列
    busiest = find_busiest_queue(group, idle, imbalance, &cpus);
    if (!busiest) {
        // 无最忙队列,重试
        schedstat_inc(sd, lb_nobusyq[idle]);
        goto redo;
    }

    // 加锁:保护源队列与目标队列,避免并发修改
    double_lock_balance(this_rq, busiest);

    // 3. 调用move_tasks:从最忙队列迁移任务到当前CPU队列
    nr_moved = move_tasks(this_rq, this_cpu, busiest,
                           imbalance, sd, idle, &all_pinned);

    // 解锁
    double_unlock_balance(this_rq, busiest);

    if (nr_moved) {
        // 成功迁移任务,更新统计
        schedstat_add(sd, lb_moved[idle], nr_moved);
        *balance = 1;
    }

out_balanced:
    return nr_moved;
}

代码说明load_balance严格遵循 “找忙→迁移” 逻辑,仅当存在明确负载不均衡时才执行任务迁移,避免无效操作。

4.2 核心算法:find_busiest_group 最忙调度组查找

find_busiest_group负责遍历调度域内所有调度组,计算每组平均负载,筛选出负载超过阈值且最高的调度组,是负载均衡的核心决策函数

4.2.1 find_busiest_group 核心源码
static struct sched_group *
find_busiest_group(struct sched_domain *sd, int this_cpu,
                   unsigned long *imbalance, enum cpu_idle_type idle,
                   int *sd_idle, cpumask_t *cpus, int *balance)
{
    struct sched_group *group, *busiest = NULL;
    unsigned long max_load = 0;    // 最忙组负载
    unsigned long total_load = 0;  // 域总负载
    unsigned long total_pwr = 0;   // 域总算力
    unsigned long this_load = 0;   // 当前组负载

    // 遍历调度域内所有调度组
    group = sd->groups;
    do {
        unsigned long load;
        int local_group;  // 是否为当前CPU所属组
        int i, nr_cpus = 0;
        unsigned long avg_load = 0;

        local_group = cpu_isset(this_cpu, group->cpumask);

        // 遍历组内所有CPU,计算组平均负载
        for_each_cpu_mask(i, group->cpumask) {
            struct rq *rq = cpu_rq(i);
            if (*sd_idle && !idle_cpu(i))
                *sd_idle = 0;

            // 负载计算:本地组削波谷,远程组削波峰
            if (local_group)
                load = target_load(i, load_idx);  // 目标负载(当前组)
            else
                load = source_load(i, load_idx);  // 源负载(远程组)

            avg_load += load;
            nr_cpus++;
        }

        if (!nr_cpus)
            goto nextgroup;

        // 计算组平均负载(按算力归一化)
        avg_load = (avg_load * SCHED_LOAD_SCALE) / group->cpu_power;
        total_load += avg_load;
        total_pwr += group->cpu_power;

        // 记录当前CPU所属组负载
        if (local_group) {
            this_load = avg_load;
            this = group;
            goto nextgroup;
        }

        // 筛选最忙组:负载>max_load
        if (avg_load > max_load) {
            max_load = avg_load;
            busiest = group;
        }

nextgroup:
        group = group->next;
    } while (group != sd->groups);

    // 无有效最忙组,返回NULL
    if (!busiest || this_load >= max_load)
        goto out_balanced;

    // 计算域平均负载
    avg_load = (SCHED_LOAD_SCALE * total_load) / total_pwr;

    // 不均衡判断:最忙组负载 > 平均负载*imbalance_pct(125%)
    if (this_load >= avg_load ||
        100 * max_load <= sd->imbalance_pct * this_load)
        goto out_balanced;

    // 计算不平衡量:需迁移的负载权重
    *imbalance = max_load - avg_load;
    return busiest;

out_balanced:
    *imbalance = 0;
    return NULL;
}
4.2.2 核心逻辑拆解
  1. 遍历调度组:通过do-while循环遍历调度域内所有sched_group
  2. 组负载计算
    • 本地组(当前 CPU 所属组):调用target_load计算,削波谷(避免过度迁移);
    • 远程组:调用source_load计算,削波峰(优先迁移高负载);
  3. 算力归一化:按组cpu_power(核心数)归一化负载,消除不同大小调度组的负载偏差;
  4. 不均衡判断:仅当最忙组负载 > 平均负载 ×125% 时,才判定为不均衡,返回最忙组;否则返回 NULL,终止均衡。

4.3 最忙队列查找:find_busiest_queue

在最忙调度组内,遍历所有 CPU 的rq队列,通过weighted_cpuload计算 CPU 负载,筛选出负载最高的运行队列,作为任务迁移的源队列。

4.3.1 find_busiest_queue 源码
static struct rq *
find_busiest_queue(struct sched_group *group, enum cpu_idle_type idle,
                   unsigned long imbalance, cpumask_t *cpus)
{
    struct rq *busiest = NULL, *rq;
    unsigned long max_load = 0;
    int i;

    // 遍历组内所有CPU
    for_each_cpu_mask(i, group->cpumask) {
        unsigned long wl;
        if (!cpu_isset(i, *cpus))
            continue;

        rq = cpu_rq(i);
        // 计算CPU加权负载(PELT算法)
        wl = weighted_cpuload(i);

        // 过滤:单任务且负载超过不均衡量,不迁移
        if (rq->nr_running == 1 && wl > imbalance)
            continue;

        // 筛选最忙队列
        if (wl > max_load) {
            max_load = wl;
            busiest = rq;
        }
    }
    return busiest;
}

4.4 任务迁移核心:move_tasks 与 detach/attach

move_tasks负责从源队列(busiest)挑选可迁移任务,执行detach_task摘除、attach_task挂载,完成任务跨核迁移。

4.4.1 move_tasks 核心源码(含 detach/attach)
static int move_tasks(struct rq *this_rq, int this_cpu, struct rq *busiest,
                      unsigned long imbalance, struct sched_domain *sd,
                      enum cpu_idle_type idle, bool *all_pinned)
{
    struct task_struct *p, *n;
    int nr_moved = 0;
    unsigned long load_moved = 0;

    *all_pinned = true;

    // 遍历源队列CFS任务红黑树
    for_each_task_safe(p, n, &busiest->cfs_tasks) {
        // 跳过不可迁移任务(绑定CPU、实时任务等)
        if (!can_migrate_task(p, this_cpu, sd))
            continue;

        *all_pinned = false;

        // 负载达标:迁移足够负载后退出
        if (load_moved >= imbalance)
            break;

        // 1. detach_task:从源队列摘除任务
        detach_task(busiest, p);
        // 2. attach_task:挂载到目标队列
        attach_task(this_rq, p);

        // 更新统计
        nr_moved++;
        load_moved += p->shturl.cc/UqGp;

        // 任务切换:触发调度
        set_task_cpu(p, this_cpu);
    }

    return nr_moved;
}

// detach_task:从源rq摘除任务
static void detach_task(struct rq *rq, struct task_struct *p)
{
    dequeue_task_fair(rq, p, 0);  // 从CFS红黑树摘除
    rq->nr_running--;              // 就绪数减1
}

// attach_task:挂载到目标rq
static void attach_task(struct rq *rq, struct task_struct *p)
{
    enqueue_task_fair(rq, p, 0);  // 加入CFS红黑树
    rq->nr_running++;              // 就绪数加1
}
4.4.2 迁移关键约束
  1. 可迁移判断(can_migrate_task)
    • 排除绑定 CPU 的任务(cpumask限制);
    • 排除实时任务(SCHED_FIFO/SCHED_RR);
    • 排除正在运行的任务(避免抢占冲突)。
  2. 负载控制:迁移负载不超过imbalance,避免目标队列过载;
  3. 锁保护:迁移全程持有源队列与目标队列锁,防止并发修改导致队列损坏。

4.5 实操验证:负载均衡触发与任务迁移

4.5.1 构造负载不均衡场景

使用stress工具在 CPU0 上创建高负载任务:

# 安装stress
sudo apt install -y stress
# CPU0绑定4个压力任务
taskset -c 0 stress -c 4 -t 300 &
# 查看CPU负载
mpstat -P ALL 1

输出可见:CPU0 100% 负载,其他 CPU 0% 负载,触发负载均衡。

4.5.2 Ftrace 跟踪负载均衡函数
# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 清空跟踪缓存
echo > /sys/kernel/debug/tracing/trace
# 设置跟踪函数
echo load_balance >> /sys/kernel/debug/tracing/set_ftrace_filter
echo find_busiest_group >> /sys/kernel/debug/tracing/set_ftrace_filter
echo move_tasks >> /sys/kernel/debug/tracing/set_ftrace_filter
# 开启跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 查看跟踪日志
sudo cat /sys/kernel/debug/tracing/trace

日志可清晰看到:find_busiest_group识别 CPU0 所在组为最忙组,move_tasks执行任务从 CPU0 迁移到 CPU1/2/3,验证源码逻辑与实际运行一致。

五、常见问题与解答

Q1:负载均衡为什么不推送任务,而是拉取任务?

解答:推送模式需高负载 CPU 主动发起迁移,会导致锁竞争激烈(高负载 CPU 本身临界区繁忙),且易引发 “惊群效应”;拉模式由空闲 CPU 主动拉取,分散锁竞争,减少高负载 CPU 开销,更适合多核并发场景。

Q2:为什么设置 imbalance_pct 阈值(默认 125%),不立即均衡?

解答:频繁任务迁移会导致CPU 缓存失效(任务迁移后缓存数据丢失)、上下文切换开销增大,反而降低性能。阈值设计允许轻微负载波动,仅当负载差异超过 25% 时才迁移,平衡均衡精度与迁移开销。

Q3:NUMA 架构下,负载均衡会跨 NUMA 节点迁移任务吗?

解答:会,但优先同 NUMA 节点内迁移。调度域分层设计:中层域(NUMA 节点内)均衡优先,顶层域(跨 NUMA)均衡延后,减少跨节点内存访问延迟(跨 NUMA 访问时延是同节点的 2-3 倍)。

Q4:为什么有些高负载任务无法被迁移?

解答:常见原因:1. 任务绑定 CPU(taskset设置cpumask);2. 实时任务(SCHED_FIFO/SCHED_RR);3. 任务正在运行(需等待时间片结束);4. 任务内存大,迁移开销超过收益。

Q5:如何排查负载均衡失效(单核心长期过载)?

解答:1. 用ftrace跟踪load_balance是否触发;2. 检查imbalance_pct是否被修改;3. 用taskset查看高负载任务是否绑定 CPU;4. 检查内核是否开启CONFIG_SCHED_SMP;5. 查看dmesg是否有调度器报错。

六、实践建议与最佳实践

  1. 性能调优:合理调整 imbalance_pct

    • 高吞吐服务器:增大阈值(150%),减少迁移开销;
    • 低时延业务:减小阈值(110%),快速均衡负载,降低调度时延。
  2. NUMA 架构优化:绑定任务到 NUMA 节点

    • 对数据库、缓存等内存密集型任务,用numactl --cpunodebind=0绑定到 NUMA 节点,减少跨节点迁移,提升缓存命中率。
  3. 调试技巧:Ftrace+perf 联合定位

    • ftrace跟踪负载均衡函数调用链路;
    • perf record -g采集 CPU 负载与调度栈,定位负载不均根因。
  4. 内核定制:优化 find_busiest_group 负载计算

    • 对实时性要求高的场景,可修改负载计算逻辑,优先迁移短周期任务,减少长任务迁移开销;
    • 禁止移除调度域分层,避免跨 NUMA 无效迁移。
  5. 业务部署:避免 CPU 绑定滥用

    • 非核心业务任务不绑定 CPU,留给负载均衡动态调度;
    • 核心实时任务可绑定 CPU,保障确定性,同时避免占用过多核心。

七、总结与应用延伸

本文从理论概念、环境搭建、结构体定义、核心源码逐行解析、实操验证、问题排查到工程最佳实践,完整拆解了 Linux 负载均衡算法find_busiest_group与任务迁移实现。负载均衡本质是多核资源调度的优化机制,通过层级化调度域、拉模式迁移、阈值控制,平衡负载均衡精度与迁移开销,是 Linux 多核系统高吞吐、低时延的核心保障。

从工程应用来看,该机制支撑着 Web 服务器、数据库、工业控制、嵌入式集群等全场景多核调度;从内核研发与学术研究角度,掌握find_busiest_group负载计算、detach/attach迁移逻辑,可深入理解 Linux 调度架构、SMP/NUMA 多核协同设计、缓存优化思想,可直接用于内核源码论文撰写、性能调优报告编写、定制化调度策略开发。

建议读者基于本文提供的源码、实操命令与调试方法,自行编译内核复现负载不均衡场景,修改imbalance_pct阈值观察调度行为变化,真正做到从理论到实战吃透 Linux 负载均衡核心原理

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐