一 负载均衡介绍

1.1:负载均衡简介

以下是wikipedia对负载均衡的定义:

负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处理单元或磁盘驱动的的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载平衡通常涉及专用软件或硬件

1.2:简单解释

这个概念如何理解呢?通俗点来说假如一个请求从客户端发起,比如(查询订单列表),要选择服务器进行处理,但是我们的集群环境提供了5个服务器A\B\C\D\E,每个服务器都有处理这个请求的能力,此时客户端就必须选择一个服务器来进行处理(不存在先选择A,处理一会又选择C,又跳到D).说白了就是一个选择的问题。当请求多了的话,就要考虑各服务器的负载,一共5个服务器,不可能每次都让一个服务器都来处理吧,比如把让其他服务器来分压。这就是负载均衡的优点:避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题。

二:dubbo的loadBalance接口

2.1:loadBalance

dubbo的负载均衡策略,主体向外暴露出来是一个接口,名字叫做loadBlace,位于com.alibaba.dubbo.rpc.cluster包下,很明显根据包名就可以看出它是用来管理集群的:

这个接口就一个方法,select方法的作用就是从众多的调用的List选择出一个调用者,Invoker可以理解为客户端的调用者,dubbo专门封装一个类来表示,URL就是调用者发起的URL请求链接,从这个URL中可以获取很多请求的具体信息,Invocation表示的是调用的具体过程,dubbo用这个类模拟调用具体细节过程:

AbstractLoadBalance

2.2.1 doselect
该方法是抽象的,交给具体的子类去实现,由此也可以思考出一个问题就是:dubbo为什么要将一个接口首先做出一个实现抽象类,再由不同的子类去实现?原因是抽象类中的非抽象方法,再子类中都是必须要实现的,而他们子类的不同点就是具体做出选择的策略不同,将公共的逻辑提取出来放在抽象类里,子类不用写多余的代码,只用维护和实现最终要的自己的逻辑

protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);

2.2.2:getWeight方法
顾名思义,这个方法的含义就是获取权重,首先通过URL(URL为dubbo封装的一个实体)获取基本的权重,如果权重大于0,会获取服务启动时间,再用当前的时间-启动时间就是服务到目前为止运行了多久,因此这个upTime就可以理解为服务启动时间,再获取配置的预热时间,如果启动时间小于预热时间,就会再次调用获取权重。这个预热的方法其实dubbo针对JVM做出的一个很契合的优化,因为JVM从启动到起来都运行到最佳状态是需要一点时间的,这个时间叫做warmup,而dubbo就会对这个时间进行设定,然后等到服务运行时间和warmup相等时再计算权重,这样就可以保障服务的最佳运行状态!

int getWeight(Invoker<?> invoker, Invocation invocation) {
        int weight;
        URL url = invoker.getUrl();
        // Multiple registry scenario, load balance among multiple registries.
        // 当有多个注册地址时,根据注册地址key 获取对应服务权重
        if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
            weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
        } else {
            // 获取权重默认时100
            weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
            if (weight > 0) {
                // 以下过程主要时判断机器的启动时间和预热时间的差值(默认为10min),
                // 为什么要预热? 因为服务在启动之后JVM会对代码有一个优化的过程,预热保证了调用的体验,谨防由此引发的调用超时问题。
                long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
                if (timestamp > 0L) {
                    long uptime = System.currentTimeMillis() - timestamp;
                    if (uptime < 0) {
                        return 1;
                    }
                    int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
                    //如果服务启动时间小于规定预热时间,则将服务权重降级
                    if (uptime > 0 && uptime < warmup) {
                        weight = calculateWarmupWeight((int)uptime, warmup, weight);
                    }
                }
            }
        }
        return Math.max(weight, 0);
    }
 static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        int ww = (int) ( uptime / ((float) warmup / weight));
        return ww < 1 ? 1 : (Math.min(ww, weight));
    }

2.2.3 select
抽象类方法中有个有方法体的方法select,先判断调用者组成的List是否为null,如果是null就返回null。再判断调用者的大小,如果只有一个就返回那个唯一的调用者(试想,如果服务调用另一个服务时,当服务的提供者机器只有一个,那么就可以返回那一个,因为没有选择了!)如果这些都不成立,就继续往下走,走doSelect方法:

@Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        if (invokers == null || invokers.isEmpty())
            return null;
        if (invokers.size() == 1)
            return invokers.get(0);
        return doSelect(invokers, url, invocation);
    }

三:dubbo的几种负载均衡策略

3.1:整体架构图

Dubbo框架负载均衡算法_Dubbo框架负载均衡算法

