调度器实现负载均衡,不是看“CPU 用了多少 %”,
而是比较“每个 CPU 上 runnable 线程的调度负担”。
一、调度器眼里,“负载”到底是什么?
在调度器里,负载≈两件事的组合
负载 = 可运行线程数量 × 每个线程的“权重”
在 Linux CFS(Completely Fair Scheduler)里,更准确地说是:
负载 ≈ runnable 线程的虚拟运行时间(vruntime)分布
现在可以先这样理解:
-
runnable 线程越多 → 越“忙”
-
runnable 线程权重越高(优先级高) → 越“重”
二、调度器是“什么时候”判断负载不均的?
调度器不是每个 CPU 周期都在算,它只在特定时机做负载均衡判断。
常见的触发时机
1️⃣ 周期性负载均衡(Periodic Load Balancing)
-
内核有定时 tick
-
会定期检查:
- 各个 CPU 的 run queue 长度 / 负载指标
这是最常见的迁移来源。
2️⃣ 某个 CPU 变空闲(Idle CPU)
当某个 core 变成 idle:
“我没活干了,能不能从别人那儿偷点?”
这会触发一种典型机制:
👉 work stealing(工作窃取)
3️⃣ 线程状态变化(Blocked → Runnable)
当线程被唤醒时:
-
调度器会重新评估:
-
是回原 CPU?
-
还是放到更空闲的 CPU?
-
这一步对负载均衡影响非常大。
三、调度器是如何“比较”不同 CPU 的负载的?
我们抽象成你能记住的模型。
Step 1:调度器先看一个“CPU 组”
Linux 并不是全局乱比,而是:
- 按 调度域(Scheduling Domain) 分层比较
(比如:同一个 NUMA 节点内、同一个 socket 内)
你现在只要记住一句话:
优先在“拓扑距离近”的 CPU 之间做平衡。
Step 2:对每个 CPU,计算一个“负载指标”
调度器会为每个 CPU 的 run queue 维护一些统计量,比如:
-
runnable 线程数
-
累积权重
-
最近一段时间的运行情况
这些会被汇总成一个:
“这个 CPU 有多忙” 的数值
Step 3:判断是否“不平衡”
调度器会做类似这样的判断(概念化):
if (load(cpuA) >> load(cpuB)) {
imbalance = true;
}
注意几点:
-
是 相对比较
-
有 阈值(差一点点不会动)
-
避免频繁抖动(thrashing)
四、判断“不平衡”之后,调度器怎么做?
一旦判定“不平衡”,调度器不会“平均分配”,而是非常保守。
关键原则:少搬、慢搬、只搬合适的
Step 1:选择“源 CPU”和“目标 CPU”
-
源 CPU:负载高的
-
目标 CPU:负载低的 / idle 的
Step 2:挑选“可迁移线程”
不是所有 runnable 线程都能迁移。
调度器会过滤掉:
-
正在 Running 的线程
-
被设置了 CPU affinity 的线程
-
某些实时调度策略下的线程
剩下的才是候选。
Step 3:评估“迁移是否值得”
这一步非常关键,但常被忽略。
调度器会尽量避免:
-
刚刚跑过的线程(cache 还热)
-
预计马上又要被调度的线程
更倾向选择:
-
最近没怎么运行的
-
cache 价值相对低的
-
对整体公平性影响较小的
👉 这是“负载均衡”和“cache 局部性”的折中点。
Step 4:执行迁移
-
从源 CPU 的 run queue 移除
-
加入目标 CPU 的 run queue
-
更新线程的“目标 CPU”
五、一个非常重要但容易忽略的事实
调度器的负载均衡是“近似的、启发式的”,
而不是精确最优的。
原因很简单:
-
精确计算本身就很贵
-
决策时间太长会拖慢系统
-
硬件状态(cache、TLB)不可完全观测
所以要接受这一点:
调度器不是在算最优解,
而是在算“足够好、且代价可控的解”。
六、把现在学到的内容连成一句话
调度器通过比较各 CPU 上 runnable 线程的负载,
在特定时机检测不平衡,
并在权衡 cache 局部性成本的前提下,
将部分可迁移线程从繁忙 CPU 的 run queue
移动到空闲 CPU 的 run queue,
从而实现系统层面的负载均衡。