Load Balance
CFS负载均衡关联的模块如下图:
负载均衡的时机
五种进入负载均衡的时机:
- Passive Balancing
- Fork Balancing
- When creating a task, decide whether to run the task on the CPU where the parent task was running or migrate it to another CPU.
- If possible, select a CPU with cache affinity or an idle CPU, otherwise migrate to the runqueue of a CPU with less CPU load.
- Call wake_up_new_task() function with the SD_BALANCE_FORK flag.
- Exec Balancing
- When a task is executed, it determines whether to perform the task on the CPU on which it was previously executed or migrate it to another CPU.
- If possible, select a CPU with cache affinity or an idle CPU, otherwise migrate to the runqueue of a CPU with less CPU load.
- When migrating to a different CPU, the migrate thread is used.
- Use the SD_BALANCE_EXEC flag in the sched_exec() function.
- Wake Balancing
- When an idle task wakes up, it decides whether to run on the woken up CPU or migrate to another idle CPU.
- Use the SD_BALANCE_WAKE flag in the try_to_wake_up() function.
- Idle Balancing(即将进入idle状态时)
- When a CPU enters an idle state, it decides whether to take a task from the busiest CPU in the busiest scheduling group.
- Use the SD_BALANCE_NEWIDLE flag in the idle_balance() function.
- Fork Balancing
- Periodic Balancing
- It determines whether to rebalance by checking for each balancing cycle through a periodic schedule tick.
- The load balance cycle changes dynamically from 1 tick to max_interval (initial value 0.1 seconds).
- Finds overloaded tasks in schedule groups of schedule domains with the SD_LOAD_BALANCE flag and pull-migrates them to the current CPU to distribute the load.
- The call order is schedule tick -> raise softirq -> run_rebalance_domains() -> rebalance_domains().
- active load balancing
- It checks for rebalancing via periodic schedule ticks to determine whether to rebalance, but falls back to this method if it fails several times in certain situations.
- Use active load balancing to migrate tasks that are already running on the busiest CPU.
- Wake up the CPU stopper thread of the target CPU (which has the highest priority since it uses the stop scheduler) and push-migrate one task from that CPU runqueue to the dest runqueue, excluding the CPU stopper thread.
- It checks for rebalancing via periodic schedule ticks to determine whether to rebalance, but falls back to this method if it fails several times in certain situations.
- It determines whether to rebalance by checking for each balancing cycle through a periodic schedule tick.
找最闲的
以上几种场景中, fork/exec 和 wakeup 都是相当于有新的 task 要运行,那么需要寻找一个相对最空闲的 CPU,然后到这个 idlest CPU 上去运行该 task(此过程被叫做 task placement)。这里,"select_task_rq" 就是为 task 选择一个最适合的 runqueue。
寻找的顺序是从当前 CPU 所在的 domain 开始,依次向上,直到遇见迁移的 flag(即"SD_BALANCE_XXX")匹配的 domain,然后在该 domain 里选择负载最轻的 CPU。
wakeup 的场景还需要考虑 wake affine,即被唤醒的这个 task(称为 wakee),尽量和唤醒它的 task(称为 waker)在同一个 CPU 上运行。这里基于的假设是 cache hot,那如何判断这个 CPU 上的 cache 对于 wakee 来说是 hot 的呢?
直接扫描 cache 好像不太现实,一个近似的方法是看该 task 上次在这个 CPU 上运行的时间,是否超过了 "sched_migration_cost" 这个 sysctl 参数的设定值(新版 Linux 附加考虑的因素更多)。
针对CFS调度器,执行该动作(“找最闲”)的函数是select_task_rq_fair(),其调用关系如下:
找最忙的
作为 OS 的心跳,只要不是 NO_HZ 的 CPU,周期性的 tick 都会如约而至,这为判断是否需要均衡提供了一个绝佳的时机。但如果在每次 tick 时钟中断都去做一次 balance,那开销太大了,所以 balance 的触发是有一个间隔(interval)要求的。
每个 sched domain 的触发间隔不同,内层 sd 的迁移可以更频繁,间隔可以更短。另外,CPU 数目如果越多,均衡一次所需的工作更多,间隔往往就设的越长。
scheduler_tick() --> trigger_load_balance --> raise_softirq(SCHED_SOFTIRQ)
run_rebalance_domains() --> rebalance_domains() --> load_balance()
除了周期性的 tick,还有一个时间点也很适合进行负载均衡,就是一个 CPU 把自己手头的活做完(runqueue 为空),试图进入 idle 态的时候(称为 "new idle",标识为 "CPU_NEWLY_IDLE")。
sched() --> idle_balance() --> load_balance()
不过,在抢活来干之前,还是得权衡下任务迁移的代价。帮 A 执行一个任务,自己的 cache(可类比于自己用的开发环境)就得腾出来处理这个任务,等我自己的任务回来执行的时候(比如 QA 的同学报了 bug,需要修复),cache 就是“冷”的(需要恢复先前的开发环境)。
所以到底值不值当,得看我下一个任务还要多久需要被执行,如果算下来值当,那最后也是和周期性均衡类似,调用"load_balance" 这个核心函数,来进行 CPU 的选择和任务的迁移。
从哪来
具体的做法是:从当前 CPU 所在的最内层的 domain 开始,通过 "find_busiest_group" 找到该 domain 中最繁忙的 sched group,进而通过 "find_busiest_queue" 在这个最繁忙的 group 中挑选最繁忙的 CPU runqueue,被选中的 CPU 就成为任务迁移的 src。
那接下来从这个队列中选择哪些任务来迁移呢?这就是 "detach_tasks" 函数要完成的事了,判断的依据主要是task load 的大小,优先选择 load 重的任务。
到哪去
如果满足迁移的条件(imbalance 的程度超过了迁移的代价),那么被选中迁移的任务将从其所在的 runqueue 移除(对应 "deactive_task" 函数),并被打上 "TASK_ON_RQ_MIGRATING" 的标记,开始向着作为 dst 的当前 CPU 的 runqueue 进发(对应 "active_task" 函数)。
这里有一个例外,设置了 CPU affinity,和所在 CPU 进行了绑定(俗称被 "pin" 住)的任务不能被迁移(就像被pin 住的内存 page 不能被 swap 到磁盘上一样),对此进行判断的函数是 "can_migrate_task"。
需要注意的是,对于 CFS 调度,任务迁移到一个新的 runqueue 之后,其在之前 runqueue 上的 vruntime 值就不适用了,需要重新进行计算和矫正,否则可能造成“不公平”。
主动均衡(Active Balance
There were tasks that could not be retrieved from pull migration in the load_balance() function. Tasks running in the busiest runqueue cannot be migrated, so this enables load balancing of running tasks using the active balance.