dubbo之负载均衡策略

写在前面

为了提高系统的性能,优化用户体验,dubbo提供了负载均衡功能,即在一组服务中选择一个来进行调用,从而达到分流的效果。其对应的顶层接口是com.alibaba.dubbo.rpc.cluster.LoadBalance。我们就从这个接口的分析开始吧!

1:LoadBalance

负载均衡策略的顶层接口,源码如下:

// com.alibaba.dubbo.rpc.cluster.LoadBalance
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    // 从集群列表中选择一个invoker
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

@SPI(RandomLoadBalance.NAME)默认使用名称为NAME = "random";的负载均衡实现类,即随机选择的负载均衡策略。其实现类如下图:

在这里插入图片描述

可以看到一共有4个具体实现子类,即dubbo内置(画外音:你可以可以实现自己的负载均衡策略!!!)了4中负载均衡策略。

2:AbstractLoadBalance

源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.select
    // 2022年2月28日16:27:41
    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        if (invokers == null || invokers.isEmpty())
            return null;
        // 如果是只有一个invoker,即只有一个服务提供者,则直接返回唯一的一个invoker
        if (invokers.size() == 1)
            return invokers.get(0);
        // 2022年2月28日16:30:43
        return doSelect(invokers, url, invocation);
    }
}

2022年2月28日16:27:41处使用的是模板方法设计模式 ,提供了公共实现,通过抽象模板方法的方式让子类实现特有逻辑。2022年2月28日16:30:43处是抽象模板方法,每个具体子类根据自身具体场景提供实现,具体参考3:四种内置负载均衡策略。另外,抽象类还针对权重的获取提供了实现,具体参考2.1:getWeight

2.1:getWeight

源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.getWeight
    protected int getWeight(Invoker<?> invoker, Invocation invocation) {
        // 2022年3月1日10:39:08
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
        // 如果是大于0
        if (weight > 0) {
            // 获取当前服务提供者的启动时间,为什么要有这个逻辑,因为JVM在刚刚启动的时候会有一个预热的过程,此时如果接收100%流量可能会出现吃力的情况,
            // 所以这里需要结合服务提供者的预热时长,即已经启动的时间
            long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
            if (timestamp > 0L) {
                // 获取已经启动的毫秒数
                int uptime = (int) (System.currentTimeMillis() - timestamp);
                // 获取服务提供者service的预热时长(单位毫秒),如配置<dubbo:service ... warmup="100"/>,预热时长为100毫秒
                // ,默认的预热时长”Constants.DEFAULT_WARMUP=600000“毫秒 
                int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
                // 如果是启动时长小于需要的预热时长,则结合预热来重新计算权重(会降低权重值)
                if (uptime > 0 && uptime < warmup) {
                    // 2022年3月1日11:27:22
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }
        // 返回[1,100]的权重值
        return weight;
    }
}

2022年3月1日10:39:08处是获取当前服务提供者设置的权重 值,默认值是100,也可以通过如<dubbo:service ... weight="30"/>配置自定义权重。2022年3月1日11:27:22计算带有预热逻辑的权重值,具体参考2.2:calculateWarmupWeight

2.2:calculateWarmupWeight

源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.calculateWarmupWeight
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        // ((float) warmup / (float) weight) -> 计算1个权重值需要的预热时间
        // ((float) uptime / ((float) warmup / (float) weight)) -> 计算当前启动时间包含了多少个“1个权重值需要的预热时间”
        // 如需要的预热时间为36000 当前启动时间为18000 当前权重为20,则结算过程如下:
        // ((float) 36000 / (float) 20) -> 计算1个权重值需要的预热时间为1800
        // (float) 18000 / 1800 -> 10,即只能有一半权重
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        // 权重最小为1,(ww > weight ? weight : ww)考虑启动时长大于预热时长的情况
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }
}

3:四种内置负载均衡策略

dubbo针对负载均衡策略,一种内置提供了4中具体策略,如下图红框中的类:

在这里插入图片描述

下面我们分别来看下。

