负载均衡算法介绍及代码实现

708 阅读4分钟

这是我参与8月更文挑战的第30天,活动详情查看: 8月更文挑战

负载均衡我们应该听的比较多,网关,Nginx,Dubbo这些组件都会用到负载均衡算法,将工作任务进行平衡的分摊到各个操作单元上去,例如Nginx就是将前端请求按照一定算法将请求交给后面的服务区响应,避免后端服务承受请求太多而打挂,也可以很好的将多个资源协同起来服务,需要将资源协同,就会涉及到任务如何分发,已经在分发的过程根据不同的资源来承受能力来指派不同的任务,这就需要负载均衡算法。常见的负载均衡算法主要有:轮询(RoundRobin) 、随机(Random) 、源地址哈希(Hash) 、加权轮询(WRR)、加权随机(WR)、最小连接数(LC)

轮询(RoundRobin)

  • 概述

轮询就是排好队一个一个来,就像时钟一样,排成一个圈,然后一步一步轮着的走,原理就是将用户的请求从1到N分配,当到了N的时候将指针置为1,让后又从1到N开始执行。这种方式实现简答,对于节点资源可以随意的加减,当时对于有些节点资源性能弱的情况下,无法区别对待,性能弱的机器也要与性能强的机器达到一样的处理能力。

  • 算法实现介绍

这种实现算法比较简单,可以采用双向链表来实现,在这里就不采用这种方法,可以采用数组方式来实现

  • 简易代码
public class RoundRobin {
    public static void main(String[] args) {
        String[] ips = new String[]{"192,10.1.1", "192,10.1.2", "192,10.1.3"};
        int i = 0;
        while (true) {
            System.out.println(ips[i++]);
            if (i >= ips.length) {
                i = 0;
            }
        }
    }
}

随机(Random)

  • 概述

从节点资源中采取随机数的方式获取一个节点资源来执行任务

  • 算法实现介绍

也采用数组方式,在数组长度的范围内取一个随机数,然后通过数组的小标来获取里面的资源信息,时间起来也比较简单

  • 简易代码
public class Random {
    public static void main(String[] args) {
        String[] ips = new String[]{"192,10.1.1", "192,10.1.2", "192,10.1.3"};
        while (true) {
            System.out.println(ips[new java.util.Random().nextInt(ips.length)]);
        }
    }
}

源地址哈希(Hash)

  • 概述

例如在Nginx中,对于请求的IP地址做一个Hash值,然后将相同的Hash值路由到后端的同一台机器下,这种常用的用途是:后端的Session会话没有做集群,需要将相同的请求路由到相同的后端的服务中,如果路由到另外的机器,Session就不存在,就会频繁的让用户去重新登录,可以有效地做到会话保持。这种算法可能造成热点问题,在一段时间内,如果大量的请求IP地址的Hash值相同的Key一样就会被路由到同一台机器上,就会造成该机器访问量急剧增长,其他机器没有什么的访问量,严重的话可能会造成该机器宕机。

  • 算法实现介绍

该算法的实现可以采用HashMap来实现,key值作为Hash值的key,Value则存放后端服务的IP,key采用请求IP的后3位对提供的机器数取余数存放

  • 简易代码
public class Hash {

    public static void main(String[] args) {
        String[] ips = new String[]{"192.10.1.11", "192.10.12.22", "192.10.1.99"};
        HashMap<Integer, String> map = new HashMap<>(ips.length);
        //构建hash
        for (String ip : ips) {
            int key = Integer.parseInt(ip.split("\\.")[3]) % ips.length;
            map.put(key, ip);
        }
        //模拟请求
        for (int i = 0; i < 5; i++) {
            //随机产生最后3位数字
            int ip = new Random().nextInt(100);
            int hashKey = ip % ips.length;
            System.out.println("ip尾数:" + ip + ",hashKey:" + hashKey + ",获取IP:" + map.get(hashKey));
        }

    }
}

上面的算法针对如果key相同的话,会被覆盖掉,在这里只做效果展示对于太多的细节并没有考虑到。

加权轮询(WRR)

  • 概述

加权轮询在轮询的基础上增加了权重,在轮询的前提下,权重大的机器被分配到的任务多一些,在Nginx中该算法用的比较多,可以有效地做到权重大(机器条件好)的机器接受到更多的请求,可以处理多的请求。加权轮询分为普通加权轮询算法及平滑的加权轮询。

  • 普通的加权轮
    • 算法实现介绍

