负载均衡算法1--轮询

轮询算法分为简单轮询(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 +
                    '}';
        }

    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343