Dubbo笔记 ⑨ : 消费者启动流程 - RegistryProtocol#refer

一、前言

本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。


Dubbo笔记⑧ : 消费者启动流程 - ReferenceConfig#get 一文中,我们讲到了消费者引用服务时会调用 RegistryProtocol#referDubboProtocol#refer 来创建 Invoke,其中

  • RegistryProtocol#refer 完成与注册中心的交互工作。
  • DubboProtocol#refer 则是建立了与服务提供者的网络连接。

本文我们来看一下RegistryProtocol#refer 的实现过程,在分析RegistryDirectory 的调用过程前,我们先来介绍一下 Directory 。下面内容部分来源于官方描述: 服务目录

1. Directory 的概念

Directory 即服务目录, 服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。在一个服务集群中,服务提供者数量并不是一成不变的,如果集群中新增了一台机器,相应地在服务目录中就要新增一条服务提供者记录。或者,如果服务提供者的配置修改了,服务目录中的记录也要做相应的更新。如果这样说,服务目录和注册中心的功能不就雷同了吗?确实如此,这里这么说是为了方便大家理解。实际上服务目录在获取注册中心的服务配置信息后,会为每条配置信息生成一个 Invoker 对象,并把这个 Invoker 对象存储起来,这个 Invoker 才是服务目录最终持有的对象。Invoker 有什么用呢?看名字就知道了,这是一个具有远程调用功能的对象。讲到这大家应该知道了什么是服务目录了,它可以看做是 Invoker 集合,且这个集合中的元素会随注册中心的变化而进行动态调整。


服务目录目前内置的实现有两个,分别为 StaticDirectory 和 RegistryDirectory,它们均是 AbstractDirectory 的子类。AbstractDirectory 实现了 Directory 接口,这个接口包含了一个重要的方法定义,即 list(Invocation),用于列举 Invoker。下面我们来看一下他们的继承体系图。

在这里插入图片描述
如上,Directory 继承自 Node 接口,Node 这个接口继承者比较多,像 Registry、Monitor、Invoker 等均继承了这个接口。这个接口包含了一个获取配置信息的方法 getUrl,实现该接口的类可以向外提供配置信息。另外,RegistryDirectory 实现了 NotifyListener 接口,当注册中心节点信息发生变化后,RegistryDirectory 可以通过此接口方法得到变更信息,并根据变更信息动态调整内部 Invoker 列表。

1.1 AbstractDirectory

AbstractDirectory 封装了 Invoker 列举流程,具体的列举逻辑则由子类实现,这是典型的模板模式。AbstractDirectory 的整个实现很简单

public abstract class AbstractDirectory<T> implements Directory<T> {

    // logger
    private static final Logger logger = LoggerFactory.getLogger(AbstractDirectory.class);
	// 当前 注册中心URL 或者 直连URL
    private final URL url;
	
    private volatile boolean destroyed = false;
	// 消费者URL
    private volatile URL consumerUrl;

    protected RouterChain<T> routerChain;

    public AbstractDirectory(URL url) {
        this(url, null);
    }

    public AbstractDirectory(URL url, RouterChain<T> routerChain) {
        this(url, url, routerChain);
    }

    public AbstractDirectory(URL url, URL consumerUrl, RouterChain<T> routerChain) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
		// 如果是注册中心的协议,则进行进一步解析
        if (url.getProtocol().equals(Constants.REGISTRY_PROTOCOL)) {
            Map<String, String> queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
            this.url = url.addParameters(queryMap).removeParameter(Constants.MONITOR_KEY);
        } else {
            this.url = url;
        }

        this.consumerUrl = consumerUrl;
        setRouterChain(routerChain);
    }
    
	// 根据调用信息获取到服务提供者列表
    @Override
    public List<Invoker<T>> list(Invocation invocation) throws RpcException {
        if (destroyed) {
            throw new RpcException("Directory already destroyed .url: " + getUrl());
        }
		// 这里直接交由子类实现,也就是 StaticDirectory 和 RegistryDirectory 的实现
        return doList(invocation);
    }

    @Override
    public URL getUrl() {
        return url;
    }

    public RouterChain<T> getRouterChain() {
        return routerChain;
    }

    public void setRouterChain(RouterChain<T> routerChain) {
        this.routerChain = routerChain;
    }

    protected void addRouters(List<Router> routers) {
        routers = routers == null ? Collections.emptyList() : routers;
        routerChain.addRouters(routers);
    }

    public URL getConsumerUrl() {
        return consumerUrl;
    }

    public void setConsumerUrl(URL consumerUrl) {
        this.consumerUrl = consumerUrl;
    }

    public boolean isDestroyed() {
        return destroyed;
    }

    @Override
    public void destroy() {
        destroyed = true;
    }

    protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException;

}

