8、Dubbo源码系列-路由策略

295 阅读7分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

前面文章分析了Dubbo客户端启动相关流程,其中提到了路由策略,当时由于篇幅问题,未展开详细讲解,本文将带大家一起深入分析下其实现。

一、温故知新

    private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
        ... ...  
        // 1、建立路由规则
        directory.buildRouterChain(subscribeUrl);
        // 2、订阅服务提供方者地址
        directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));

        // 3、包装容错策略到invoker
        Invoker invoker = cluster.join(directory);
        return invoker;
    }
    
    public void buildRouterChain(URL url) {
        this.setRouterChain(RouterChain.buildChain(url));
    }
    
    public static <T> RouterChain<T> buildChain(URL url) {
        return new RouterChain<>(url);
    }

前文分析RegistryProtocol-doRefer方法时,提到过,首先调用directory.buildRouterChain方法进行路由策略的建立,然后通过RouterChain.buildChain方法创建RouterChain对象,最终在进行远程方法调用的时候,会通过RouterChain.route方法过滤获取到可用的invoke list。那就让我们来看看RouterChain到底是什么东东吧。

二、RouterChain详解

其实故名思义,其本意为“路由链”,也就说要进行一系列的链式调用。最终过滤出来可用的的invokelist,供后面路由策略调用。前文也分析过,在进行RouterChain初始化的时候会进行四种路由策略的初始化,从初始化方法可以看到,默认四种routers实现,优先级分别是MockInvokersSelector>TagRouter>AppRouter>ServiceRouter

  • MockInvokersSelector
    • 该类与服务降级、Mock功能有关,其route方法判断invocation.need.mock参数的的值,默认情况未配置,因此不会执行任何操作。
  • TagRouter
    • tag路由是dubbo2.6.6新增的功能,可以实现流量隔离,访问指定的tag服务。consumer端可以通过RpcContext.getContext().setAttachment("dubbo.tag", "xxx")设置tag,即可访问到对应的服务。
  • AppRouter & ServiceRouter
    • 都继承了ListenableRouter类,区别在于调用ListenableRouter构造函数的时候,传参不一样,两个类的路由规则其实都是委托给ConditionRouter集合。

三、路由策略选择

拿到可用的invoke list后,就要进行负载均衡选择最终的invoke啦。集群容错策略以FailoverClusterInvoker为例,执行其doInvoke方法进行方法调用时,首先通过select获取到最终执行方法调用的invoke,这里的选择,就是经历负载均衡策略之后的结果。

  public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
      ... ... 
      Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
      ... ... 

查看select实现如下:

    protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
                                List<Invoker<T>> invokers, List<Invoker<T>> selected) 
        ... ... 
        Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
        ... ...
    }

查看doSelect实现如下:

    private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
                                List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
        ... ... 
        Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
        ... ...

    }

内部其实是调用loadbalance.select方法进行实际的负载均衡选择。

四、LoadBalance分析

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

可以看到LoadBalance本身也是一个SPI,默认实现为RandomLoadBalance,查看select实现,最终调用的是抽象方法doSelect。

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

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

查看doSelect实现如下: image.png

实际上Dubbo提供了四种负载均衡策略,分别是RandomLoadBalance-随机、ConsistentHashLoadBalance-一致性hash、LeastActiveLoadBalance-最小连接数、RoundRobinLoadBalance-轮询。

4.1 RandomLoadBalance-随机

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 服务提供方总个数
        int length = invokers.size();
        // 权重是否一样
        boolean sameWeight = true;
        // 存放所有服务提供者设置的权重
        int[] weights = new int[length];
        // 第一个服务提供者的权重
        int firstWeight = getWeight(invokers.get(0), invocation);
        weights[0] = firstWeight;
        // 所有权重之和
        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;
            }
        }
        
        // 如果所有提供者的权重不一样,并且至少有一个提供者的权重 >0,则基于总权重,随机选一个
        if (totalWeight > 0 && !sameWeight) {
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            for (int i = 0; i < length; i++) {
                offset -= weights[i];
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // 所有提供者的权重都一样,就基于服务提供方的个数轮训随机
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

4.2 ConsistentHashLoadBalance-一致性hash

    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;
        int identityHashCode = System.identityHashCode(invokers);
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);
    }