3.1:RandomLoadBalance

基于权重的随机选择策略,源码如下:

// com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
public class RandomLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "random";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 服务提供者invoker的总个数
        int length = invokers.size();
        // 所有服务提供者invoker的总权重
        int totalWeight = 0; 
        // 所有的服务提供者invoker具有相同的权重?
        boolean sameWeight = true;
        // 遍历所有invoker
        for (int i = 0; i < length; i++) {
            // 2022年3月1日16:19:35
            int weight = getWeight(invokers.get(i), invocation);
            // 总权重
            totalWeight += weight; 
            // 判断是否所有invoker权重都相同,每次都是与前一个比(此处定义一个记录上一个invoker权重的变量是否好些???)
            if (sameWeight && i > 0
                    && weight != getWeight(invokers.get(i - 1), invocation)) {
                sameWeight = false;
            }
        }
        // 不是所有invoker的权重值都相同情况,需要根据基于权重值来随机选择一个
        if (totalWeight > 0 && !sameWeight) {
            // 基于总权重计算一个偏移量
            int offset = random.nextInt(totalWeight);
            // 2022年3月1日16:47:37
            for (int i = 0; i < length; i++) {
                offset -= getWeight(invokers.get(i), invocation);
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // 如果是所有invoker都有一样的权重,均匀的选择一个就可以了,当然直接使用上述选择逻辑也是可以了,但是这样做会节省CPU资源,因此改行代码
        // 可以看做是一种优化
        return invokers.get(random.nextInt(length));
    }

}

2022年3月1日16:19:35处是计算权重值,具体参考2.1:getWeight2022年3月1日16:47:37处通过随机值来选择一个invoker,如下:

假设有3个invoker,invoker1->权重10,invoker2->权重20,invoker3->权重30,则总权重值=40,生成40以内的随机数,如下几种情况:
    [0,9]:选中invoker1,因为[0,9]-10=[-10,-1] < 0
    [10,29]:选中invoker2,因为[10,29]-10-20=[-20,-1] < 0
    [30,59]:选中invoker3,因为[30,59]-10-20-30=[-30,-1] < 0

如下测试代码:

class FakeCls {
    public static void main(String[] args) throws Exception {
        // 模拟invoker集合
        List<String> invokerList = new ArrayList<>();
        invokerList.add("invoker_1");
        invokerList.add("invoker_2");
        invokerList.add("invoker_3");
        // 模拟每个invoker的权重值
        List<Integer> weightList = new ArrayList<>();
        weightList.add(10);
        weightList.add(20);
        weightList.add(30);
        testLoopRatio(100, invokerList, weightList);
        testLoopRatio(10000, invokerList, weightList);
        testLoopRatio(1000000, invokerList, weightList);
    }

    private static void testLoopRatio(int loopNum, List<String> invokerList, List<Integer> weightList) {
        // 100次测试比例
//        int loopNum = 100;
        float invoker1Selected = 0;
        float invoker2Selected = 0;
        float invoker3Selected = 0;
        for (int i = 0; i < loopNum; i++) {
            String selectedInvoker = doSelect(invokerList, weightList);
            if ("invoker_1".equals(selectedInvoker)) {
                invoker1Selected++;
            } else if ("invoker_2".equals(selectedInvoker)) {
                invoker2Selected++;
            } else if ("invoker_3".equals(selectedInvoker)) {
                invoker3Selected++;
            }
        }
        System.out.println(loopNum + "调用最终比例:1:" + (invoker2Selected / invoker1Selected) + ":" + ((invoker3Selected / invoker1Selected)));
    }

