负载均衡算法

Posted Icedzzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了负载均衡算法相关的知识,希望对你有一定的参考价值。

转载于https://www.yuque.com/renyong-jmovm/kb/gwu187

文章目录


分布式定义:分布式是后端架构的一种,比如单体架构和分布式架构,单体架构是指整个系统部署在一个进程里,而分布式是指整个系统是部署在不同的进程里的。

负载均衡定义:由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,都可以单独对外提供服务而无须其他服务器的辅助。
为什么要负载均衡?
通过某种负载分担技术,将外部发送来的请求均匀分配到对称结构中的某一台服务器上,而接收到请求的服务器独立地回应客户的请求。负载均衡能够平均分配客户请求到服务器阵列,借此提供快速获取重要数据,解决大量并发访问服务问题,这种集群技术可以用最少的投资获得接近于大型主机的性能。

**常见的负载均衡算法:**随机算法、加权轮询、一致性hash、最小活跃数算法。

随机算法

随机从服务器列表中挑出服务器接收请求。
定义一个List作为服务器列表进行测试:

public class ServerIps 
    private static final List<String> LIST = Arrays.asList(
            "192.168.0.1",
            "192.168.0.2",
            "192.168.0.3",
            "192.168.0.4",
            "192.168.0.5",
            "192.168.0.6",
            "192.168.0.7",
            "192.168.0.8",
            "192.168.0.9",
            "192.168.0.10"
    );
    //给每个ip一个权重值
    public static final Map<String, Integer> WEIGHT_LIST = new HashMap<String, Integer>();
    static 
        // 权重之和为50
        WEIGHT_LIST.put("192.168.0.1", 1);
        WEIGHT_LIST.put("192.168.0.2", 8);
        WEIGHT_LIST.put("192.168.0.3", 3);
        WEIGHT_LIST.put("192.168.0.4", 6);
        WEIGHT_LIST.put("192.168.0.5", 5);
        WEIGHT_LIST.put("192.168.0.6", 5);
        WEIGHT_LIST.put("192.168.0.7", 4);
        WEIGHT_LIST.put("192.168.0.8", 7);
        WEIGHT_LIST.put("192.168.0.9", 2);
        WEIGHT_LIST.put("192.168.0.10", 9);
    

随机算法的实现有两种,第一种利用java.util.Random()随机生成服务器List下标,方法实现简单,但适用于每台机器性能差不多时候,生产中可能某些机器的性能更高一点,它可以处理更多的请求,所以,我们可以对每台服务器设置一个权重,每个服务器调用的概率分布应该近似于权重的分布,即权重随机算法


    /**
     * 随机法
     * @return
     */
    public static String RandomgetServer() 
        // 生成一个随机数作为list的下标值
        java.util.Random random = new java.util.Random();
        int randomPos = random.nextInt(ServerIps.LIST.size());
        return ServerIps.LIST.get(randomPos);
    

    /**
     * 权重随机算法
     * 这种实现方法在遇到权重之和特别大的时候就会比较消耗内存,
     * 因为需要对ip地址进行复制,权重之和越大那么ips就需要越多的内存
     * @return
     */
    public static String WeightRandomgetServer() 
        // 生成一个随机数作为list的下标值
        List<String> ips = new ArrayList<String>();
        for (String ip : ServerIps.WEIGHT_LIST.keySet()) 
            Integer weight = ServerIps.WEIGHT_LIST.get(ip);
            // 按权重进行复制
            for (int i=0; i<weight; i++) 
                ips.add(ip);
            
        
        java.util.Random random = new java.util.Random();
        int randomPos = random.nextInt(ips.size());
        return ips.get(randomPos);
    

    /**
     * 权重随机算法v2
     * 给每个ip一个权重(5,3,2)区间,如[0,5],[5,8],[8,10]如果生成的随机数在哪个区间,则返回该服务器
     * @return
     */
    public static String WeightRandomgetServerv2() 

        int totalWeight = 0;
        boolean sameWeight = true; // 如果所有权重都相等,那么随机一个ip就好了
        Object[] weights = ServerIps.WEIGHT_LIST.values().toArray();
        for (int i = 0; i < weights.length; i++) 
            Integer weight = (Integer) weights[i];
            totalWeight += weight;
            if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) 
                sameWeight = false;
            
        
        java.util.Random random = new java.util.Random();
        int randomPos = random.nextInt(totalWeight);
        if (!sameWeight) 
            for (String ip : ServerIps.WEIGHT_LIST.keySet()) 
                Integer value = ServerIps.WEIGHT_LIST.get(ip);
                if (randomPos < value) 
                    return ip;
                
                randomPos = randomPos - value;
            
        
        return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[new java.util.Random().nextInt(ServerIps.WEIGHT_LIST.size())];
    

轮询算法

