数据结构和算法专题---7、负载均衡算法与应用

2023-12-15 13:08:42

本章我们会对负载均衡算法做个简单介绍,包括常用的负载均衡算法(轮询、随机、哈希等)的概述、实现方式、典型场景做个说明。

概述

负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。既然涉及到多个机器,就涉及到任务如何分发,这就是负载均衡算法问题。

轮询(RoundRobin)

概述

轮询即排好队,一个接一个。前面调度算法中用到的时间片轮转,就是一种典型的轮询。但是前面使用数组和下标轮询实现。这里尝试手动写一个双向链表形式实现服务器列表的请求轮询算法。

实现

package com.ls.cloud.sys.alg.balance;

import java.util.LinkedList;

public class RR {

    //当前服务节点
    Server current;

    //初始化轮询类,多个服务器ip用逗号隔开
    public RR(String serverName){
        System.out.println("init server list : "+serverName);
        String[] names = serverName.split(",");
        for (int i = 0; i < names.length; i++) {
            Server server = new Server(names[i]);
            if (current == null){
                //如果当前服务器为空,说明是第一台机器,current就指向新创建的server
                this.current = server;
                //同时,server的前后均指向自己。
                current.prev = current;
                current.next = current;
            }else {
                //否则说明已经有机器了,按新加处理。
                addServer(names[i]);
            }
        }

    }
    //添加机器
    void addServer(String serverName){
        System.out.println("add server : "+serverName);
        Server server = new Server(serverName);
        Server next = this.current.next;
        //在当前节点后插入新节点
        this.current.next = server;
        server.prev = this.current;

        //修改下一节点的prev指针
        server.next = next;
        next.prev=server;
    }
    //将当前服务器移除,同时修改前后节点的指针,让其直接关联
    //移除的current会被回收期回收掉
    void remove(){
        System.out.println("remove current = "+current.name);
        this.current.prev.next = this.current.next;
        this.current.next.prev = this.current.prev;
        this.current = current.next;
    }
    //请求。由当前节点处理即可
    //注意:处理完成后,current指针后移
    void request(){
        System.out.println(this.current.name);
        this.current = current.next;
    }

    public static void main(String[] args) throws InterruptedException {
        //初始化两台机器
        RR rr = new RR("192.168.0.1,192.168.0.2");
        //启动一个额外线程,模拟不停的请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    rr.request();
                }
            }
        }).start();

        //3s后,3号机器加入清单
        Thread.currentThread().sleep(3000);
        rr.addServer("192.168.0.3");

        //3s后,当前服务节点被移除
        Thread.currentThread().sleep(3000);
        rr.remove();

    }

    class Server{
        Server prev;
        Server next;
        String name;
        public Server(String name){
            this.name = name;
        }
    }

}

结果分析

init server list : 192.168.0.1,192.168.0.2
add server : 192.168.0.2
192.168.0.1
192.168.0.2
192.168.0.1
192.168.0.2
192.168.0.1
add server : 192.168.0.3
192.168.0.2
192.168.0.3
192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.1
remove current = 192.168.0.2
192.168.0.3
192.168.0.1
192.168.0.3
192.168.0.1
  • 初始化后,只有1,2,两者轮询
  • 3加入后,1,2,3,三者轮询
  • 移除2后,只剩1和3轮询

优缺点

  • 实现简单,机器列表可以自由加减,且时间复杂度为o(1)
  • 无法针对节点做偏向性定制,节点处理能力的强弱无法区分对待

随机

概述

从可服务的列表中随机取一个提供响应。随机存取的场景下,适合使用数组更高效的实现下标随机读取。

实现

定义一个数组,在数组长度内取随机数,作为其下标即可。非常简单

package com.ls.cloud.sys.alg.balance;

import java.util.ArrayList;
import java.util.Random;