    private static Random random = new Random();
    protected static String doSelect(List<String> invokers, List<Integer> weightList) {
        // 服务提供者invoker的总个数
        int length = invokers.size();
        // 所有服务提供者invoker的总权重
        int totalWeight = 0;
        // 所有的服务提供者invoker具有相同的权重?
        boolean sameWeight = true;
        // 遍历所有invoker
        for (int i = 0; i < length; i++) {
            // 2022年3月1日16:19:35
//            int weight = getWeight(invokers.get(i), invocation);
            int weight = weightList.get(i);
            // 总权重
            totalWeight += weight;
            // 判断是否所有invoker权重都相同,每次都是与前一个比(此处定义一个记录上一个invoker权重的变量是否好些???)
            if (sameWeight && i > 0
                    && weight != /*getWeight(invokers.get(i - 1), invocation)*/weightList.get(i - 1)) {
                sameWeight = false;
            }
        }
        // 不是所有invoker的权重值都相同情况,需要根据基于权重值来随机选择一个
        if (totalWeight > 0 && !sameWeight) {
            // 基于总权重计算一个偏移量
            int offset = random.nextInt(totalWeight);
            // 2022年3月1日16:47:37
            for (int i = 0; i < length; i++) {
                offset -= /*getWeight(invokers.get(i), invocation)*/weightList.get(i);
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        return invokers.get(random.nextInt(length));
    }
}

当数据量越大时越接近权重的真实比例,如下是我本地的几次测试结果:

100调用最终比例:1:1.5625:3.6875
10000调用最终比例:1:2.0654087:3.2238994
1000000调用最终比例:1:1.9939269:2.9953523

100调用最终比例:1:1.8333334:2.7222223
10000调用最终比例:1:1.991716:2.925444
1000000调用最终比例:1:2.0033574:3.0026846

100调用最终比例:1:2.0526316:2.2105262
10000调用最终比例:1:2.0618114:3.0581396
1000000调用最终比例:1:2.004641:2.9994535

100调用最终比例:1:1.65:2.35
10000调用最终比例:1:2.0584257:3.091636
1000000调用最终比例:1:1.9908692:3.0004914

3.2:RoundRobinLoadBalance

当使用配置<dubbo:reference ... loadbalance="roundrobin"/>时会使用该轮询负载均衡器。

源码如下:

public class RoundRobinLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "roundrobin";
    
    private static int RECYCLE_PERIOD = 60000;
    
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // dongshi.daddy.service.cluster.ClusterService.sayHi 接口名.方法名
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        // 接口方法->一组invoker的轮询状态
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map == null) {
            // 不存在才添加
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
            map = methodWeightMap.get(key);
        }
        // 总权重
        int totalWeight = 0;
        // 当前最大的轮询计数值,WeightedRoundRobin中的current
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        // 被选择的Invoker
        Invoker<T> selectedInvoker = null;
        // 被选择的invoker对应的WeightedRoundRobin
        WeightedRoundRobin selectedWRR = null;
        for (Invoker<T> invoker : invokers) {
            // 获取当前invoker的标识字符串,如dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService,不同invoker主要是ip,port不同
            String identifyString = invoker.getUrl().toIdentityString();
            // 2022年3月2日12:06:12
            // 获取当前invoker的带权重的轮询状态对象
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            // 2022年3月2日13:05:45 
            int weight = getWeight(invoker, invocation);
            // 权重最小为0,不同于random负载均衡的最下权重值1
            if (weight < 0) {
                weight = 0;
            }
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                // 设置权重
                weightedRoundRobin.setWeight(weight);
                // dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService -> roundRobin
                map.putIfAbsent(identifyString, weightedRoundRobin);
                weightedRoundRobin = map.get(identifyString);
            }
            // 权重在运行过程中发生改变
            if (weight != weightedRoundRobin.getWeight()) {
                weightedRoundRobin.setWeight(weight);
            }
            // 获取当前的计数值,每次+weight
            long cur = weightedRoundRobin.increaseCurrent();
            // 设置当前为最新的更新时间
            weightedRoundRobin.setLastUpdate(now);
            // 当前的轮询计数值大于最大的轮询计数值
            if (cur > maxCurrent) {
                // 设置当前的轮询计数值到为最大的轮询计数值
                maxCurrent = cur;
                // 设置当前的invoker为选中的invoker
                selectedInvoker = invoker;
                // 设置选中WeightedRoundRobin,对应于选中的invoker
                selectedWRR = weightedRoundRobin;
            }
            // 累加总权重
            totalWeight += weight;
        }
        // 这里的代码不知道何时会执行,可以先忽略
        if (!updateLock.get() && invokers.size() != map.size()) {
            if (updateLock.compareAndSet(false, true)) {
                try {
                    // copy -> modify -> update reference
                    ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<String, WeightedRoundRobin>();
                    newMap.putAll(map);
                    Iterator<Entry<String, WeightedRoundRobin>> it = newMap.entrySet().iterator();
                    while (it.hasNext()) {
                        Entry<String, WeightedRoundRobin> item = it.next();
                        if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
                            it.remove();
                        }
                    }
                    methodWeightMap.put(key, newMap);
                } finally {
                    updateLock.set(false);
                }
            }
        }
        if (selectedInvoker != null) {
            // 减去总权重值
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // 这里正常执行不到,可以看做是兜底代码,防止因为前面逻辑有bug导致没有选择任何的invoker,因此可以忽略!
        return invokers.get(0);
    }
}