最简单实现方法:循环调用

    // 当前循环的位置
    private static Integer pos = 0;
    public static String getServer() 
        String ip = null;
        // pos同步
        synchronized (pos) 
            if (pos >= ServerIps.LIST.size()) 
                pos = 0;
            
            ip = ServerIps.LIST.get(pos);
            pos++;
        
        return ip;
    

改进方法:
假设我们有三台服务器 servers = [A, B, C],对应的权重为 weights = [ 2, 5, 1], 总权重为8,我们可以理解为有8台“服务器”,这是8台“不具有并发功能”,其中有2台为A,5台为B,1台为C,一次调用过来的时候,需要按顺序访问,比如有10次调用,那么服务器调用顺序为AABBBBBCAA,调用编号会越来越大,而服务器是固定的,所以需要把调用编号“缩小”,这里对调用编号进行取余,除数为总权重和,比如:1号调用,1%8=1;2号调用,2%8=2;3号调用,3%8=3;8号调用,8%8=0;9号调用,9%8=1…
实现方法

    public static String WeightRoundGetServer() 
        int totalWeight = 0;
        boolean sameWeight = true; // 如果所有权重都相等,那么随机一个ip就好了
        Object[] weights = ServerIps.WEIGHT_LIST.values().toArray();
        for (int i = 0; i < weights.length; i++) 
            Integer weight = (Integer) weights[i];
            totalWeight += weight;
            if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) 
                sameWeight = false;
            
        
        Integer sequenceNum = Sequece.getAndIncrement();
        Integer offset = sequenceNum % totalWeight;
        offset = offset == 0 ?  totalWeight : offset;
        if (!sameWeight) 
            for (String ip : ServerIps.WEIGHT_LIST.keySet()) 
                Integer weight = ServerIps.WEIGHT_LIST.get(ip);
                if (offset <= weight) 
                    return ip;
                
                offset = offset - weight;
            
        
        String ip = null;
        synchronized (pos) 
            if (pos >= ServerIps.LIST.size()) 
                pos = 0;
            
            ip = ServerIps.LIST.get(pos);
            pos++;
        
        return ip;
    

但是这种算法有一个缺点:一台服务器的权重特别大的时候,他需要连续的的处理请求,但是实际上我们想达到的效果是,对于100次请求,只要有100*8/50=16次就够了,这16次不一定要连续的访问,比如假设我们有三台服务器 servers = [A, B, C],对应的权重为 weights = [5, 1, 1] , 总权重为7,那么上述这个算法的结果是:AAAAABC,那么如果能够是这么一个结果呢:AABACAA,把B和C平均插入到5个A中间,这样是比较均衡的了。可以通过平滑加权轮询实现。

平滑加权轮询
思路:每个服务器对应两个权重,分别为 weight 和 currentWeight。其中 weight 是固定的,currentWeight 会动态调整,初始值为0。当有新的请求进来时,遍历服务器列表,让它的 currentWeight 加上自身权重。遍历完成后,找到最大的 currentWeight,并将其减去权重总和,然后返回相应的服务器即可。
如上,经过平滑性处理后,得到的服务器序列为 [A, A, B, A, C, A, A],相比之前的序列 [A, A, A, A, A, B, C],分布性要好一些。初始情况下 currentWeight = [0, 0, 0],第7个请求处理完后,currentWeight 再次变为 [0, 0, 0]。
实现:

private static Map<String, Weight> weightMap = new HashMap<String, Weight>();
    public static String WeightRoundgetServerV2()
        // java8
        int totalWeight = ServerIps.WEIGHT_LIST.values().stream().reduce(0, (w1, w2) -> w1+w2);
        // 初始化weightMap,初始时将currentWeight赋值为weight
        if (weightMap.isEmpty()) 
            ServerIps.WEIGHT_LIST.forEach((key, value) -> 
                weightMap.put(key, new Weight(key, value, value));
            );
        
        // 找出currentWeight最大值
        Weight maxCurrentWeight = null;
        for (Weight weight : weightMap.values()) 
            if (maxCurrentWeight == null || weight.getCurrentWeight() > maxCurrentWeight.getCurrentWeight()) 
                maxCurrentWeight = weight;
            
        
        // 将maxCurrentWeight减去总权重和
        maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight() - totalWeight);
        // 所有的ip的currentWeight统一加上原始权重
        for (Weight weight : weightMap.values()) 
            weight.setCurrentWeight(weight.getCurrentWeight() + weight.getWeight());
        
        // 返回maxCurrentWeight所对应的ip
        return maxCurrentWeight.getIp();

    

一致性哈希算法

