调度器是如何进行负载判断的?

51 阅读3分钟

调度器实现负载均衡,不是看“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,
从而实现系统层面的负载均衡。