这里需要关注的就是 AbstractDirectory#list 方法。当消费者进行服务调用时,Dubbo 会将调用相关信息封装成Invocation进行调用。在调用过程中会通过 AbstractDirectory#list 来获取服务提供者列表,而 AbstractDirectory#list 的具体实现则交由子类。

1.2 StaticDirectory

StaticDirectory 即静态服务目录,顾名思义,它内部存放的 Invoker 是不会变动的。所以,理论上它和不可变 List 的功能很相似。下面我们来看一下这个类的实现。但是需要注意的是,StaticDirectory 中的Invoker 不可变并非是指其内部服务列表不会动态更新,而是对应的每个注册中心或者分组的Invoker不会变化。

StaticDirectory 的应用场景主要有两种 : 多注册中心 或 多分组情况。这些在 Dubbo笔记⑧ : 消费者启动流程 - ReferenceConfig#get 中已经讲述,这里不再赘述。

public class StaticDirectory<T> extends AbstractDirectory<T> {
    private static final Logger logger = LoggerFactory.getLogger(StaticDirectory.class);
	// Invoker 列表,在 StaticDirectory 构造时就已经初始化
    private final List<Invoker<T>> invokers;

    public StaticDirectory(List<Invoker<T>> invokers) {
        this(null, invokers, null);
    }

    public StaticDirectory(List<Invoker<T>> invokers, RouterChain<T> routerChain) {
        this(null, invokers, routerChain);
    }

    public StaticDirectory(URL url, List<Invoker<T>> invokers) {
        this(url, invokers, null);
    }

    public StaticDirectory(URL url, List<Invoker<T>> invokers, RouterChain<T> routerChain) {
        super(url == null && invokers != null && !invokers.isEmpty() ? invokers.get(0).getUrl() : url, routerChain);
        if (invokers == null || invokers.isEmpty())
            throw new IllegalArgumentException("invokers == null");
        this.invokers = invokers;
    }
	// 服务提供者的接口
    @Override
    public Class<T> getInterface() {
        return invokers.get(0).getInterface();
    }
	// 检测服务目录是否可用
    @Override
    public boolean isAvailable() {
        if (isDestroyed()) {
            return false;
        }
         // 只要有一个 Invoker 是可用的,就认为当前目录是可用的
        for (Invoker<T> invoker : invokers) {
            if (invoker.isAvailable()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void destroy() {
        if (isDestroyed()) {
            return;
        }
        // 调用父类销毁逻辑
        super.destroy();
         // 遍历 Invoker 列表,并执行相应的销毁逻辑
        for (Invoker<T> invoker : invokers) {
            invoker.destroy();
        }
        invokers.clear();
    }

	// 构建路由链
    public void buildRouterChain() {
        RouterChain<T> routerChain = RouterChain.buildChain(getUrl());
        routerChain.setInvokers(invokers);
        this.setRouterChain(routerChain);
    }
	
	// 通过路由链路来进行路由,获取最终的 Invoker 列表
    @Override
    protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
        List<Invoker<T>> finalInvokers = invokers;
        if (routerChain != null) {
            try {
                finalInvokers = routerChain.route(getConsumerUrl(), invocation);
            } catch (Throwable t) {
                logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
            }
        }
        return finalInvokers == null ? Collections.emptyList() : finalInvokers;
    }

}

1.3 RegistryDirectory

RegistryDirectory 是一种动态服务目录,实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。收到变更通知后,RegistryDirectory 可根据配置变更信息刷新 Invoker 列表。


在介绍完Directory 的概念后,我们这里开始对 RegistryDirectory 进行分析。一般情况下我们使用单注册中心,所以只会涉及到 RegistryDirectory 的内容。

二、RegistryProtocol#refer

RegistryProtocol#refer 是在存在注册中心的情况下才会调用,我们这里假设只有一个zk注册中心,服务协议为 dubboRegistryProtocol#refer的实现如下 :

    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    	//取 registry 参数值,并将其设置为协议头
        url = url.setProtocol(url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY)).removeParameter(REGISTRY_KEY);
        Registry registry = registryFactory.getRegistry(url);
        if (RegistryService.class.equals(type)) {
            return proxyFactory.getInvoker((T) registry, type, url);
        }
        // group="a,b" or group="*"
        // 将 url 查询字符串转为 Map
        Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
        // 获取 group 配置
        String group = qs.get(Constants.GROUP_KEY);
        if (group != null && group.length() > 0) {
            if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
            	// 1. 如果是多分组的情况下,通过 SPI 加载 MergeableCluster 实例,并调用 doRefer 继续执行服务引用逻辑。
                return doRefer(getMergeableCluster(), registry, type, url);
            }
        }
        // 2. 调用 doRefer 继续执行服务引用逻辑
        return doRefer(cluster, registry, type, url);
    }

