一、轮询
轮询算法是最简单的一种负载均衡算法。它的原理是把来自用户的请求轮流分配给内部的服务器:从服务器1开始,直到服务器N,然后重新开始循环。
特点:优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。由于该算法中每个请求按时间顺序逐一分配到不同的服务器处理,因此适用于服务器性能相近的集群情况,其中每个服务器承载相同的负载。但对于服务器性能不同的集群而言,该算法容易引发资源分配不合理等问题。
j = i;
do
{
j = (j + 1) mod n;
i = j;
return Si;
} while (j != i);
return NULL;
二、加权轮询
在加权轮询中,每个服务器会有各自的 weight
。一般情况下,weight
的值越大意味着该服务器的性能越好,可以承载更多的请求。该算法中,客户端的请求按权值比例分配,当一个请求到达时,优先为其分配权值最大的服务器。
特点:加权轮询可以应用于服务器性能不等的集群中,使资源分配更加合理化。
//简单的Nginx负载均衡
http {
upstream cluster {
server a weight=1;
server b weight=2;
server c weight=4;
}
...
}
1.普通加权轮询
max_weight(服务器中最大权值),cur_weight(当服务器的权值),index(下标),gcd(最大公约数)
先将cur_weight = max_weight, 从下标0开始进行查询服务器序列,找到满足ss[index] >=cur_weight 的服务器序列中的对应的服务器下标并返回。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
typedef struct
{
int weight;
char name[2];
}server;
int getsum(int *set, int size)
{
int i = 0;
int res = 0;
for (i = 0; i < size; i++)
res += set[i];
return res;
}
int gcd(int a, int b)
{
int c;
while(b)
{
c = b;
b = a % b;
a = c;
}
return a;
}
int getgcd(int *set, int size)
{
int i = 0;
int res = set[0];
for (i = 1; i < size; i++)
res = gcd(res, set[i]);
return res;
}
int getmax(int *set, int size)
{
int i = 0;
int res = set[0];
for (i = 1; i < size; i++)
{
if (res < set[i]) res = set[i];
}
return res;
}
int lb_wrr__getwrr(server *ss, int size, int gcd, int maxweight, int *i, int *cw)
{
while (1)
{
*i = (*i + 1) % size;
if (*i == 0)
{
*cw = *cw - gcd;
if (*cw <= 0)
{
*cw = maxweight;
if (*cw == 0)
{
return -1;
}
}
}
if (ss[*i].weight >= *cw)
{
return *i;
}
}
}
void wrr(server *ss, int *weights, int size)
{
int i = 0;
int gcd = getgcd(weights, size);
int max = getmax(weights, size);
int sum = getsum(weights, size);
int index = -1;
int curweight = 0;
for (i = 0; i < sum; i++)
{
lb_wrr__getwrr(ss, size, gcd, max, &(index), &(curweight));
printf("%s(%d) ", ss[index].name, ss[index].weight);
}
printf("\n");
return;
}
server *initServers(char **names, int *weights, int size)
{
int i = 0;
server *ss = calloc(size, sizeof(server));
for (i = 0; i < size; i++)
{
ss[i].weight = weights[i];
memcpy(ss[i].name, names[i], 2);
}
return ss;
}
int main()
{
int i = 0;
int weights[] = {1, 2, 4};
char *names[] = {"a", "b", "c"};
int size = sizeof(weights) / sizeof(int);
server *ss = initServers(names, weights, size);
printf("server is ");
for (i = 0; i < size; i++)
{
printf("%s(%d) ", ss[i].name, ss[i].weight);
}
printf("\n");
printf("\nwrr sequence is ");
wrr(ss, weights, size);
return;
}
过程:
(1).i=-1;cur_weight= 0,进入循环,i=2时返回,此时cur_weight= max_weight;
(2)i= 2 ,cur_weight= max_weight进入,cw =3, i = 2时返回。
(3)i= 2,cw = 3进入,i = 1 ,cw = 2返回
(4)i= 1,cw = 2进入, i= 2, cw = 2返回
(5)i= 2, cw = 2 进入,i = 0, cw = 1返回
(6)i = 0, cw = 1进入, i= 1, cw = 1返回
(7)i = 1, cw = 1进入, i = 2 , cw =1.
每次cw-1就是一次新的服务器序列的新的遍历查询。
2.平滑的加权轮询
该算法的原理如下:
每个服务器都有两个权重变量:
a:weight,配置文件中指定的该服务器的权重,这个值是固定不变的;
b:current_weight,服务器目前的权重。一开始为0,之后会动态调整。
(注意total是不变的,因为每次减掉一个节点total后,每个节点都会加一次自身权重,所以总共又增加了一个total)每次选出节点后,都是裁掉这个节点权重一个total;自身权重越大的节点增长越快,那么比其他节点大的几率就越高,被选中的机会就越多;而自身权重比较低的,自身current_weight增长比较慢,所以比其他大的几率小,被选中的机会就少。(挨的刀子是一样大的,但是哪棵韭菜长得快,哪棵就更容易挨刀子;东北大米年收一次,海南能收3次)
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
typedef struct
{
int weight;
int cur_weight;
char name[3];
}server;
int getsum(int *set, int size)
{
int i = 0;
int res = 0;
for (i = 0; i < size; i++)
res += set[i];
return res;
}
server *initServers(char **names, int *weights, int size)
{
int i = 0;
server *ss = calloc(size+1, sizeof(server));
for (i = 0; i < size; i++)
{
ss[i].weight = weights[i];
memcpy(ss[i].name, names[i], 3);
ss[i].cur_weight = 0;
}
return ss;
}
int getNextServerIndex(server *ss, int size)
{
int i ;
int index = -1;
int total = 0;
for (i = 0; i < size; i++)
{
ss[i].cur_weight += ss[i].weight;
total += ss[i].weight;
if (index == -1 || ss[index].cur_weight < ss[i].cur_weight)
{
index = i;
}
}
ss[index].cur_weight -= total;
return index;
}
void wrr_nginx(server *ss, int *weights, int size)
{
int i = 0;
int index = -1;
int sum = getsum(weights, size);
for (i = 0; i < sum; i++)
{
index = getNextServerIndex(ss, size);
printf("%s(%d) ", ss[index].name, ss[index].weight);
}
printf("\n");
}
int main()
{
int i = 0;
int weights[] = {4, 2, 1};
char *names[] = {"a", "b", "c"};
int size = sizeof(weights) / sizeof(int);
server *ss = initServers(names, weights, size);
printf("server is ");
for (i = 0; i < size; i++)
{
printf("%s(%d) ", ss[i].name, ss[i].weight);
}
printf("\n");
printf("\nwrr_nginx sequence is ");
wrr_nginx(ss, weights, size);
return;
}
//上述代码的运行结果如下:
server is a(4) b(2) c(1)
wrr_nginx sequence is a(4) b(2) a(4) c(1) a(4) b(2) a(4)
//如果服务器配置为:{a(5),b(1), c(1)},则运行结果如下:
server is a(5) b(1) c(1)
wrr_nginx sequence is a(5) a(5) b(1) a(5) c(1) a(5) a(5)
三、ip哈希
ip_hash
依据发出请求的客户端 IP 的 hash 值来分配服务器,该算法可以保证同 IP 发出的请求映射到同一服务器,或者具有相同 hash 值的不同 IP 映射到同一服务器。使用ip_hash这种负载均衡以后,可以保证用户的每一次会话都只会发送到同一台特定的Tomcat里面,它的session不会跨到其他的tomcat里面去的;
特点:该算法在一定程度上解决了集群部署环境下 Session 不共享的问题。
(一)、 如何在nginx里面使用ip_hash?
如图所示:
直接添加ip_hash关键字即可,后续同一ip的访问将只会请求同一个服务器。
注意事项
- 一旦使用了ip_hash,当我们需要移除一台服务器的时候,不能直接删除这个配置项,而是需要在这台服务器配置后面加上关键字down,表示不可用;
- 因为如果直接移除配置项,会导致hash算法发生更改,后续所有的请求都会发生混乱;
⼀致性哈希算法-ConsistentHashLoadBalance
四、其他算法
(一)、URL hash url_hash
是根据请求的 URL 的 hash 值来分配服务器。该算法的特点是,相同 URL 的请求会分配给固定的服务器,当存在缓存的时候,效率一般较高。然而 Nginx 默认不支持这种负载均衡算法,需要依赖第三方库。
(二)、最小连接数(Least Connections)
假设共有 台服务器,当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。连接数可以理解为当前处理的请求数。
前⾯⼏种⽅法主要⽬标是使服务端分配到的调⽤次数尽量均衡,但是实际情况是这样吗?调⽤次数相同,服务器的负载就均衡吗?当然不是,这⾥还要考虑每次调⽤的时间,⽽最⼩活跃数算法则是解决这种问题的。
活跃调⽤数越⼩,表明该服务提供者效率越⾼,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提供者。
特点:根据候选服务器当前的请求连接数,动态分配。
场景:适用于对系统负载较为敏感或请求连接时长相差较大的场景。
参考链接