可以看出抽象的负载均衡下的类分为5个,这5个类表示了5种负载均衡策略,分别是一致性Hash均衡算法、随机调用法、轮询法、最少活动调用法、最短响应时间

RandomLoadBalance

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // Number of invokers
        int length = invokers.size();
        // Every invoker has the same weight?
        boolean sameWeight = true;
        // the weight of every invokers
        int[] weights = new int[length];
        // the first invoker's weight
        int firstWeight = getWeight(invokers.get(0), invocation);
        weights[0] = firstWeight;
        // The sum of weights
        int totalWeight = firstWeight;
        for (int i = 1; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            // save for later use
            weights[i] = weight;
            // Sum
            totalWeight += weight;
            if (sameWeight && weight != firstWeight) {
                sameWeight = false;
            }
        }
        //(2)
        if (totalWeight > 0 && !sameWeight) {
            // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);//2-1
            // Return a invoker based on the random value.
            for (int i = 0; i < length; i++) {
                offset -= weights[i];//2-2
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        //(1)
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

该策略主要有两种情况
(1) 如果所有服务权重相同,则ThreadLocalRandom.current().nextInt(length) 等概率随机获取一个服务返回.
(2)如果服务权重不相同,则2-1 根据权重和求出一个随机数(每个数出现的概率相同),然后计算看该数落在哪个区间段,权重和小于0的,则返回.
举个例子:两个服务 A权重90、B权重10.则totalWeight = 100,虽然offset的值是在0-99之间等概率出现的,但是选择A服务的概率有90,B的概率有10,为什么?把totalWeight变成一个100等分线段,A占据了其中的90份长度,而B占据了10份长度.所以这个offset随机数落在A区间的概率有90%,B 有10%

LeastActiveLoadBalance

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // Number of invokers
        int length = invokers.size();
        // The least active value of all invokers
        int leastActive = -1;
        // The number of invokers having the same least active value (leastActive)
        int leastCount = 0;
        // The index of invokers having the same least active value (leastActive)
        int[] leastIndexes = new int[length];
        // the weight of every invokers
        int[] weights = new int[length];
        // The sum of the warmup weights of all the least active invokers
        int totalWeight = 0;
        // The weight of the first least active invoker
        int firstWeight = 0;
        // Every least active invoker has the same weight value?
        boolean sameWeight = true;


        // Filter out all the least active invokers
        for (int i = 0; i < length; i++) {
            Invoker<T> invoker = invokers.get(i);
            // Get the active number of the invoker 
            //默认值是0 
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
            // Get the weight of the invoker's configuration. The default value is 100.
            int afterWarmup = getWeight(invoker, invocation);
            // save for later use
            weights[i] = afterWarmup;
            // If it is the first invoker or the active number of the invoker is less than the current least active number
            //(1)
            if (leastActive == -1 || active < leastActive) {
                // Reset the active number of the current invoker to the least active number
                leastActive = active;
                // Reset the number of least active invokers
                leastCount = 1;
                // Put the first least active invoker first in leastIndexes
                leastIndexes[0] = i;
                // Reset totalWeight
                totalWeight = afterWarmup;
                // Record the weight the first least active invoker
                firstWeight = afterWarmup;
                // Each invoke has the same weight (only one invoker here)
                sameWeight = true;
                // If current invoker's active value equals with leaseActive, then accumulating.
            } else if (active == leastActive) {
                // Record the index of the least active invoker in leastIndexes order
                leastIndexes[leastCount++] = i;
                // Accumulate the total weight of the least active invoker
                totalWeight += afterWarmup;
                // If every invoker has the same weight?
                if (sameWeight && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // Choose an invoker from all the least active invokers
        //(2)
        if (leastCount == 1) {
            // If we got exactly one invoker having the least active value, return this invoker directly.
            return invokers.get(leastIndexes[0]);
        }
        //(3-1)
        if (!sameWeight && totalWeight > 0) {
            // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on 
            // totalWeight.
            int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
            // Return a invoker based on the random value.
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexes[i];
                offsetWeight -= weights[leastIndex];
                if (offsetWeight < 0) {
                    return invokers.get(leastIndex);
                }
            }
        }
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        //(3-2)
        return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
    }
整个算法过程分为三个阶段
   (1)  找到该服务所有提供者中最小并发连接数的一个或多个服务,并计算其权重和,
   (2) 如果最小连接数的服务提供者只有一个,则直接返回给服务
   (3) 如果最小连接数的服务提供者大于1个,则分为以下两种情况
      3-1)如果所有服务权重值不同,则按RandomLoadBalance(2)过程选出服务提供者
      3-2)如果所有服务权重相同,则随机返回一个,leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]

