# 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 的页故障追踪,是优化生产环境中调度相关性能问题的基石。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部