轮询算法分为简单轮询(Round-Robin)和加权轮询(Weighted-Round-Robin)。
简单轮询(Round-Robin)
简单轮询是最简单的一种负载均衡算法,其把来自用户的请求轮流分配给内部的服务器:从服务器1开始,直到服务器N,然后重新开始循环。
public class SimpleRoundRobinLoadBalance implements LoadBalance{
private AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public ServerInfo select(List<ServerInfo> serverInfos) {
if(serverInfos==null || serverInfos.size()==0){
return null;
}
// 获取当前的调用编号(每来一次请求则累加1)
int sequence = atomicInteger.getAndIncrement();
// 调用编号与服务器个数取余
int index = sequence % serverInfos.size();
return serverInfos.get(index);
}
}
可以看到,简单轮询只需要维护一个递增的请求编号即可,每来1次请求则递增1,为保证线程安全,将其设置为AtomicInteger类型。
简单轮询算法假设所有服务器的性能均相同,不关心每台服务器的当前连接数和响应速度。当请求服务间隔时间变化比较大时,简单轮询算法容易导致服务器间的负载不平衡,故简单轮询适用于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。
加权轮询(Weighted-Round-Robin)
现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。
加权轮询的实现方式就比较多样化:
迭代轮询
其基本思想是用请求序号和所有服务器的总权重求余数mod,然后在服务器间不断循环遍历,每遍历一个服务器,如果该服务器的当前权重>0,则将该服务器的权重减1,同时mod减1,直到mod降为0之后,若当前遍历所在的服务器权重>0,则返回该服务器。
下述代码为迭代轮询的简单实现:
public class WeightedRoundRobinLoadBalance1 implements LoadBalance{
public static final String NAME = "roundrobin";
private AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public ServerInfo select(List<ServerInfo> serverInfos) {
// 获取当前的调用编号(每来一次请求则累加1)
int sequence = atomicInteger.getAndIncrement();
int totalWeight = 0;
int maxWeight = 0;
int minWeight = 0;
Map<ServerInfo, IntegerWrapper> weightMap = new HashMap<>();
for(ServerInfo serverInfo: serverInfos){
totalWeight += serverInfo.getWeight();
maxWeight = Math.max(maxWeight, serverInfo.getWeight());
minWeight = Math.min(minWeight, serverInfo.getWeight());
weightMap.put(serverInfo, new IntegerWrapper(serverInfo.getWeight()));
}
int index = sequence % (serverInfos.size());
if(minWeight < maxWeight && minWeight >= 0){
int mod = sequence % totalWeight;
for(int i=0; i<maxWeight; i++){
for(Map.Entry<ServerInfo, IntegerWrapper> entry: weightMap.entrySet()){
IntegerWrapper value = entry.getValue();
// 如果 mod = 0,且权重大于0,返回相应的服务器
if(mod == 0 && value.value > 0){
return entry.getKey();
}
// mod != 0,且权重大于0,此时对权重和 mod 分别进行自减操作
if(value.value > 0){
mod--;
value.decrement();
}
}
}
}
return serverInfos.get(index);
}
// IntegerWrapper 是一个 int 包装类,主要包含了一个自减方法。
// 包装类主要方便更新map中的value值
private static final class IntegerWrapper {
private int value;
public IntegerWrapper() {
}
public IntegerWrapper(int value) {
this.value = value;
}
public void decrement() {
this.value--;
}
}
}
下面以1个实际例子说明:
假设3台服务器A、B、C的权重分别为[5, 2, 1],则总权重为5+2+1=8
第1次请求 mod=0%8=0 进行0次递减 权重最后变为[[5], 2, 1] 5>0 返回A
第2次请求 mod=1%8=1 进行1次递减 权重最后变为[4, [2], 1] 2>0 返回B
第3次请求 mod=2%8=2 进行2次递减 权重最后变为[4, 1, [1]] 1>0 返回C
第4次请求 mod=3%8=3 进行3次递减 权重最后变为[[4], 1, 0] 3>0 返回A
第5次请求 mod=4%8=4 进行4次递减 权重最后变为[3, [1], 0] 1>0 返回B
第6次请求 mod=5%8=5 进行5次递减 权重最后变为[[3], 0, 0] 3>0 返回A
第7次请求 mod=6%8=6 进行6次递减 权重最后变为[[2], 0, 0] 2>0 返回A
第8次请求 mod=7%8=7 进行7次递减 权重最后变为[[1], 0, 0] 1>0 返回A
第1次请求 mod=8%8=0 进行0次递减 权重最后变为[[5], 2, 1] 5>0 返回A
...
该方法需要在mod == 0 && v.getValue() > 0 条件成立的情况下才会被返回相应的服务器。假如mod很大,比如10000,50000,甚至更大时,select方法需要进行很多次计算才能将mod减为0。由此可知,select的效率与mod有关,时间复杂度为O(mod)。mod又受最大权重maxWeight的影响,因此当某个服务提供者配置了非常大的权重,此时该方法会产生比较严重的性能问题。
双层遍历轮询
所谓双层遍历,外层为0->最大权重,内层为0->服务器个数。
public class WeightedRoundRobinLoadBalance2 implements LoadBalance{
public static final String NAME = "roundrobin";
private AtomicInteger atomicInteger = new AtomicInteger(0);
private AtomicInteger indexSeq = new AtomicInteger(0);
@Override
public ServerInfo select(List<ServerInfo> serverInfos) {
int maxWeight = 0;
int minWeight = 0;
for(ServerInfo serverInfo: serverInfos){
maxWeight = Math.max(maxWeight, serverInfo.getWeight());
minWeight = Math.min(minWeight, serverInfo.getWeight());
}
if(minWeight < maxWeight && minWeight >= 0){
while(true){
int index = indexSeq.getAndIncrement() % serverInfos.size();
if(index == 0){
atomicInteger.getAndIncrement();
}
int currentWeight = atomicInteger.get() % maxWeight;
if(serverInfos.get(index).getWeight() > currentWeight){
return serverInfos.get(index);
}
}
}
return serverInfos.get(atomicInteger.getAndIncrement() % serverInfos.size());
}
}
下面举例说明:
假设服务器 [A, B, C] 对应权重 [5, 2, 1]。
第一轮循环,currentWeight = 1,权重大于1的有A和B,从左向右依次返回A、B
第二轮循环,currentWeight = 2,权重大于2的仅有A,直接返回A
第三轮循环,currentWeight = 3,权重大于3的仅有A,直接返回A
第四轮循环,currentWeight = 4,权重大于4的仅有A,直接返回A
第五轮循环,currentWeight = 0,权重大于0的有A、B和C,从左向右依次返回 A, B, C
该负载均衡器需要维护2个状态变量,sequence和indexSeq,sequence用于外层遍历,当indexSeq值为0,则sequence执行累加操作。
双层遍历轮询仍存在问题,在某些情况下选出的服务器序列不够均匀。比如,服务器 [A, B, C] 对应权重 [5, 1, 1]。进行7次负载均衡后,选择出来的序列为 [A, A, A, A, A, B, C]。前5个请求全部都落在了服务器 A上,这将会使服务器 A 短时间内接收大量的请求,压力陡增。而 B 和 C 此时无请求,处于空闲状态。而我们期望的结果是这样的 [A, A, B, A, C, A, A],不同服务器可以穿插获取请求。
平滑加权轮询负载均衡
Nginx 的平滑加权轮询负载均衡。每个服务器对应两个权重,分别为 weight 和 currentWeight。其中 weight 是固定的,currentWeight 会动态调整,初始值为0。当有新的请求进来时,遍历服务器列表,让它的 currentWeight 加上自身权重。遍历完成后,找到最大的 currentWeight,并将其减去权重总和,然后返回相应的服务器即可。
上面描述不是很好理解,下面还是举例进行说明。这里仍然使用服务器 [A, B, C] 对应权重 [5, 1, 1] 的例子说明,现在有7个请求依次进入负载均衡逻辑,选择过程如下:
请求编号 | currentWeight 数组 | 选择结果 | 减去权重总和后的 currentWeight 数组 |
---|---|---|---|
1 | [5, 1, 1] | A | [-2, 1, 1] |
2 | [3, 2, 2] | A | [-4, 2, 2] |
3 | [1, 3, 3] | B | [1, -4, 3] |
4 | [6, -3, 4] | A | [-1, -3, 4] |
5 | [4, -2, 5] | C | [4, -2, -2] |
6 | [9, -1, -1] | A | [2, -1, -1] |
7 | [7, 0, 0] | A | [0, 0, 0] |
如上,经过平滑性处理后,得到的服务器序列为 [A, A, B, A, C, A, A],相比之前的序列 [A, A, A, A, A, B, C],分布性要好一些。初始情况下 currentWeight = [0, 0, 0],第7个请求处理完后,currentWeight 再次变为 [0, 0, 0]。
以上就是平滑加权轮询的计算过程,接下来,我们来看看如何实现上面的计算过程的。
public class WeightedRoundRobinLoadBalance3 implements LoadBalance{
public static final String NAME = "roundrobin";
private Map<ServerInfo, WeightedRoundRobin> map = new ConcurrentHashMap<>();
@Override
public ServerInfo select(List<ServerInfo> serverInfos) {
int maxWeight = 0;
int minWeight = 0;
int totalWeight = 0;
for(ServerInfo serverInfo: serverInfos){
int weight = serverInfo.getWeight();
maxWeight = Math.max(maxWeight, weight);
minWeight = Math.min(minWeight, weight);
totalWeight += weight;
}
int maxCurrentWeight = 0;
ServerInfo selectedServerInfo = null;
WeightedRoundRobin selectedWeightedRoundRobin = null;
for(ServerInfo serverInfo: serverInfos){
int weight = serverInfo.getWeight();
if(map.get(serverInfo) == null){
map.put(serverInfo, new WeightedRoundRobin(weight, 0));
}
WeightedRoundRobin weightedRoundRobin = map.get(serverInfo);
weightedRoundRobin.setCurrent(weightedRoundRobin.getCurrent() + weightedRoundRobin.getWeight());
if(weightedRoundRobin.getCurrent() > maxCurrentWeight){
maxCurrentWeight = weightedRoundRobin.getCurrent();
// 更新选中的
selectedServerInfo = serverInfo;
selectedWeightedRoundRobin = weightedRoundRobin;
}
}
selectedWeightedRoundRobin.setCurrent(selectedWeightedRoundRobin.getCurrent() - totalWeight);
return selectedServerInfo;
}
private class WeightedRoundRobin{
private int weight;
private int current;
public WeightedRoundRobin() {
}
public WeightedRoundRobin(int weight, int current) {
this.weight = weight;
this.current = current;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public int getCurrent() {
return current;
}
public void setCurrent(int current) {
this.current = current;
}
@Override
public String toString() {
return "WeightedRoundRobin{" +
"weight=" + weight +
", current=" + current +
'}';
}
}
}