Dubbo源码阅读——集群容错


服务化必然离不开集群的概念。同一服务会部署在多台机器上,于是有了一系列集群的内容。服务调用失败时的处理措施也是需要考虑的,是重试呢,还是抛出异常,亦或是只打印异常等;还有如何获取服务提供者的服务列表,以及如何自定义路由规则,比如指定ip的消费者调用指定ip的提供者;服务消费者需要决定选择哪个服务提供者进行调用。

1. 集群模块的内容

由上述几个问题引出dubbo集群容错相关内容:

  • 集群容错策略:对应接口:Cluster,核心方法:join
  • 服务字典:对应接口:Directory,核心方法:list
  • 服务路由:对应接口:Router,核心方法:route
  • 负载均衡:对应接口:LoadBalance,核心方法:select

以及三个Invoker接口的抽象类或实现类:AbstractClusterInvokerMergeableClusterInvokerMockClusterInvoker。后两个是特殊类型。
再详细研究各模块内容之前,先看看dubbo集群内容的工作原理和触发时机。

2. 集群内容总体流程

在这里插入图片描述
dubbo集群模块的工作主要包含两个阶段。
第一阶段,①,发生在服务消费端启动时,服务引用过程的创建Invoker阶段。此时RegistryProtocol的doRefer方法调用Cluster接口的join方法,得到一个ClusterInvoker。
第二阶段,②③④⑤,发生在服务调用阶段,ClusterInvoker的invoke方法被调用,触发这几个动作:

  1. 调用Directory的list方法,得到服务列表List<Invoker<T>>,也叫服务字典。如图,服务列表有两种,静态列表和实时从注册中心获取变化的动态列表。默认动态列表。
  2. 在list方法内部,获取到服务列表后,再调用Router的route方法,进一步筛选,得到路由后的服务列表。如图,路由也有两种,脚本路由条件路由。默认为条件路由。
  3. 对于大部分集群策略,还会有负载均衡的过程,即调用LoadBalance的select方法,获取到真实的Invoker,比如DubboInvoker。负载均衡策略有加权随机加权轮询最小活跃数一致性哈希四种,默认为加权随机算法。
  4. 得到真实的Invoker后,Cluster层的内容也算完了,之后就是调用真实Invoker的invoke方法,它会调用服务目标方法,完成RPC调用。

介绍完集群层的总体内容,本文下面详细研究集群容错机制。服务字典,服务路由和负载均衡在后续文章介绍。

3. 集群容错机制

3.1 代码结构图

集群容错机制增强了整个应用的鲁棒性。Cluster接口一共有九种实现,分别对应九种ClusterInvoker,支持SPI扩展。其中有七种通用容错机制,ClusterInvoker继承AbstractClusterInvoker,和两种特殊容错机制,直接implements Invoker接口。
下面是ClusterInvoker的类图:

在这里插入图片描述

3.2 九种容错机制概览

dubbo实现了七种集群容错机制可供选择,当某个服务远程调用出现异常时,如网络抖动、服务暂时不可用,可以根据不同策略自动容错。另外还有两种机制用于本地测试和服务降级。下面介绍通用得其中容错机制。

机制名 简介 优缺点 适用场景
Failover 失败重试其他节点 ,可通过retris属性设置重试次数,dubbo默认容错机制 导致接口延迟增大,并加重下游服务的负载 读操作和幂等的写操作
Failfast 失败快速返回异常 受网络抖动影响较大 非幂等接口的调用
Failsafe 失败直接忽略异常 \ 如某些不重要的日志同步
Failback 失败自动记录在失败队列里,并由一个定时线程池定时重试 \ 异步或要求最终一致性的请求
Forking 并行调用多个服务,只要有一个返回,则立即返回结果。可通过forks属性设置最大并行调用数 浪费服务器资源 适用于对实时性要求极高的调用
Broadcast 广播所有服务,任意节点报错则报错 \ 状态更新后的广播
Avaliable 遍历所有列表,找到一个可用节点,则请求并返回结果。若没有可用节点则抛异常 \ 最简单的方式
Mock 广播调用所有服务,任意节点报错则报错 特殊机制 服务降级
Mergeable 可以自动把多个请求节点得到的请求合并 特殊机制 本地测试

4. 集群容错源码解析

前面说过,集群内容从第一阶段,服务引用过程开始,RegistryProtocol的doRefer方法调用Cluster接口的join方法,得到一个ClusterInvoker。下面先看看Cluster接口

4.1 Cluster

Cluster接口支持Dubbo SPI扩展,定义了join方法,默认实现类是FailoverCluster,join方法的实现也很简单,就是new一个对应的ClusterInvoker返回。

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

}

后面看看第二阶段,服务引用时Cluster接口的invoke方法被调用。

4.2 AbstractClusterInvoker#invoke()

