本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,严禁用于任何商业用途。
msn: yfydz_no1@hotmail.com
来源:http://yfydz.cublog.cn
5. 均衡调度算法
5.1 算法说明
均衡调度算法是IPVS实现均衡功能的理论精髓,其他各种东西都只算是程序技巧,所以优先介绍。IPVS支持8种静态均衡算法,以下文字直接拷贝自IPVS网站:
*************************quote start**********************************
2. 内核中的连接调度算法
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)
下面,我们先介绍这八种连接调度算法的工作原理和算法流程,会在以后的文章中描述怎么用它们。
2.1. 轮叫调度
轮叫调度(Round Robin Scheduling)算法就是以轮叫的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
在系统实现时,我们引入了一个额外条件,当服务器的权值为零时,表示该服务器不可用而不被调度。这样做的目的是将服务器切出服务(如屏蔽服务器故障和系统维护),同时与其他加权算法保持一致。所以,算法要作相应的改动,它的算法流程如下:
轮叫调度算法流程
假设有一组服务器S = {S0, S1, …, Sn-1},一个指示变量i表示上一次选择的
服务器,W(Si)表示服务器Si的权值。变量i被初始化为n-1,其中n > 0。
假设有一组服务器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;
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的轮叫调度优越很多。
2.2. 加权轮叫调度
加权轮叫调度(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初始化为零。
假设有一组服务器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;
}
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,所有的新连接都会被丢掉。加权轮叫调度也无需记录当前所有连接的状态,所以它也是一种无状态调度。
2.3. 最小连接调度
最小连接调度(Least-Connection Scheduling)算法是把新的连接请求分配到当前连接数最小的服务器。最小连接调度是一种动态调度算法,它通过服务器当前所活跃的连接数来估计服务器的负载情况。调度器需要记录各个服务器已建立连接的数目,当一个请求被调度到某台服务器,其连接数加1;当连接中止或超时,其连接数减一。
在系统实现时,我们也引入当服务器的权值为零时,表示该服务器不可用而不被调度,它的算法流程如下:
最小连接调度算法流程
假设有一组服务器S = {S0, S1, ..., Sn-1},W(Si)表示服务器Si的权值,
C(Si)表示服务器Si的当前连接数。
假设有一组服务器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;
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状态,而性能低的服务器已经忙于处理所收到的连接,还不断地收到新的连接请求。
2.4. 加权最小连接调度
加权最小连接调度(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)。同时保证服务器的权值为零时,服务器不被调
度。所以,算法只要执行以下流程。
权值都大于零,所以判断条件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;
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;
2.5. 基于局部性的最少链接调度
基于局部性的最少链接调度(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为当前系统
时间。
假设有一组服务器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) then {
n = WLC(S);
if (n is NULL) then return NULL;
ServerNode[dest_ip].server = n;
} else {
n = ServerNode[dest_ip].server;
if ((n is dead) OR
(C(n) > W(n) AND
there is a node m with C(m) < W(m)/2))) then {
n = WLC(S);
if (n is NULL) then return NULL;
ServerNode[dest_ip].server = n;
}
}
ServerNode[dest_ip].lastuse = Now;
return n;
n = WLC(S);
if (n is NULL) then return NULL;
ServerNode[dest_ip].server = n;
} else {
n = ServerNode[dest_ip].server;
if ((n is dead) OR
(C(n) > W(n) AND
there is a node m with C(m) < W(m)/2))) then {
n = WLC(S);
if (n is NULL) then return NULL;
ServerNode[dest_ip].server = n;
}
}
ServerNode[dest_ip].lastuse = Now;
return n;
此外,对关联变量ServerNode[dest_ip]要进行周期性的垃圾回收(Garbage Collection),将过期的目标IP地址到服务器关联项进行回收。过期的关联项是指哪些当前时间(实现时采用系统时钟节拍数jiffies)减去最近使用时间超过设定过期时间的关联项,系统缺省的设定过期时间为24小时。
2.6. 带复制的基于局部性最少链接调度
带复制的基于局部性最少链接调度(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为对集合进行调整的设定时间。
假设有一组服务器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) then return NULL;
add n into ServerSet[dest_ip];
} else {
n = WLC(ServerSet[dest_ip]);
if ((n is NULL) OR
(n is dead) OR
(C(n) > W(n) AND
there is a node m with C(m) < W(m)/2))) then {
n = WLC(S);
if (n is NULL) then return NULL;
add n into ServerSet[dest_ip];
} else
if (|ServerSet[dest_ip]| > 1 AND
Now - ServerSet[dest_ip].lastmod > T) then {
m = WGC(ServerSet[dest_ip]);
remove m from ServerSet[dest_ip];
}
}
ServerSet[dest_ip].lastuse = Now;
if (ServerSet[dest_ip] changed) then
ServerSet[dest_ip].lastmod = Now;
return n;
n = WLC(S);
if (n is NULL) then return NULL;
add n into ServerSet[dest_ip];
} else {
n = WLC(ServerSet[dest_ip]);
if ((n is NULL) OR
(n is dead) OR
(C(n) > W(n) AND
there is a node m with C(m) < W(m)/2))) then {
n = WLC(S);
if (n is NULL) then return NULL;
add n into ServerSet[dest_ip];
} else
if (|ServerSet[dest_ip]| > 1 AND
Now - ServerSet[dest_ip].lastmod > T) then {
m = WGC(ServerSet[dest_ip]);
remove m from ServerSet[dest_ip];
}
}
ServerSet[dest_ip].lastuse = Now;
if (ServerSet[dest_ip] changed) then
ServerSet[dest_ip].lastmod = Now;
return n;
此外,对关联变量ServerSet[dest_ip]也要进行周期性的垃圾回收(Garbage Collection),将过期的目标IP地址到服务器关联项进行回收。过期的关联项是指哪些当前时间(实现时采用系统时钟节拍数jiffies)减去最近使用时间(lastuse)超过设定过期时间的关联项,系统缺省的设定过期时间为24小时。
2.7. 目标地址散列调度
目标地址散列调度(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倍的权值,则表示服务器已超载。
假设有一组服务器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) OR
(W(n) == 0) OR
(C(n) > 2*W(n))) then
return NULL;
return n;
if ((n is dead) OR
(W(n) == 0) OR
(C(n) > 2*W(n))) then
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
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
2.8. 源地址散列调度
源地址散列调度(Source Hashing Scheduling)算法正好与目标地址散列调度算法相反,它根据请求的源IP地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。它采用的散列函数与目标地址散列调度算法的相同。它的算法流程与目标地址散列调度算法的基本相似,除了将请求的目标IP地址换成请求的源IP地址,所以这里不一一叙述。
在实际应用中,源地址散列调度和目标地址散列调度可以结合使用在防火墙集群中,它们可以保证整个系统的唯一出入口。
*************************quote end**********************************
5.2 算法具体实现
每个调度算法的实现就是填写一个ip_vs_scheduler结构,在IPVS服务ip_vs_service结构中指向它即可,这样在连接到达该服务时,通过调度算法选择具体的目的主机。每个算法作为一个单独的内核模块,可由内核配置是否包括该模块。
以下以最简单的rr算法来说明,该算法在net/ipv4/ipvs/ip_vs_rr.c中定义。
rr算法结构定义:
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()等函数);
在RR算法中,这三个函数都很简单:
static int ip_vs_rr_init_svc(struct ip_vs_service *svc)
{
// 其实RR算法也没有什么专用调度数据,sched_data被初始化为目的服务器链表头
svc->sched_data = &svc->destinations;
return 0;
}
{
// 其实RR算法也没有什么专用调度数据,sched_data被初始化为目的服务器链表头
svc->sched_data = &svc->destinations;
return 0;
}
static int ip_vs_rr_done_svc(struct ip_vs_service *svc)
{
// 空函数,因为对RR没什么可释放的
return 0;
}
static int ip_vs_rr_update_svc(struct ip_vs_service *svc)
{
// sched_data重新指向服务器链表头
svc->sched_data = &svc->destinations;
return 0;
}
而算法核心函数schedule()则是在ip_vs_schedule()函数中在新建IPVS连接前调用,找到真正的服务器提供服务,建立IPVS连接。
/*
* Round-Robin Scheduling
*/
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;
* Round-Robin Scheduling
*/
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;
IP_VS_DBG(6, "ip_vs_rr_schedule(): Scheduling...\n");
write_lock(&svc->sched_lock);
// p实际就是实际目的服务器的链表中的某一个节点
// sched_data保存的是上一次调度时用到的节点
p = (struct list_head *)svc->sched_data;
p = p->next;
// q是p的下一项
q = p;
do {
/* skip list head */
if (q == &svc->destinations) {
q = q->next;
continue;
}
// 只要当前链表目的服务器不是超载而且该服务器权重不为0,就返回该节点
dest = list_entry(q, struct ip_vs_dest, n_list);
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;
// p实际就是实际目的服务器的链表中的某一个节点
// sched_data保存的是上一次调度时用到的节点
p = (struct list_head *)svc->sched_data;
p = p->next;
// q是p的下一项
q = p;
do {
/* skip list head */
if (q == &svc->destinations) {
q = q->next;
continue;
}
// 只要当前链表目的服务器不是超载而且该服务器权重不为0,就返回该节点
dest = list_entry(q, struct ip_vs_dest, n_list);
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);
IP_VS_DBG(6, "RR: server %u.%u.%u.%u:%u "
"activeconns %d refcnt %d weight %d\n",
NIPQUAD(dest->addr), ntohs(dest->port),
atomic_read(&dest->activeconns),
atomic_read(&dest->refcnt), atomic_read(&dest->weight));
// 保存要使用的节点到sched_data,下次调度时就会取下一个节点,实现轮询
svc->sched_data = q;
write_unlock(&svc->sched_lock);
IP_VS_DBG(6, "RR: server %u.%u.%u.%u:%u "
"activeconns %d refcnt %d weight %d\n",
NIPQUAD(dest->addr), ntohs(dest->port),
atomic_read(&dest->activeconns),
atomic_read(&dest->refcnt), atomic_read(&dest->weight));
return dest;
}
}
5.3 系统调度
系统基本调度函数为ip_vs_schedule(),在TCP、UDP的conn_shedule中调用,而AH、ESP协议不管:
/* net/ipv4/ipv4/ip_vs_core.c */
/*
* IPVS main scheduling function
* It selects a server according to the virtual service, and
* creates a connection entry.
* Protocols supported: TCP, UDP
*/
struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, const struct sk_buff *skb)
{
struct ip_vs_conn *cp = NULL;
struct iphdr *iph = skb->nh.iph;
struct ip_vs_dest *dest;
__u16 _ports[2], *pptr;
* IPVS main scheduling function
* It selects a server according to the virtual service, and
* creates a connection entry.
* Protocols supported: TCP, UDP
*/
struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, const struct sk_buff *skb)
{
struct ip_vs_conn *cp = NULL;
struct iphdr *iph = skb->nh.iph;
struct ip_vs_dest *dest;
__u16 _ports[2], *pptr;
// TCP/UDP头指针,[0]为源端口,[1]为目的端口
pptr = skb_header_pointer(skb, iph->ihl*4,
sizeof(_ports), _ports);
if (pptr == NULL)
return NULL;
pptr = skb_header_pointer(skb, iph->ihl*4,
sizeof(_ports), _ports);
if (pptr == NULL)
return NULL;
/*
* Persistent service
*/
// 如果是固定服务,调用ip_vs_sched_persist()
if (svc->flags & IP_VS_SVC_F_PERSISTENT)
return ip_vs_sched_persist(svc, skb, pptr);
* Persistent service
*/
// 如果是固定服务,调用ip_vs_sched_persist()
if (svc->flags & IP_VS_SVC_F_PERSISTENT)
return ip_vs_sched_persist(svc, skb, pptr);
/*
* Non-persistent service
*/
if (!svc->fwmark && pptr[1] != svc->port) {
// 目的端口不等于服务端口,IPVS不处理该包
if (!svc->port)
IP_VS_ERR("Schedule: port zero only supported "
"in persistent services, "
"check your ipvs configuration\n");
return NULL;
}
// 调用调度器的调度函数获取一个目的服务器指针
dest = svc->scheduler->schedule(svc, skb);
if (dest == NULL) {
IP_VS_DBG(1, "Schedule: no dest found.\n");
return NULL;
}
* Non-persistent service
*/
if (!svc->fwmark && pptr[1] != svc->port) {
// 目的端口不等于服务端口,IPVS不处理该包
if (!svc->port)
IP_VS_ERR("Schedule: port zero only supported "
"in persistent services, "
"check your ipvs configuration\n");
return NULL;
}
// 调用调度器的调度函数获取一个目的服务器指针
dest = svc->scheduler->schedule(svc, skb);
if (dest == NULL) {
IP_VS_DBG(1, "Schedule: no dest found.\n");
return NULL;
}
/*
* Create a connection entry.
*/
// 新建一个IPVS连接
cp = ip_vs_conn_new(iph->protocol,
iph->saddr, pptr[0],
iph->daddr, pptr[1],
dest->addr, dest->port?dest->port:pptr[1],
0,
dest);
if (cp == NULL)
return NULL;
* Create a connection entry.
*/
// 新建一个IPVS连接
cp = ip_vs_conn_new(iph->protocol,
iph->saddr, pptr[0],
iph->daddr, pptr[1],
dest->addr, dest->port?dest->port:pptr[1],
0,
dest);
if (cp == NULL)
return NULL;
IP_VS_DBG(6, "Schedule fwd:%c c:%u.%u.%u.%u:%u v:%u.%u.%u.%u:%u "
"d:%u.%u.%u.%u:%u conn->flags:%X conn->refcnt:%d\n",
ip_vs_fwd_tag(cp),
NIPQUAD(cp->caddr), ntohs(cp->cport),
NIPQUAD(cp->vaddr), ntohs(cp->vport),
NIPQUAD(cp->daddr), ntohs(cp->dport),
cp->flags, atomic_read(&cp->refcnt));
// 服务和连接相关计数器统计
ip_vs_conn_stats(cp, svc);
return cp;
}
"d:%u.%u.%u.%u:%u conn->flags:%X conn->refcnt:%d\n",
ip_vs_fwd_tag(cp),
NIPQUAD(cp->caddr), ntohs(cp->cport),
NIPQUAD(cp->vaddr), ntohs(cp->vport),
NIPQUAD(cp->daddr), ntohs(cp->dport),
cp->flags, atomic_read(&cp->refcnt));
// 服务和连接相关计数器统计
ip_vs_conn_stats(cp, svc);
return cp;
}
固定调度函数,用在多连接协议处理中将子连接与主连接挂钩:
/*
* IPVS persistent scheduling function
* It creates a connection entry according to its template if exists,
* or selects a server and creates a connection entry plus a template.
* Locking: we are svc user (svc->refcnt), so we hold all dests too
* Protocols supported: TCP, UDP
*/
static struct ip_vs_conn *
ip_vs_sched_persist(struct ip_vs_service *svc,
const struct sk_buff *skb,
__u16 ports[2])
{
struct ip_vs_conn *cp = NULL;
struct iphdr *iph = skb->nh.iph;
struct ip_vs_dest *dest;
struct ip_vs_conn *ct;
__u16 dport; /* destination port to forward */
__u32 snet; /* source network of the client, after masking */
* IPVS persistent scheduling function
* It creates a connection entry according to its template if exists,
* or selects a server and creates a connection entry plus a template.
* Locking: we are svc user (svc->refcnt), so we hold all dests too
* Protocols supported: TCP, UDP
*/
static struct ip_vs_conn *
ip_vs_sched_persist(struct ip_vs_service *svc,
const struct sk_buff *skb,
__u16 ports[2])
{
struct ip_vs_conn *cp = NULL;
struct iphdr *iph = skb->nh.iph;
struct ip_vs_dest *dest;
struct ip_vs_conn *ct;
__u16 dport; /* destination port to forward */
__u32 snet; /* source network of the client, after masking */
/* Mask saddr with the netmask to adjust template granularity */
// 网络部分地址
snet = iph->saddr & svc->netmask;
// 网络部分地址
snet = iph->saddr & svc->netmask;
IP_VS_DBG(6, "p-schedule: src %u.%u.%u.%u:%u dest %u.%u.%u.%u:%u "
"mnet %u.%u.%u.%u\n",
NIPQUAD(iph->saddr), ntohs(ports[0]),
NIPQUAD(iph->daddr), ntohs(ports[1]),
NIPQUAD(snet));
"mnet %u.%u.%u.%u\n",
NIPQUAD(iph->saddr), ntohs(ports[0]),
NIPQUAD(iph->daddr), ntohs(ports[1]),
NIPQUAD(snet));
/*
* As far as we know, FTP is a very complicated network protocol, and
* it uses control connection and data connections. For active FTP,
* FTP server initialize data connection to the client, its source port
* is often 20. For passive FTP, FTP server tells the clients the port
* that it passively listens to, and the client issues the data
* connection. In the tunneling or direct routing mode, the load
* balancer is on the client-to-server half of connection, the port
* number is unknown to the load balancer. So, a conn template like
* <caddr, 0, vaddr, 0, daddr, 0> is created for persistent FTP
* service, and a template like <caddr, 0, vaddr, vport, daddr, dport>
* is created for other persistent services.
*/
if (ports[1] == svc->port) {
// 数据包目的端口是服务端口,正向请求子连接
/* Check if a template already exists */
// 查找连接模板, 专门区分了是否是FTP端口,所以程序在此没有扩展性
// 源地址用的是网络部分地址,源端口用0
// 所谓模板,应该就是指主连接,相当于netfilter跟踪子连接时端口部分不进行限制
// 不过这里IPVS跟踪子连接作的没有netfilter好
if (svc->port != FTPPORT)
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, ports[1]);
else
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, 0);
* As far as we know, FTP is a very complicated network protocol, and
* it uses control connection and data connections. For active FTP,
* FTP server initialize data connection to the client, its source port
* is often 20. For passive FTP, FTP server tells the clients the port
* that it passively listens to, and the client issues the data
* connection. In the tunneling or direct routing mode, the load
* balancer is on the client-to-server half of connection, the port
* number is unknown to the load balancer. So, a conn template like
* <caddr, 0, vaddr, 0, daddr, 0> is created for persistent FTP
* service, and a template like <caddr, 0, vaddr, vport, daddr, dport>
* is created for other persistent services.
*/
if (ports[1] == svc->port) {
// 数据包目的端口是服务端口,正向请求子连接
/* Check if a template already exists */
// 查找连接模板, 专门区分了是否是FTP端口,所以程序在此没有扩展性
// 源地址用的是网络部分地址,源端口用0
// 所谓模板,应该就是指主连接,相当于netfilter跟踪子连接时端口部分不进行限制
// 不过这里IPVS跟踪子连接作的没有netfilter好
if (svc->port != FTPPORT)
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, ports[1]);
else
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, 0);
if (!ct || !ip_vs_check_template(ct)) {
// 找不到连接模板或者连接模板的目的服务器不可用
/*
* No template found or the dest of the connection
* template is not available.
*/
// 使用调度器调度找一个服务器
dest = svc->scheduler->schedule(svc, skb);
if (dest == NULL) {
IP_VS_DBG(1, "p-schedule: no dest found.\n");
return NULL;
}
// 找不到连接模板或者连接模板的目的服务器不可用
/*
* No template found or the dest of the connection
* template is not available.
*/
// 使用调度器调度找一个服务器
dest = svc->scheduler->schedule(svc, skb);
if (dest == NULL) {
IP_VS_DBG(1, "p-schedule: no dest found.\n");
return NULL;
}
/*
* Create a template like <protocol,caddr,0,
* vaddr,vport,daddr,dport> for non-ftp service,
* and <protocol,caddr,0,vaddr,0,daddr,0>
* for ftp service.
*/
// 建立一个新连接模板,如果是FTP服务,目的端口不确定
if (svc->port != FTPPORT)
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr,
ports[1],
dest->addr, dest->port,
IP_VS_CONN_F_TEMPLATE,
dest);
else
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr, 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
if (ct == NULL)
return NULL;
* Create a template like <protocol,caddr,0,
* vaddr,vport,daddr,dport> for non-ftp service,
* and <protocol,caddr,0,vaddr,0,daddr,0>
* for ftp service.
*/
// 建立一个新连接模板,如果是FTP服务,目的端口不确定
if (svc->port != FTPPORT)
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr,
ports[1],
dest->addr, dest->port,
IP_VS_CONN_F_TEMPLATE,
dest);
else
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr, 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
if (ct == NULL)
return NULL;
ct->timeout = svc->timeout;
} else {
/* set destination with the found template */
// 找到连接模板,目的服务器用连接的目的服务器
dest = ct->dest;
}
// 目的端口为目的服务器端口
dport = dest->port;
} else {
// 数据包目的端口不是服务端口,可能是反向请求的子连接
/*
* Note: persistent fwmark-based services and persistent
* port zero service are handled here.
* fwmark template: <IPPROTO_IP,caddr,0,fwmark,0,daddr,0>
* port zero template: <protocol,caddr,0,vaddr,0,daddr,0>
*/
// 找相关连接模板,此时用的端口都是0
if (svc->fwmark)
ct = ip_vs_ct_in_get(IPPROTO_IP, snet, 0,
htonl(svc->fwmark), 0);
else
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, 0);
} else {
/* set destination with the found template */
// 找到连接模板,目的服务器用连接的目的服务器
dest = ct->dest;
}
// 目的端口为目的服务器端口
dport = dest->port;
} else {
// 数据包目的端口不是服务端口,可能是反向请求的子连接
/*
* Note: persistent fwmark-based services and persistent
* port zero service are handled here.
* fwmark template: <IPPROTO_IP,caddr,0,fwmark,0,daddr,0>
* port zero template: <protocol,caddr,0,vaddr,0,daddr,0>
*/
// 找相关连接模板,此时用的端口都是0
if (svc->fwmark)
ct = ip_vs_ct_in_get(IPPROTO_IP, snet, 0,
htonl(svc->fwmark), 0);
else
ct = ip_vs_ct_in_get(iph->protocol, snet, 0,
iph->daddr, 0);
if (!ct || !ip_vs_check_template(ct)) {
// 没找到连接模板或连接模板目的服务不可用
/*
* If it is not persistent port zero, return NULL,
* otherwise create a connection template.
*/
if (svc->port)
return NULL;
//
// 使用调度器调度找一个服务器
dest = svc->scheduler->schedule(svc, skb);
if (dest == NULL) {
IP_VS_DBG(1, "p-schedule: no dest found.\n");
return NULL;
}
// 没找到连接模板或连接模板目的服务不可用
/*
* If it is not persistent port zero, return NULL,
* otherwise create a connection template.
*/
if (svc->port)
return NULL;
//
// 使用调度器调度找一个服务器
dest = svc->scheduler->schedule(svc, skb);
if (dest == NULL) {
IP_VS_DBG(1, "p-schedule: no dest found.\n");
return NULL;
}
/*
* Create a template according to the service
*/
// 建立一个新连接模板
if (svc->fwmark)
ct = ip_vs_conn_new(IPPROTO_IP,
snet, 0,
htonl(svc->fwmark), 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
else
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr, 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
if (ct == NULL)
return NULL;
* Create a template according to the service
*/
// 建立一个新连接模板
if (svc->fwmark)
ct = ip_vs_conn_new(IPPROTO_IP,
snet, 0,
htonl(svc->fwmark), 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
else
ct = ip_vs_conn_new(iph->protocol,
snet, 0,
iph->daddr, 0,
dest->addr, 0,
IP_VS_CONN_F_TEMPLATE,
dest);
if (ct == NULL)
return NULL;
ct->timeout = svc->timeout;
} else {
/* set destination with the found template */
// 找到连接模板,目的服务器用连接的目的服务器
dest = ct->dest;
}
dport = ports[1];
}
} else {
/* set destination with the found template */
// 找到连接模板,目的服务器用连接的目的服务器
dest = ct->dest;
}
dport = ports[1];
}
/*
* Create a new connection according to the template
*/
// 建立一个新连接
cp = ip_vs_conn_new(iph->protocol,
iph->saddr, ports[0],
iph->daddr, ports[1],
dest->addr, dport,
0,
dest);
if (cp == NULL) {
ip_vs_conn_put(ct);
return NULL;
}
* Create a new connection according to the template
*/
// 建立一个新连接
cp = ip_vs_conn_new(iph->protocol,
iph->saddr, ports[0],
iph->daddr, ports[1],
dest->addr, dport,
0,
dest);
if (cp == NULL) {
ip_vs_conn_put(ct);
return NULL;
}
/*
* Add its control
*/
// 将连接模块作为新连接的主连接
ip_vs_control_add(cp, ct);
// get的时候增加了连接模板ct的引用计数,现在减少之
ip_vs_conn_put(ct);
* Add its control
*/
// 将连接模块作为新连接的主连接
ip_vs_control_add(cp, ct);
// get的时候增加了连接模板ct的引用计数,现在减少之
ip_vs_conn_put(ct);
// 连接服务计数器统计
ip_vs_conn_stats(cp, svc);
return cp;
}
ip_vs_conn_stats(cp, svc);
return cp;
}