Linux 内核中的任务迁移:负载均衡算法如何在调度域之间分配 CPU

本文深入解析 Linux 内核调度器的任务迁移机制,详细说明调度域、调度组和负载均衡算法如何在多核与 NUMA 架构下分配 CPU,介绍周期性均衡、idle 均衡、active balance 等路径的触发条件和迁移代价,并给出生产环境观察方法与调优建议。

均衡不等于每个 CPU 使用率一样高

很多人注意到 Linux 内核的任务迁移,是因为在多核服务器上发现一段本来很快的程序,跑了两次时间差很大。第一反应通常是 CPU 频率、中断、内存分配,但真正把任务从一个 CPU 搬到另一个 CPU 的,是内核调度器。Linux 内核的任务迁移机制并不是一个简单的搬运动作,它背后是一整套基于调度域(scheduling domain)的负载均衡算法。理解它,首先要放下“均衡就是让 CPU 使用率平均”的直觉。

AI technology illustration

CFS 调度器追踪的负载,和 top 显示的 CPU 使用率不是同一个东西。调度器关注的是每个调度实体对 runqueue 的负载贡献:一个任务有多重、最近一段时间运行了多少、当前是否在排队等待。使用率可以很高,但可运行队列可能只有一个任务;使用率不高时,反而可能积压了大量短任务。任务迁移要解决的问题,是让一个调度域内不同 CPU 上的可运行工作量差异不要过大,而不是把百分比抹平。

举个例子,两台双路服务器的 CPU 使用率看起来都是 80%,一台每个 CPU 上只有一个长时间运行的计算任务,另一台每个 CPU 上排了四五个短请求线程。后者的用户体感会差很多,因为任务在 runqueue 里等待的时间被拉长了。调度器用 load 而不是 usage 来判断是否迁移,就是为了抓住这种差别。

调度域:把迁移成本变成拓扑模型

任务迁移的代价并不统一。同一个物理核心上两个超线程之间迁移,和跨 NUMA 节点迁移,完全不是一回事。如果调度器每次都在整机范围内挑选目标 CPU,很多迁移都会把任务从本地缓存丢到远端内存。调度域就是给这种选择加了一层拓扑约束。

内核里调度域的核心结构大致长这样:

struct sched_domain {
    struct sched_group *groups;
    cpumask_var_t span;

    unsigned long min_interval;
    unsigned long max_interval;
    unsigned int busy_factor;
    unsigned int level;
    int flags;
};

span 描述这个调度域覆盖哪些 CPU,groups 则是这个域内部的分组。每个调度组通常对应下一层的一个子域,例如一个物理核心或一个 NUMA 节点。调度器在做负载均衡时,先看整个域的 span,再比较不同的 group 之间谁更忙。

在一台双路四核八线程的机器上,常见的层级可能是:先是 SMT 兄弟线程组成一个调度组,然后是共享缓存的一组物理核,再往上是一个 NUMA 节点,最后才是整机。域与域之间的迁移成本和频率都不一样,调度器对近距离的迁移更宽容,对跨域的迁移会更谨慎。这也是为什么 NUMA 系统上任务迁移的总次数看起来比单路机器少,因为跨域迁移的触发阈值更高。

负载均衡怎么判断“谁更忙”

周期性负载均衡由调度 tick 拉起来。每次触发时,调度器会做一次域内扫描,大致是下面这个过程:

  1. 统计当前域内所有调度组的负载信息;
  2. 找到最忙的调度组,算出它和当前组之间的负载差额;
  3. 在最忙的调度组里找出负载最高的 CPU;
  4. 从那个 CPU 上挑一个合适的任务,放到目标 CPU 的 runqueue,并尽量选择迁移代价较小的任务。

如果周期性均衡仍然不能把最忙 CPU 的负载降下来,调度器会用 active balance:直接要求最忙 CPU 停下来,把一个任务主动推给另一个 CPU。这个过程并不是零成本的,被推过去的任务很可能失去原本的热缓存。

除了周期性路径,还有 idle 路径。当某个 CPU 进入空闲状态,它会主动看同域内其它 CPU 是不是有任务可以拉过来。这个行为对延迟敏感场景影响很大,因为被拉过来的任务通常等于放弃当前 CPU 上的缓存亲和性。像很多在线服务出现的 p99 抖动,其实不一定是业务代码变慢,而是调度器趁某个 CPU 空闲时把别人的任务塞了进来。

几种负载均衡路径:触发时机与代价

路径 触发时机 主要行为 常见代价
周期性均衡 调度 tick 比较调度组负载,把任务从最忙组迁到更空的组 迁移后缓存冷,短任务可能白迁
idle 均衡 CPU 进入空闲 空闲 CPU 从其它 runqueue 拉任务 打断唤醒亲和性,造成尾延迟抖动
active balance 周期性均衡无法缓解热点 强制最忙 CPU 推任务到目标 CPU 目标 CPU 可能成为新的热点
misfit 均衡 CPU 容量差异明显(大小核) 把高需求任务迁向大核 容量判断不准时调度策略会反复