2022年3月2日12:06:12处是获取某个invoker对应的带权重的轮询对象,具体参考3.2.1:WeightedRoundRobin2022年3月2日13:05:45处是获取当前invoker权重值,具体参考2.1:getWeight。如下测试选择invoker的代码:

class FakeCls {
    private static void testDubboRoundRobinLoadbalance() {
        // 模拟invoker集合
        List<String> invokerList = new ArrayList<>();
        invokerList.add("invoker_1");
        invokerList.add("invoker_2");
        invokerList.add("invoker_3");
        // 模拟每个invoker的权重值
        List<Integer> weightList = new ArrayList<>();
        weightList.add(10);
        weightList.add(20);
        weightList.add(30);
        Foo f = new Foo();
        for (int i = 0; i < 50; i++) {
            System.out.println(f.doSelectWithRoundRobin(invokerList, weightList));
        }
    }
    protected static class WeightedRoundRobin {
        private int weight;
        private AtomicLong current = new AtomicLong(0);
        private long lastUpdate;
        public int getWeight() {
            return weight;
        }
        public void setWeight(int weight) {
            this.weight = weight;
            current.set(0);
        }
        public long increaseCurrent() {
            return current.addAndGet(weight);
        }
        public void sel(int total) {
            current.addAndGet(-1 * total);
        }
        public long getLastUpdate() {
            return lastUpdate;
        }
        public void setLastUpdate(long lastUpdate) {
            this.lastUpdate = lastUpdate;
        }
    }
    private ConcurrentMap<String, ConcurrentMap<String, WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap<String, ConcurrentMap<String, WeightedRoundRobin>>();
//    private AtomicBoolean updateLock = new AtomicBoolean();
    protected String doSelectWithRoundRobin(List<String> invokerList, List<Integer> weightList) {
        // dongshi.daddy.service.cluster.ClusterService.sayHi 接口名.方法名
//        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        String key = "dongshi.daddy.service.cluster.ClusterService.sayHi";
        // 接口方法->一组invoker的轮询状态
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map == null) {
            // 不存在才添加
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
            map = methodWeightMap.get(key);
        }
        // 总权重
        int totalWeight = 0;
        // 当前最大的轮询计数值,WeightedRoundRobin中的current
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        // 被选择的Invoker
//        Invoker<T> selectedInvoker = null;
        String selectedInvoker = null;
        // 被选择的invoker对应的WeightedRoundRobin
        WeightedRoundRobin selectedWRR = null;
//        for (Invoker<T> invoker : invokers) {
        for (int i = 0; i < invokerList.size(); i++) {
            // 获取当前invoker的标识字符串,如dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService,不同invoker主要是ip,port不同
//            String identifyString = invoker.getUrl().toIdentityString();
            String invoker = invokerList.get(i);
//            String identifyString = invoker.getUrl().toIdentityString();
            String identifyString = invoker.toString();
            // 2022年3月2日12:06:12
            // 获取当前invoker的带权重的轮询状态对象
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            // 2022年3月2日13:05:45
//            int weight = getWeight(invoker, invocation);
            int weight = weightList.get(i);
            // 权重最小为0,不同于random负载均衡的最下权重值1
            if (weight < 0) {
                weight = 0;
            }
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                // 设置权重
                weightedRoundRobin.setWeight(weight);
                // dubbo://192.168.64.1:20826/dongshi.daddy.service.cluster.ClusterService -> roundRobin
                map.putIfAbsent(identifyString, weightedRoundRobin);
                weightedRoundRobin = map.get(identifyString);
            }
            // 权重在运行过程中发生改变
            if (weight != weightedRoundRobin.getWeight()) {
                weightedRoundRobin.setWeight(weight);
            }
            // 获取当前的计数值,每次+weight
            long cur = weightedRoundRobin.increaseCurrent();
            // 设置当前为最新的更新时间
            weightedRoundRobin.setLastUpdate(now);
            // 当前的轮询计数值大于最大的轮询计数值
            if (cur > maxCurrent) {
                // 设置当前的轮询计数值到为最大的轮询计数值
                maxCurrent = cur;
                // 设置当前的invoker为选中的invoker
                selectedInvoker = invoker;
                // 设置选中WeightedRoundRobin,对应于选中的invoker
                selectedWRR = weightedRoundRobin;
            }
            // 累加总权重
            totalWeight += weight;
        }
        if (selectedInvoker != null) {
            // 减去总权重值
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // 这里正常执行不到,可以看做是兜底代码,防止因为前面逻辑有bug导致没有选择任何的invoker,因此可以忽略!
        return invokerList.get(0);
    }
}

