8. 多核调度与负载均衡机制
8.1 SMP架构下的调度核心挑战
- CPU负载均衡:要把进程均衡地分配到各个CPU核心上,避免出现「部分CPU核心占满,其他核心空闲」的情况,最大化CPU利用率;
- 缓存亲和性:进程在同一个CPU上运行时,它的代码和数据会被缓存到该CPU的L1/L2/L3缓存中,访问速度极快。如果进程频繁在多个CPU之间迁移,会导致缓存失效,内存访问延迟大幅增加,性能下降;
- 调度延迟:负载均衡的操作需要加锁,遍历多个CPU的就绪队列,会带来一定的调度开销,不能影响进程的调度延迟;
- 功耗优化:在移动端、嵌入式场景下,需要尽量让进程集中在部分CPU核心上运行,让其他核心进入低功耗状态,降低系统功耗;
- NUMA架构的挑战:多NUMA节点架构下,跨NUMA节点的内存访问延迟是本地访问的2~3倍,调度器需要尽量让进程在同一个NUMA节点内运行,避免跨NUMA节点的内存访问。
8.2 调度域与调度组:负载均衡的层级结构
8.2.1 调度域与调度组的层级划分
┌─────────────────────────────────────────────────────────────────────────────┐ │ 系统级调度域 (SD_SYSTEM) │ │ 最高层级 | 跨所有NUMA节点 | 负载均衡周期: 100-200ms | 仅极端情况触发 │ └───────────────────────────────────┬───────────────────────────────────────┘ │ ┌───────────────────────────────┴───────────────────────────────┐ │ NUMA调度域 (SD_NUMA) - 节点0 │ │ 跨CPU插槽 | 共享本地内存 | 负载均衡周期: 50-100ms │ └───────────────────────────────┬───────────────────────────────┘ │ ┌───────────────────────────────┴───────────────────────────────┐ │ Package调度域 (SD_PACKAGE) - 插槽0 │ │ 单CPU插槽 | 共享L3缓存 | 负载均衡周期: 10-20ms │ └───────────────────────────────┬───────────────────────────────┘ │ ┌───────────────────────────────┴───────────────────────────────┐ │ Core调度域 (SD_CORE) - 物理核心0 │ │ 单物理核心 | 共享L1/L2缓存 | 负载均衡周期: 1-2ms │ └───────────────────────────────┬───────────────────────────────┘ │ ┌───────────────┴───────────────┐ │ 调度组1: 超线程0 (CPU0) │ 调度组2: 超线程1 (CPU1) └───────────────┬───────────────┘ │ ┌───────┴───────┐ │ CPU调度域(SD_CPU) │ │ 最底层 | 单逻辑CPU | 无负载均衡 │ └───────────────┘8.2.2 核心数据结构
1.调度域struct sched_domain
struct sched_domain { // 调度域的CPU掩码,包含该调度域内的所有CPU struct cpumask span; // 父调度域,层级结构的上一级 struct sched_domain *parent; // 该调度域内的调度组链表 struct sched_group *groups; // 负载均衡的参数 unsigned int min_interval; // 最小均衡间隔 unsigned int max_interval; // 最大均衡间隔 unsigned int busy_factor; // 繁忙时的均衡间隔因子 unsigned int imbalance_pct; // 负载不均衡的阈值 // 负载均衡的状态 unsigned long next_balance; // 下一次均衡的时间 struct lb_env *env; // 负载均衡的环境变量 };2.调度组struct sched_group
struct sched_group { // 调度组的CPU掩码 struct cpumask span; // 调度组链表的下一个节点 struct sched_group *next; // 调度组的负载统计信息 struct sg_lb_stats *stats; // 调度组的权重 unsigned int group_weight; };- 负载均衡是层级化执行的,从最底层的SMT调度域开始,依次向上到MC调度域、NUMA调度域;
- 每个调度域只负责自己范围内的CPU负载均衡,只有当下层调度域无法解决负载不均衡问题时,才会触发上层调度域的均衡;
- 越底层的调度域,均衡间隔越短,不均衡阈值越低,因为进程迁移的开销小;越上层的调度域,均衡间隔越长,不均衡阈值越高,因为进程迁移的开销大,尽量避免跨NUMA节点的进程迁移。
8.3 负载均衡的三大触发场景
8.3.1 周期性负载均衡(Periodic Load Balancing)
- 每个时钟tick,内核调用update_process_times(),最终调用rebalance_domains();
- rebalance_domains()从最底层的调度域开始,依次向上遍历所有调度域;
- 对于每个调度域,检查是否到达了next_balance时间,如果到达了,就调用load_balance()执行负载均衡;
- load_balance()计算该调度域内各个调度组的负载,找到最繁忙的调度组和最空闲的调度组;
- 如果两个调度组的负载差超过了imbalance_pct设置的阈值,就从繁忙的调度组中选择合适的进程,迁移到空闲的调度组的CPU上;
- 更新调度域的next_balance时间,设置下一次均衡的时间。
- 优先选择正在睡眠的进程,而不是正在运行的进程,因为睡眠进程的缓存已经冷了,迁移的开销小;
- 优先选择在当前CPU上运行时间最短的进程,缓存热度最低;
- 优先选择优先级低的进程,避免影响高优先级/实时进程;
- 绝对不能迁移设置了CPU亲和性的进程,只能在允许的CPU范围内迁移。
8.3.2 唤醒时的负载均衡(Wake-up Balancing)
- 进程被唤醒时,内核调用select_task_rq()接口,由调度类实现选核逻辑;
- CFS调度器的select_task_rq_fair()会执行唤醒亲和性算法,优先选择以下CPU:
- 进程上一次运行的CPU(缓存亲和性优先),如果该CPU空闲,直接选择;
- 进程上一次运行的CPU所在的调度域内的空闲CPU,保证缓存亲和性的同时,利用空闲CPU;
- 系统中负载最低的CPU,保证负载均衡。
- 选择好目标CPU后,把进程加入目标CPU的就绪队列,如果目标CPU的当前进程优先级更低,触发抢占。
8.3.3 空闲CPU的负载均衡(Idle Load Balancing)
- CPU进入idle空闲进程时,调用schedule_idle(),触发空闲负载均衡newidle_balance();
- ewidle_balance()从最底层的调度域开始,遍历所有调度域,寻找繁忙的调度组;
- 从繁忙的调度组中拉取合适的进程,迁移到当前空闲的CPU上;
- 如果拉取到了进程,就调度该进程运行,否则继续执行idle进程,进入低功耗状态。
- 空闲负载均衡的优先级最高,只要CPU空闲,就会立即触发,不需要等待周期性均衡;
- 空闲负载均衡只会拉取进程,不会推送进程,避免影响繁忙CPU的运行;
- 对于功耗敏感的场景,可以通过调优参数关闭空闲负载均衡,让CPU保持空闲,进入低功耗状态。
8.4 CPU亲和性与调度控制
8.4.1 CPU亲和性的系统调用
8.4.2 CPU亲和性的工程最佳实践
1.关键业务进程绑定固定CPU
最佳实践:每个核心业务进程绑定一个独立的CPU核心,不要多个进程共享同一个核心;把进程绑定到同一个NUMA节点的CPU上,避免跨NUMA节点访问。
2.中断亲和性与进程亲和性绑定
示例:把网卡中断绑定到0号CPU,把网络处理进程也绑定到0号CPU。
3.隔离CPU核心
内核启动参数:isolcpus=2,3 隔离2号和3号CPU核心;
最佳实践:实时进程必须绑定到隔离的CPU核心上,避免其他进程的抢占和干扰,保证实时性。
4.NUMA架构的亲和性设置
最佳实践:用numactl工具启动进程,绑定CPU和内存节点:
8.5 能量感知调度EAS
8.5.1 EAS的核心设计思想
- 能耗模型:内核通过设备树获取CPU的能耗模型,包括不同频率、不同负载下的CPU功耗;
- CPU调频子系统:和schedutil调频器深度配合,根据进程的负载动态调整CPU频率,平衡性能和功耗;
- 异构架构支持:原生支持ARM big.LITTLE异构架构(大核+小核),把性能要求高的进程调度到大核,性能要求低的进程调度到小核,最大化能效比。
8.5.2 EAS的核心调度规则
- 进程唤醒时,根据进程的负载计算性能需求,选择能满足性能需求的、功耗最低的CPU核心;
- 对于轻负载进程,优先调度到小核,降低功耗;
- 对于重负载、延迟敏感的进程,调度到大核,保证性能;
- 尽量让进程集中在部分核心上运行,让其他核心进入深度空闲状态,降低功耗;
- 只有当所有核心的负载超过阈值时,才会唤醒更多的核心,平衡负载。
8.6 多核调度的工程实践与避坑指南
8.6.1 CPU负载不均衡的排查与解决
- 用top → 按1,查看每个CPU核心的使用率,确认负载不均衡的情况;
- 用ps -eLo pid,tid,psr,comm查看每个线程运行的CPU核心,找到占满CPU的线程;
- 检查这些线程是否设置了CPU亲和性,绑定到了固定的CPU核心;
- 检查系统的负载均衡参数是否被修改,比如sched_migration_cost_ns被设置得过大,导致进程无法迁移;
- 检查是否是NUMA架构,进程是否都集中在同一个NUMA节点;
- 用perf sched记录调度事件,查看进程的迁移情况,确认负载均衡是否正常工作。
- 对于多线程业务,不要给线程设置固定的CPU亲和性,让调度器自动均衡负载;
- 调小sched_migration_cost_ns参数,降低进程迁移的阈值,提升负载均衡的灵敏度;
- 调大sched_nr_migrate参数,增加一次负载均衡最多迁移的进程数,加快负载均衡的速度;
- 对于NUMA架构,开启NUMA均衡,保证NUMA节点之间的负载均衡;
- 优化业务代码,把单线程的任务拆分为多线程,充分利用多核CPU。
8.6.2 进程频繁迁移的性能优化
- 用pidstat -w 1查看进程的上下文切换次数,cswch/s数值过高说明进程频繁被切换;
- 用ps -eLo pid,tid,psr,comm持续查看线程运行的CPU核心,确认线程是否频繁在多个CPU之间迁移;
- 用perf stat -e cache-misses ./your_program查看进程的缓存缺失率,确认是否缓存失效严重。
- 给进程设置CPU亲和性,绑定到固定的CPU核心/NUMA节点,避免跨CPU迁移;
- 调大sched_migration_cost_ns参数,增加进程迁移的成本,减少进程迁移的频率;
- 调大sched_wakeup_granularity_ns参数,减少唤醒抢占的频率,避免进程频繁被切换;
- 对于多线程业务,使用线程池,绑定每个线程到固定的CPU核心,提升缓存命中率。
8.6.3 NUMA架构的性能优化
- 用numactl -H查看系统的NUMA节点拓扑、每个节点的CPU和内存;
- 用numastat查看每个NUMA节点的内存分配情况,numa_miss数值过高说明跨节点内存访问过多;
- 用perf stat -e numa_hit,numa_miss,numa_foreign ./your_program查看进程的NUMA访问统计。
- 用numactl工具绑定进程到同一个NUMA节点的CPU和内存,避免跨节点访问;
- 开启内核NUMA均衡,echo 1 > /proc/sys/kernel/numa_balancing,让内核自动把进程和内存迁移到同一个NUMA节点;
- 业务代码优化,避免多个NUMA节点的进程共享同一块内存,减少跨节点内存访问;
- 对于数据库等大内存应用,开启大页,减少TLB缺失,同时绑定大页到对应的NUMA节点。
8.6.4 避坑指南
1.不要盲目绑定CPU亲和性
最佳实践:只有CPU密集型、延迟敏感的核心进程才需要绑定CPU亲和性;普通的后台进程、IO密集型进程,让调度器自动分配CPU即可,不需要手动绑定。
2.隔离CPU核心的正确用法
3.跨NUMA节点的进程迁移禁止
4.实时进程的CPU绑定