# Linux内核调度器负载均衡与NUMA感知调度深度实战:从SMP拓扑到数据中心优化
## 引言
在现代多核系统与多路服务器中,仅有一个好的调度算法(如 CFS 或 EEVDF)是不够的。任务选好了,该在哪个 CPU 上运行? NUMA 架构下跨节点访问内存的延迟可能比高出 3–5 倍,缓存失效、总线争用、迁移开销——这些才是数据中心场景中最大的性能变量。本文从内核调度器负载均衡的核心数据结构出发,深度剖析 SMP 负载均衡、NUMA 感知调度、进程迁移机制,以及调优实战。
---
## 一、调度域调度组拓扑:CPU 分层世界
### 1.1 struct sched_domain
Linux 将 CPU 组织为分层的调度域(sched_domain),每层对应一种物理拓扑:
```c
struct sched_domain {
struct sched_domain *parent; /* 父域(先填满下再往上) */
struct sched_group *groups; /* 本轮负载均衡的组 */
unsigned long min_interval; /* 均衡最小间隔 */
unsigned long max_interval; /* 均衡最大间隔 */
unsigned int balance_interval;
unsigned int nr_balance_failed;
...
};
```
对于一个典型的双路服务器(2 Socket × 28 Core × 2 HT = 112 个逻辑 CPU),调度域层次为:
```
DIE 域 ── 56 CPU(同一片 die 上的所有核心)
MC 域 ── 28 CPU(同一插座的物理核心)
SMT 域 ── 2 个逻辑 CPU(超线程对)
```
内核按从低到高的顺序执行负载均衡:先填满同一 Socket 内的物理核,再跨 Socket 拉取任务。
### 1.2 struct sched_group
每个调度域将 CPU 划分为若干组(sched_group):
```c
struct sched_group {
atomic_t ref;
unsigned int group_weight; /* 组内 CPU 数 */
struct sched_group_capacity * __percpu *capacity; /* 剩余容量 */
unsigned long cpumask[0]; /* 组内 CPU 掩码 */
};
```
组的 average_load 决定了是否需要从邻居组拉取任务。
---
## 二、load_balance:核心均衡算法
### 2.1 can_migrate_task 过滤
在开始迁移前,内核通过 `can_migrate_task` 判断任务是否适合在源/目标 CPU 间迁移:
```c
static int can_migrate_task(struct task_struct *p, struct lb_env *env)
{
if (task_running(env->src_rq, p)) /* 正在运行?不迁移 */
return 0;
if (!cpumask_test_cpu(env->dst_cpu, p->cpus_ptr)) /* 目标不在允许集 */
return 0;
if (p->migration_flags & MGC_MIGRATE_IMMUNE) /* 免疫任务 */
return 0;
return 1;
}
```
### 2.2 find_busiest_group
这是负载均衡的心跳:在它的调度组中找到"最繁忙"的组:
```c
static inline struct sched_group *
find_busiest_group(struct lb_env *env)
{
...
/* 1. 统计各组总负载(加权 runnable + blocked) */
/* 2. 比较 average_load */
/* 3. 若最繁忙组比不繁忙组超 threshold,判定为不平衡 */
...
}
```
内核使用 `busiest->sum_h_nr_running > local->sum_h_nr_running * env->sd->imbalance_pct / 100` 作为阈值判断是否需要迁移。默认 imbalance_pct 在 SMT 层为 117(17%),在 MC 层为 125(25%),即当繁忙组比本地组多出 25% 的任务数时才执行拉取,避免乒乓震荡。
### 3 种典型迁移策略
| 类型 | 触发条件 | 迁移方向 |
|------|---------|---------|
| IDLE 均衡 | 目标 CPU IDLE | 从最繁忙 IDLE 邻居拉 1 任务 |
| NEWLY_IDLE 均衡 | 本地刚进入 IDLE | 从邻居组拉一批任务 |
| PERIODIC 均衡 | 定时 tick 触发 | 最繁忙组向本地组迁移 |
---
## 三、NUMA 感知调度:内存位置即性能
### 3.1 NUMA 拓扑基础
```
┌─────────────────┐ ┌─────────────────┐
│ Socket 0 │ │ Socket 1 │
│ M0 ── local │──│ M1 ── local │
│ C0..C27 │ │ C0..C27 │
│ Distance: 10 │ │ Distance: 18 │
└─────────────────┘ └─────────────────┘
```
本地节点访问距离 = 10(基准),远端节点 = 18(延迟约 1.8 倍)。
### 3.2 NUMA Balancing(AutoNUMA)
4.19 版后引入的自动 NUMA 平衡:
1. **扫描概期**:内核周期性扫描进程的虚拟地址页
2. **页故障追踪**:触发 PROT_NONE → #PF 后记录故障 CPU 位置
3. **升级决策**:若 >50% 故障发生在远端,标记为候选迁移
4. **迁移执行**:调用 `migrate_misplaced_page()` 将页拉到本地
```c
static void task_numa_dump_stats(struct seq_file *m, struct task_struct *p)
{
seq_printf(m, "numa_faults local:%lu node0:%lu node1:%lu\n",
p->numa_faults_locality[LOCAL],
p->numa_faults_locality[REMOTE],
p->numa_faults_locality[EQUAL]);
}
```
### 3.3 task_numa_placement & numa_migrate_needed
当进程选定 NUMA 节点后,内核通过 `mbind()` 或 `set_mempolicy` 绑定。但更好的方式是让 AutoNUMA 自动完成:
```bash
# 查看某个进程的 NUMA 页分布
cat /proc//numa_maps
# 手动触发全部页迁移
numactl --cpunodebind=0 --membind=0 ./huge_app
# 关闭 AutoNUMA(数据库等场景需要显式绑定)
echo 0 > /proc/sys/kernel/numa_balancing
```
---
## 四、Process Migration:进程如何在 CPU 间跳转
### 4.1 迁移路径图
```
source_rq dst_rq
│ ▲
│ deactivate_task() │
│ set_task_cpu() │
│ │ activate_task()
│ │ enqueue_task()
└────── migrate_task() ──────┘
```
关键步骤:
1. `deactivate_task(p, DEQUEUE_SAVED)`:从源队列取出,保存统计
2. `set_task_cpu(p, dst_cpu)`:改变任务的 CPU 归属
3. `activate_task(dst_rq, p, ENQUEUE_NOCLOCK)`:放入目标队列
### 4.2 缓存与 NUMA 代价
进程迁移的最大代价不是代码执行,而是:
- **冷缓存**:L1/L2 上原有缓存全部失效,需要从 L3/内存重新填充
- **远端内存**:若进程的页仍在旧节点,首次访问陷入远端 NUMA 延迟
- **Cache line ping-pong**:多核间共享 cache line 引发频繁缓存一致性通信
内核通过 `migrate_immserse_after` 过滤:
```c
static int can_migrate_task(struct task_struct *p, struct lb_env *env)
{
...
/* 若任务最近刚被迁移(last_migration_time < 250ms),跳过 */
if (env->src_rq->clock_task - p->se.avg.last_jiffies <
sysctl_sched_migration_cost)
return 0;
...
}
```
`migration_cost`(默认 500000ns)控制迁移冷却窗口。
---
## 五、Internode Migration (NUMA 进程迁移)
当 AutoNUMA 决定将进程整体迁移到新节点时:
```c
static void migrate_task_to_node(struct task_struct *p, int nid)
{
...
/* 1. 迁移所有匿名页 */
migrate_pages(&p->mm->mmap->mmap, p->mm, new_node);
/* 2. 调用 __set_cpus_allowed_ptr 将线程绑定到新节点 CPU */
cpumask_copy(cpus_allowed, &cpus_of_node[nid]);
/* 3. 更新进程的 numa_preferred_nid */
p->numa_preferred_nid = nid;
...
}
```
---
## 六、调优实战
### 6.1 查看调度域拓扑
```bash
# 列出所有调度域
cat /proc/schedstat | head -20
# 查看某 CPU 的域层次
cat /sys/devices/system/cpu/cpu0/domain*/name
# 查看当前负载均衡统计
grep -r "load_balance" /proc/sched_debug/
```
### 6.2 关键 sysctl 参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| sched_migration_cost | 500μs | 迁移冷却时间 |
| Sched_migrate_deferred | 关 | 延迟迁移 |
| numa_balancing | 开(4.19+) | AutoNUMA 是否启用 |
| numa_scan_delay | 1s | AutoNUMA 首次扫描间隔 |
| numa_scan_size | 256MB/秒 | 扫描带宽 |
### 6.3 数据库场景优化
数据库(如 PostgreSQL、MySQL 8.0 InnoDB)通常需要显式 NUMA 策略:
```bash
# 1. 关闭内核 AutoNUMA 避免额外开销
sysctl -w kernel.numa_balancing=0
# 2. 显式绑定 PG 到 Socket 0
numactl --cpunodebind=0 --membind=0 postgres -D /data/pgsql
# 3. 使用 huge page + 本地内存分配
echo 1024 > /proc/sys/vm/nr_hugepages
sysctl -w vm.nr_hugepages_mempolicy=256
# 4. CFS 调优:减少不必要迁移
sysctl -w kernel.sched_min_granularity_ns=10000000
sysctl -w kernel.sched_wakeup_granularity_ns=15000000
```
### 6.4 计算密集型 AI/ML 训练场景
GPU Direct RDMA 任务需要 PCIe 拓扑感知调度:
```bash
# 训练进程绑定到 GPU 同 NUMA 节点
# 查看 GPU 节点
for gpu in /sys/bus/pci/devices/*; do
cat $gpu/numa_node 2>/dev/null && ls $gpu/drm
done
# 绑定训练进程
numactl --cpunodebind=$(cat /sys/bus/pci/devices/0000:1b:00.0/numa_node) \
--membind=$(cat /sys/bus/pci/devices/0000:1b:00.0/numa_node) \
python train_imagenet.py
```
---
## 七、深入源码:load_balance 核心函数
以 6.1 内核的 `load_balance()` 为例:
```c
int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_busy_type busy_type)
{
struct lb_env env = {
.sd = sd,
.dst_cpu = this_cpu,
.dst_rq = this_rq,
.dst_grpmask = sched_group_cpus(sd->groups),
.idle = busy_type,
.loop_break = sched_nr_migrate,
};
struct sched_group *group;
int ld_moved = 0;
/* 1. 检查 sd 是否过期(topology change) */
update_sd_stats(env, this_cpu);
/* 2. 找到源 CPU(busiest idle CPU) */
group = find_busiest_group(&env);
if (!group)
goto out;
/* 3. CPU 隔离/热插拔检查 */
env.src_cpu = group_balance_cpu(env, group);
/* 4. 执行迁移 */
ld_moved = move_tasks(&env);
out:
return ld_moved;
}
```
---
## 八、总结
Linux 调度器的负载均衡将硬件拓扑抽象为分层调度域,配合 NUMA AutoNUMA 实现了从双核 IoT 到 8 路服务器的统一模型。对于 DBA 和基础设施工程师:
- 大数据库关闭 AutoNUMA 做显式绑定
- AI 训练任务利用 PCIe NUMA 亲和性
- 通用服务器保持 AutoNUMA 默认开,配合 CFS 冷却参数调优
理解 `sched_domain` 的构建、`load_balance` 的触发路径、`can_migrate_task` 的过滤以及 AutoNUMA 的页故障追踪,是优化生产环境中调度相关性能问题的基石。

发表评论 取消回复