现在互联网公司使用的都是多CPU(多核)的服务器了,Linux操作系统会自动把任务分配到不同的处理器上,并尽可能的保持负载均衡。那Linux内核是怎么做到让各个CPU的压力均匀的呢?
做一个负载均衡机制,重点在于:
1. 何时检查并调整负载情况?
2. 如何调整负载?
先看第一个问题。
如果让我这样的庸俗程序员来设计,我第一个想到的就是每隔一段时间检查一次负载是否均衡,不均则调整之,这肯定不是最高效的办法,但肯定是实现上最简单的。实际上,2.6.20版linux kernel的确使用软中断来定时调整多CPU上的压力(调用函数run_rebalance_domains),每秒1次。
但每秒一次还是不能满足要求,对很多应用来说,1秒太长了,一秒钟内如果发生负载失衡对很多web应用都是不能接受的,何况其他实时应用。最好kernel能够紧跟进程的变化来调整。
那么,好,我们在进程创建和进程exit的时候检查并调整负载呢?可以,但是不完整,一个进程创建以后如果频繁的睡眠、醒来、睡眠、醒来,它这样折腾对CPU的负载是有影响的,你就不管它了吗?说到底,我们其实关注的是进程是否在使用CPU,而不是它是否诞生了。所以,我们应该在进程睡眠和醒来这两个时间点检查CPU们的负载。
再看第二个问题,怎么调整负载呢?从最繁忙的那个CPU上挪一个进程到最闲的那个CPU上,如果负载还不均衡,就再挪一个进程,如果还不均衡,继续挪....这也是个最笨的方法,但它却真的是linux CPU负载均衡的核心,不过实际的算法在此基础上有很多细化。对于Intel的CPU,压缩在同一个chip上的多核是共享同一个L2的(如下图,里面的一个Processor其实就是一个chip),如果任务能尽可能的分配在同一个chip上,L2 cache就可以继续使用,这对运行速度是有帮助的。所以除非“很不均衡”,否则尽量不要把一个chip上的任务挪到其他chip上。
于是,为了应对这种CPU core之间的异质性——在不同的core之间迁移任务,代价不同——Linux kernel引入了sched_domain和sched_group的概念。sched_domain和sched_group的具体原理,可参考刘勃的文章和英文资料。
【代码剖析】
SMP负载均衡检查或调整在两个内核函数里发生:
1. schedule()。当进程调用了sleep、usleep、poll、epoll、pause时,也就是调用了可能睡去的操作时都会转为内核代码里对schedule()函数的调用。
2. try_to_wake_up() 。说白了就是进程刚才睡了,现在要醒来,那醒来以后跑在哪个CPU上呢?这个选择CPU的过程,也就是负载均衡的过程。
我们先看schedule()的代码,我们忽略函数前面那些和负载均衡无关的代码(本文代码以内核2.6.20版为准):
[kernel/sched.c --> schedule() ]
3489 cpu = smp_processor_id();
3490 if (unlikely(!rq->nr_running)) {
3491 idle_balance(cpu, rq);
3492 if (!rq->nr_running) {
3493 next = rq->idle;
3494 rq->expired_timestamp = 0;
3495 wake_sleeping_dependent(cpu);
3496 goto switch_tasks;
3497 }
3498 }
每个CPU都有一个运行队列即这里的
rq,运行队列里放着该CPU要运行的进程,如果运行队列里没有进程了,就说明当前CPU没有可调度的任务了,那就要调用idle_balance从其它CPU上“平衡”一些(就是挪一些)进程到当前rq里。
再看
idle_balance()的实现:
[kernel/sched.c --> idle_balance()]
2806 /*
2807 * idle_balance is called by schedule() if this_cpu is about to become
2808 * idle. Attempts to pull tasks from other CPUs.
2809 */
2810 static void idle_balance(int this_cpu, struct rq *this_rq)
2811 {
2812 struct sched_domain *sd;
2813 int pulled_task = 0;
2814 unsigned long next_balance = jiffies + 60 * HZ;
2815
2816 for_each_domain(this_cpu, sd) {
2817 unsigned long interval;
2818
2819 if (!(sd->flags & SD_LOAD_BALANCE))
2820 continue;
2821
2822 if (sd->flags & SD_BALANCE_NEWIDLE)
2823 /* If we've pulled tasks over stop searching: */
2824 pulled_task = load_balance_newidle(this_cpu,
2825 this_rq, sd);
2826
2827 interval = msecs_to_jiffies(sd->balance_interval);
2828 if (time_after(next_balance, sd->last_balance + interval))
2829 next_balance = sd->last_balance + interval;
2830 if (pulled_task)
2831 break;
2832 }
2833 if (!pulled_task)
2834 /*
2835 * We are going idle. next_balance may be set based on
2836 * a busy processor. So reset next_balance.
2837 */
2838 this_rq->next_balance = next_balance;
2839 }
从子
sched_domain到父sched_domain遍历该CPU对应的domain(2816行),并调用load_balance_newidle,我们继续:
[kernel/sched.c --> load_balance_newidle()]
2730 static int
2731 load_balance_newidle(int this_cpu, struct rq *this_rq, struct sched_domain *sd)
2732 {
2733 struct sched_group *group;
2734 struct rq *busiest = NULL;
2735 unsigned long imbalance;
2736 int nr_moved = 0;
2737 int sd_idle = 0;
2738 cpumask_t cpus = CPU_MASK_ALL;
2739
2740 /*
2741 * When power savings policy is enabled for the parent domain, idle
2742 * sibling can pick up load irrespective of busy siblings. In this case,
2743 * let the state of idle sibling percolate up as IDLE, instead of
2744 * portraying it as NOT_IDLE.
2745 */
2746 if (sd->flags & SD_SHARE_CPUPOWER &&
2747 !test_sd_parent(sd, SD_POWERSAVINGS_BALANCE))
2748 sd_idle = 1;
2749
2750 schedstat_inc(sd, lb_cnt[NEWLY_IDLE]);
2751 redo:
2752 group = find_busiest_group(sd, this_cpu, &imbalance, NEWLY_IDLE,
2753 &sd_idle, &cpus, NULL);
2754 if (!group) {
2755 schedstat_inc(sd, lb_nobusyg[NEWLY_IDLE]);
2756 goto out_balanced;
2757 }
2758
2759 busiest = find_busiest_queue(group, NEWLY_IDLE, imbalance,
2760 &cpus);
2761 if (!busiest) {
2762 schedstat_inc(sd, lb_nobusyq[NEWLY_IDLE]);
2763 goto out_balanced;
2764 }
2765
2766 BUG_ON(busiest == this_rq);
2767
2768 schedstat_add(sd, lb_imbalance[NEWLY_IDLE], imbalance);
2769
2770 nr_moved = 0;
2771 if (busiest->nr_running > 1) {
2772 /* Attempt to move tasks */
2773 double_lock_balance(this_rq, busiest);
2774 nr_moved = move_tasks(this_rq, this_cpu, busiest,
2775 minus_1_or_zero(busiest->nr_running),
2776 imbalance, sd, NEWLY_IDLE, NULL);
原来就是我们上面说的“笨办法”,针对当前CPU所属的每个domain(从子到父),找到该
sched_domain里最忙的sched_group(2752行),再从该group里找出最忙的运行队列(2759行),最后从该“最忙”运行队列里挑出几个进程到当前CPU的运行队列里。move_tasks函数到底挪多少进程到当前CPU是由第4和第5个参数决定的,第4个参数是指最多挪多少个进程,第5个参数是指最多挪多少“压力”。有了这两个参数限制,就不会挪过头了(即把太多进程挪到当前CPU,造成新的不均衡)。
举个例子,假如有一台8核的机器,两个CPU插槽,也就是两个chip,每个chip上4个核,再假设现在core 4最忙,core 0第二忙,如图:
按照
刘勃的文章里的提法,首先是core domain,即Processor 0属于domain 1,Processor 1属于domain 2,其中domain 1包含4个sched_group,每个group对应一个core,如下图(group未画出):
所有评论(0)