简介
负载均衡的主要问题为:内核如何为一个任务选择一个合适的cpu运行,让系统既不会出现一核有难,八核围观的情况,也让系统效率更高,节能省电。
目前内核在迁移进程的时候,主要在进程非running时进行,有两种情况,一种是进程刚刚创建或者睡眠唤醒,也就是还没有成为就绪态时,内核会为其挑选一个合适的cpu运行,被称为任务放置。一种是已经是就绪态了,但是没有cpu资源,处在runnable状态还没有running时,内核会根据负载情况,将runnable进程迁移到其他cpu运行,本文主要介绍第二种情况的负载均衡,因为这种负载均衡最终都会调用到load_balance, 所以就用load_balance来代表。
本文只粗略介绍load_balance的触发时机。
周期性负载均衡
之前一直有一个疑问,内核到底是在什么时候进行调度的,内核仿佛一直在运行一样,默默看着其他进程运行,记录它们的运行时间,某个时机再去进行调度。但事实上,内核调度更多的,靠的是时钟中断,称为tick, 每过一段时间(一般1/1024 s),时钟都会发送一个中断,触发内核的tick,然后开始进行调度,这里只看负载均衡相关的代码。
scheduler_tick
trigger_load_balance
void trigger_load_balance(struct rq *rq)
{
/* Don't need to rebalance while attached to NULL domain */
if (unlikely(on_null_domain(rq)))
return;
if (time_after_eq(jiffies, rq->next_balance))
raise_softirq(SCHED_SOFTIRQ);
nohz_balancer_kick(rq);
}
由此可见,cpu每过一段时间,会触发软中断,还会有nohz kick一下。
软中断中的负载均衡
先看软中断部分,会运行到run_rebalance_domains
run_rebalance_domains
rebalance_domains
load_balance
注意到,load_balance的第一个参数,this_cpu, 也正是触发这个软中断的cpu,而这个cpu,是作为dst_cpu, 也就是任务会被拉取到这个cpu上。换句话说,触发软中断的cpu会把任务拉取到自己身上。
static int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_idle_type idle,
int *continue_balancing)
{
int ld_moved, cur_ld_moved, active_balance = 0;
struct sched_domain *sd_parent = sd->parent;
struct sched_group *group;
struct rq *busiest;
struct rq_flags rf;
struct cpumask *cpus = this_cpu_cpumask_var_ptr(load_balance_mask);
struct lb_env env = {
.sd = sd,
.dst_cpu = this_cpu,
.dst_rq = this_rq,
.dst_grpmask = sched_group_span(sd->groups),
.idle = idle,
.loop_break = sched_nr_migrate_break,
.cpus = cpus,
.fbq_type = all,
.tasks = LIST_HEAD_INIT(env.tasks),
};
所以这是负载均衡的第一个触发时机:当tick时,cpu发现自己负载不重,而且满足负载均衡的条件(时间满足,不平衡性满足),就会触发软中断将任务拉到自己身上。
为什么是软中断时执行,因为负载均衡也是需要时间的,如果在中断处理函数中执行,cpu会有很长的时间收不到中断,是不好的。
nohz 负载均衡
内核是靠着频繁的tick进行调度的,但是tick会占用cpu时间,如果cpu上只有一个任务,不需要调度,那就不需要tick,同时,如果cpu上一个任务都没有,进入idle状态,那么也不需要tick。
那么这些关掉了tick的cpu什么时候醒来呢,涉及到负载均衡,我们只关注因为idle状态关闭tick的cpu,因为它们闲着,可以多干活,可以发现,代码逻辑核心就是通过cpu之间的中断 ipi 对idle的cpu进行通知。
nohz_balancer_kick
判断一下是否kick idle cpu,要是自己任务多,时间满足,就kick一下
kick_ilb
smp_send_reschedule
idle的cpu收到这个中断后,就要起来干活了:
smp_reschedule_interrupt
scheduler_ipi
if (unlikely(got_nohz_idle_kick())) {
this_rq()->idle_balance = 1;
raise_softirq_irqoff(SCHED_SOFTIRQ);
}
发现和上面是触发相同的软中断, 同样是运行到run_rebalance_domains,但是首先执行到nohz_idle_balance。
run_rebalance_domains
nohz_idle_balance
_nohz_idle_balance
for_each_cpu(balance_cpu, nohz.idle_cpus_mask)
rebalance_domains
load_balance
一个有正常tick的cpu,当周期性触发tick,并发现自己挺忙的时,就会找一个idle cpu kick它一下,这个idle cpu就会自己触发自己的软中断,然后开始进行nohz_idle_balance,和上面单纯的将任务拉取到自己身上不同,这个idle cpu还会遍历其他所有的idle cpu,让他们开始干活了。
为什么要让idle cpu做这件事?因为既然有tick 的cpu都忙不过来,才会kick,就不能让它来做这件事,而是找个idle cpu来负责把其他idle cpu唤醒干活。
newidle balance
上述都算是周期性触发负载均衡,也就是隔一段时间检查一下cpu之间任务平不平衡,不平衡均衡一下。还有一种情况,内核知道自己很可能不平衡了,那就是newidle balance。
在调度时,内核会选择下一个运行的任务,在cfs调度中是pick_next_task_fair。但是如果发现已经没有任务可选了,那么就很可能要进入idle状态了,即没事可做。
可是这个cpu上没事可做,想idle一会,其他cpu可能正热火朝天着,所以在进入idle之前,还要再进行一次负载均衡,即看看有没有任务能拉过来做, 整体逻辑相当于是被kick了。
pick_next_task_fair
没有任务要做了
newidle_balance
nohz_newidle_balance
load_balance
这里同样也会尝试nohz balance, 唯一的不同可能干一会发现自己有任务得干,就再kick一下一个idle cpu,把任务交给它。整体来说,和被kick了没有什么太大的差别。
结语
本文粗略介绍了load balance的时机,下一篇文章会介绍load balance的工作流程。