上面的逻辑可以总结为:

  1. 获取注册中心协议类型,并获取注册中心实例。
  2. 获取group 配置,根据 group 决定 doRefer 方法的 Cluster 参数。多分组情况下是 MergeableCluster,默认情况下是 FailoverCluster。
  3. doRefer 方法创建一个 RegistryDirectory 实例,交由RegistryDirectory 建立路由链路、订阅节点、容错机制的包装,最终获取到服务节点的一个Invoker。

从上面的代码中可以看到,RegistryProtocol#doRefer 核心的逻辑都交于 RegistryDirectory 来完成了。

在Dubbo中,一个注册中心(或直连URL)会有唯一一个对应的 RegistryDirectory 对象,其为 Directory 接口的实现类, Directory 也为 SPI 接口,代表了多个invoker(对于消费端来说,每个invoker代表了一个服务提供者),其内部维护着一个List,并且这个List的内容是动态变化的,比如当服务提供者集群新增或者减少机器时,服务注册中心就会推送当前服务提供者的地址列表,然后Directory中的List就会根据服务提供者地址列表相应变化。


1. 多分组情况的处理

在上面我们可以注意到,如果消费者引用了多个分组的服务,使用的集群容错策略则通过 getMergeableCluster() 方法指定为 MergeableCluster 。而默认的策略为 FailoverCluster。

关于容错机制,我们这里先简单介绍一下,后续开文章详细分析。

1.1 容错机制 Cluster

每个集群容错策略都会包装出一个对应的 Invoker,如 MergeableCluster 会返回MergeableClusterInvoker ,FailoverCluster 会返回 FailoverClusterInvoker 。

下面是Dubbo 提供的八种容错机制,如有需要,详参 Dubbo笔记 ⑬ :Dubbo 集群组件 之 Cluster & ClusterInvoker

  • FailoverClusterInvoker :failover翻译为故障转移,所以该类如果第一次调用失败,那么会第二次尝试,第二次尝试会访问其他服务提供者。该类是默认的集群容错策略。
  • FailfastClusterInvoker :该类的策略与java里面多线程访问集合的fast-fail是一样的。调用父类的select方法选出一个服务提供者,然后访问该提供者,如果失败了,直接向调用者抛出异常结束。
  • FailsafeClusterInvoker :安全失败集群调用器。调用的过程与FailfastClusterInvoker是类似的,不同的是当访问失败时,FailsafeClusterInvoker不直接抛出异常,而是返回一个值为null的AsyncRpcResult对象。也就是说当访问远程服务失败时,调用方不会收到异常,而是收到一个null的返回值。
  • AvailableClusterInvoker :可用集群调用器。前面提到doInvoke的入参有远程服务提供者的列表invokers。AvailableClusterInvoker遍历invokers,当遍历到第一个服务可用的提供者时,便访问该提供者,成功返回结果,如果访问时失败抛出异常终止遍历。
  • FailbackClusterInvoker :该类如果访问远程服务成功,将返回值返回调用者。
    如果访问失败,会向调用者返回null,然后调用addFailed方法,创建定时任务再次执行服务调用。
    该容错策略适用于通知类型服务,调用者不需要服务的返回值。该类涉及到两个参数:retries和failbacktasks。
  • ForkingClusterInvoker :该策略是同时对多个服务提供者进行调用,只要有一个服务调用成功即将结果返回调用者。通常用于要求实时的操作,但需要浪费更多的服务资源。使用该策略,需要做好服务幂等性以及返回值的处理。
  • MergeableClusterInvoker :该集群容错策略是对多个服务端返回结果合并。
  • BroadcastClusterInvoker :该策略是遍历调用每个远程服务提供者,遍历的集合是doInvoke的入参invokers,也就是所有的远程服务提供者。调用是同步的,如果提供者比较多,会非常耗时。在遍历的过程中,有异常不会中断遍历,将异常记录,遍历完后抛出异常

2. RegistryProtocol#doRefer

RegistryProtocol#doRefer方法中,Dubbo完成了对服务目录的创建和调用。代码如下:

    private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    	// 创建 RegistryDirectory 实例
        RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
        // 设置注册中心和协议
        directory.setRegistry(registry);
        directory.setProtocol(protocol);
        // all attributes of REFER_KEY
        Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
         // 生成服务消费者订阅的 URL,供后面使用
        URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
        // 如果引用的接口不是 * && 消费者可以注册(register 属性为 true ) 则会注册服务消费者,在 consumers 目录下新节点
        if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
            registry.register(getRegisteredConsumerUrl(subscribeUrl, url));
        }
        //  1. 建立路由规则链,即 解析并设置了routerChain属性
        directory.buildRouterChain(subscribeUrl);
         // 2. 订阅 providers、configurators、routers 等节点数据
        directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
		 // 3. 包装机器容错机制到invoker 
		 // 一个注册中心可能有多个服务提供者,因此这里需要将多个服务提供者合并为一
        Invoker invoker = cluster.join(directory);
        ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
        return invoker;
    }

