1. 简介
前面文章分析了服务的导出与引用过程,从本篇文章开始,我将开始分析 Dubbo 集群容错方面的源码。这部分源码包含四个部分,分别是服务目录 Directory、服务路由 Router、集群 Cluster 和负载均衡 LoadBalance。这几个部分的源码逻辑比较独立,我会分四篇文章进行分析。本篇文章作为集群容错的开篇文章,将和大家一起分析服务目录相关的源码。在进行深入分析之前,我们先来了解一下服务目录是什么。服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。在一个服务集群中,服务提供者数量并不是一成不变的,如果集群中新增了一台机器,相应地在服务目录中就要新增一条服务提供者记录。或者,如果服务提供者的配置修改了,服务目录中的记录也要做相应的更新。如果这样说,服务目录和注册中心的功能不就雷同了吗。确实如此,这里这么说是为了方便大家理解。实际上服务目录在获取注册中心的服务配置信息后,会为每条配置信息生成一个 Invoker 对象,并把这个 Invoker 对象存储起来,这个 Invoker 才是服务目录最终持有的对象。Invoker 有什么用呢?看名字就知道了,这是一个具有远程调用功能的对象。讲到这大家应该知道了什么是服务目录了,它可以看做是 Invoker 集合,且这个集合中的元素会随注册中心的变化而进行动态调整。
好了,关于服务目录这里就先介绍这些,大家先有个大致印象即可。接下来我们通过继承体系图来了解一下服务目录的家族成员都有哪些。
2. 继承体系
服务目录目前内置的实现有两个,分别为 StaticDirectory 和 RegistryDirectory,它们均是 AbstractDirectory 的子类。AbstractDirectory 实现了 Directory 接口,这个接口包含了一个重要的方法定义,即 list(Invocation),用于列举 Invoker。下面我们来看一下他们的继承体系图。
如上,Directory 继承自 Node 接口,Node 这个接口继承者比较多,像 Registry、Monitor、Invoker 等继承了这个接口。这个接口包含了一个获取配置信息的方法 getUrl,实现该接口的类可以向外提供配置信息。另外,大家注意看 RegistryDirectory 实现了 NotifyListener 接口,当注册中心节点信息发生变化后,RegistryDirectory 可以通过此接口方法得到变更信息,并根据变更信息动态调整内部 Invoker 列表。
现在大家对服务目录的继承体系应该比较清楚了,下面我们深入到源码中,探索服务目录是如何实现的。
3. 源码分析
本章我将分析 AbstractDirectory 和它两个子类的源码。这里之所以要分析 AbstractDirectory,而不是直接分析子类是有一定原因的。AbstractDirectory 封装了 Invoker 列举流程,具体的列举逻辑则由子类实现,这是典型的模板模式。所以,接下来我们先来看一下 AbstractDirectory 的源码。
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
if (destroyed) {
throw new RpcException("Directory already destroyed...");
}
// 调用 doList 方法列举 Invoker,这里的 doList 是模板方法,由子类实现
List<Invoker<T>> invokers = doList(invocation);
// 获取路由器
List<Router> localRouters = this.routers;
if (localRouters != null && !localRouters.isEmpty()) {
for (Router router : localRouters) {
try {
// 获取 runtime 参数,并根据参数决定是否进行路由
if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
// 进行服务路由
invokers = router.route(invokers, getConsumerUrl(), invocation);
}
} catch (Throwable t) {
logger.error("Failed to execute router: ...");
}
}
}
return invokers;
}
// 模板方法,由子类实现
protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException;
上面就是 AbstractDirectory 的 list 方法源码,这个方法封装了 Invoker 的列举过程。如下:
- 调用 doList 获取 Invoker 列表
- 根据 Router 的 getUrl 返回值为空与否,以及 runtime 参数决定是否进行服务路由
以上步骤中,doList 是模板方法,需由子类实现。Router 的 runtime 参数这里简单说明一下,这个参数决定了是否在每次调用服务时都执行路由规则。如果 runtime 为 true,那么每次调用服务前,都需要进行服务路由。这个对性能造成影响,慎重配置。关于该参数更详细的说明,请参考官方文档。
介绍完 AbstractDirectory,接下来我们开始分析子类的源码。
3.1 StaticDirectory
StaticDirectory 即静态服务目录,顾名思义,它内部存放的 Invoker 是不会变动的。所以,理论上它和不可变 List 的功能很相似。下面我们来看一下这个类的实现。
public class StaticDirectory<T> extends AbstractDirectory<T> {
// Invoker 列表
private final List<Invoker<T>> invokers;
// 省略构造方法
@Override
public Class<T> getInterface() {
// 获取接口类
return invokers.get(0).getInterface();
}
// 检测服务目录是否可用
@Override
public boolean isAvailable() {
if (isDestroyed()) {
return false;
}
for (Invoker<T> invoker : invokers) {
if (invoker.isAvailable()) {
// 只要有一个 Invoker 是可用的,就任务当前目录是可用的
return true;
}
}
return false;
}
@Override
public void destroy() {
if (isDestroyed()) {
return;
}
// 调用父类销毁逻辑
super.destroy();
// 遍历 Invoker 列表,并执行相应的销毁逻辑
for (Invoker<T> invoker : invokers) {
invoker.destroy();
}
invokers.clear();
}
@Override
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
// 列举 Inovker,也就是直接返回 invokers 成员变量
return invokers;
}
}
以上就是 StaticDirectory 的代码逻辑,很简单,大家都能看懂,我就不多说了。下面来看看 RegistryDirectory,这个类的逻辑比较复杂。
3.2 RegistryDirectory
RegistryDirectory 是一种动态服务目录,它实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。收到变更通知后,RegistryDirectory 可根据配置变更信息刷新 Invoker 列表。RegistryDirectory 中有几个比较重要的逻辑,第一是 Invoker 的列举逻辑,第二是接受服务配置变更的逻辑,第三是 Invoker 的刷新逻辑。接下来,我将按顺序对这三块逻辑。
3.2.1 列举 Invoker
Invoker 列举逻辑封装在 doList 方法中,这是个模板方法,前面已经介绍过了。那这里就不过多啰嗦了,我们直入主题吧。
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) {
// 服务提供者关闭或禁用了服务,此时抛出 No provider 异常
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION,
"No provider available from registry ...");
}
List<Invoker<T>> invokers = null;
// 获取 Invoker 本地缓存
Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap;
if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
// 获取方法名和参数列表
String methodName = RpcUtils.getMethodName(invocation);
Object[] args = RpcUtils.getArguments(invocation);
// 检测参数列表的第一个参数是否为 String 或 enum 类型
if (args != null && args.length > 0 && args[0] != null
&& (args[0] instanceof String || args[0].getClass().isEnum())) {
// 通过 方法名 + 第一个参数名称 查询 Invoker 列表,具体的使用场景暂时没想到
invokers = localMethodInvokerMap.get(methodName + "." + args[0]);
}
if (invokers == null) {
// 通过方法名获取 Invoker 列表
invokers = localMethodInvokerMap.get(methodName);
}
if (invokers == null) {
// 通过星号 * 获取 Invoker 列表
invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
}
if (invokers == null) {
Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
if (iterator.hasNext()) {
// 通过迭代器获取 Invoker 列表
invokers = iterator.next();
}
}
}
// 返回 Invoker 列表
return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
}
以上代码进行多次尝试,以期从 localMethodInvokerMap 中获取到 Invoker 列表。一般情况下,普通的调用可通过方法名获取到对应的 Invoker 列表,泛化调用可通过 ***** 获取到 Invoker 列表。按现有的逻辑,不管什么情况下,***** 到 Invoker 列表的映射关系 <*****, invokers> 总是存在的,也就意味着 localMethodInvokerMap.get(Constants.ANY_VALUE) 总是有值返回。除非这个值是 null,才会通过通过迭代器获取 Invoker 列表。至于什么情况下为空,我暂时未完全搞清楚,我猜测是被路由规则(用户可基于 Router 接口实现自定义路由器)处理后,可能会得到一个 null。目前仅是猜测,未做验证。
本节的逻辑主要是从 localMethodInvokerMap 中获取 Invoker,localMethodInvokerMap 源自 RegistryDirectory 类的成员变量 methodInvokerMap。doList 方法可以看做是对 methodInvokerMap 变量的读操作,至于对 methodInvokerMap 变量的写操作,这个将在后续进行分析。