Dubbo的容错机制

聊到容错就不得不提到下面这张图,比较详细的勾画了整个处理流程对象之间的关系。下面我会结合这张图已经源码,聊一聊Dubbo的容错机制。

 

 

这里的 Invoker 是 Provider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息,Directory 代表多个 Invoker,可以把它看成 List<Invoker>

但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更,Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker

对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个

 

所有上面的图中我们可以分成两条线去理解,首先是横向的,从Cluster->Dictionary

这是从多个Invoker抽象到一个的过程。因为上面我们说了Dictionary中包含了多个Invoker

第二条线是从Dictionary向下的一条线。这里理解为Dictionary中是包含了多个实际的Invoker的,但是包含哪些Invoker?

这就是下面这条线的处理,Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等

LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。

 

 

这个就是在源码中的类图,都是在Cluster包里面。然后我们也知道现在的Rpc框架都是在Client端进行的负载均衡和容错,这样做的目的是减轻了中心服务端的压力。

而这里Cluster是一个接口,具体负载均衡的实现策略是通过support包中的实现类来实现的

 

 这里的负载均衡策略都是支持Dubbo的SPI的,所以如果Dubbo提供的策略不能满足业务需求,我们也可以自定义策略。

这里标注一下,Dubbo默认提供六种负载策略:

  • Failover Cluster
  • Failfast Cluster
  • Failsafe Cluster
  • Failback Cluster
  • Forking Cluster
  • Broadcast Cluster

这里我们对默认的策略方式Failover 的策略进行分析:
首先按照类的层级,我们先看一下抽象接口Cluster的内容是什么

/**
 * Cluster. (SPI, Singleton, ThreadSafe)
 * <p>
 * <a href="http://en.wikipedia.org/wiki/Computer_cluster">Cluster</a>
 * <a href="http://en.wikipedia.org/wiki/Fault-tolerant_system">Fault-Tolerant</a>
 *
 */
@SPI(FailoverCluster.NAME)
public interface Cluster {

    /**
     * Merge the directory invokers to a virtual invoker.
     *
     * @param <T>
     * @param directory
     * @return cluster invoker
     * @throws RpcException
     */
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;
}

这里定义了一个抽象方法join,根据注释我们知道方法的作用是将Dictionary添加到Cluster中

然后我们再来看看Failover :

/**
 * {@link FailoverClusterInvoker}
 *
 */
public class FailoverCluster implements Cluster {

    public final static String NAME = "failover";

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<T>(directory);
    }
}

这里实现了join方法,返回的是一个FailoverClusterInvoker对象,所以我们猜测Failover的真正实现应该是在FailoverClusterInvoker里面

所以我们接下来去看看FailoverClusterInvoker里面写了什么

/**
 * When invoke fails, log the initial error and retry other invokers (retry n times, which means at most n different invokers will be invoked)
 * Note that retry causes latency.
 * <p>
 * <a href="http://en.wikipedia.org/wiki/Failover">Failover</a>
 *
 */
public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {

FailoverClusterInvoker里面的代码比较多,我们首先看一下类头部。

这里有两部分信息,一个是注释,大意是当调用失败的时候,记录下错误并且用其他的Invoker去继续调用

从这里可以知道Failover这个策略的含义就是失败后换一个Invoker重试,当然这里你可以设置重试的次数等等。

 

第二就是类的实现,是通过继承了AbstractClusterInvoker实现的功能

我们再追到AbstractClusterInvoker的时候,又发现AbstractClusterInvoker实际上是实现了Invoker接口。

 

下面我们还是按照类的关系进行继续分析:

/**
 * Invoker. (API/SPI, Prototype, ThreadSafe)
 *
 * @see org.apache.dubbo.rpc.Protocol#refer(Class, org.apache.dubbo.common.URL)
 * @see org.apache.dubbo.rpc.InvokerListener
 * @see org.apache.dubbo.rpc.protocol.AbstractInvoker
 */
public interface Invoker<T> extends Node {

    /**
     * get service interface.
     *
     * @return service interface.
     */
    Class<T> getInterface();

    /**
     * invoke.
     *
     * @param invocation
     * @return result
     * @throws RpcException
     */
    Result invoke(Invocation invocation) throws RpcException;

}

Invoker这里比较简单,就是定义了两个方法,一个是获取Interface,另一个是方法的实际调用。

AbstractClusterInvoker里面的代码很多,这里就不全罗列出来了,不过我们可以知道,AbstractClusterInvoker是实现了Invoker接口的,那么就一定会实现其中的方法。

下面罗列一下的主要方法:

  • doSelect
  • reselect
  • invoke

其中前两个方法是根据负载均衡来选择一个Invoker的,具体的内容就不多解释了,可以参考之前的文章Dubbo的负载均衡

这里我们重点说说实现的invoke方法:

@Override
    public Result invoke(final Invocation invocation) throws RpcException {
        checkWhetherDestroyed();

        // binding attachments into invocation.
        Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
            ((RpcInvocation) invocation).addAttachments(contextAttachments);
        }

        List<Invoker<T>> invokers = list(invocation);
        LoadBalance loadbalance = initLoadBalance(invokers, invocation);
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        return doInvoke(invocation, invokers, loadbalance);
    }