运行testDubboRoundRobinLoadbalance测试,如下是我本地的运行结果:

invoker_3
invoker_2
invoker_1
invoker_3
invoker_2
invoker_3
...

3,2,1,3,2,3一个循环,可以看到是和权重等比例出现的,和随机的区别就是,是有固定规律的,但是最终被选中的次数的比例是和权重值等比的。如果是将权重改成一致,则执行顺序是1,2,3,1,2,3...

3.2.1:WeightedRoundRobin

源码如下:

// com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance.WeightedRoundRobin
protected static class WeightedRoundRobin {
    // invoker对应的权重值
    private int weight;
    // 辅助轮询原子变量
    private AtomicLong current = new AtomicLong(0);
    private long lastUpdate;
    public int getWeight() {
        return weight;
    }
    public void setWeight(int weight) {
        this.weight = weight;
        current.set(0);
    }
    public long increaseCurrent() {
        // 每次新增权重值
        return current.addAndGet(weight);
    }
    public void sel(int total) {
        current.addAndGet(-1 * total);
    }
    public long getLastUpdate() {
        return lastUpdate;
    }
    public void setLastUpdate(long lastUpdate) {
        this.lastUpdate = lastUpdate;
    }
}

3.3:LeastActiveLoadBalance

在服务提供者端设置<dubbo:reference ... loadbalance="leastactive"/>可以使用最小活跃负载均衡器,源码如下:

// com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
public class LeastActiveLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "leastactive";
    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // invoker的个数
        int length = invokers.size();
        // 所有invoker中的最小活跃值,比如三个invoker活跃值分别是1,1,6,则该值是1
        int leastActive = -1;
        // 拥有最小活跃值的个数,比如三个invoker活跃值分别是1,1,6,则该值是2
        int leastCount = 0; 
        // 拥有相同最小活跃值的索引位数组
        int[] leastIndexs = new int[length];
        // 总权重(计算预热后的权重) 
        int totalWeight = 0; 
        int firstWeight = 0; 
        // 所有的invoker拥有相同的权重值?
        boolean sameWeight = true; 
        for (int i = 0; i < length; i++) {
            // 获取当前的invoker
            Invoker<T> invoker = invokers.get(i);
            // 2022年3月2日15:51:52
            // 获取当前invoker的活跃值
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
            // 2022年3月2日15:55:55
            int afterWarmup = getWeight(invoker, invocation);
            // 还没有设置真实的活跃值,或者是当前invoker的活跃值小于当前最小的活跃值 
            if (leastActive == -1 || active < leastActive) {
                // 记录最小的活跃值
                leastActive = active;
                // 重置拥有最小活跃值的invoker的个数为1,比如当前活跃值是2,8,2,1,1,则当执行到第一个1时lastCount=2,这里就会重置为1
                leastCount = 1;
                // 重置拥有最小活跃值的索引位置数组,比如当前活跃值是2,8,2,1,1,则当执行到第一个1时数组为[0,2],这里就会重置为[3]
                leastIndexs[0] = i; 
                // 重置拥有相同最小活跃值的invoker的总权重值,比如当前活跃值对应权重值是2->20,8->10,2->25,1->50,1->40,则当执行到第一个1该值为20+25=45
                // ,此处重置totalWeight=50
                totalWeight = afterWarmup;
                // 重置拥有最小活跃值的第一个权重值,比如当前活跃值对应权重值是2->20,8->10,2->25,1->50,1->40,则当执行到第一个1该值为20
                 // ,此处重置firstWeight=50
                firstWeight = afterWarmup;
                // 重置拥有相同活跃值的invoker集合是否都拥有相同的权重
                sameWeight = true; 
            // 如果是当前的活跃值和当前最小的活跃值相等,则执行累加计算
            } else if (active == leastActive) {
                // 记录当前invoker的索引位置
                leastIndexs[leastCount++] = i;
                // 累加拥有最小活跃值的权重值 
                totalWeight += afterWarmup; 
                // 并非所有拥有最小活跃值的invoker权重都相等的情况
                if (sameWeight && i > 0
                        && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // 拥有最小活跃值的invoker只有1个
        if (leastCount == 1) {
            // 直接返回唯一拥有最小活跃值的invoker
            return invokers.get(leastIndexs[0]);
        }
        // 2022年3月2日16:17:35
        if (!sameWeight && totalWeight > 0) {
            int offsetWeight = random.nextInt(totalWeight) + 1;
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexs[i];
                offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
                if (offsetWeight <= 0)
                    return invokers.get(leastIndex);
            }
        }
        // 所有拥有最小活跃值的invoker都拥有相同的权重值,则随机选择一个
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}

2022年3月2日15:51:52处RpcStatus是用来维护远程调用状态的对象,用来辅助QOS(个人猜测,非官方说法!!!),2022年3月2日15:55:55处是获取权重值,具体参考2.1:getWeight2022年3月2日16:17:35处是随机选择一个,可以参考3.1:RandomLoadBalance

3.4:ConsistentHashLoadBalance

基于一致性哈希 实现的负载均衡算法,源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.doSelect
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 获取调用的服务类的方法名称
        String methodName = RpcUtils.getMethodName(invocation);
        // 服务类名称.方法名称 如:dongshi.daddy.service.cluster.ClusterService.sayHi
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // 获取对象的哈希值
        int identityHashCode = System.identityHashCode(invokers);
        // private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
        // 处获取对应的服务类方法的选择器,因为`服务类+方法`组合组成负载均衡的集群,所以这里以此为单位来操作
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            // 2022年3月8日17:58:30
            // 创建ConsistentHashSelector实例
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            // 获取一致性哈希选择器
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        // 2022年3月8日17:55:45
        return selector.select(invocation);
    }
}

2022年3月8日17:58:30处是创建一致性哈希选择器,具体参考3.4.1:ConsistentHashSelector2022年3月8日17:55:45处是通过一致性哈希选择器选择一个执行器,具体参考3.4.3:select

3.4.1:ConsistentHashSelector