先看看各种 Cluster Invoker 的父类 AbstractClusterInvoker的invoke()方法,

    @Override
    public Result invoke(final Invocation invocation) throws RpcException {
        checkWhetherDestroyed();
        LoadBalance loadbalance = null;

        // binding attachments into invocation.
        Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
            ((RpcInvocation) invocation).addAttachments(contextAttachments);
        }
		// 调用directory的list方法,获取路由后服务列表
        List<Invoker<T>> invokers = list(invocation);
        if (invokers != null && !invokers.isEmpty()) {
        	// 获取负载均衡策略
            loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                    .getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
        }
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        // doInvoke抽象方法,由各子类实现
        return doInvoke(invocation, invokers, loadbalance);
    }

AbstractClusterInvoker的invoke()方法,首先做了服务列表,服务路由,获取负载均衡策略等工作,然后后面的集群容错逻辑由各个子类取实现。下面看看各个子类的代码

4.2.1 FailoverClusterInvoker

FailoverClusterInvoker 在调用失败时,会自动切换 Invoker 进行重试。这个是dubbo默认使用的实现类。

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyinvokers = invokers;
        checkInvokers(copyinvokers, invocation);
        // 获取重试次数配置
        int len = getUrl().getMethodParameter(invocation.getMethodName(), 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++) {
            if (i > 0) {
                checkWhetherDestroyed();
                // 重新调用之前,重新获取最新的服务列表
                copyinvokers = list(invocation);
                // check again
                checkInvokers(copyinvokers, invocation);
            }
            // 调用select方法负载均衡,拿到真实的Invoker
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List) invoked);
            try {
            	// 调用真实invoker的invoke方法
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn("...");
                }
                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("...");
    }

如上,FailoverClusterInvoker 的 doInvoke 方法首先是获取重试次数,然后根据重试次数进行循环调用,失败后进行重试。在 for 循环内,首先是通过负载均衡组件选择一个 Invoker,然后再通过这个 Invoker 的 invoke 方法进行远程调用。如果失败了,记录下异常,并进行重试。重试时会再次调用父类的 list 方法列举 Invoker。
select方法在调用loadbalance.selct方法前后都做了一些逻辑处理,比如读取粘滞连接配置(sticky ,尽可能获取同一个服务器的服务)、失败重选等,这里不再细说。

4.2.2 FailbackClusterInvoker

FailbackClusterInvoker 会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重传,适合执行消息通知等操作。

	@Override
    protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        try {
            checkInvokers(invokers, invocation);
            Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
            return invoker.invoke(invocation);
        } catch (Throwable e) {
            logger.error("Failback to invoke method " + invocation.getMethodName() + ", wait for retry in background. Ignored exception: "
                    + e.getMessage() + ", ", e);
            // 失败执行addFailed,返回空RpcResult
            addFailed(invocation, this);
            return new RpcResult(); // ignore
        }
    }

	private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
		// double check 保证线程安全
        if (retryFuture == null) {
            synchronized (this) {
                if (retryFuture == null) {
                	// 定时任务执行重传,每隔5秒执行一次
                    retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {

                        @Override
                        public void run() {
                            // collect retry statistics
                            try {
                                retryFailed();
                            } catch (Throwable t) { // Defensive fault tolerance
                                logger.error("Unexpected error occur at collect statistic", t);
                            }
                        }
                    }, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD, TimeUnit.MILLISECONDS);  // RETRY_FAILED_PERIOD = 5 * 1000
                }
            }
        }
        failed.put(invocation, router);
    }
    
	void retryFailed() {
        if (failed.size() == 0) {
            return;
        }
        // 遍历 failed,对失败的调用进行重试
        for (Map.Entry<Invocation, AbstractClusterInvoker<?>> entry : new HashMap<Invocation, AbstractClusterInvoker<?>>(
                failed).entrySet()) {
            Invocation invocation = entry.getKey();
            Invoker<?> invoker = entry.getValue();
            try {
                invoker.invoke(invocation);
                failed.remove(invocation);
            } catch (Throwable e) {
                logger.error("Failed retry to invoke method " + invocation.getMethodName() + ", waiting again.", e);
            }
        }
    }

上面代码也很简单,首先是 doInvoker,该方法负责初次的远程调用。若远程调用失败,则通过 addFailed 方法将调用信息存入到 failed 中,等待定时重试。addFailed 在开始阶段会根据 retryFuture 为空与否,来决定是否开启定时任务。retryFailed 方法则是包含了失败重试的逻辑,该方法会对 failed 进行遍历,然后依次对 Invoker 进行调用。调用成功则将 Invoker 从 failed 中移除,调用失败则忽略失败原因。

4.2.3 FailfastClusterInvoker