这里我们把 RegistryProtocol#doRefer 的代码我们划分为三个部分:

  1. 构建路由链 : RegistryDirectory通过 RegistryDirectory#buildRouterChain 构建了路由链。

  2. 服务订阅 : RegistryDirectory订阅了providers、configurators、routers,以监听子节点的变化,并刷新自己服务信息。

     directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                    PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
    
  3. 添加容错策略:这里会将筛选出来的 Invoker包过一层 容错机制。

    Invoker invoker = cluster.join(directory);
    

上面我们看到了RegistryProtocol#doRefer 的方法交由 RegistryDirectory 来完成,下面我们来在 RegistryDirectory 中看看上面三点的具体实现。

三、RegistryDirectory

RegistryDirectory 即服务目录,在消费者启动时,RegistryDirectory 完成了以下三个工作:

  1. RegistryDirectory#buildRouterChain : 构建引用服务的路由链。当消费者发起调用时,路由链会筛选满足路由条件的提供者列表进入下一过程的处理。
  2. RegistryDirectory#subscribe : 订阅引用服务的相关节点。当提供者相关信息更新后,消费者通过订阅的这些节点可以感知并更新自身的引用信息。
  3. cluster.join(directory) :添加容错策略。决定消费者一次调用失败后的策略,是重试还是抛出异常亦或是返回一个空的结果集。

下面我们来详细看一下上述三点的具体实现 :

1. RegistryDirectory#buildRouterChain

对于路由,官方描述如下:【服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。在详细分析服务路由的源码之前,先来介绍一下服务路由是什么。服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。】


而 RegistryDirectory#buildRouterChain 在这里构建了路由链路,实现如下:

	// RegistryDirectory#buildRouterChain 
    public void buildRouterChain(URL url) {
    	// 创建一个 RouterChain 并赋值给 RegistryDirectory#routerChain
        this.setRouterChain(RouterChain.buildChain(url));
    }

可以看到这里是将 RouterChain.buildChain(url) 返回的 RouterChain 赋值给了 RegistryDirectory #routerChain 属性。 RouterChain.buildChain(url) 的作用是创建了一个新的 RouterChain, 实现如下:

	// 该段代码在 org.apache.dubbo.rpc.cluster.RouterChain 中
	public static <T> RouterChain<T> buildChain(URL url) {
		// 创建一个 RouterChain
        return new RouterChain<>(url);
    }
	
    private RouterChain(URL url) {
    	// 1. 通过 Dubbo SPI  获取获取到所有激活的 RouterFactory。
        List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)
                .getActivateExtension(url, (String[]) null);
		// 2. 将 RouterFactory 中转换为 Router
        List<Router> routers = extensionFactories.stream()
                .map(factory -> factory.getRouter(url))
                .collect(Collectors.toList());
		// 3. 保存routers并进行排序。
        initWithRouters(routers);
    }


    }

1.1 获取 RouterFactory

上面我们看到了dubbo通过下面的语句加载了RouterFactory 的SPI实现类。

		// 调用了 org.apache.dubbo.common.extension.ExtensionLoader#getActivateExtension(org.apache.dubbo.common.URL, java.lang.String[], java.lang.String) 方法
    	// 1. 通过 Dubbo SPI  获取获取到所有激活的 RouterFactory。
        List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)
                .getActivateExtension(url, (String[]) null);

其中 RouterFactory 实现类如下:

file=org.apache.dubbo.rpc.cluster.router.file.FileRouterFactory
script=org.apache.dubbo.rpc.cluster.router.script.ScriptRouterFactory
condition=org.apache.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
app=org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
tag=org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory
mock=org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory

这里并不会加载所有的 RouterFactory,我们在Dubbo笔记衍生篇②:Dubbo SPI 原理 中提到过,通过上面的方法加载的并非是全部 SPI实现类,而是 被 @Activate 注解修饰 && key 匹配 && group 匹配的类。而上面调用时传值key和group 都是null,所以这里会加载被 @Activate 注解修饰的RouterFactory 。即最终会加载的类RouterFactory 是下面四个类:

service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
app=org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
tag=org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory
mock=org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory

其优先级为 MockRouterFactory > TagRouterFactory > AppRouterFactory > ServiceRouterFactory

1.2 获取 Router

我们这里以 ServiceRouterFactory 为例,可以发现通过 ServiceRouterFactory 可以获取到 ServiceRouter。

@Activate(order = 300)
public class ServiceRouterFactory extends CacheableRouterFactory {

    public static final String NAME = "service";

    @Override
    protected Router createRouter(URL url) {
        return new ServiceRouter(url);
    }

}

其他 RouterFactory 也相同, 各自 RouterFactory 会获取到各自的Router 类。即最终 RegistryDirectory#routerChain 中的 routers 集合为下面四个类。

MockInvokersSelectorTagRouterAppRouterServiceRouter