服务器集群接收到一次请求调用时,可以根据根据请求的信息,比如客户端的ip地址,或请求路径与请求参数等信息进行哈希,可以得出一个哈希值,特点是对于相同的ip地址,或请求路径和请求参数哈希出来的值是一样的,只要能再增加一个算法,能够把这个哈希值映射成一个服务端ip地址,就可以使相同的请求(相同的ip地址,或请求路径和请求参数)落到同一服务器上。
因为客户端发起的请求情况是无穷无尽的(客户端地址不同,请求参数不同等等),所以对于的哈希值也是无穷大的,所以我们不可能把所有的哈希值都进行映射到服务端ip上,所以这里需要用到哈希环

  • 哈希值如果需要ip1和ip2之间的,则应该选择ip2作为结果;
  • 哈希值如果需要ip2和ip3之间的,则应该选择ip3作为结果;
  • 哈希值如果需要ip3和ip4之间的,则应该选择ip4作为结果;
  • 哈希值如果需要ip4和ip1之间的,则应该选择ip1作为结果;

但特殊情况,如果某台服务器挂了,如ip4,则会发现,ip3和ip1直接的范围是比较大的,会有更多的请求落在ip1上,这是不“公平的”

解决这个问题需要加入虚拟节点,比如:
实际上,这只是处理这种不均衡性的一种思路,实际上就算哈希环本身是均衡的,你也可以增加更多的虚拟节点来使这个环更加平滑,比如:

实现:

public class ConsistentHash 
    private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();
    private static final int VIRTUAL_NODES = 160;
    static 
        // 对每个真实节点添加虚拟节点,虚拟节点会根据哈希算法进行散列
        for (String ip : ServerIps.LIST) 
            for (int i = 0; i < VIRTUAL_NODES; i++) 
                int hash = getHash(ip+"VN"+i);
                virtualNodes.put(hash, ip);
            
        
    
    private static String getServer(String client) 
        int hash = getHash(client);
        // 得到大于该Hash值的排好序的Map
        SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
        // 大于该hash值的第一个元素的位置
        Integer nodeIndex = subMap.firstKey();
        // 如果不存在大于该hash值的元素,则返回根节点
        if (nodeIndex == null) 
            nodeIndex = virtualNodes.firstKey();
        
        // 返回对应的虚拟节点名称
        return subMap.get(nodeIndex);
    
    private static int getHash(String str) 
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
        // 如果算出来的值为负数则取其绝对值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
    
    public static void main(String[] args) 
        // 连续调用10次,随机10个client
        for (int i = 0; i < 10; i++) 
            System.out.println(getServer("client" + i));
        
    

最小活跃数算法

前面几种方法主要目标是使服务端分配到的调用次数尽量均衡,但是实际情况是这样吗?调用次数相同,服务器的负载就均衡吗?当然不是,这里还要考虑每次调用的时间,而最小活跃数算法则是解决这种问题的。
活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提供者。在具体实现中,每个服务提供者对应一个活跃数。初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最小活跃数负载均衡算法的基本思想。除了最小活跃数,最小活跃数算法在实现上还引入了权重值。所以准确的来说,最小活跃数算法是基于加权最小活跃数算法实现的。举个例子说明一下,在一个服务提供者集群中,有两个性能优异的服务提供者。某一时刻它们的活跃数相同,则会根据它们的权重去分配请求,权重越大,获取到新请求的概率就越大。如果两个服务提供者权重相同,此时随机选择一个即可。
实现:

    private static String getServer() 
        // 找出当前活跃数最小的服务器
        Optional<Integer> minValue = ServerIps.ACTIVITY_LIST.values().stream().min(Comparator.naturalOrder());
        if (minValue.isPresent()) 
            List<String> minActivityIps = new ArrayList<>();
            ServerIps.ACTIVITY_LIST.forEach((ip, activity) -> 
                if (activity.equals(minValue.get())) 
                    minActivityIps.add(ip);
                
            );
            // 最小活跃数的ip有多个,则根据权重来选,权重大的优先
            if (minActivityIps.size() > 1) 
                // 过滤出对应的ip和权重
                Map<String, Integer> weightList = new LinkedHashMap<String, Integer>();
                ServerIps.WEIGHT_LIST.forEach((ip, weight) -> 
                    if (minActivityIps.contains(ip)) 
                        weightList.put(ip, ServerIps.WEIGHT_LIST.get(ip));
                    
                );
                int totalWeight = 0;
                boolean sameWeight = true; // 如果所有权重都相等,那么随机一个ip就好了
                Object[] weights = weightList.values().toArray();
                for (int i = 0; i < weights.length; i++) 
                    Integer weight = (Integer) weights[i];
                    totalWeight += weight;
                    if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) 
                        sameWeight = <

以上是关于负载均衡算法的主要内容,如果未能解决你的问题,请参考以下文章

传授“带权重的负载均衡实现算法”独家设计思路!

负载均衡的常用算法

nignx 负载均衡的几种算法介绍

客户端负载均衡Ribbon之二:Loadbalance的几种算法以及在ribbon中的使用

负载均衡算法--加权轮询法(Weight Round Robin)

负载均衡-2.Haproxy