应用级算法系列(五)-负载均衡算法

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

6.1 轮询(RR) - 【拓展】

1)概述

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

public class RR {

    class Server{
        Server prev;
        Server next;
        String name;
        public Server(String name){
            this.name = name;
        }
    }
  
  
    //当前服务节点
    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();

    }



}

  • 初始化后,只有1,2,两者轮询
  • 3加入后,1,2,3,三者轮询
  • 移除2后,只剩1和3轮询

4)优缺点

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

6.2 随机(Random)

1)概述

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

2)实现

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

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");
    }
}

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

6.3 源地址哈希(Hash)

1)概述

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

2)实现

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

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.1."+ i;
            hash.request(ip);
        }

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

}

  • 初始化后,只有1,2,下标为末尾ip取余数,多次运行,响应的机器不变,实现了会话保持
  • 3加入后,重新hash,机器分布发生变化
  • 2被移除后,原来hash到2的请求被重新定位给3响应

6.4 加权轮询(WRR)

1)概述

WeightRoundRobin,轮询只是机械的旋转,加权轮询弥补了所有机器一视同仁的缺点。在轮询的基础上,初始化时,机器携带一个比重。

2)实现

维护一个链表,每个机器根据权重不同,占据的个数不同。轮询时权重大的,个数多,自然取到的次数变大。举个例子:a,b,c 三台机器,权重分别为4,2,1,排位后会是a,a,a,a,b,b,c,每次请求时,从列表中依次取节点,下次请求再取下一个。到末尾时,再从头开始。

但是这样有一个问题:机器分布不够均匀,扎堆出现了....

解决:为解决机器平滑出现的问题,nginx的源码中使用了一种平滑的加权轮询的算法,规则如下:

  • 每个节点两个权重,weight和currentWeight,weight永远不变是配置时的值,current不停变化
  • 变化规律如下:选择前所有current+=weight,选current最大的响应,响应后让它的current-=total
次数响应前被选中响应后
14,2,1a-3,2,1
21,4,2b1,-3,2
35,-1,3a-2,-1,3
42,1,4c2,1,-3
56,3,-2a-1,3,-2
63,5,-1b3,-2,-1
77,0,0a0,0,0

统计:a=4,b=2,c=1 且分布平滑均衡

import java.util.ArrayList;

public class WRR {
  
    class Node{
        int weight,currentWeight;
        String name;
        public Node(String name,int weight){
            this.name = name;
            this.weight = weight;
            this.currentWeight = 0;
        }

        @Override
        public String toString() {
            return String.valueOf(currentWeight);
        }
    }
  
  
    //所有节点的列表
    ArrayList<Node> list ;
    //总权重
    int total;

    //初始化节点列表,格式:a#4,b#2,c#1
    public WRR(String nodes){
        String[] ns = nodes.split(",");
        list = new ArrayList<>(ns.length);
        for (String n : ns) {
            String[] n1 = n.split("#");
            int weight = Integer.valueOf(n1[1]);
            list.add(new Node(n1[0],weight));
            total += weight;
        }
    }

    //获取当前节点
    Node getCurrent(){
        //执行前,current加权重
        for (Node node : list) {
            node.currentWeight += node.weight;
        }

        //遍历,取权重最高的返回
        Node current = list.get(0);
        int i = 0;
        for (Node node : list) {
            if (node.currentWeight > i){
                i = node.currentWeight;
                current = node;
            }
        }
        return current;
    }

    //响应
    void request(){
        //获取当前节点
        Node node = this.getCurrent();
        //第一列,执行前的current
        System.out.print(list.toString()+"---");
        //第二列,选中的节点开始响应
        System.out.print(node.name+"---");
        //响应后,current减掉total
        node.currentWeight -= total;
        //第三列,执行后的current
        System.out.println(list);
    }

    public static void main(String[] args) {
        WRR wrr = new WRR("a#4,b#2,c#1");
        //7次执行请求,看结果
        for (int i = 0; i < 7; i++) {
            wrr.request();
        }
    }


}

6.5 加权随机(WR)

1)概述

WeightRandom,机器随机被筛选,但是做一组加权值,根据权值不同,选中的概率不同。在这个概念上,可以认为随机是一种等权值的特殊情况。

2)实现

设计思路依然相同,根据权值大小,生成不同数量的节点,节点排队后,随机获取。这里的数据结构主要涉及到随机的读取,所以优选为数组。

与随机相同的是,同样为数组随机筛选,不同在于,随机只是每台机器1个,加权后变为多个。

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

public class WR {
    //所有节点的列表
    ArrayList<String> list ;
    //初始化节点列表
    public WR(String nodes){
        String[] ns = nodes.split(",");
        list = new ArrayList<>();
        for (String n : ns) {
            String[] n1 = n.split("#");
            int weight = Integer.valueOf(n1[1]);
            for (int i = 0; i < weight; i++) {
                list.add(n1[0]);
            }
        }
    }

    void request(){
        //下标,随机数,注意因子
        int i = new Random().nextInt(list.size());
        System.out.println(list.get(i));
    }

    public static void main(String[] args) {
        WR wr = new WR("a#2,b#1");
        for (int i = 0; i < 9; i++) {
            wr.request();
        }
    }

}

  • 运行9次,a,b交替出现,a=6,b=3,满足2:1比例
  • 注意!既然是随机,就存在随机性,不见得每次执行都会严格比例。样本趋向无穷时,比例约准确

6.6 最小连接数(LC) 

1)概述

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

2)实现

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

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;
        }
    }
}

  • 初始化后,堆节点值都为0,即每个机器连接数都为0
  • 堆顶连接后,下沉,堆重新排序,最小堆规则保持成立

6.7 应用案例

1)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关键字一起使用。
 

2)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的可用性

 

3)dubbo负载均衡

使用Service注解

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

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值