我们可以根据每个机器的权重来维护一个队列,比如a,b,c三台机器,a权重5,b权重3,c权重1,当我们知道这些权重后,就根据权重的总和在内部维护一个队列,然后依次从队列中取出轮询,这种算法就是会造成机器分布均匀。所以Nginx就采用的是平滑的加权轮询

  • 简易代码
public class WRR {


    static class Node {
        public String name;
        public int weight;

        public Node(String name, int weight) {
            this.name = name;
            this.weight = weight;
        }


    }

    public static void main(String[] args) {
        //普通加权轮询
        List<String> list = init(new Node("a", 5), new Node("b", 3), new Node("c", 1));
        System.out.println("构建的轮询队列:" + list.toString());

        //模型请求
        int i = 0;
        for (int j = 0; j < 10; j++) {
            System.out.println(list.get(i++));
            if (i >= list.size()) {
                i = 0;
            }
        }
    }


    private static List<String> init(Node... nodes) {
        List<String> list = new ArrayList<>();
        //这里需要对node按照weight排序,只是展示效果就不排序
        for (Node node : nodes) {
            for (int i = 0; i < node.weight; i++) {
                list.add(node.name);
            }
        }
        return list;
    }
}

执行结果

image.png

可以看出执行的队列是:a, a, a, a, a, b, b, b, c,然后采用轮询的方式从轮询队列中获取数据

  • 平滑的加权轮询
    • 算法实现介绍

采用Nginx的实现方式: 每个节点两个权重,weight和currentWeight,weight永远不变是配置时的值,current不停变化 变化规律如下:选择前所有current+=weight,选current最大的响应,响应后让它的current-=total

  • 简易代码
public class WRR {

    static class Node {
        public String name;
        public int weight;
        public int currentWeight;

        public Node(String name, int weight) {
            this.name = name;
            this.weight = weight;
        }


    }

    public List<Node> nodeList;
    private int totalWeight;

    public WRR(Node... nodes) {
        nodeList = new ArrayList<>();
        for (Node node : nodes) {
            nodeList.add(node);
            totalWeight += node.weight;
        }
    }

    /**
     * 获取当前执行节点
     *
     * @return
     */
    private Node get() {
        //在选取前: currentWeight = currentWeight + weight
        for (Node node : nodeList) {
            node.currentWeight += node.weight;
        }
        Node current = nodeList.get(0);
        int i = 0;
        for (Node node : nodeList) {
            if (node.currentWeight > i) {
                i = node.currentWeight;
                current = node;

            }
        }
        //在响应后:currentWeight = currentWeight + total
        current.currentWeight -= totalWeight;
        return current;
    }


    public static void main(String[] args) {
        //普通加权轮询
        WRR wrr = new WRR(new Node("a", 5), new Node("b", 3), new Node("c", 1));

        for (int j = 0; j < 10; j++) {
            System.out.println(wrr.get().name);
        }
    }
}

     执行结果

image.png

从执行结果可以看出,散列执行,权重大的执行次数多

加权随机(WR)

  • 概述

加权随机就是在随机的基础上增加权重,权重大的机器,在随机池中选中的概率大一点。

  • 算法实现介绍

跟普通的加权轮询差不多,在初始之前,根据权重构建随机队列,然后随机选中一个节点

  • 简易代码
public class WR {
    static class Node {
        public String name;
        public int weight;

        public Node(String name, int weight) {
            this.name = name;
            this.weight = weight;
        }


    }

    public static void main(String[] args) {
        //普通加权轮询
        List<String> list = init(new Node("a", 5), new Node("b", 3), new Node("c", 1));
        System.out.println("构建的加权队列:" + list.toString());

        //模型请求
        for (int j = 0; j < 5; j++) {
            System.out.println(list.get(new Random().nextInt(list.size())));
        }
    }


    private static List<String> init(Node... nodes) {
        List<String> list = new ArrayList<>();
        //这里需要对node按照weight排序,只是展示效果就不排序
        for (Node node : nodes) {
            for (int i = 0; i < node.weight; i++) {
                list.add(node.name);
            }
        }
        return list;
    }
}

执行结果

image.png

最小连接数(LC)

  • 概述

在每次选中机器的时候,选中当前连接数最小的机器来应用,将请求分发给该机器后,对于该机器的请求数+1,然后进行排序,每次来的时候就选择连接数最小的一个机器来处理请求

  • 算法实现介绍

排序可以采用最小堆排序算法来进行排序,然后选择一个最小的连接数节点,使用后对于该节点的连接数+1,重新进行排序