public class Rand {
    ArrayList<String> ips ;
    public Rand(String nodeNames){
        System.out.println("init list : "+nodeNames);
        String[] nodes = nodeNames.split(",");
        //初始化服务器列表,长度取机器数
        ips = new ArrayList<>(nodes.length);
        for (String node : nodes) {
            ips.add(node);
        }
    }
    //请求
    void request(){
        //下标,随机数,注意因子
        int i = new Random().nextInt(ips.size());
        System.out.println(ips.get(i));
    }
    //添加节点,注意,添加节点会造成内部数组扩容
    //可以根据实际情况初始化时预留一定空间
    void addnode(String nodeName){
        System.out.println("add node : "+nodeName);
        ips.add(nodeName);
    }
    //移除
    void remove(String nodeName){
        System.out.println("remove node : "+nodeName);
        ips.remove(nodeName);
    }


    public static void main(String[] args) throws InterruptedException {
        Rand rd = new Rand("192.168.0.1,192.168.0.2");

        //启动一个额外线程,模拟不停的请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    rd.request();
                }
            }
        }).start();

        //3s后,3号机器加入清单
        Thread.currentThread().sleep(3000);
        rd.addnode("192.168.0.3");

        //3s后,当前服务节点被移除
        Thread.currentThread().sleep(3000);
        rd.remove("192.168.0.2");
    }
}

结果分析

init list : 192.168.0.1,192.168.0.2
192.168.0.2
192.168.0.2
192.168.0.1
192.168.0.2
192.168.0.2
add node : 192.168.0.3
192.168.0.1
192.168.0.3
192.168.0.1
192.168.0.1
192.168.0.3
192.168.0.1
remove node : 192.168.0.2
192.168.0.3
192.168.0.1
192.168.0.1
192.168.0.3
  • 初始化为1,2,两者不按顺序轮询,而是随机出现
  • 3加入服务节点列表
  • 移除2后,只剩1,3,依然是两者随机,无序

源地址哈希(Hash)

概述

对当前访问的ip地址做一个hash值,相同的key被路由到同一台机器去。场景常见于分布式集群环境下,用户登录时的请求路由和会话保持。

实现

使用HashMap可以实现请求值到对应节点的服务,其查找时的时间复杂度为o(1)。固定一种算法,将请求映射到key上即可。举例,将请求的来源ip末尾,按机器数取余作为key:

package com.ls.cloud.sys.alg.balance;

import java.util.ArrayList;
import java.util.Random;

public class Hash {
    ArrayList<String> ips ;
    public Hash(String nodeNames){
        System.out.println("init list : "+nodeNames);
        String[] nodes = nodeNames.split(",");
        //初始化服务器列表,长度取机器数
        ips = new ArrayList<>(nodes.length);
        for (String node : nodes) {
            ips.add(node);
        }
    }
    //添加节点,注意,添加节点会造成内部Hash重排,思考为什么呢???
    //这是个问题!在一致性hash中会进入详细探讨
    void addnode(String nodeName){
        System.out.println("add node : "+nodeName);
        ips.add(nodeName);
    }
    //移除
    void remove(String nodeName){
        System.out.println("remove node : "+nodeName);
        ips.remove(nodeName);
    }
    //映射到key的算法,这里取余数做下标
    private int hash(String ip){
        int last = Integer.valueOf(ip.substring(ip.lastIndexOf(".")+1,ip.length()));
        return last % ips.size();
    }
    //请求
    //注意,这里和来访ip是有关系的,采用一个参数,表示当前的来访ip
    void request(String ip){
        //下标
        int i = hash(ip);
        System.out.println(ip+"-->"+ips.get(i));
    }

    public static void main(String[] args) {
        Hash hash = new Hash("192.168.0.1,192.168.0.2");
        for (int i = 1; i < 10; i++) {
            //模拟请求的来源ip
            String ip = "192.168.0."+ i;
            hash.request(ip);
        }

        hash.addnode("192.168.0.3");
        for (int i = 1; i < 10; i++) {
            //模拟请求的来源ip
            String ip = "192.168.0."+ i;
            hash.request(ip);
        }

        hash.remove("192.168.0.2");
        for (int i = 1; i < 10; i++) {
            //模拟请求的来源ip
            String ip = "192.168.0."+ i;
            hash.request(ip);
        }
    }

}

结果分析