一致性hash算法实现主要依赖ConsistentHashSelector类,构造函数以及核心select方法如下

        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();
            // 获取设置的虚拟阶段个数
            this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
            String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
            argumentIndex = new int[index.length];
            for (int i = 0; i < index.length; i++) {
                argumentIndex[i] = Integer.parseInt(index[i]);
            }
            // 遍历invokers列表,计算每个阶段的hash值,保存到virtualInvokers
            for (Invoker<T> invoker : invokers) {
                String address = invoker.getUrl().getAddress();
                for (int i = 0; i < replicaNumber / 4; i++) {
                    byte[] digest = md5(address + i);
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            // 获取参数一致性hash算法的key
            String key = toKey(invocation.getArguments());
            // 根据key计算md5
            byte[] digest = md5(key);
            // 计算key对应的hash环上哪一个节点
            return selectForKey(hash(digest, 0));
        }

这里多说一句,其实Guava包里也提供了一个一致性hash算法实现,有兴趣的同学可以自行研究下。

4.3 LeastActiveLoadBalance-最小连接数

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 服务提供者数量
        int length = invokers.size();
        // 最小调用次数
        int leastActive = -1;
        // 调用次数=最小调用次数的 服务提供者的数量
        int leastCount = 0;
        // 调用次数=最小调用次数的 服务提供这的下标
        int[] leastIndexes = new int[length];
        // 记录每个服务的权重
        int[] weights = new int[length];
        // 调用次数=最小调用次数的 服务提供者的权重和
        int totalWeight = 0;
        // 记录第一个,调用次数=最小调用次数的 服务提供者的权重
        int firstWeight = 0;
        // 权重相同
        boolean sameWeight = true;


        // 过滤中所有的调用次数=最小调用次数的服务提供者
        for (int i = 0; i < length; i++) {
            Invoker<T> invoker = invokers.get(i);
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
            int afterWarmup = getWeight(invoker, invocation);
            weights[i] = afterWarmup;
            if (leastActive == -1 || active < leastActive) {
                leastActive = active;
                leastCount = 1;
                leastIndexes[0] = i;
                totalWeight = afterWarmup;
                firstWeight = afterWarmup;
                sameWeight = true;
            } else if (active == leastActive) {
                leastIndexes[leastCount++] = i;
                totalWeight += afterWarmup;
                if (sameWeight && i > 0
                        && afterWarmup != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        // 如果只有一个最小调用次数的的invoker,则直接返回
        if (leastCount == 1) {
            return invokers.get(leastIndexes[0]);
        }
        
        // 存在多个invoke的最小调用次数相同,并且多个权重不一样,则在最小调用次数的集合里随机选一个
        if (!sameWeight && totalWeight > 0) {
            int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexes[i];
                offsetWeight -= weights[leastIndex];
                if (offsetWeight < 0) {
                    return invokers.get(leastIndex);
                }
            }
        }
        // 存在多个最小invoker的最小调用次数相同,并且权重一样,则直接在最小的集合里随机一个
        return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
    }

4.4 RoundRobinLoadBalance-轮询

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        
        // 获取调用方法的key
        String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
        // 获取该调用方法对应的每个服务提供者的WeightedRoundRobin对象
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map == null) {
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
            map = methodWeightMap.get(key);
        }
        
        // 遍历所有服务提供者,计算总权重最大的提供者
        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();
            WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
            int weight = getWeight(invoker, invocation);

            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(identifyString, weightedRoundRobin);
            }
            // 权重发生了变化
            if (weight != weightedRoundRobin.getWeight()) {
                weightedRoundRobin.setWeight(weight);
            }
            long cur = weightedRoundRobin.increaseCurrent();
            weightedRoundRobin.setLastUpdate(now);
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = invoker;
                selectedWRR = weightedRoundRobin;
            }
            totalWeight += weight;
        }
        
        // 更新map
        if (!updateLock.get() && invokers.size() != map.size()) {
            if (updateLock.compareAndSet(false, true)) {
                try {
                    // 拷贝新值
                    ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<>(map);
                    // 移除过期的
                    newMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
                    methodWeightMap.put(key, newMap);
                } finally {
                    updateLock.set(false);
                }
            }
        }
        // 返回选择的select对象
        if (selectedInvoker != null) {
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // 兜底,应该不会走到这
        return invokers.get(0);
    }

这里的轮训算法,选择的时候参考了机器权重,假如当前服务提供者有三台机器,权重都一样,那么就会按照A、B、C顺序的方式依次选择每台机器。引入了权重就相对复杂一点,以A、B、C对应的权重分别为1、2、3为例

  • 第一次调用
机器(weight,current)计算(weight,current)选择机器选择后的(weight,current)
A(1,0)(1,1)选择机器C(1,1)
B(2,0)(2,2)选择机器C(2,2)
C(3,0)(3,3)选择机器C(3,-3)

第一次调用服务时,WeightedRoundRobin里的weight,current如二列所示,执行完选择权重和最大的代码后,weight,current如第三列所示,此时机器C的权重最大,因此,第一次选择,会选择机器C用来发起远程服务调用。

  • 第二次调用
机器(weight,current)计算(weight,current)选择机器选择后的(weight,current)
A(1,2)(1,2)选择机器B(1,2)
B(2,2)(2,4)选择机器B(2,-2)
C(3,-3)(3,0)选择机器B(3,0)

第二次调用服务时,WeightedRoundRobin里的weight,current如二列所示,执行完选择权重和最大的代码后,weight,current如第三列所示,此时机器B的权重最大,因此,第二次选择,会选择机器B用来发起远程服务调用。

  • 第三次调用
机器(weight,current)计算(weight,current)选择机器选择后的(weight,current)
A(1,2)(1,3)选择机器A(1,-3)
B(2,-2)(2,0)选择机器A(2,0)
C(3,0)(3,3)选择机器A(3,3)

第三次调用服务时,WeightedRoundRobin里的weight,current如二列所示,执行完选择权重和最大的代码后,weight,current如第三列所示,此时机器A的权重最大,因此,第三次选择,会选择机器B用来发起远程服务调用。

四、小节

本文主要分析了Dubbo负载均衡实现,其中提到的几种负载均衡算法也是平时工作中经常用到的,能理解最好,实在不理解也先有个印象,等日后工作中用到时候,最起码知道去哪里参考(手动🐶)。