FailfastClusterInvoker 只会进行一次调用,失败后立即抛出异常。适用于幂等操作,比如新增记录。
这个代码就更简单了,失败直接抛异常。

    @Override
    public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        checkInvokers(invokers, invocation);
        // 选择 Invoker
        Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
        try {
            // 调用 Invoker
            return invoker.invoke(invocation);
        } catch (Throwable e) {
            if (e instanceof RpcException && ((RpcException) e).isBiz()) {
                // 抛出异常
                throw (RpcException) e;
            }
            // 抛出异常
            throw new RpcException(..., "Failfast invoke providers ...");
        }
    }

4.2.4 FailsafeClusterInvoker

FailsafeClusterInvoker 是一种失败安全的 Cluster Invoker。所谓的失败安全是指,当调用过程中出现异常时,FailsafeClusterInvoker 仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。

    @Override
    public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        try {
            checkInvokers(invokers, invocation);
            // 选择 Invoker
            Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
            // 进行远程调用
            return invoker.invoke(invocation);
        } catch (Throwable e) {
			// 打印错误日志,但不抛出
            logger.error("Failsafe ignore exception: " + e.getMessage(), e);
            // 返回空结果忽略错误
            return new RpcResult();
        }
    }

4.2.5 ForkingClusterInvoker

ForkingClusterInvoker 会在运行时通过线程池创建多个线程,并发调用多个服务提供者。只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。ForkingClusterInvoker 的应用场景是在一些对实时性要求比较高读操作(注意是读操作,并行写操作可能不安全)下使用,但这将会耗费更多的资源。

public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
   try {
       checkInvokers(invokers, invocation);
       final List<Invoker<T>> selected;
       // 获取 forks 配置
       final int forks = getUrl().getParameter(Constants.FORKS_KEY, Constants.DEFAULT_FORKS);
       // 获取超时配置
       final int timeout = getUrl().getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        // 如果 forks 配置不合理,则直接将 invokers 赋值给 selected
       if (forks <= 0 || forks >= invokers.size()) {
           selected = invokers;
       } else {
           selected = new ArrayList<Invoker<T>>();
           // 循环选出forks个invoker,放入selected
           for (int i = 0; i < forks; i++) {
               Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
               if (!selected.contains(invoker)) {//Avoid add the same invoker several times.
                   selected.add(invoker);
               }
           }
       }
       RpcContext.getContext().setInvokers((List) selected);
       final AtomicInteger count = new AtomicInteger();
       final BlockingQueue<Object> ref = new LinkedBlockingQueue<Object>();
       // 遍历 selected 列表
       for (final Invoker<T> invoker : selected) {
       		// 为每个 Invoker 创建一个执行线程
           executor.execute(new Runnable() {
               @Override
               public void run() {
                   try {
                       Result result = invoker.invoke(invocation);
                       // 将结果放到阻塞队列中
                       ref.offer(result);
                   } catch (Throwable e) {
                       int value = count.incrementAndGet();
                       // 仅在value大于等于selected.size() 时,也就是全部调用失败,才将异常对象放到阻塞队列中
                       if (value >= selected.size()) {
                           ref.offer(e);
                       }
                   }
               }
           });
       }
       try {
           Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
           if (ret instanceof Throwable) {
               Throwable e = (Throwable) ret;
               throw new RpcException(e instanceof RpcException ? ((RpcException) e).getCode() : 0, "Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e.getCause() != null ? e.getCause() : e);
           }
           return (Result) ret;
       } catch (InterruptedException e) {
           throw new RpcException("Failed to forking invoke provider " + selected + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e);
       }
   } finally {
       // clear attachments which is binding to current thread.
       RpcContext.getContext().clearAttachments();
   }
}

如上,ForkingClusterInvoker 的流程是,首先获取forks参数,也就是同时调用invoker的数量,默认为invoker总数。然后选出forks个invoker,并各自用独立的线程同时调用,得到结果RpcResult或Throwable都放入阻塞队列,后面再去poll一个结果,如果是RpcResult,则正常返回。如果是Throwable,说明全部调用失败,抛出异常。
ForkingClusterInvoker 的特性是,只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。这里逻辑的关键代码就是value >= selected.size()这里的判断。这个判断成立说明invoker全部调用失败,这是才在阻塞队列里放异常结果。

4.2.6 BroadcastClusterInvoker

BroadcastClusterInvoker 会逐个调用每个服务提供者,如果其中一台报错,在循环调用结束后,BroadcastClusterInvoker 会抛出异常。该类通常用于通知所有提供者更新缓存或日志等本地资源信息。
实现方式很简单,这里就不贴代码了。

5. 总结

本节介绍集群容错部分总体内容,包括服务字典(Directory)、集群容错策略(Cluster)、服务路由(Router)、负载均衡(LoadBalance),以及这几部分内容的关键方法和调用时机。然后详细解读了集群容错策略部分的源码,其他三个部分的源码细节,后续博客继续详细解读。

发布了43 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/103779800