init list : 192.168.0.1,192.168.0.2
192.168.0.1-->192.168.0.2
192.168.0.2-->192.168.0.1
192.168.0.3-->192.168.0.2
192.168.0.4-->192.168.0.1
192.168.0.5-->192.168.0.2
192.168.0.6-->192.168.0.1
192.168.0.7-->192.168.0.2
192.168.0.8-->192.168.0.1
192.168.0.9-->192.168.0.2
add node : 192.168.0.3
192.168.0.1-->192.168.0.2
192.168.0.2-->192.168.0.3
192.168.0.3-->192.168.0.1
192.168.0.4-->192.168.0.2
192.168.0.5-->192.168.0.3
192.168.0.6-->192.168.0.1
192.168.0.7-->192.168.0.2
192.168.0.8-->192.168.0.3
192.168.0.9-->192.168.0.1
remove node : 192.168.0.2
192.168.0.1-->192.168.0.3
192.168.0.2-->192.168.0.1
192.168.0.3-->192.168.0.3
192.168.0.4-->192.168.0.1
192.168.0.5-->192.168.0.3
192.168.0.6-->192.168.0.1
192.168.0.7-->192.168.0.3
192.168.0.8-->192.168.0.1
192.168.0.9-->192.168.0.3
  • 初始化后,只有1,2,下标为末尾ip取余数,多次运行,响应的机器不变,实现了会话保持
  • 3加入后,重新hash,机器分布发生变化
  • 2被移除后,原来hash到2的请求被重新定位给3响应

最小连接数(LC)

概述

LeastConnections,即统计当前机器的连接数,选最少的去响应新的请求。前面的算法是站在请求维度,而最小连接数是站在机器的维度。

实现

定义一个链接表记录机器的节点id和机器连接数量的计数器。内部采用最小堆做排序处理,响应时取堆顶节点即是最小连接数。

结果分析

package com.ls.cloud.sys.alg.balance;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class LC {
    //节点列表
    Node[] nodes;

    //初始化节点,创建堆
    // 因为开始时各节点连接数都为0,所以直接填充数组即可
    LC(String ns){
        String[] ns1 = ns.split(",");
        nodes = new Node[ns1.length+1];
        for (int i = 0; i < ns1.length; i++) {
            nodes[i+1] = new Node(ns1[i]);
        }
    }

    //节点下沉,与左右子节点比对,选里面最小的交换
    //目的是始终保持最小堆的顶点元素值最小
    //i:要下沉的顶点序号
    void down(int i) {
        //顶点序号遍历,只要到1半即可,时间复杂度为O(log2n)
        while ( i << 1  <  nodes.length){
            //左子,为何左移1位?回顾一下二叉树序号
            int left = i<<1;
            //右子,左+1即可
            int right = left+1;
            //标记,指向 本节点,左、右子节点里最小的,一开始取i自己
            int flag = i;
            //判断左子是否小于本节点
            if (nodes[left].get() < nodes[i].get()){
                flag = left;
            }
            //判断右子
            if (right < nodes.length && nodes[flag].get() > nodes[right].get()){
                flag = right;
            }
            //两者中最小的与本节点不相等,则交换
            if (flag != i){
                Node temp = nodes[i];
                nodes[i] = nodes[flag];
                nodes[flag] = temp;
                i = flag;
            }else {
                //否则相等,堆排序完成,退出循环即可
                break;
            }

        }

    }

    //请求。非常简单,直接取最小堆的堆顶元素就是连接数最少的机器
    void request(){
        System.out.println("---------------------");
        //取堆顶元素响应请求
        Node node = nodes[1];
        System.out.println(node.name + " accept");
        //连接数加1
        node.inc();
        //排序前的堆
        System.out.println("before:"+Arrays.toString(nodes));
        //堆顶下沉
        down(1);
        //排序后的堆
        System.out.println("after:"+Arrays.toString(nodes));
    }

    public static void main(String[] args) {
        //假设有7台机器
        LC lc = new LC("a,b,c,d,e,f,g");
        //模拟10个请求连接
        for (int i = 0; i < 10; i++) {
            lc.request();
        }
    }

    class Node{
        //节点标识
        String name;
        //计数器
        AtomicInteger count = new AtomicInteger(0);
        public Node(String name){
            this.name = name;
        }
        //计数器增加
        public void inc(){
            count.getAndIncrement();
        }
        //获取连接数
        public int get(){
            return count.get();
        }

        @Override
        public String toString() {
            return name+"="+count;
        }
    }
}

