关于linux内核cpu进程的负载均衡

晨曦之光 发布于 2012/04/10 15:01
阅读 661
收藏 0

2.6内核中进程调度模块的负载均衡行为分为“拉”和“推”,推这里不考虑,关于拉均衡有一篇文章特别好,具体出处就不记得了,我当时用的百度快照,那篇文章我认为最精彩的部分就是下面摘录的这段话:
当某个 cpu 负载过轻而另一个 cpu 负载较重时,系统会从重载 cpu 上"拉"进程过来,这个"拉"的负载平衡操作实现在 load_balance() 函数中。load_balance() 有两种调用方式,分别用于当前 cpu 不空闲和空闲两种状态,我们称之为"忙平衡"和"空闲平衡":
无论当前 cpu 是否繁忙或空闲,时钟中断(rebalance_tick()函数中)每隔一段时间(BUSY_REBALANCE_TICK)都会启动一次 load_balance() 平衡负载,这种平衡称为"忙平衡"。
Linux 2.6 倾向于尽可能不做负载平衡,因此在判断是否应该"拉"的时候做了很多限制:
1.系统最繁忙的 cpu 的负载超过当前 cpu 负载的 25% 时才进行负载平衡;
2.当前 cpu 的负载取当前真实负载和上一次执行负载平衡时的负载的较大值,平滑负载凹值;
3.各 cpu 的负载情况取当前真实负载和上一次执行负载平衡时的负载的较小值,平滑负载峰值;
4.对源、目的两个就绪队列加锁之后,再确认一次源就绪队列负载没有减小,否则取消负载平衡动作;
5.源就绪队列中以下三类进程参与负载情况计算,但不做实际迁移:
5.1.正在运行的进程
5.2.不允许迁移到本 cpu 的进程(根据 cpu_allowed 属性)
5.3.进程所在 cpu 上一次调度事件发生的时间(runqueue::timestamp_last_tick,在时钟中断中取值)与进程被切换下来的时间(task_struct::timestamp)之差小于某个阀值(cache_decay_ticks的nanosecond值),--该进程还比较活跃,cache 中的信息还不够凉。
...
以上的论述是基于2.6.2内核的,我看了那篇文章之后,感觉内核负载均衡的设计非常完美,然后我很迫切的翻起代码寻找一些蛛丝马迹,可是却没有找到明显的上面说到的东西,很是郁闷,然后我又研究了一下新内核的代码,发现了linux内核在做负载均衡的时候一个很巧妙的技巧,相比2.6.2内核只能说有过之而无不及。首先先说说负载均衡的策略,和2.6.2一样,也是尽量避免做负载均衡,除非非做不可了,因为负载均衡只是一个优化吞吐量和性能手段,它不是目的,因此没有必要让它占用太多的处理器时间,当然也没有必要提供编程接口了,机器有几个cpu,是否做负载均衡这些信息对用户是不可见的,但是用户可以通过设置一些微调参数从而影响内核负载均衡的行为,这种方式在linux中是很常见的。下面就来说一下新内核的负载均衡,linux有一个特点,如果一个特性是好的,那么即使高版本的内核也要向下兼容它,这里的兼容只是原理上,新版本的内核可能通过一种不同的方式实现这种老的理念,那么我们就在2.6.28内核中找一下2.6.2的负载均衡的影子吧。
在新内核中,有一个函数特别有意思,就是update_cpu_load,这个函数更新了rq数据结构中的cpu_load数组字段,这个cpu_load数组在负载均衡中很重要,它代表了该队列所属cpu的负载,其实负载在cfs之前就是该cpu上运行的进程的数量,在cfs中代表当前cpu上所有的进程的权值之和,不过一个负载为何用一个数组而不仅仅是一个数字呢,这就是技巧所在,linux不仅仅根据当前这次负载均衡时的各个cpu负载来决定如何行动,linux还不丢失cpu以前的负载,因为这个涉及到缓存的热度等等cpu亲和相关的概念,但是负载均衡并不是在一个条件下触发的,它在很多条件下都会被触发,而这些条件对cpu亲和的要求并不相同,比如新创建进程的时候就不用考虑什么亲和的问题,还有如果是在运行idle的时候进行的负载均衡,那可能要考虑cpu亲和,但是考虑的程度绝对没有运行非idle进程时进行的负载均衡考虑cpu亲和的程度高,于是一个数字就不能代表这一切了,必须有个地方保留曾经的cpu负载信息,如果仅仅为了将曾经的负载信息考虑进去,那么也没有必要用一个数组,类比LRU的理论算法,用一个整数就可以了,每个时钟嘀嗒中,原来的cpu负载右移若干位,然后当前的负载加上去,负载均衡的时候用这个值作为参考进行一些变换后(因为要和当前cpu负载的单位一致,如果不变换的话,那么曾经的负载信息就会无故地加到上次时钟嘀嗒时cpu负载的低位上)和当前的cpu负载进行平滑行为就可以了,平滑行为和上述摘录的文章段落一致,我感觉比2.6.2实现的更好,因为2.6.2的实现只是用这次和上次的cpu负载信息进行平滑操作,上次以前的cpu负载信息将失效,虽然不是那么容易,但是还是可能出现大的跳跃。还没有完,上面说不能用一个数来保存cpu负载信息用于负载均衡的原因就在于还要区分不同的情况,比如空闲均衡,忙均衡或者exec均衡等,可是上面想到的那个用移位的方式保存以前的负载信息很绝妙以至于不能不用,不就是还需要保存一个情况信息吗,其实很好办,将不同的情况都用刚才的那个移位算法,每种情况保存一个数字,多种情况就成了一个数组,于是这个cpu_load数组就出来了,我估计作者就是这么想到的,另外为了省去负载均衡时和当时的cpu负载进行平滑操作时还要进行变换,在向cpu_load数组中的元素赋值的时候就做好变换,于是新的算法不用移位实现,用比例实现,也就是说按照不同的情况,也就是数组的不同元素,老的负载信息在cpu_load元素最终结果中所占的比例不同,最后我们来看看这个函数:
static void update_cpu_load(struct rq *this_rq)
{
         unsigned long this_load = this_rq->load.weight;    //当前的cpu的当前负载
         int i, scale;
         this_rq->nr_load_updates++;
         for (i = 0, scale = 1; i < CPU_LOAD_IDX_MAX; i++, scale += scale) { //CPU_LOAD_IDX_MAX代表负载均衡的情况数量
                 unsigned long old_load, new_load;               //这个算法其实很简单,写成式子以后你会恍然大悟的,见下面。
                 old_load = this_rq->cpu_load[i];
                 new_load = this_load;
                 if (new_load > old_load)
                         new_load += scale-1;
                 this_rq->cpu_load[i] = (old_load*(scale-1) + new_load) >> i;
         }
}
当i=0,scale=1时:
(old_load*0+new_load)/1
当i=1,scale=2时:
(old_load*1+new_load)/2
当i=2,scale=4时:
(old_load*3+new_load)/4
当i=3,scale=8时:
(old_load*7+new_load)/8
当i=4,scale=16时:
(old_load*15+new_load)/16
可以看到i越大,当前的cpu的负载在均衡负载中所占的比例越小,也就是说,i越大,越应该以曾经的值作为参考,设想一下,忙均衡这种情况使用的cpu_load的索引应该很高,而空闲均衡使用的索引应该很小但是不是最小,fork/exec时的均衡应该就是0,按照设想,查阅一下代码确认一下:
.busy_idx               = 3,                    /
.idle_idx               = SD_IDLE_IDX,          /
.newidle_idx            = SD_NEWIDLE_IDX,       /
.wake_idx               = 1,                    /
.forkexec_idx           = SD_FORKEXEC_IDX,      /
看来确实如此,猜测正确,不过以上仅仅是linux新内核负载均衡的第一部分,还有第二部分呢,第一部分设置好了数据结构,第二部分就应该进行实际的负载均衡了,和2.6.2的策略一样,也是一个平滑峰值和凹值的过程,前面已经说过,这样做是为了避免均衡风暴,过度负载均衡不是好事,平滑操作实际上就是放弃极端情况,极端意味着峰值和凹值,极端了不好,这个道理谁都知道的。比如找到了一个cpu的负载很大,那么就不能因为它这次的负载大就将它的进程拉过来一部分,因为这只是当前的情况,我们必须考察这个cpu的历史负载情况,而且,通过取当前值和历史值中比较小的值可以实现平滑操作,因为这样可以最大限度地推迟负载均衡行为,使得它不到万不得已不会执行。在拉方式的负载均衡中,都是往当前cpu拉进程,只有在当前cpu相对别的cpu足够空闲的时候才会进行,如果当前cpu瞬间空闲了,那么内核也不会很莽撞的马上进行负载均衡操作,内核会检测当前cpu的历史信息,取个最大值最为当前cpu的负载,这样也是为了将负载均衡推迟到不能再推为止,同样也是为了在极坏的情况下才进行负载均衡操作。于是这个第二部分主要就体现在两个函数上:
static unsigned long source_load(int cpu, int type)   //这个函数计算源cpu的负载,内核通过这个值决定是否将进程从这个cpu迁出。
{
         struct rq *rq = cpu_rq(cpu);
         unsigned long total = weighted_cpuload(cpu);
         if (type == 0 || !sched_feat(LB_BIAS))
                 return total;
         return min(rq->cpu_load[type-1], total);    //回答一个最小值,指示内核尽量不要迁移我的进程
}
static unsigned long target_load(int cpu, int type)  //这个函数计算目的cpu的负载,内核通过这个值决定是否将进程迁移到这个cpu。
{
         struct rq *rq = cpu_rq(cpu);
         unsigned long total = weighted_cpuload(cpu);
         if (type == 0 || !sched_feat(LB_BIAS))
                 return total;
         return max(rq->cpu_load[type-1], total);   //回答一个最大值,告诉内核我的进程足够多,尽量不要给我进程
}
以上这一前一后的操作基本思想就是忙cpu尽力挽留自己的进程,即使自己再忙,而空闲cpu尽量拒绝进程到来,即使cpu再空闲,好像它们达成了一致,多的不给,少的不要。这就是负载均衡第二部分的精髓,另外还有一个cpu调度域的问题,如果计算出处于一个调度域内的别的cpu组的负载的最大值,那么就要取一个最大值了,这又是一个技巧,就是在前面的尽量不负载均衡的前提下,尽量在一个最小调度域内部的一个cpu组进行负载均衡,比如一个调度域有3个cpu组,需要拉进程的cpu处于第2组,在找到一个最忙的cpu组的操作下,它应该尽量的找到第2组,因此在它自己的组内部计算cpu负载时就是调用的target_load而不是source_load,这难道不就很可能要实际进行负载均衡了吗?别急,下面还有很多限制条件呢,反正不管怎样,应该避免负载均衡,如果非要进行,那么就在本cpu组内部进行。其实,对于内核自动的负载均衡操作,每隔一段时间就会尝试执行一次,就像页面置换一样,负载均衡分为忙均衡和闲均衡,对于不同的情况,当前cpu负载信息取cpu_load数组的不同元素。
另外还有一个负载均衡的执行方式,就是推方式,这个就是用内核迁移线程完成的了,就不多说了。


原文链接:http://blog.csdn.net/dog250/article/details/5303546
加载中
返回顶部
顶部