ShortestResponseLoadBalance

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // Number of invokers
        int length = invokers.size();
        // Estimated shortest response time of all invokers
        long shortestResponse = Long.MAX_VALUE;
        // The number of invokers having the same estimated shortest response time
        int shortestCount = 0;
        // The index of invokers having the same estimated shortest response time
        int[] shortestIndexes = new int[length];
        // the weight of every invokers
        int[] weights = new int[length];
        // The sum of the warmup weights of all the shortest response  invokers
        int totalWeight = 0;
        // The weight of the first shortest response invokers
        int firstWeight = 0;
        // Every shortest response invoker has the same weight value?
        boolean sameWeight = true;

        // Filter out all the shortest response invokers
        for (int i = 0; i < length; i++) {
            Invoker<T> invoker = invokers.get(i);
            RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
            // Calculate the estimated response time from the product of active connections and succeeded average elapsed time.
            long succeededAverageElapsed = rpcStatus.getSucceededAverageElapsed();
            int active = rpcStatus.getActive();
            long estimateResponse = succeededAverageElapsed * active;
            int afterWarmup = getWeight(invoker, invocation);
            weights[i] = afterWarmup;
            // Same as LeastActiveLoadBalance
            if (estimateResponse < shortestResponse) {
                shortestResponse = estimateResponse;
                shortestCount = 1;
                shortestIndexes[0] = i;
                totalWeight = afterWarmup;
                firstWeight = afterWarmup;
                sameWeight = true;
            } else if (estimateResponse == shortestResponse) {
                shortestIndexes[shortestCount++] = i;
                totalWeight += afterWarmup;
                if (sameWeight && i > 0
                        && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        if (shortestCount == 1) {
            return invokers.get(shortestIndexes[0]);
        }
        if (!sameWeight && totalWeight > 0) {
            int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
            for (int i = 0; i < shortestCount; i++) {
                int shortestIndex = shortestIndexes[i];
                offsetWeight -= weights[shortestIndex];
                if (offsetWeight < 0) {
                    return invokers.get(shortestIndex);
                }
            }
        }
        return invokers.get(shortestIndexes[ThreadLocalRandom.current().nextInt(shortestCount)]);
    }

整个算法过程和LeastActiveLoadBalance类似,整个算法过程分为三个阶段
(1) 找到该服务所有提供者中响应时间最短的一个或多个服务,并计算其权重和,
(2) 如果响应时间最短的服务提供者只有一个,则直接返回给服务
(3) 如果响应时间最短的服务提供者大于1个,则分为以下两种情况
3-1)如果所有服务权重值不同,则按RandomLoadBalance(2)过程选出服务提供者
3-2)如果所有服务权重相同,则随机返回一个,leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]

RoundRobinLoadBalance

@Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.computeIfAbsent(key, k -> new ConcurrentHashMap<>());
        int totalWeight = 0;
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        Invoker<T> selectedInvoker = null;
        WeightedRoundRobin selectedWRR = null;
        for (Invoker<T> invoker : invokers) {
            String identifyString = invoker.getUrl().toIdentityString();
            int weight = getWeight(invoker, invocation);
            WeightedRoundRobin weightedRoundRobin = map.computeIfAbsent(identifyString, k -> {
                WeightedRoundRobin wrr = new WeightedRoundRobin();
                wrr.setWeight(weight);
                return wrr;
            });

            if (weight != weightedRoundRobin.getWeight()) {
                //weight changed
                weightedRoundRobin.setWeight(weight);
            }
            //累加权重
            long cur = weightedRoundRobin.increaseCurrent();
            weightedRoundRobin.setLastUpdate(now);
            // 如果权重大于最新的 更新
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = invoker;
                selectedWRR = weightedRoundRobin;
            }
            totalWeight += weight;
        }
        // 删除长时间没更新的对象
        if (invokers.size() != map.size()) {
            map.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
        }
        if (selectedInvoker != null) {
        // -1 * totalWeight 类似于清0
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // should not happen here
        return invokers.get(0);
    }

ConsistentHashLoadBalance

一致性Hash算法,doSelect方法进行选择。一致性Hash负载均衡涉及到两个主要的配置参数为hash.arguments与hash.nodes:当进行调用时候根据调用方法的哪几个参数生成key,并根据key来通过一致性hash算法来选择调用节点。例如调用方法invoke(Strings1,Strings2);若hash.arguments为1(默认值),则仅取invoke的参数1(s1)来生成hashCode。

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // using the hashcode of list to compute the hash only pay attention to the elements in the list
        int invokersHashCode = invokers.hashCode();
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != invokersHashCode) {
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);
    }