前言
从这一章节开始讲Dubbo的集群容错.
集群容错部分包含四个部分
- 服务目录Directory
- 服务路由Router
- 集群Cluster
- 负载均衡LoadBalance
本章讲服务目录Directory.服务目录是什么,官方文档的描述如下:
服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。
在一个服务集群中,服务提供者数量并不是一成不变的,如果集群中新增了一台机器,相应地在服务目录中就要新增一条服务提供者记录。或者,如果服务提供者的配置修改了,服务目录中的记录也要做相应的更新。如果这样说,服务目录和注册中心的功能不就雷同了吗?确实如此,这里这么说是为了方便大家理解。实际上服务目录在获取注册中心的服务配置信息后,会为每条配置信息生成一个 Invoker 对象,并把这个 Invoker 对象存储起来,这个 Invoker 才是服务目录最终持有的对象。Invoker 有什么用呢?看名字就知道了,这是一个具有远程调用功能的对象。讲到这大家应该知道了什么是服务目录了,它可以看做是 Invoker 集合,且这个集合中的元素会随注册中心的变化而进行动态调整。
下面开始Directory的源码分析.文章中运行的是官方的Demo:
https://github.com/apache/incubator-dubbo/tree/dubbo-2.6.4/dubbo-demo
Demo中采用的注册中心是multicast的方式,但dubbo更提倡把zookeeper作为注册中心.所以我把
dubbo-demo-consumer/src/main/resources/META-INF/spring/dubbo-demo-consumer.xml
dubbo-demo-provider/src/main/resources/META-INF/spring/dubbo-demo-provider.xml中的<dubbo:registry>
标签替换成了以下:
1
| <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
|
推荐阅读:
https://dubbo.incubator.apache.org/zh-cn/
https://segmentfault.com/blog/dubboanalysis
继承体系
Directory 接口的方法,用于列举invoker:
1
| List<Invoker<T>> list(Invocation invocation) throws RpcException;
|
实现Node接口的还有Registry,Monitor,Invoker等.它包含以下方法
所以实现该接口的类通常可以向外提供配置信息
NotifyListener 接口:
当注册中心节点信息发生变化后,RegistryDirectory 可以通过此接口方法得到变更信息,并根据变更信息动态调整内部 Invoker 列表。
AbstractDirectory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override public List<Invoker<T>> list(Invocation invocation) throws RpcException { if (destroyed) { throw new RpcException("Directory already destroyed .url: " + getUrl()); } List<Invoker<T>> invokers = doList(invocation); List<Router> localRouters = this.routers; if (localRouters != null && !localRouters.isEmpty()) { for (Router router : localRouters) { try { 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: " + getUrl() + ", cause: " + t.getMessage(), t); } } } return invokers; }
|
list()的核心部分doList()是模板的抽象方法,该部分由子类实现.
- 调用 doList 获取 Invoker 列表
- 根据 Router 的 getUrl 返回值为空与否,以及 runtime 参数决定是否进行服务路由
Router的runtime参数决定了是否每次调用服务是都执行路由规则.如果 runtime 为 true,那么每次调用服务前,都需要进行服务路由.
StaticDirectory
StaticDirectory是AbstractDirectory的子类之一,StaticDirectory 即静态服务目录,顾名思义,它内部存放的 Invoker 是不会变动的。所以,理论上它和不可变 List 的功能很相似。所以它的源码也非常简约,当doList()
被调用时,无论参数的invocation是什么,它都直接返回存储在变量中的invokers列表
RegistryDirectory
由于RegistryDirectory实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。收到变更通知后,RegistryDirectory 可根据配置变更信息刷新 Invoker 列表。
它的包括三个重要的方法:
- RegistryDirectory#doList
列举Invoker列表
- RegistryDirectory#notify
接收服务配置变更的逻辑
- RegistryDirectory#refreshInvoker
Invoker 列表的刷新
接下来依次分析
RegistryDirectory#doList 列举Invoker列表
源码的大致分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public List<Invoker<T>> doList(Invocation invocation) { if (forbidden) { throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry ..."); } List<Invoker<T>> invokers = null; Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
if (invokers == null) { invokers = localMethodInvokerMap.get(methodName); } if (invokers == null) { invokers = localMethodInvokerMap.get(Constants.ANY_VALUE); } }
return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers; }
|
debug跟踪结果
获取 Invoker 本地缓存放入localMethodInvokerMap时,取出来的内容如下:
有两个key.分别是”sayHello”和”*”,他们的value是同样的InvokerDelegate
类型.
这个invokerDelegate里面包含了:
- providerURL…服务端url
- invoker…封装了netty客户端的Invoker
- url…消费者URL
*
是用于泛化调用的,而这里*
对应的value与”sayHello”相同.
示例中最后返回了”sayHello”对应的invoker.在debug的过程中我注意到,每一次消费者试图调用生产者提供的方法时该toList()
方法都会被调用.
doList 方法可以看做是对 methodInvokerMap 变量的读操作,至于对 methodInvokerMap 变量的写操作,下一节进行分析。
RegistryDirectory#notify 接收服务配置变更的逻辑
notify()是来自NotifyListener接口的方法.当配置发生改变时该方法就会被调用.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @Override public synchronized void notify(List<URL> urls) { List<URL> invokerUrls = new ArrayList<URL>(); List<URL> routerUrls = new ArrayList<URL>(); List<URL> configuratorUrls = new ArrayList<URL>(); for (URL url : urls) { String protocol = url.getProtocol(); String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY); if (Constants.ROUTERS_CATEGORY.equals(category) || Constants.ROUTE_PROTOCOL.equals(protocol)) { routerUrls.add(url); } else if (Constants.CONFIGURATORS_CATEGORY.equals(category) || Constants.OVERRIDE_PROTOCOL.equals(protocol)) { configuratorUrls.add(url); } else if (Constants.PROVIDERS_CATEGORY.equals(category)) { invokerUrls.add(url); } else { logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost()); } } if (configuratorUrls != null && !configuratorUrls.isEmpty()) { this.configurators = toConfigurators(configuratorUrls); } if (routerUrls != null && !routerUrls.isEmpty()) { List<Router> routers = toRouters(routerUrls); if (routers != null) { setRouters(routers); } } List<Configurator> localConfigurators = this.configurators; this.overrideDirectoryUrl = directoryUrl; if (localConfigurators != null && !localConfigurators.isEmpty()) { for (Configurator configurator : localConfigurators) { this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl); } } refreshInvoker(invokerUrls); }
|
当配置发生变更时,变更后的URL列表会作为参数带入到该方法中.URL中包含”category”这一参数.根据URL的category对应的内容完成特定的行为.
category的值可能是routers,configurators,providers.如果是routers或configurators,把它从URL转换成特定的配置对象并进行保存.如果是providers那么为它调用refreshInvoker()方法.
其实这一部份的代码跟踪在上一章已经分析过了,这里再简单总结一下:routers,configurators,providers都会去ZK对应的节点下获取数据,但这里除了生产者放入的providers节点存在ProviderURL之外,routers,configurators节点都是空的.对于空的两个节点,我们临时为它们分别生成了empty://
开头的URL.
所以最后进入notify()
的仍是3个URL(1个providerURL,2个empty://
开头的URL).但由于empty://
开头的URL是不会被受理的.所以configurators和router对象都没有被生成.只有providers的URL被保存,并进入了refreshInvoker()
RegistryDirectory#refreshInvoker Invoker 列表的刷新
refreshInvoker 方法是保证 RegistryDirectory 随注册中心变化而变化的关键所在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| private void refreshInvoker(List<URL> invokerUrls) { if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { this.forbidden = true; this.methodInvokerMap = null; destroyAllInvokers(); } else { this.forbidden = false; Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) { invokerUrls.addAll(this.cachedInvokerUrls); } else { this.cachedInvokerUrls = new HashSet<URL>(); this.cachedInvokerUrls.addAll(invokerUrls); } if (invokerUrls.isEmpty()) { return; } Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls); Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); 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; } this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap; this.urlInvokerMap = newUrlInvokerMap; try { destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); } catch (Exception e) { logger.warn("destroyUnusedInvokers error. ", e); } } }
|
这个方法的核心工作是什么?
- 将providerURL转换成
<url, Invoker>
的Map
- 把
<url, Invoker>
的Map转换成<methodName, Invoker 列表>
的map
- 把
<methodName, Invoker 列表>
的map保存到methodInvokerMap.方便toList()方法调用
- 合并多组 Invoker
- 销毁无用 Invoker
那么toInvokers()
和toMethodInvokers()
将会是该方法的重点.下面分别分析这两个方法
RegistryDirectory#toInvokers providerURL 转 Map<url, Invoker>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| 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 (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) { continue; } } if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) { continue; } if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) { logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() + " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost() + ", supported protocol: " + ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions())); continue; } URL url = mergeUrl(providerUrl);
String key = url.toFullString(); if (keys.contains(key)) { continue; } keys.add(key); Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key); if (invoker == null) { try { boolean enabled = true; if (url.hasParameter(Constants.DISABLED_KEY)) { enabled = !url.getParameter(Constants.DISABLED_KEY, false); } else { enabled = url.getParameter(Constants.ENABLED_KEY, true); } if (enabled) { 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); } if (invoker != null) { newUrlInvokerMap.put(key, invoker); } } else { newUrlInvokerMap.put(key, invoker); } } keys.clear(); return newUrlInvokerMap; }
|
大致流程如下:
如果Consumer的配置里面指定了protocol,检查Provider的URL是否支持该protocol.
消费者是否支持provider的url.示例中就是检查是否能用SPI加载DubboProtocol类
缓存未命中时,调用DubboProtocol的refer()创建invoker.
并把该invoker封装成InvokerDelegate类型
关于第3步,在上一章中做过分析.
实际上就是根据marge后的(消费者+提供者)URL创建netty的客户端连接并封装成invoker.具体可以参考我的上一篇文章
要注意的是,该方法返回的Map<url,Invoker>
的url并单纯的providerURL,还合并了Consumer的一些配置.此时产生的url如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| dubbo://172.17.0.1:20880/com.alibaba.dubbo.demo.DemoService? anyhost=true& application=demo-consumer& check=false& dubbo=2.0.2& generic=false& interface=com.alibaba.dubbo.demo.DemoService& methods=sayHello& pid=25772& qos.port=33333& register.ip=172.17.0.1& remote.timestamp=1555722960408& side=consumer& timestamp=1555730193393
|
RegistryDirectory#toMethodInvokers Map<url, Invoker>
转Map<methodName, Invoker 列表>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>(); List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>(); if (invokersMap != null && invokersMap.size() > 0) { for (Invoker<T> invoker : invokersMap.values()) { String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY); if (parameter != null && parameter.length() > 0) { String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter); if (methods != null && methods.length > 0) { for (String method : methods) { if (method != null && method.length() > 0 && !Constants.ANY_VALUE.equals(method)) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); if (methodInvokers == null) { methodInvokers = new ArrayList<Invoker<T>>(); newMethodInvokerMap.put(method, methodInvokers); } methodInvokers.add(invoker); } } } } invokersList.add(invoker); } } List<Invoker<T>> newInvokersList = route(invokersList, null); newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList); if (serviceMethods != null && serviceMethods.length > 0) { for (String method : serviceMethods) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); if (methodInvokers == null || methodInvokers.isEmpty()) { methodInvokers = newInvokersList; } newMethodInvokerMap.put(method, route(methodInvokers, method)); } } for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); Collections.sort(methodInvokers, InvokerComparator.getComparator()); newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers)); } return Collections.unmodifiableMap(newMethodInvokerMap); }
|
大致工作如之下:
构建普通方法的map:
遍历参数Map<url, Invoker>
的value值
通过invoker.getUrl().getParameter(Constants.METHODS_KEY).拿到方法名
构建成Map<methodName, Invoker 列表>
进行服务级别的路由.:
也就是存储<*, newInvokersList>
映射关系,这里的value就是直接取之前普通方法的invoker集合.
排序,转成不可变列表
之后Map<methodName, Invoker 列表>
就完成了.在没有多个组的情况下,这个map就是前几小结toList()
中需要获取的map.
最后
关于合并多组invoker和销毁复用Invoker的部分,我这里暂时就不分析了.如果需要可以参考官方文档
https://dubbo.incubator.apache.org/zh-cn/docs/source_code_guide/directory.html
文中有多个(3个?)地方用到了Router#route()方法.如:
RegistryDirectory#toMethodInvokers和AbstractDirectory#toList.
以后会进行分析