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

# 介绍
之前的文章我们详细说了服务调用的过程,今天我们就来细化一下MockCluster到FailoverClusterInvoker的调用过程。
当我们有多个服务提供者时,需要根据不同的策略从众多的提供者中选出合适的Invoker来发起调用,那么这些Invoker存放在哪?就在服务目录中。
服务目录还会根据路由策略策略对最终返回的Invoker进行再次过滤。比如ip为A的Consumer只能让它调用ip为B的producer,路由的配置有很多方式,我们详聊。
整体的流程下如图所示

- Invoker从Directory(服务目录)获取List<Invoker>
- 路由策略对Directory返回的List<Invoker>进行二次过滤
- 初始化路由策略
- 根据路由策略和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的过程
主要逻辑如下:
- 只有一个服务提供者,且协议为empty则会禁用改服务
- 根据providerURLs生成新的Invokers,如果某个providerURL的Invoker已经存在则不重新生成,否则生成Invoker
- 销毁旧的providerURLs生成的Invoker(被新的providerURL用了的Invoker不会销毁哈)
大概逻辑如下图

# FailoverClusterInvoker
FailoverClusterInvoker的继承关系如下图
可以看到所有的集群容错的Invoker都实现了AbstractClusterInvoker接口
AbstractClusterInvoker接口主要抽象了如下两部分的逻辑
- 从Directory中获取可以发起调用的Invoker列表
- 初始化负载均衡策略
这样各种集群容错的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才直接发起调用
路由分为如下三种
- 条件路由:使用Dubbo定义的语法规则去写路由规则
- 文件路由:框架从文件中读取路由规则
- 脚本路由:使用jdk自身的脚本解析引擎解析路由规则脚本
我在工作中基本没有配置过路由规则,所以我们就简单介绍一下条件路由的配置和实现,让大家对路由的实现有一个基本的认知即可
# 格式
=> 之前的为消费者匹配条件,所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。
=> 之后为提供者地址列表的过滤条件,所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。
如果匹配条件为空,表示对所有消费方应用,如:=> host != 10.20.153.11 如果过滤条件为空,表示禁止访问,如:host = 10.20.153.10 =>
# 表达式
参数支持:
- 服务调用信息,如:method, argument 等,暂不支持参数路由
- URL 本身的字段,如:protocol, host, port 等
- 以及 URL 上的所有参数,如:application, organization 等
条件支持:
- 等号 = 表示"匹配",如:host = 10.20.153.10
- 不等号 != 表示"不匹配",如:host != 10.20.153.10
值支持:
- 以逗号 , 分隔多个值,如:host != 10.20.153.10,10.20.153.11
- 以星号 * 结尾,表示通配,如:host != 10.20.*
- 以美元符 $ 开头,表示引用消费者参数,如: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 & host = 2.2.2.2,1.1.1.1,3.3.3.3 & method = sayHello => host = 1.2.3.4 & 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;
}
← Dubbo服务引入过程 注册中心 →