Dubbo集群容错之LoadBalance

605 阅读4分钟

1. 前言

前面的文章已经分析了,Dubbo在做集群调用时,Directory会先根据Router做路由规则过滤,最后幸存下来的Invoker才会对它们做负载均衡。 ​

LoadBalance是Dubbo提供的负载均衡接口,截止2.7.8版本,Dubbo内置了五种负载均衡策略。LoadBalance也是可以通过SPI加载的,开发者可以很方便的扩展自定义的负载均衡策略。

2. 源码分析

LoadBalance接口定义十分简单,它只有一个功能,从一组Invoke里选出一个最终等待被调用的Invoker。

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

LoadBalance只负责实现负载均衡算法,Dubbo并没有直接使用它,而是在AbstractClusterInvoker#select()对它做了一层包装,包装的目的是为了实现以下功能:

  1. 粘滞连接。
  2. 连接可用性检测。
  3. 失败服务规避。

AbstractClusterInvoker#select()会在负载均衡之前处理粘滞连接的问题,「粘滞连接」是Dubbo的一个特性,适用于有状态的服务,尽可能的让消费者总是向同一个提供者发起调用,除非该提供者挂了。

protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
                            List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    String methodName = invocation == null ? StringUtils.EMPTY_STRING : invocation.getMethodName();
    // 是否开启粘滞连接?
    boolean sticky = invokers.get(0).getUrl()
        .getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);
    if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
        stickyInvoker = null;
    }
    if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
        if (availablecheck && stickyInvoker.isAvailable()) {
            return stickyInvoker;
        }
    }
    Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
    if (sticky) {
        stickyInvoker = invoker;
    }
    return invoker;
}

紧接着,AbstractClusterInvoker#doSelect()方法会进行负载均衡,选出的Invoker首先会判断之前是否已经调用过,服务重试时会规避掉之前已经失败的服务,然后再对Invoker做连接可用性检测,以此为依据判断是否需要重新负载。

private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
                            List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers.size() == 1) {
        return invokers.get(0);
    }
    // 负载均衡选出Invoker
    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);

    /**
     * 判断是否需要重新负载均衡
     * 1.选出的Invoker已经被使用过
     * 2.连接不可用且开启连接可用性检测
     */
    if ((selected != null && selected.contains(invoker))
        || (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
        try {
            // 重新负载
            Invoker<T> rInvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
            if (rInvoker != null) {
                invoker = rInvoker;
            } else {
                int index = invokers.indexOf(invoker);
                try {
                    invoker = invokers.get((index + 1) % invokers.size());
                } catch (Exception e) {
                    logger.warn(e.getMessage() + " may because invokers list dynamic change, ignore.", e);
                }
            }
        } catch (Throwable t) {
            logger.error("cluster reselect fail reason is :" + t.getMessage() + " if can not solve, you can set cluster.availablecheck=false in url", t);
        }
    }
    return invoker;
}

上述是Dubbo调用LoadBalance之前的包装处理,接下来我们看看原始的LoadBalance是如何实现的。 ​

截止2.7.8版本,Dubbo内置了五种负载均衡策略:

策略说明
Random加权随机,默认算法
RoundRobin加权轮询
LeastActive最少活跃调用数,慢的提供者会接收到更少的请求
ConsistentHash一致性哈希,相同参数请求同一提供者
ShortestResponse最快响应,选出响应时间最短的提供者

类的继承关系如下图: image.png 抽象类的select()方法很简单,只是判断如果提供者就一个的话,直接返回,因为没得选。提供者有多个,才会调用子类的doSelect()方法。

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

RandomLoadBalance是Dubbo默认的负载均衡策略,我们以它为例分析下,其他算法会在后面的文章分析。 ​

RandomLoadBalance采用加权随机算法,理论上来说,权重越大的提供者,被随机到的概率也就越大,参数weight用来设置权重。 加权随机的算法过程:

  1. 判断所有提供者权重是否相同,计算总权重。
  2. 权重相同,直接对数量做随机,返回结束。
  3. 权重不同,对总权重随机一个偏移量offset。
  4. 根据offset落在权重数组的位置,返回提供者。
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];
    // 第0个提供者的权重
    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;
        }
    }
    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));
}

下面这张图,描述了这个过程: image.png

3. 总结

LoadBalance是Dubbo提供的负载均衡接口,截止2.7.8版本Dubbo内置了五种负载均衡策略,Random加权随机是默认的策略。 Dubbo并没有直接使用LoadBalance,而是在AbstractClusterInvoker包装了一层,包装的目的是为了实现粘滞连接特性、Invoker连接可用性检测、避免Invoker重复调用等功能。