分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow
也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!
第一部分:Linux负载均衡的设计
一.负载均衡的原则
1.确保每个cpu核心的负载均衡;
2.在cpu和cache以及内存布局的影响下加权执行1。
对于一般多核心cpu情况,以上两个原则可以简述为下面的原则:
1.尽量不执行进程迁移,以确保cache的热度;
2.除非各个cpu的负载已经严重失衡,执行负载均衡
二.系统以及cpu的拓扑结构
这个道理看似简单,然而如果对于一个大型的综合系统,要想设计一个适用于各种情况的负载均衡体系,却不是很简单。Linux内核的负载均衡设计的相当完美。对于负载均衡,可以分为以下几种情况:
以系统复杂度为核心的分类:
情况一:cpu无任何cache
这种情况下,需要随时保持各cpu上运行的进程数量的均衡
情况二:多处理器,每个处理器有处理器独享的cache
这种情况下,进行负载均衡会影响处理器的cache利用率,负载均衡带来的效益会被cache刷新的开销抵消掉一部分或大部分。
情况三:多处理器,每个处理器有多个处理器核心,每个核心有独享的一级cache,同一处理器的多个核心有共享的二级,三级cache
这种情况下,情况二是要考虑的,然而对于同一处理器的不同核心之间由于存在共享的二级cache,多个核心之间的负载均衡抵消掉的cache利用率收益远远小于不同处理器之间的负载均衡作同样的事情。
情况四:情况三的前提下,每一个处理器的每一个核心又开启了超线程(Intel术语)。
由于超线程使用同一套计算资源,且共享cache,因此其上的负载均衡几乎不会影响cache利用率。然而如果超线程核的调度算法以及操作系统的调度算法设计的不好,造成一个操作系统线程长期使用超线程核,也会造成上一个切换出的操作系统线程的cache被挤出去,遗憾的是,一般情况下我们无力优化操作系统的调度算法,并且无法接触cpu的smt调度算法。
情况五:情况一到四的前提下,增加不对称内存。
这种情况下,负载均衡对cache利用率的影响显然是不可避免的,同时还会影响访问内存的时间,也就是内存的利用效率,这就是NUMA的情况...
以上五种情况基本就是一个复杂系统从最简单到最复杂的排列,如果我们不以整个系统为核心,而以处理器为核心,应该是以下四种排列情况:
以处理器为核心的分类:
情况一:单个处理器开启超线程
这是我们熟知的SMT情况
情况二:单个处理器多个核心
这是我们熟知的多核处理器情况
情况三:多个处理器
这是我们熟知的SMP情况
情况四:多处理器,多内存域
这是我们熟知的NUMA情况,内存对于不同的处理器来讲,其访问效率是不同的。
三.负载均衡基础设施以及cpu拓扑结构(静态设施)
进程在不同cpu之间的迁移和cache利用率总的来说是对立的,并且根据源cpu和目的cpu之间的关系不同这种对立的程度也不同。由于进程迁移是基于cpu的,而cpu最小级别的就是超线程处理器的一个smt核,次小的一级就是一个多核cpu的核,然后就是一个物理cpu封装,再往后就是cpu阵列,根据这些cpu级别的不同,Linux将所有同一级别的cpu归为一个“调度组”,然后将同一级别的所有的调度组组成一个“调度域”,负载均衡首先在调度域的各个调度组之间进行,然后再在最低一级的cpu上进行,注意负载均衡是基于最小一级的cpu的。整个架构如下图所示:
四.负载均衡算法分析(动态表现)
迁移一个进程的代价就是削弱cache的作用。因此,只要在拥有cache的处理器之间迁移进程,势必会付出这个代价,因此在设计中必然需要一种“阻力”来尽量不做进程迁移,除非万不得已!这种“阻力”就是负载均衡原则2中的“加权”系数。
1).历史负载值的影响
为防止处理器负载曲线的上下颠簸,用历史值来加权当前值是一个不错的方式,也就是说,所谓的负载曲线不再基于时间点,而是基于时间段。然而历史负载值对总负载的影响肯定没有当前的负载值对总负载的影响大,一个时间点的负载值随着时间的流逝,对负载均衡时总负载的计算的影响应该逐渐减小。因此Linux的负载均衡器设计了一个公式专门用于负载均衡过程中对cpu总负载的计算。该公式如下:
total_load=(previous_load*(delta-1)+nowa_load)/delta
其中delta是一个可变的系数,在linux 2.6.18中设置了3个delta,分别为1,2,4,当然还可以更多,比如高一些版本的内核中delta的取值有CPU_LOAD_IDX_MAX种,CPU_LOAD_IDX_MAX由宏来定义。比如当delta为2的时候,上述公式成为:
total_load=(previous_load+nowa_load)/2
相当于历史值占据整个load值一半,而当前值占据另一半。
2).波峰/波谷的平滑化
让历史值参与计算总负载解决了同一条负载曲线颠簸的问题,但是在负载均衡时是比较两条负载曲线同一时间点上的值,当二者相差大于一个阀值时,实施进程迁移。为了做到“尽量不做进程迁移”这个原则,必须将两条负载曲线的波峰和波谷平滑掉。如果进程迁移源cpu的负载曲线此时正好在波峰,目的cpu的负载曲线此时正好在波谷,此时就需要将波峰和波谷削平,让源cpu的负载下降a,而目的cpu的负载上升b,这样它们之间的负载差就会减少a+b,这个“阻力”足以阻止很多的进程迁移操作。
3).负载曲线平滑操作的基准
负载均衡平滑操作时需要两个值,即上述的a和b,这两个值决定了削平波峰/波谷的幅度,幅度越大,阻碍负载均衡的“力度”也就越大,反之“力度”也就越小。根据参与负载均衡的cpu的层次级别的不同,这种幅度应该不同,幸运的是,可以根据调整“负载均衡过程中对cpu总负载的计算公式”中的delta来影响幅度的大小,这样,1)和2)就在这点上获得了统一。对于目的cpu,取计算得到的total_load和nowa_load之间的最大值,而对于源cpu,则取二者最小值。可以看出,在公式中,如果delta等于1,则不执行削波峰/波谷操作,这适用于smt的情况,delta越大,历史负载值的影响也就越大,削波峰/波谷后的源cpu负载曲线和目的cpu负载曲线的差值曲线越趋于平滑,这样就越能阻止负载均衡操作(差分算法....)。
4).自下而上的遍历方式
Linux在基于调度域进行负载均衡的时候采用的是自下而上的遍历方式,这样就优先在对cache影响最小的cpu之间进行负载均衡,同时这种均衡操作会增加本cpu的负载,反过来在比较高的调度域级别上有力的阻止了对cache影响很大的cpu之间的负载均衡。我们知道,调度域是从对cache影响最小的最底层向高层构建的。
5).结论
随着cpu级别的提高,由于负载均衡对cache利用率的影响逐渐增大,“阻力”也应该逐渐加大,因此负载均衡对应调度域使用的delta也应该增加。算法的根本要点是什么呢?画幅图就一目了然了,delta越大,负载值受历史值的影响越大,因此按照公式所示,只有持续单调递增的cpu负载,在源cpu选择时才会被选中,偶然一次的高负载并不足以引起其上的进程迁移至别处,相应的,只有负载持续单调递减,才会引起其它cpu上的进程迁移至此,这正体现了负载以一个时间段而不是一个时间点为统计周期!而级别越高的cpu间的进程迁移,需要的“阻力”越大,因此就越受历史值的影响,因为只要历史中有一次负载很小,就会很明显的反应在当前,同样的道理,历史中有一次的负载很大,也很容易反映在当前;反之,所需“阻力”越小,就越容易受当前负载值的影响,极端的情况下,超线程的不同逻辑cpu之间的负载计算公式中delta为1,因此它们的负载计算结果完全就是该cpu的当前负载!
结论有三:
5.1).通过“负载均衡过程中对cpu总负载的计算公式”平滑了单独cpu的负载曲线,使之不受突变的影响,平滑程度根据delta微调
5.2).通过“削掉波峰/波谷”平滑了源cpu和目的cpu负载曲线在负载均衡这个时间点的差值,尽可能阻止进程迁移,阻止程度根据delta微调
5.3).执行负载均衡的过程中,一轮负载均衡在每一层的效果需要随着级别的升高而降低,这通过自下而上的遍历方式来完成
6).引申
total_load的计算公式实际上使用了一个数列,该数列是一个“等比数列+微扰数列”的和数列,等比的比值的分母决定着数列的平滑程度,而微扰数列则是cpu的当前真实负载,它根据delta的取值不同对整个cpu的负载影响不同,为了连续化数列,我们设这两个数列为函数f(x)和g(x),证明如下:
6.1).算法改进
不能从delta中得到随着d的增加,阻碍负载均衡的力度将加大这个事实虽然在技术上通过自下而上的遍历方式解决了,然而这使得算法依赖了一个操作方式,这在数学却不是很完美,因此可以改进,引入一个参数k来微调g(x),而不是依赖d来微调,如果配置k和d相等,那么新算法将回退到老算法:
有了新的负载计算公式,我们可以控制一个变量k,然后得知,随着d的增加,负载均衡实际发生的可能性将降低。
五.Linux负载均衡的类型以及时机
1.周期性忙负载均衡:在时钟中断中针对当前cpu调用,负载计算时更多受到历史值的影响;
2.周期性空闲负载均衡:当前cpu上没有进程可运行时调用,适当减少历史值的影响,和忙负载均衡的周期相差一个busy_factor因子(该因子可配置)。
3.唤醒进程负载均衡:Linux内核倾向于本地唤醒进程,也就是说将进程唤醒在本cpu上。在网络应用中,这显得尤为重要,众所周知,网卡中断某一个cpu,该cpu处理软中断,软中断处理协议栈,在cpu的处理过程中网络数据相应进入cache,此时唤醒用户态进程继续处理应用数据,如果是本地唤醒的话,应用程序可以有效利用cpu中已经被内核载入的cache。
4.进程新创建的时候会进行负载均衡,因此多了一个进程可能会引起负载失衡。
5.进程调用exec的时候会进行负载均衡,和4一样,这两种负载均衡都是“自负载”均衡,也就是要为自己选择一个cpu来运行
6.当前cpu马上进入idle的时候,会进行负载均衡
7.push平衡,这是一种将本地进程“推”给其他cpu的负载均衡方式
第二部分:Linux负载均衡的实现(2.6.18内核)
一.数据结构
1.sched_group结构
- struct sched_group {
- struct sched_group *next; //下一个group指针
- cpumask_t cpumask; //该group包含的cpu掩码
- unsigned long cpu_power;
- };
2.sched_domain结构
- struct sched_domain {
- /* These fields must be setup */
- struct sched_domain *parent; /* top domain must be null terminated */
- struct sched_group *groups; /* the balancing groups of the domain */
- cpumask_t span; /* span of all CPUs in this domain */
- unsigned long min_interval; /* Minimum balance interval ms */
- unsigned long max_interval; /* Maximum balance interval ms */
- unsigned int busy_factor; /* less balancing by factor if busy */
- unsigned int imbalance_pct; //判断该调度域是否已经均衡的一个基准值。
- unsigned long long cache_hot