均衡调度算法是IPVS实现均衡功能的理论精髓,其他各种东西都只算是程序技巧,现在介绍. IPVS支持8种静态均衡算法,以下文字直接拷贝自IPVS网站: IPVS在内核中的负载均衡是以连接为粒度的.在HTTP协议(非持久)中,每个对象从WEB服务器上获取都需要建立一个TCP连接, 同一用户的不同请求会被调度到不同的服务器上,所以这种细粒度的调度在一定程度上可以避免单个用户访问的突发性引起服务器间的负载不平衡. 在内核中的连接调度算法上,IPVS已实现了以下八种调度算法: 轮叫调度(Round-Robin Scheduling) 加权轮叫调度(Weighted Round-Robin Scheduling) 最小连接调度(Least-Connection Scheduling) 加权最小连接调度(Weighted Least-Connection Scheduling) 基于局部性的最少链接(Locality-Based Least Connections Scheduling) 带复制的基于局部性最少链接(Locality-Based Least Connections with Replication Scheduling) 目标地址散列调度(Destination Hashing Scheduling) 源地址散列调度(Source Hashing Scheduling) 下面,我们先介绍这八种连接调度算法的工作原理和算法流程,然后会在下面描述怎么用它们. 轮叫调度 : (Round Robin Scheduling)算法就是以轮叫的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。 算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。 在系统实现时,我们引入了一个额外条件,当服务器的权值为零时,表示该服务器不可用而不被调度。 这样做的目的是将服务器切出服务(如屏蔽服务器故障和系统维护),同时与其他加权算法保持一致。所以,算法要作相应的改动,它的算法流程如下: 轮叫调度算法流程 假设有一组服务器S = {S0, S1, …, Sn-1},一个指示变量i表示上一次选择的服务器,W(Si)表示服务器Si的权值。变量 i 被初始化为n-1,其中n > 0。 j = i; do { j = (j + 1) mod n; if (W(Sj) > 0) { i = j; return Si; } } while (j != i); return NULL; 轮叫调度算法假设所有服务器处理性能均相同,不管服务器的当前连接数和响应速度。 该算法相对简单,不适用于服务器组中处理性能不一的情况,而且当请求服务时间变化比较大时,轮叫调度算法容易导致服务器间的负载不平衡。 虽然Round-Robin DNS方法也是以轮叫调度的方式将一个域名解析到多个IP地址,但轮叫DNS方法的调度粒度是基于每个域名服务器的, 域名服务器对域名解析的缓存会妨碍轮叫解析域名生效,这会导致服务器间负载的严重不平衡。 这里,IPVS轮叫调度算法的粒度是基于每个连接的,同一用户的不同连接都会被调度到不同的服务器上,所以这种细粒度的轮叫调度要比DNS的轮叫调度优越很多. 加权轮叫调度 :(这个算法很有技巧,也很高效)执行前不需要对服务器处理性能排序。 (Weighted Round-Robin Scheduling)算法可以解决服务器间性能不一的情况,它用相应的权值表示服务器的处理性能,服务器的缺省权值为1。 假设服务器A的权值为1,B的权值为2,则表示服务器B的处理性能是A的两倍。加权轮叫调度算法是按权值的高低和轮叫方式分配请求到各服务器。 权值高的服务器先收到的连接,权值高的服务器比权值低的服务器处理更多的连接,相同权值的服务器处理相同数目的连接数。加权轮叫调度算法流程如下: 加权轮叫调度算法流程 假设有一组服务器S = {S0, S1, …, Sn-1},W(Si)表示服务器Si的权值,一个指示变量i表示上一次选择的服务器, 指示变量cw表示当前调度的权值,max(S)表示集合S中所有服务器的最大权值,gcd(S)表示集合S中所有服务器权值的最大公约数。 变量i初始化为-1,cw初始化为零。 while (true) { i = (i + 1) mod n; if (i == 0) { cw = cw - gcd(S); if (cw <= 0) { cw = max(S); if (cw == 0) return NULL; } } if (W(Si) >= cw) return Si; } 例如,有三个服务器A、B和C分别有权值4、3和2,则在一个调度周期内(mod sum(W(Si)))调度序列为AABABCABC。 加权轮叫调度算法还是比较简单和高效。当请求的服务时间变化很大,单独的加权轮叫调度算法依然会导致服务器间的负载不平衡。 从上面的算法流程中,我们可以看出当服务器的权值为零时,该服务器不被被调度; 当所有服务器的权值为零,即对于任意i有 W(Si)=0,则没有任何服务器可用,算法返回NULL,所有的新连接都会被丢掉。 加权轮叫调度也无需记录当前所有连接的状态,所以它也是一种无状态调度。 最小连接调度 : (Least-Connection Scheduling)算法是把新的连接请求分配到当前连接数最小的服务器。 最小连接调度是一种动态调度算法,它通过服务器当前所活跃的连接数来估计服务器的负载情况。 调度器需要记录各个服务器已建立连接的数目,当一个请求被调度到某台服务器,其连接数加1;当连接中止或超时,其连接数减一。 在系统实现时,我们也引入当服务器的权值为零时,表示该服务器不可用而不被调度,它的算法流程如下: 最小连接调度算法流程 假设有一组服务器S = {S0, S1, ..., Sn-1},W(Si)表示服务器Si的权值,C(Si)表示服务器Si的当前连接数。 for (m = 0; m < n; m++) { if (W(Sm) > 0) { for (i = m+1; i < n; i++) { if (W(Si) <= 0) continue; if (C(Si) < C(Sm)) m = i; } return Sm; } } return NULL; 当各个服务器有相同的处理性能时,最小连接调度算法能把负载变化大的请求分布平滑到各个服务器上,所有处理时间比较长的请求不可能被发送到同一台服务器上。 但是,当各个服务器的处理能力不同时,该算法并不理想,因为TCP连接处理请求后会进入TIME_WAIT状态,TCP的TIME_WAIT一般为2分钟, 此时连接还占用服务器的资源,所以会出现这样情形,性能高的服务器已处理完所收到的连接,连接处于TIME_WAIT状态, 而性能低的服务器已经忙于处理所收到的连接,还不断地收到新的连接请求。 加权最小连接调度 : (Weighted Least-Connection Scheduling)算法是最小连接调度的超集,各个服务器用相应的权值表示其处理性能。 服务器的缺省权值为1,系统管理员可以动态地设置服务器的权值。加权最小连接调度在调度新连接时尽可能使服务器的已建立连接数和其权值成比例。 加权最小连接调度的算法流程如下: 加权最小连接调度的算法流程 假设有一组服务器S = {S0, S1, ..., Sn-1},W(Si)表示服务器Si的权值,C(Si)表示服务器Si的当前连接数。 所有服务器当前连接数的总和为CSUM = ΣC(Si) (i=0, 1, .. , n-1)。当前的新连接请求会被发送服务器Sm, 当且仅当服务器Sm满足以下条件 (C(Sm) / CSUM)/ W(Sm) = min { (C(Si) / CSUM) / W(Si)} (i=0, 1, . , n-1) 其中W(Si)不为零 因为CSUM在这一轮查找中是个常数,所以判断条件可以简化为 C(Sm) / W(Sm) = min { C(Si) / W(Si)} (i=0, 1, . , n-1) 其中W(Si)不为零 因为除法所需的CPU周期比乘法多,且在Linux内核中不允许浮点除法,服务器的权值都大于零, 所以判断条件C(Sm) / W(Sm) > C(Si) / W(Si) 可以进一步优化为C(Sm)*W(Si) > C(Si)* W(Sm)。 同时保证服务器的权值为零时,服务器不被调度。所以,算法只要执行以下流程。 for (m = 0; m < n; m++) { if (W(Sm) > 0) { for (i = m+1; i < n; i++) { if (C(Sm)*W(Si) > C(Si)*W(Sm)) m = i; } return Sm; } } return NULL; 基于局部性的最少链接调度 : (Locality-Based Least Connections Scheduling,以下简称为LBLC)算法是针对请求报文的目标IP地址的负载均衡调度,目前主要用于Cache集群系统, 因为在Cache集群中客户请求报文的目标IP地址是变化的。这里假设任何后端服务器都可以处理任一请求,算法的设计目标是在服务器的负载基本平衡情况下, 将相同目标IP地址的请求调度到同一台服务器,来提高各台服务器的访问局部性和主存Cache命中率,从而整个集群系统的处理能力。 LBLC调度算法先根据请求的目标IP地址找出该目标IP地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器; 若服务器不存在,或者该服务器超载且有服务器处于其一半的工作负载,则用“最少链接”的原则选出一个可用的服务器,将请求发送到该服务器。 该算法的详细流程如下: LBLC调度算法流程 假设有一组服务器S = {S0, S1, ..., Sn-1},W(Si)表示服务器Si的权值,C(Si)表示服务器Si的当前连接数。 ServerNode[dest_ip]是一个关联变量,表示目标IP地址所对应的服务器结点,一般来说它是通过Hash表实现的。 WLC(S)表示在集合S中的加权最小连接服务器,即前面的加权最小连接调度。Now为当前系统时间。 if (ServerNode[dest_ip] is NULL) { n = WLC(S); if (n is NULL) return NULL; ServerNode[dest_ip].server = n; } else { n = ServerNode[dest_ip].server; if ((n is dead) || (C(n) > W(n) && (there is a node m with C(m) < W(m)/2))) { n = WLC(S); if (n is NULL) return NULL; ServerNode[dest_ip].server = n; } } ServerNode[dest_ip].lastuse = Now; return n; 此外,对关联变量ServerNode[dest_ip]要进行周期性的垃圾回收(Garbage Collection),将过期的目标IP地址到服务器关联项进行回收。 过期的关联项是指哪些当前时间(实现时采用系统时钟节拍数jiffies)减去最近使用时间超过设定过期时间的关联项,系统缺省的设定过期时间为24小时。 带复制的基于局部性最少链接调度 : (Locality-Based Least Connections with Replication Scheduling,以下简称为LBLCR)算法也是针对目标IP地址的负载均衡,目前主要用于Cache集群系统。 它与LBLC算法的不同之处是它要维护从一个目标IP地址到一组服务器的映射,而LBLC算法维护从一个目标IP地址到一台服务器的映射。 对于一个“热门”站点的服务请求,一台Cache 服务器可能会忙不过来处理这些请求。 这时,LBLC调度算法会从所有的Cache服务器中按“最小连接”原则选出一台Cache服务器,映射该“热门”站点到这台Cache服务器, 很快这台Cache服务器也会超载,就会重复上述过程选出新的Cache服务器。 这样,可能会导致该“热门”站点的映像会出现在所有的Cache服务器上,降低了Cache服务器的使用效率。 LBLCR调度算法将“热门”站点映射到一组Cache服务器(服务器集合),当该“热门”站点的请求负载增加时,会增加集合里的Cache服务器,来处理不断增长的负载; 当该“热门”站点的请求负载降低时,会减少集合里的Cache服务器数目。 这样,该“热门”站点的映像不太可能出现在所有的Cache服务器上,从而提供Cache集群系统的使用效率。 LBLCR算法先根据请求的目标IP地址找出该目标IP地址对应的服务器组;按“最小连接”原则从该服务器组中选出一台服务器, 若服务器没有超载,将请求发送到该服务器;若服务器超载;则按“最小连接”原则从整个集群中选出一台服务器,将该服务器加入到服务器组中,将请求发送到该服务器。同时,当该服务器组有一段时间没有被修改,将最忙的服务器从服务器组中删除,以降低复制的程度。 LBLCR调度算法的流程如下: LBLCR调度算法流程 假设有一组服务器S = {S0, S1, ..., Sn-1},W(Si)表示服务器Si的权值,C(Si)表示服务器Si的当前连接数。 ServerSet[dest_ip]是一个关联变量,表示目标IP地址所对应的服务器集合,一般来说它是通过Hash表实现的。 WLC(S)表示在集合S中的加权最小连接服务器,即前面的加权最小连接调度; WGC(S)表示在集合S中的加权最大连接服务器。 Now为当前系统时间,lastmod表示集合的最近修改时间,T为对集合进行调整的设定时间。 if (ServerSet[dest_ip] is NULL) then { n = WLC(S); if (n is NULL) return NULL; add n into ServerSet[dest_ip]; } else { n = WLC(ServerSet[dest_ip]); if ((n is NULL) || (n is dead) || (C(n) > W(n) && (there is a node m with C(m) < W(m)/2))) { n = WLC(S); if (n is NULL) return NULL; add n into ServerSet[dest_ip]; } else if (|ServerSet[dest_ip]| > 1 && Now - ServerSet[dest_ip].lastmod > T) { m = WGC(ServerSet[dest_ip]); remove m from ServerSet[dest_ip]; } } ServerSet[dest_ip].lastuse = Now; if (ServerSet[dest_ip] changed) ServerSet[dest_ip].lastmod = Now; return n; 此外,对关联变量ServerSet[dest_ip]也要进行周期性的垃圾回收(Garbage Collection),将过期的目标IP地址到服务器关联项进行回收。 过期的关联项是指哪些当前时间(实现时采用系统时钟节拍数jiffies)减去最近使用时间(lastuse)超过设定过期时间的关联项,系统缺省的设定过期时间为24小时。 目标地址散列调度 : (Destination Hashing Scheduling)算法也是针对目标IP地址的负载均衡,但它是一种静态映射算法,通过一个散列(Hash)函数将一个目标IP地址映射到一台服务器。 目标地址散列调度算法先根据请求的目标IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载, 将请求发送到该服务器,否则返回空。该算法的流程如下: 目标地址散列调度算法流程 假设有一组服务器S = {S0, S1, ..., Sn-1},W(Si)表示服务器Si的权值,C(Si)表示服务器Si的当前连接数。 ServerNode[]是一个有256个桶(Bucket)的Hash表,一般来说服务器的数目会远小于256,当然表的大小也是可以调整的。 算法的初始化是将所有服务器顺序、循环地放置到ServerNode表中。若服务器的连接数目大于2倍的权值,则表示服务器已超载。 n = ServerNode[hashkey(dest_ip)]; if ((n is dead) || (W(n) == 0) || (C(n) > 2*W(n))) return NULL; return n; 在实现时,我们采用素数乘法Hash函数,通过乘以素数使得散列键值尽可能地达到较均匀的分布。所采用的素数乘法Hash函数如下: 素数乘法Hash函数 static inline unsigned hashkey(unsigned int dest_ip) { return (dest_ip* 2654435761UL) & HASH_TAB_MASK; } 其中,2654435761UL是2到2^32 (4294967296)间接近于黄金分割的素数, (sqrt(5) - 1) / 2 = 0.618033989 2654435761 / 4294967296 = 0.618033987 源地址散列调度 : (Source Hashing Scheduling)算法正好与目标地址散列调度算法相反,它根据请求的源IP地址, 作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。 它采用的散列函数与目标地址散列调度算法的相同。它的算法流程与目标地址散列调度算法的基本相似, 除了将请求的目标IP地址换成请求的源IP地址,所以这里不一一叙述。 在实际应用中,源地址散列调度和目标地址散列调度可以结合使用在防火墙集群中,它们可以保证整个系统的唯一出入口。 下面我们看算法具体实现. 每个调度算法的实现就是填写一个ip_vs_scheduler结构,在IPVS服务ip_vs_service结构中指向它即可,这样在连接到达该服务时,通过调度算法选择具体的目的主机。 每个算法作为一个单独的内核模块,可由内核配置是否包括该模块。 以下以最简单的rr算法来说明,该算法在net/ipv4/ipvs/ip_vs_rr.c中定义。 static struct ip_vs_scheduler ip_vs_rr_scheduler = { .name = "rr", /* name */ .refcnt = ATOMIC_INIT(0), .module = THIS_MODULE, .init_service = ip_vs_rr_init_svc, .done_service = ip_vs_rr_done_svc, .update_service = ip_vs_rr_update_svc, .schedule = ip_vs_rr_schedule, }; init_service()函数进行算法初始化,在虚拟服务ip_vs_service和调度器绑定时调用 (ip_vs_bind_scheduler()函数); done_service()函数进行算法的清除,在虚拟服务ip_vs_service和调度器解除绑定时调用(ip_vs_unbind_scheduler()函数); update_service()函数在目的服务器变化时调用(如 ip_vs_add_dest(), ip_vs_edit_dest()等函数); static int ip_vs_rr_init_svc(struct ip_vs_service *svc) { //其实RR算法也没有什么专用调度数据,sched_data被初始化为目的服务器链表头 svc->sched_data = &svc->destinations; return 0; } static int ip_vs_rr_done_svc(struct ip_vs_service *svc) { return 0; //没什么可释放的 } static int ip_vs_rr_update_svc(struct ip_vs_service *svc) { svc->sched_data = &svc->destinations; //sched_data重新指向服务器链表头 return 0; } static struct ip_vs_dest * ip_vs_rr_schedule(struct ip_vs_service *svc, const struct sk_buff *skb) { struct list_head *p, *q; struct ip_vs_dest *dest; write_lock(&svc->sched_lock); //p实际就是实际目的服务器的链表中的某一个节点 //sched_data保存的是上一次调度时用到的节点 p = (struct list_head *)svc->sched_data; p = p->next; q = p; do { /* skip list head */ if (q == &svc->destinations) { q = q->next; continue; } dest = list_entry(q, struct ip_vs_dest, n_list); //只要当前链表目的服务器不是超载而且该服务器权重不为0,就返回该节点 if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) && atomic_read(&dest->weight) > 0) /* HIT */ goto out; q = q->next; } while (q != p); write_unlock(&svc->sched_lock); return NULL; out: //保存要使用的节点到sched_data,下次调度时就会取下一个节点,实现轮询 svc->sched_data = q; write_unlock(&svc->sched_lock); return dest; } rr模块初始花函数是 static int __init ip_vs_rr_init(void) { INIT_LIST_HEAD(&ip_vs_rr_scheduler.n_list); //初始化连表 //注册调度其中就是把调度结构连接到一个全局连表中 //list_add(&scheduler->n_list, &ip_vs_schedulers); //查找时通过name字段,所以name字段不能为空 return register_ip_vs_scheduler(&ip_vs_rr_scheduler); } [/IPVS调度算法]
IPVS源代码分析----调度算法
最新推荐文章于 2019-09-10 19:52:38 发布