结果分析

---------------------
a accept
before:[null, a=1, b=0, c=0, d=0, e=0, f=0, g=0]
after:[null, b=0, d=0, c=0, a=1, e=0, f=0, g=0]
---------------------
b accept
before:[null, b=1, d=0, c=0, a=1, e=0, f=0, g=0]
after:[null, d=0, e=0, c=0, a=1, b=1, f=0, g=0]
---------------------
d accept
before:[null, d=1, e=0, c=0, a=1, b=1, f=0, g=0]
after:[null, e=0, d=1, c=0, a=1, b=1, f=0, g=0]
---------------------
e accept
before:[null, e=1, d=1, c=0, a=1, b=1, f=0, g=0]
after:[null, c=0, d=1, f=0, a=1, b=1, e=1, g=0]
---------------------
c accept
before:[null, c=1, d=1, f=0, a=1, b=1, e=1, g=0]
after:[null, f=0, d=1, g=0, a=1, b=1, e=1, c=1]
---------------------
f accept
before:[null, f=1, d=1, g=0, a=1, b=1, e=1, c=1]
after:[null, g=0, d=1, f=1, a=1, b=1, e=1, c=1]
---------------------
g accept
before:[null, g=1, d=1, f=1, a=1, b=1, e=1, c=1]
after:[null, g=1, d=1, f=1, a=1, b=1, e=1, c=1]
---------------------
g accept
before:[null, g=2, d=1, f=1, a=1, b=1, e=1, c=1]
after:[null, d=1, a=1, f=1, g=2, b=1, e=1, c=1]
---------------------
d accept
before:[null, d=2, a=1, f=1, g=2, b=1, e=1, c=1]
after:[null, a=1, b=1, f=1, g=2, d=2, e=1, c=1]
---------------------
a accept
before:[null, a=2, b=1, f=1, g=2, d=2, e=1, c=1]
after:[null, b=1, a=2, f=1, g=2, d=2, e=1, c=1]
  • 初始化后,堆节点值都为0,即每个机器连接数都为0
  • 堆顶连接后,下沉,堆重新排序,最小堆规则保持成立

应用案例

nginx upstream

upstream frontend {    
#源地址hash    
ip_hash;    
server 192.168.0.1:8081;    
server 192.168.0.2:8082 weight=1 down;    
server 192.168.0.3:8083 weight=2;    
server 192.168.0.4:8084 weight=3 backup;    
server 192.168.0.5:8085 weight=4 max_fails=3 fail_timeout=30s;
}
  • ip_hash:即源地址hash算法
  • down:表示当前的server暂时不参与负载
  • weight:即加权算法,默认为1,weight越大,负载的权重就越大。
  • backup:备份机器,只有其它所有的非backup机器down或者忙的时候,再请求backup机器。
  • max_fails:最大失败次数,默认值为1,这里为3,也就是最多进行3次尝试fail_timeout:超时时间为30秒,默认值是10s。
  • 注意!weight和backup不能和ip_hash关键字一起使用。

springcloud ribbon IRule

#设置负载均衡策略 eureka‐application‐service为调用的服务的名称
eureka‐application‐service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
  • RoundRobinRule:轮询
  • RandomRule:随机AvailabilityFilteringRule:先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务轮询
  • WeightedResponseTimeRule:根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大。刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够,会切换到该策略
  • RetryRule:先按照RoundRobinRule的策略,如果获取服务失败则在指定时间内重试,获取可用的服务
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • ZoneAvoidanceRule:默认规则,综合判断server所在区域的性能和server的可用性

dubbo负载均衡

使用Service注解

@Service(loadbalance = "roundrobin",weight = 100) 
  • RandomLoadBalance: 随机,这种方式是dubbo默认的负载均衡策略
  • RoundRobinLoadBalance:轮询
  • LeastActiveLoadBalance:最少活跃次数,dubbo框架自定义了一个Filter,用于计算服务被调用的次数
  • ConsistentHashLoadBalance:一致性hash

文章来源:https://blog.csdn.net/u014526891/article/details/134870356
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。