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

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 拉起来。每次触发时,调度器会做一次域内扫描,大致是下面这个过程:
- 统计当前域内所有调度组的负载信息;
- 找到最忙的调度组,算出它和当前组之间的负载差额;
- 在最忙的调度组里找出负载最高的 CPU;
- 从那个 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/