当消费者发起调用时 获取到的 提供者列表会通过 MockInvokersSelector、TagRouter、AppRouter、ServiceRouter 进行路由筛选。筛选后的提供者列表 再经过负载均衡策略后才能最终确定调用的提供者。

关于 Dubbo 集群容错相关内容,如有需要详参:Dubbo笔记 ⑫ :Dubbo 集群组件概述

1.3 排序 Router

Router 继承了 Comparable 接口。这里就是根据排序规则对 Router 进行排序。


    public void initWithRouters(List<Router> builtinRouters) {
        this.builtinRouters = builtinRouters;
        this.routers = new CopyOnWriteArrayList<>(builtinRouters);
        // 排序
        this.sort();
	}

    private void sort() {
        Collections.sort(routers);
    }

2. RegistryDirectory#subscribe

RegistryDirectory#subscribe 订阅并监听zk上的服务节点,通过节点回调完成了服务列表的动态更新功能。这里需要注意:该部分逻辑是为了兼容 2.7 以下版本, Dubbo 2.7 版本之后提供了新的监听方式,如有需要详参: Dubbo笔记衍生篇⑩:2.7 版本的动态配置监听


RegistryDirectory#subscribe 方法完成了 RegistryDirectory 订阅 providers、configurators、routers 节点的订阅,当providers、configurators、routers 子节点变化时会通过回调通知RegistryDirectory。其实现如下:

    public void subscribe(URL url) {
    	// 记录消费者 URL,记录当前 RegistryDirectory 代表哪个 URL
        setConsumerUrl(url);
        // 添加到监听器集合
        consumerConfigurationListener.addNotifyListener(this);
        serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
        // 这里调用的是 ZookeeperRegistry#subscribe,会调用 FailbackRegistry#subscribe
        // url 为订阅的节点信息,this 即订阅结束后的回调监听器实例
        registry.subscribe(url, this);
    }

关于 ZookeeperRegistry#subscribe 的调用流程我们在Dubbo笔记⑤ : 服务发布流程 - Protocol#export 4. 服务订阅 中有过阐述。这里简单说明一下:

ZookeeperRegistry#subscribe 方法中完成了两件事:

  1. 订阅指定节点,设置回调方法为ZookeeperRegistry.this.notify (这里的 ZookeeperRegistry.this.notify 实现方法也是 RegistryDirectory#notify )。消费者会同时会获取到 providers、configurators、routers 的子节点并进行回调,而提供者只会订阅 configurators 节点。
  2. 同步更新自己的本地节点信息,更新方法为 RegistryDirectory#notify。在订阅结束后,需要获取zk上的节点信息,更新自身本地的信息。

所以这里分为两步 :

  1. ZookeeperRegistry.this.notify : 消费者订阅 providers、configurators、routers 节点,并设置节点更新时回调方法为 ZookeeperRegistry.this.notify (这里会调用 RegistryDirectory#notify ),当 提供者列表、动态配置、路由更新时会回调 RegistryDirectory#notify 方法,从而对象消费者端的动态配置、路由、提供者列表做相应的处理。
  2. notify(url, listener, urls); : 消费者在完成订阅后,会立即同步获取 providers、configurators、routers 子节点信息,封装成 URL 回调给 RegistryDirectory#notify 。从而完成了服务启动阶段获取到 服务、配置、路由 并进行处理的过程。

当消费者启动时会设置 ZookeeperRegistry.this.notify 作为节点回调,当节点发生变化时会回调 ZookeeperRegistry.this.notify 方法。同时 消费者也会立即调用一次 notify(url, listener, urls); 来根据获取到节点信息进行解析配置。

其中 ZookeeperRegistry.this.notifynotify(url, listener, urls); 的调用流程如下 :

1. org.apache.dubbo.registry.support.FailbackRegistry#notify
2. org.apache.dubbo.registry.support.FailbackRegistry#doNotify
3. org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List<org.apache.dubbo.common.URL>)
4. org.apache.dubbo.registry.integration.RegistryDirectory#notify

第一步、第二步基本没做什么事情,这里需要注意的是在第三步 AbstractRegistry#notify 中将URL 按照类别划分(即 首先将URl按照 providers、configurators、routers等类别划分后分别进行回调,即如果urls中有 providers、routers 等不同节点,这里会将其分成 providers urls, routers urls 等后再分别调用。详参Dubbo笔记⑤ : 服务发布流程 - Protocol#export )分别进行回调。


下面我们来直接看一下第四步 RegistryDirectory#notify 的实现:

2.1 RegistryDirectory#notify

通过上面的调用顺序我们可以知道最终回调的都是 RegistryDirectory#notify,当消费者启动后会立即调用一次 RegistryDirectory#notify 进行节点信息更新,同时当节点信息发生变化时,会通过回调调用 RegistryDirectory#notify 方法进行更新。所以我们这里直接看 RegistryDirectory#notify 的实现:

  	@Override
    public synchronized void notify(List<URL> urls) {
    	//  对 URLs 进行过滤
        List<URL> categoryUrls = urls.stream()
                .filter(this::isValidCategory)
                .filter(this::isNotCompatibleFor26x)
                .collect(Collectors.toList());

        /**
         * TODO Try to refactor the processing of these three type of urls using Collectors.groupBy()?
         */
         // 1. 筛选出配置信息URL 并转换成 configurators,保存到 RegistryDirectory#configurators 属性中
         // 筛选条件是 url protocol = override || url category = configurators
        this.configurators = Configurator.toConfigurators(classifyUrls(categoryUrls, UrlUtils::isConfigurator))
                .orElse(configurators);
		// 2. 筛选出路由URL 并转换成Router 添加到 RegistryDirectory#routerChain 中
		// 筛选条件是 url protocol = route || url category = routers
		// RouterChain 保存了服务提供者的URL列表转换为invoker列表和可用服务提供者对应的invokers列表和路由规则信息
        toRouters(classifyUrls(categoryUrls, UrlUtils::isRoute)).ifPresent(this::addRouters);

        // providers
        // 3. 筛选出 提供者URL 并进行服务提供者的更新
        // 筛选条件是 url protocol != override && url protocol != route &&  url category = providers
        refreshOverrideAndInvoker(classifyUrls(categoryUrls, UrlUtils::isProvider));
    }
	
	private void refreshOverrideAndInvoker(List<URL> urls) {
        // mock zookeeper://xxx?mock=return null
        // 重写URL(也就是把mock=return null等信息拼接到URL中)并保存到overrideDirectoryUrl中
        overrideDirectoryUrl();
        // 刷新 服务提供者 URL,根据URL 生成Invoker
        refreshInvoker(urls);
    }

这里对于 configurators、routers 节点的处理都比较简单,根据 url 的 Protocol 或 category 属性 来进行分类,并保存到 RegistryDirectory 相应的属性中。

我们这里关键的逻辑在对提供者列表的处理,也就是 refreshInvoker(urls); 中,refreshInvoker(urls); 完成了URL 转化成Invoker 的过程,并且在此过程中与服务提供者创建了网络连接(默认是Netty服务)。

RegistryDirectory#refreshInvoker 实现如下:

   private void refreshInvoker(List<URL> invokerUrls) {
        Assert.notNull(invokerUrls, "invokerUrls should not be null");
        /******************* 1. 如果只有一个 empty协议,则表示所有服务禁用,销毁所有协议 ******************/
		// 如果只有一个 协议为 empty 的url,则表明需要销毁所有协议,因为empty 协议为空协议,个人理解就是为了防止空url存在而生成的无意义的url
        if (invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls
                .get(0)
                .getProtocol())) {
              // 设置禁止访问
            this.forbidden = true; // Forbid to access
            // invoker设置为空集合
            this.invokers = Collections.emptyList();
            routerChain.setInvokers(this.invokers);
            destroyAllInvokers(); // Close all invokers
        }
        /******************* 2. 服务可用,RUL 转Invoker ******************/
		 else {
        	// 设置允许访问
            this.forbidden = false; // Allow to access
            // urlInvokerMap 需要使用本地引用,因为 urlInvokerMap自身可能随时变化,可能指向 null
            Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
            if (invokerUrls == Collections.<URL>emptyList()) {
                invokerUrls = new ArrayList<>();
            }
            // 这里防止并发更新,cachedInvokerUrls 被  volatile 修饰
            if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
                invokerUrls.addAll(this.cachedInvokerUrls);
            } else {
                this.cachedInvokerUrls = new HashSet<>();
                this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
            }
            if (invokerUrls.isEmpty()) {
                return;
            }
            // 将 url 转换成 Invoker,key为 url.toFullString(), value 为 Invoker
            // 2.1 URL 转 Invoker
            Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map

            // state change
            // If the calculation is wrong, it is not processed.
            if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
                logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls
                        .toString()));
                return;
            }
			// 转化为不可修改 list,防止并发修改
            List<Invoker<T>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values()));
            // pre-route and build cache, notice that route cache should build on original Invoker list.
            // toMergeMethodInvokerMap() will wrap some invokers having different groups, those wrapped invokers not should be routed.
            // 保存服务提供者 Invoker。这里会通知 router 来进行消息更新。
            routerChain.setInvokers(newInvokers);
            //  如果引用多个分组的服务,则进行合并 : 当消费端引用多个分组的服务时,dubbo会对每个分组创建一个对应的StaticDirectory对象
            // 2.2 对多组服务的处理
            this.invokers = multiGroup ? toMergeInvokerList(newInvokers) : newInvokers;
            this.urlInvokerMap = newUrlInvokerMap;

            try {
            	// 2.3 销毁无用的 Invoker
                destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
            } catch (Exception e) {
                logger.warn("destroyUnusedInvokers error. ", e);
            }
        }
    }
    
	

