# Dubbo源码解析:服务目录和路由

在这里插入图片描述

# 介绍

在这里插入图片描述 之前的文章我们详细说了服务调用的过程,今天我们就来细化一下MockCluster到FailoverClusterInvoker的调用过程

当我们有多个服务提供者时,需要根据不同的策略从众多的提供者中选出合适的Invoker来发起调用,那么这些Invoker存放在哪?就在服务目录中。

服务目录还会根据路由策略策略对最终返回的Invoker进行再次过滤。比如ip为A的Consumer只能让它调用ip为B的producer,路由的配置有很多方式,我们详聊。

整体的流程下如图所示 在这里插入图片描述

  1. Invoker从Directory(服务目录)获取List<Invoker>
  2. 路由策略对Directory返回的List<Invoker>进行二次过滤
  3. 初始化路由策略
  4. 根据路由策略和List<Invoker>,选择一个Invoker发起调用

组件之间的关系如下图,我们接着详细分析一下我们上面提到的组件 在这里插入图片描述

# Directory(服务目录)

在这里插入图片描述

RegistryDirectory:Invoker列表是根据注册中心的推送而进行变化的,实现了NotifyListener接口,当注册中心监听的路径发生变化的时候,会回调NotifyListener#notify方法,这样就能更新Invoker列表

StaticDirectory:当使用来多注册中心时,把所有注册中心的invoker列表汇集到一个invoker列表中

其中StaticDirectory的代码很简单,就是一个存取Invoker的过程,而RegistryDirectory则很有意思,能根据produer的状态动态蔬菜Invoker列表

那么这个Invoker列表是如何动态刷新的呢? 在这里插入图片描述 在服务引入的过程中,会订阅服务提供者节点的变化,包含如下三种。然后根据这些节点信息生成Invoker,路由信息,配置信息。 在这里插入图片描述 当这些节点的信息发生变化的时候,会回调NotifyListener接口,而RegistryDirectory实现了这个接口,因此你可以从RegistryDirectory#notify方法看到更新逻辑的实现

在这里插入图片描述

这部分事件传递的过程比较乱,因为我画了一张图

TreeCacheListener:是curator框架中定义的关于zookeeper的事件 ChildListener:是dubbo中定义的关于zookeeper的事件,因为zookeeper的客户端不只有curator一个,所有抽象了一波 NotifyListener:是注册中心的的事件回调,因为Dubbo支持多种注册中心,因此又抽象了一个接口 RegistryDirectory:实现了NotifyListener接口,因此可以在这个类中看到节点发生变化的更新逻辑

订阅服务提供者的三种类型节点

// RegistryDirectory
public void subscribe(URL url) {
    setConsumerUrl(url);
    consumerConfigurationListener.addNotifyListener(this);
    serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
    registry.subscribe(url, this);
}

当节点的信息发生变化时,回调如下方法。更新路由信息,重新生成Invoker等

public synchronized void notify(List<URL> urls) {

    // 省略部分代码

    // 配置信息
    // 将configurators类型的url转为Configurator,保存到configurators字段中
    List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
    this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators);

    // 路由信息
    List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
    toRouters(routerURLs).ifPresent(this::addRouters);

    // providers
    // 服务提供者信息
    List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());

    // 刷新invoker列表
    refreshOverrideAndInvoker(providerURLs);
}

在这里插入图片描述

下图是ZookeeperRegistry#doSubscribe的实现,第一次订阅或者节点发生变化,都会执行ZookeeperRegistry#notify方法,这个方法会回调RegistryDirectory#notify方法,并更新缓存 在这里插入图片描述 RegistryDirectory#refreshOverrideAndInvoker(providerURLs) 方法就是根据providerURLs生成Invoker的过程

主要逻辑如下:

  1. 只有一个服务提供者,且协议为empty则会禁用改服务
  2. 根据providerURLs生成新的Invokers,如果某个providerURL的Invoker已经存在则不重新生成,否则生成Invoker
  3. 销毁旧的providerURLs生成的Invoker(被新的providerURL用了的Invoker不会销毁哈)

大概逻辑如下图 在这里插入图片描述

# FailoverClusterInvoker

FailoverClusterInvoker的继承关系如下图 在这里插入图片描述 可以看到所有的集群容错的Invoker都实现了AbstractClusterInvoker接口

AbstractClusterInvoker接口主要抽象了如下两部分的逻辑

  1. 从Directory中获取可以发起调用的Invoker列表
  2. 初始化负载均衡策略

