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,一个Setproviders。这里invoked是记录了每次调用选择了哪个Invoker,避免下次调用的时候又选出之前调用失败的Invoker,然后providers同样保存了调用了哪些Invoker,主要是为了log记录。 - 循环阶段:这里我们前面说了很多,这个Failover策略在调用失败的时候,会根据负载均衡的策略进行重试的,那么是怎么重试的呢?就在这个for循环里面。这里我们看到for循环的次数是那个len的变量,这个变量上面我们已经说了,是获取设置的重试次数的。然后这里我们通过select选出的Invoker,去调用对应的invoke方法,如果失败了就记录一下,然后继续循环获取其他的Invoker继续执行调用,如果调用成功了,那么就直接放回result。
上面就是Failover的实现原理,其他的几种策略在模式上都差不多,喜欢的读者可以自行去查看相关的源码。

浙公网安备 33010602011771号