上面可以看到整个过程还是比较简单:

  1. 如果 invokerUrls 只有一个 empty 协议,则认为需要销毁所有 Invoker,则对Invoker进行销毁。
  2. 如果 invokerUrls 并非只有一个empty url,则表示当前有服务启用,准备将 URL 转换为 Invoker 。
    1. toInvokers(invokerUrls); :URL 转 Invoker
    2. toMergeInvokerList(newInvokers) :多个分组服务的处理
    3. destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); :销毁无用 Invoker

我们下面对 toInvokers(invokerUrls);toMergeInvokerList(newInvokers) 进行分析:

2.1.1 toInvokers(invokerUrls);

在上面的代码中我们可以看到 toInvokers(invokerUrls); 方法将URL 转换为 Invoker。显然这个方法是非常重要的。

 Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);

那么我们需要看一下 RegistryDirectory#toInvokers 的实现如下 :

	// org.apache.dubbo.registry.integration.RegistryDirectory#toInvokers
    private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
        Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
        if (urls == null || urls.isEmpty()) {
            return newUrlInvokerMap;
        }
        Set<String> keys = new HashSet<String>();
        String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
        for (URL providerUrl : urls) {
            // If protocol is configured at the reference side, only the matching protocol is selected	
            // 检测服务提供者协议是否被服务消费者所支持
            // 如果消费者指定了了调用服务的协议,则只使用指定的协议,即如果消费者指定提供者协议类型为 dubbo,则只会需要协议类型为dubbo的提供者。
            if (queryProtocols != null && queryProtocols.length() > 0) {
                boolean accept = false;
                String[] acceptProtocols = queryProtocols.split(",");
                for (String acceptProtocol : acceptProtocols) {
                    if (providerUrl.getProtocol().equals(acceptProtocol)) {
                        accept = true;
                        break;
                    }
                }
                if (!accept) {
                   // 若服务提供者协议头不被消费者所支持,则忽略当前 providerUrl
                    continue;
                }
            }
            // 跳过 空协议
            if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
                continue;
            }
            // 没有能够处理该协议的 Protocol实现类,则跳过
            if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
             	// .... 日志打印
                continue;
            }
            // 合并提供者url,Dubbo的配置可能存在多个,这里按照顺序合并
            // 合并顺序 override(动态配置) > -D (启动参数) > Consumer(消费者端参数) > Provider (提供者端参数)
            URL url = mergeUrl(providerUrl);

            String key = url.toFullString(); // The parameter urls are sorted
            // 避免重复解析
            if (keys.contains(key)) { // Repeated url
                continue;
            }
            keys.add(key);
            // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
            // 从缓存中获取 Invoker
            Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
            // 获取与 url 对应的 Invoker
            Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
            // 如果缓存中不存在当前URL对应的invoker,则进行远程引用
            if (invoker == null) { // Not in the cache, refer again
                try {
                    boolean enabled = true;
                    // 对 disabled 和 enabled 参数的校验,即服务是否启用
                    if (url.hasParameter(Constants.DISABLED_KEY)) {
                        enabled = !url.getParameter(Constants.DISABLED_KEY, false);
                    } else {
                        enabled = url.getParameter(Constants.ENABLED_KEY, true);
                    }
                    if (enabled) {
                    	// 调用 生成invoker委托类
                        invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
                }
                // 将 Invoker 保存到 缓存 newUrlInvokerMap 中
                if (invoker != null) { // Put new invoker in cache
                    newUrlInvokerMap.put(key, invoker);
                }
            } else {
                newUrlInvokerMap.put(key, invoker);
            }
        }
        keys.clear();
        // 返回 url 转换成的 Invoker Map 
        return newUrlInvokerMap;
    }

可以看到整个逻辑如下:

  1. 如果消费者指定了服务提供者的协议类型,则按照指定协议来获取URL
  2. 如果第一步获取成功,或者没有指定协议类型,则会对URL进行合并、缓存校验等过程。
  3. 如果当前URL 没有创建Invoker (缓存没有命中),则会通过下面这一句来创建Invoker。
      invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
    

这里我们可以看到关键的 逻辑在 protocol.refer(serviceType, url),由于当前服务的协议是Dubbo,所以这里的调用顺序应为:

	refprotocol.refer => XxxProtocolWrapper#refer  => DubboProtocol#refer
  1. 关于 XxxProtocolWrapper#refer,详参:Dubbo笔记衍生篇③:ProtocolWrapper

  2. 关于 DubboProtocol#refer 的内容 详参: Dubbo笔记⑪ : 消费者启动流程 - DubboProtocol#refer


2.1.2 toMergeInvokerList(newInvokers)