这样各种集群容错的Invoker就能根据Invoker列表和负载均衡策略筛选出最终调用的DubboInvoker,然后发起调用 在这里插入图片描述

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

    // 从Directory获取,并且经过路由规则过滤后的Invoker
    List<Invoker<T>> invokers = list(invocation);
    // 初始化负载均衡策略
    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    // 具体选择哪个Invoker发起调用的过程交给子类去实现
    return doInvoke(invocation, invokers, loadbalance);
}

# Router(路由)

前面我们说过,从Directory中根据调用信息找到的Invoker并不能直接拿来调用,需要经过路由规则过滤后的Invoker才直接发起调用

路由分为如下三种

  1. 条件路由:使用Dubbo定义的语法规则去写路由规则
  2. 文件路由:框架从文件中读取路由规则
  3. 脚本路由:使用jdk自身的脚本解析引擎解析路由规则脚本

我在工作中基本没有配置过路由规则,所以我们就简单介绍一下条件路由的配置和实现,让大家对路由的实现有一个基本的认知即可

# 格式

=> 之前的为消费者匹配条件,所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。

=> 之后为提供者地址列表的过滤条件,所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。

如果匹配条件为空,表示对所有消费方应用,如:=> host != 10.20.153.11 如果过滤条件为空,表示禁止访问,如:host = 10.20.153.10 =>

# 表达式

参数支持:

  1. 服务调用信息,如:method, argument 等,暂不支持参数路由
  2. URL 本身的字段,如:protocol, host, port 等
  3. 以及 URL 上的所有参数,如:application, organization 等

条件支持:

  1. 等号 = 表示"匹配",如:host = 10.20.153.10
  2. 不等号 != 表示"不匹配",如:host != 10.20.153.10

值支持:

  1. 以逗号 , 分隔多个值,如:host != 10.20.153.10,10.20.153.11
  2. 以星号 * 结尾,表示通配,如:host != 10.20.*
  3. 以美元符 $ 开头,表示引用消费者参数,如:host = $host

# 配置示例

排除预发布机

=> host != 172.22.3.91

白名单

register.ip != 10.20.153.10,10.20.153.11 =>

黑名单

register.ip = 10.20.153.10,10.20.153.11 =>

提供者与消费者部署在同集群内,本机只访问本机的服务:

=> host = $host

配置如下路由规则,表示ip为 172.22.3.1的服务调用方,只会调用ip为172.22.3.2的服务

host = 172.22.3.1 => host = 172.22.3.2

# 路由实现

public class ConditionRouter extends AbstractRouter {

    protected Map<String, MatchPair> whenCondition;
    protected Map<String, MatchPair> thenCondition;

}

路由时主要和whenCondition和thenCondition打交道,在构造函数中会初始化这2个map

路由条件如下时,生成的2个map如下

host != 4.4.4.4 &amp; host = 2.2.2.2,1.1.1.1,3.3.3.3 &amp; method = sayHello => host = 1.2.3.4 &amp; host != 4.4.4.4

在这里插入图片描述 在这里插入图片描述 下面就是执行路由逻辑的部分,matenWhen和matenThen的方法就是利用上面初始化好的whenCondition和thenCondition进行匹配的过程,不具体分析了,对实现感兴趣的话可以调试一下官方对这个类写的Test类(ctrl + shift + t 快捷键即可快速到达对应的Test类)

// ConditionRouter
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
        throws RpcException {
    // 不生效
    if (!enabled) {
        return invokers;
    }

    if (CollectionUtils.isEmpty(invokers)) {
        return invokers;
    }
    try {
        // 没有whenRule匹配,返回所有
        if (!matchWhen(url, invocation)) {
            return invokers;
        }
        List<Invoker<T>> result = new ArrayList<Invoker<T>>();
        // 没有thenRule,则表明服务消费者在黑名单中,返回空列表
        if (thenCondition == null) {
            logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
            return result;
        }
        for (Invoker<T> invoker : invokers) {
            // 匹配成功
            if (matchThen(invoker.getUrl(), url)) {
                result.add(invoker);
            }
        }
        if (!result.isEmpty()) {
            // result不为空,直接返回
            return result;
        } else if (force) {
            // result为空 force=true 强制返回空列表
            logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY));
            return result;
        }
    } catch (Throwable t) {
        logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
    }
    // result为空,force=false 返回所有Invoker列表
    return invokers;
}