使用的是Ketama[一致性哈希算法}(https://blog.csdn.net/wang0907/article/details/123282782) 。

源码如下:

// com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector
private static final class ConsistentHashSelector<T> {
    // 虚拟invoker,即虚拟节点
    private final TreeMap<Long, Invoker<T>> virtualInvokers;
    // 副本数,即每个invoker对应的虚拟节点个数
    private final int replicaNumber;
    private final int identityHashCode;
    private final int[] argumentIndex;

    ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
        // 初始化虚拟节点
        this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
        // 初始化哈希值,有什么用???
        this.identityHashCode = identityHashCode;
        URL url = invokers.get(0).getUrl();
        // 获取虚拟节点个数,默认是160
        this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
        // 在选择目标invoker时使用方法参数中的哪个位置的参数来计算哈希值[0]
        String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
        // 赋值到argumentIndex,后续根据方法入参生成用于计算哈希值的key时,会用到,参考方法com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.toKey
        argumentIndex = new int[index.length];
        for (int i = 0; i < index.length; i++) {
            argumentIndex[i] = Integer.parseInt(index[i]);
        }
        // 循环处理每一个invokers为其生成虚拟节点
        for (Invoker<T> invoker : invokers) {
            // 如192.168.64.1:20826
            String address = invoker.getUrl().getAddress();
            // 2022年3月10日10:45:08
            for (int i = 0; i < replicaNumber / 4; i++) {
                // 以地址+i作为虚拟节点的哈希值
                byte[] digest = md5(address + i);
                // 依次处理哈希结果的16byte数据添加到虚拟节点
                for (int h = 0; h < 4; h++) {
                    // 2022年3月10日10:50:24
                    long m = hash(digest, h);
                    virtualInvokers.put(m, invoker);
                }
            }
        }
    }
}

2022年3月10日10:45:08处每4个虚拟机节点作为一组这一组对应的是一个节点,为什么是4呢,因为后续计算哈希使用的是md5算法,而md5算法的结果是一个128bit即16byte的结果,这样将16byte平分为4份,在生成了对应数量的虚拟节点的同时,也充分利用了结果的每一个bit,增加了结果的随机性2022年3月10日10:50:24计算哈希值,具体参考3.4.2:hash

3.4.2:hash

源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.hash
    private long hash(byte[] digest, int number) {
        return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                | (digest[number * 4] & 0xFF))
                & 0xFFFFFFFFL;
    }
}

digest可能如下图:

在这里插入图片描述

以上方法是取对应的4个byte,分别是[0,3],[4,7],[8,11],[12,15],左移运算<<是将结果扩大对应的倍数,& 0xFF是将负数转正数,如-82,结果是174,我们来简单看下这个计算过程:

在计算机中有符号数高位为1的代表负数,高位为0的代表正数,而负数是以补码形式存储的,而补码是绝对值二进制码取反然后加1的结果,因此-82在计算机中的存储形式如下:
    取绝对值:82 -> 01010010
    取反:01010010 -> 10101101
    +1:10101101 -> 10101110
    高位补1:10101110 -> 11111111 11111111 11111111 10101110
0xFF在计算机中的表示是:11111111,因为是正数,所以高位补0,结果为:00000000 00000000 00000000 11111111
-82 & 0xFF
  11111111 11111111 11111111 10101110
& 00000000 00000000 00000000 11111111
= 00000000 00000000 00000000 10101110 = 174

最后的& 0xFFFFFFFFL作用是转long。

3.4.3:select

源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.select
    public Invoker<T> select(Invocation invocation) {
        // 使用调用方法的参数作为key,如sayHi("helloooo"),这里就是helloooo
        String key = toKey(invocation.getArguments());
        // 计算md5
        byte[] digest = md5(key);
        // 2022年3月10日13:31:38
        // 计算哈希值,然后获取目标invoker
        return selectForKey(hash(digest, 0));
    }
}

2022年3月10日13:31:38处源码如下:

class FakeCls {
    // com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector.selectForKey
    private Invoker<T> selectForKey(long hash) {
        // 获取大于等于hash的子字典,然后获取第一个,就是我们需要的
        Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
        // 没有,则使用第一个,当没有落到哈希环内有这种情况???
        if (entry == null) {
            entry = virtualInvokers.firstEntry();
        }
        // 获取目标值
        return entry.getValue();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值