当消费端引用多个分组的服务时,dubbo会对每个分组创建一个对应的StaticDirectory对象。

	//入参invokers代表了一个注册中心上的所有服务提供者
    private List<Invoker<T>> toMergeInvokerList(List<Invoker<T>> invokers) {
        List<Invoker<T>> mergedInvokers = new ArrayList<>();
        Map<String, List<Invoker<T>>> groupMap = new HashMap<String, List<Invoker<T>>>();
        // 将所有服务提供者按组进行划分
        for (Invoker<T> invoker : invokers) {
            String group = invoker.getUrl().getParameter(Constants.GROUP_KEY, "");
            groupMap.computeIfAbsent(group, k -> new ArrayList<>());
            groupMap.get(group).add(invoker);
        }
		// 如果只有一个分组直接添加即可
        if (groupMap.size() == 1) {
            mergedInvokers.addAll(groupMap.values().iterator().next());
        } else if (groupMap.size() > 1) {
        	// 如果有多个分组则为每个分组创建一个StaticDirectory对象
            for (List<Invoker<T>> groupList : groupMap.values()) {
                StaticDirectory<T> staticDirectory = new StaticDirectory<>(groupList);
                // //构建路由链
                staticDirectory.buildRouterChain();
                //使用AbstractClusterInvoker实现类封装StaticDirectory对象
                mergedInvokers.add(cluster.join(staticDirectory));
            }
        } else {
            mergedInvokers = invokers;
        }
        return mergedInvokers;
    }

此时RegistryDirectory 中的结构如下:
在这里插入图片描述

3. cluster.join(directory);

在消费者建立连接后,此时的 directory 中包含 Invokers 集合(可以提供当前服务的提供者合集)、routerChain (路由链)。此时需要包装容错机制。其中 Cluster 也为 SPI 接口,默认实现为 FailoverCluster。

 Invoker invoker = cluster.join(directory);

这里的 cluster 实际上是 Cluster$Adaptive,其适配器实现如下:

public class Cluster$Adaptive implements org.apache.dubbo.rpc.cluster.Cluster {
    @Override
    public org.apache.dubbo.rpc.Invoker join(org.apache.dubbo.rpc.cluster.Directory arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException("org.apache.dubbo.rpc.cluster.Directory argument == null");
        }
        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException("org.apache.dubbo.rpc.cluster.Directory argument getUrl() == null");
        }
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("cluster", "failover");
        if (extName == null) {
            throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.cluster.Cluster) name from url(" + url.toString() + ") use keys([cluster])");
        }
        org.apache.dubbo.rpc.cluster.Cluster extension = (org.apache.dubbo.rpc.cluster.Cluster) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.cluster.Cluster.class).getExtension(extName);
        return extension.join(arg0);
    }
}

这里需要注意的是,如果消费者引用了多个分组的服务,则Dubbo使用 MergeableCluster 作为容错实现类。

我们这边没有配置并且只引用了一个分组的服务,所以走的是默认的 Cluster,即 FailoverCluster,其实现如下:

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

这里是把 directory对象包裹到了 FailoverClusterInvoker 中,而 directory 中维护了 Invoker列表,所以可以在FailoverClusterInvoker 中完成了集群容错策略。

需要注意的是对于 Cluster 接口来说,其配置文件 org.apache.dubbo.rpc.cluster.Cluster 内容如下,我们可以发现Dubbo 实际上使用 MockClusterWrapper对 Cluster 接口进行了包装。

mock=org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper
failover=org.apache.dubbo.rpc.cluster.support.FailoverCluster
failfast=org.apache.dubbo.rpc.cluster.support.FailfastCluster
failsafe=org.apache.dubbo.rpc.cluster.support.FailsafeCluster
failback=org.apache.dubbo.rpc.cluster.support.FailbackCluster
forking=org.apache.dubbo.rpc.cluster.support.ForkingCluster
available=org.apache.dubbo.rpc.cluster.support.AvailableCluster
mergeable=org.apache.dubbo.rpc.cluster.support.MergeableCluster
broadcast=org.apache.dubbo.rpc.cluster.support.BroadcastCluster
registryaware=org.apache.dubbo.rpc.cluster.support.RegistryAwareCluster

所以这里实际上的调用顺序如下图 :在这里插入图片描述

而 MockClusterWrapper 的实现如下,即将 FailoverClusterInvoker 实例包装成了MockClusterInvoker 实例。MockClusterInvoker 完成了Mock功能的实现。

public class MockClusterWrapper implements Cluster {

    private Cluster cluster;

    public MockClusterWrapper(Cluster cluster) {
        this.cluster = cluster;
    }

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new MockClusterInvoker<T>(directory,
                this.cluster.join(directory));
    }

}

4. 总结

综上 RegistryDirectory的整个工作流程如下时序图(图源《深度剖析Apache Dubbo 核心技术内幕》):

在这里插入图片描述


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
https://blog.csdn.net/weixin_38308374/article/details/105802303
https://blog.csdn.net/weixin_38308374/article/details/106736721
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值