这里的方法都是一些通用的处理,主要的内容就是return,但是这里又没有doInvoke的实现,这就是这个类的目的

具体的容错策略是通过自己的实现类来实现的,所以我们推断,针对Failover策略这个doInvoke方法应该是在FailoverClusterInvoker中实现的。

 

下面我们就来看看这个最终的方法的面纱吧:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyinvokers = invokers;
        checkInvokers(copyinvokers, invocation);
        String methodName = RpcUtils.getMethodName(invocation);
        int len = getUrl().getMethodParameter(methodName, Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        for (int i = 0; i < len; i++) {
            //Reselect before retry to avoid a change of candidate `invokers`.
            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
            if (i > 0) {
                checkWhetherDestroyed();
                copyinvokers = list(invocation);
                // check again
                checkInvokers(copyinvokers, invocation);
            }
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List) invoked);
            try {
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn("Although retry the method " + methodName
                            + " in the service " + getInterface().getName()
                            + " was successful by the provider " + invoker.getUrl().getAddress()
                            + ", but there have been failed providers " + providers
                            + " (" + providers.size() + "/" + copyinvokers.size()
                            + ") from the registry " + directory.getUrl().getAddress()
                            + " on the consumer " + NetUtils.getLocalHost()
                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                            + le.getMessage(), le);
                }
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) { // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        throw new RpcException(le.getCode(), "Failed to invoke the method "
                + methodName + " in the service " + getInterface().getName()
                + ". Tried " + len + " times of the providers " + providers
                + " (" + providers.size() + "/" + copyinvokers.size()
                + ") from the registry " + directory.getUrl().getAddress()
                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                + Version.getVersion() + ". Last error is: "
                + le.getMessage(), le.getCause() != null ? le.getCause() : le);
    }

代码比较长,保持耐心,我们分成几部分来分析:

  • Invoker检查和参数获取阶段:这里首先检查一下Invoker List,获取一下要调用的方法名称,然后通过URL获取一下设置的重试次数,默认是2。
  • 定义两个集合:这里定义了两个集合对象,一个List invoked,一个Set providers。这里invoked是记录了每次调用选择了哪个Invoker,避免下次调用的时候又选出之前调用失败的Invoker,然后providers同样保存了调用了哪些Invoker,主要是为了log记录。
  • 循环阶段:这里我们前面说了很多,这个Failover策略在调用失败的时候,会根据负载均衡的策略进行重试的,那么是怎么重试的呢?就在这个for循环里面。这里我们看到for循环的次数是那个len的变量,这个变量上面我们已经说了,是获取设置的重试次数的。然后这里我们通过select选出的Invoker,去调用对应的invoke方法,如果失败了就记录一下,然后继续循环获取其他的Invoker继续执行调用,如果调用成功了,那么就直接放回result。

上面就是Failover的实现原理,其他的几种策略在模式上都差不多,喜欢的读者可以自行去查看相关的源码。

posted @ 2020-03-13 17:49  SyrupzZ  阅读(389)  评论(0)    收藏  举报