这里需要说明一下:路径不是互斥的,一次任务迁移也可能由多个条件叠加。表格里的代价是某个路径最容易出现的问题,而不是一定会发生。调度器会通过 wake 时的亲和性判断尽量规避一部分缓存损失,但均衡任务本身无法完全避免这种代价。

三个常见误区

很多人看了上面的机制后会得出几个顺理成章但不太对的结论。

  • 负载均衡的目的是让每个 CPU 使用率接近。
  • 任务迁移越频繁,均衡效果越好。
  • 用 taskset 把任务绑到某一个 CPU 后,调度器就不会再动它。

第一点前面已经说过,调度器看的是可运行负载,不是使用率。第二点最容易在生产环境里栽跟头:迁移一次任务,目标 CPU 的 cache 是冷的,至少要经历一段 cache miss 才能恢复到原来的运行速度。短任务被迁过去,可能任务已经跑完了,负载还没热起来。第三点要特别小心:亲和性只是“允许的 CPU 范围”,调度器仍然可以在这些 CPU 之间做负载均衡。真正想不受干扰,要用 cpuset 分组或隔离 CPU,并且还得处理内核线程和中断。

这三个误区背后其实是同一个问题:把调度器当成一个简单的“搬运行”来看。实际上,调度器在迁移前要考虑任务类型、历史运行位置、目标 CPU 的容量和当前域内所有调度组的统计结果。它关心的不是每个 CPU 都在忙,而是每个 CPU 上的排队体验是否合理。

生产环境里常见的几个场景

第一种是双路 NUMA 服务器上跑并行计算。任务刚开始时可能被分配到一个 Node 上,内存页也分配在本地。调度器看到另一个 Node 比较空闲,会把一部分任务迁过去。计算是变分散了,但任务的输入数据还在第一个 Node 上,每次访问都变成远程内存访问。这种时候正确的做法不是关掉均衡,而是让每个任务在启动时绑定到自己的 Node,配合 numactl 控制内存分配。

第二种是延迟敏感型的在线服务。服务的主线程平时在几个 CPU 上稳定运行,突然有一个 CPU 空闲了 1 毫秒,idle 均衡立即从其它 CPU 拉了一个任务过来。这个任务可能是另一个服务的高负载线程,它一过来就把缓存和前端资源都占住了,主线程的 p99 开始抖动。很多团队会尝试调整 sysctl,但更常用的做法是把关键线程用亲和性和隔离 CPU 保护起来,不让闲 CPU 随便拉任务。

第三种是容器平台上的 cpuset 分区。容器组被切成很多小 cpuset,每个 cpuset 会成为独立的调度域。域划分得越碎,调度器做均衡时就只能在很小的范围内选择目标 CPU,全局负载可能非常不均匀;如果每个容器只占了物理核的一部分,迁移还可能在 NUMA 之间反复横跳。建议至少让一个 cpuset 覆盖完整的物理核心或完整的 NUMA 节点,并留意 cpuset.sched_load_balance 的设置。

怎么观察调度域和任务迁移

内核调试接口能直接看到调度域层级和统计信息,前提是内核打开了 CONFIG_SCHED_DEBUG:

# 查看 cpu0 的调度域层级
cat /sys/kernel/debug/sched/domains/cpu0

# 查看各 CPU 和各域的负载均衡统计
cat /proc/schedstat

# 临时限制一组任务可用的 CPU
taskset -c 0-3 ./server

如果 /sys/kernel/debug 不可用,另一个更简单的办法是跑两个相同负载的任务,一个固定亲和性,一个不固定,然后用 perf stat 对比 cache-misses。cache-misses 明显更高,通常意味着迁移确实过于频繁。要注意的是,统计数值本身并不能告诉你迁移是不是“错”的,还需要结合任务的实际运行时间和业务延迟来解读。

生产环境里怎么调整

我不会建议一上来就关掉负载均衡。调度域这套机制是为了在大多数场景下自动工作而设计的,很多问题出在任务特征和拓扑不匹配,而不是调度器本身有 bug。

  • 先量化,再调参。每次改动前记录 /proc/schedstat 里调度域计数和业务的延迟、吞吐指标,至少要能回答“迁移少了吗”和“变快了吗”。
  • 批处理任务考虑亲和性和 NUMA 绑定。组的大小最好和 LLC 或 NUMA 节点对齐,避免任务在一个域内反复跨节点迁移。
  • 延迟敏感服务优先隔离而不是全局调参。isolcpus 和 cpuset 比修改 sysctl 更能把扰动控制在小范围内。
  • 内核线程也要排队。常见做法是把 housekeeping CPU 单独分给内核线程和中断,不要让它们和业务线程混在一起。

任务迁移不是越少越好,也不是越快越好。最理想的迁移,是调度器能在任务刚开始变冷之前就把它放到合适的 CPU 上,而不是等 load 失衡已经很严重时才动手。

理解调度域,是理解这套行为的第一步,也是避免把性能问题误判成“CPU 不够”的关键。下次再看到某个 CPU 空闲而另一个 CPU 紧张时,先想想这个空闲 CPU 和忙碌 CPU 在调度域里隔了几层,再决定要不要让任务迁移过去。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/1028/

(0)
上一篇 2小时前
下一篇 1小时前

相关推荐