Dubbo——服务目录
引言
前面几篇文章分析了Dubbo的核心工作原理,本篇将对之前涉及到但却未细讲的服务目录进行深入分析,在开始之前先结合前面的文章思考下什么是服务目录?它的作用是什么?
正文
概念及作用
清楚Dubbo的调用过程就知道Dubbo在客户端和服务端都会为服务生成一个Invoker执行体,这个Invoker包含了所有的配置信息,也相当于是一个代理对象,所以这也就引发出几个问题:
- 怎么管理Invoker?不可能让用户自己去管理,也不可能客户端每次调用服务时都新创建Invoker。
- 当服务存在集群时,选择使用哪一个Invoker?怎么选择?
- 服务列表变化时,Invoker列表怎么更新?
针对以上问题,Dubbo引入了服务目录的概念,简单的说就是Invoker的集合,由框架自身统一管理Invoker列表,并且提供订阅服务功能,使得服务变更时,会自动更新Invoker列表;同时当存在集群时,可以使得外部以统一的方法使用Invoker,即用户不用关心怎么选择使用哪一个Invoker(在之前的源码分析中我们看到过cluster.join就是实现该功能的API,将多个Invoker合成一个Invoker)。
继承结构
了解了基本概念后,我们来看看服务目录的继承体系:

Directory继承Node接口,该接口是Dubbo中节点的高度抽象,它提供了获取url配置、判断节点是否可用以及销毁节点的接口,由各个子类实现,只要是和服务节点相关的实现都可以实现该接口。比如Invoker、Directory、Registry等。目录的内置实现有StaticDirectory和RegistryDirectory两个,第一个是静态目录服务,其中的Inovker列表是不会改变的;而RegistryDirectory实现了NotifyListener接口,表示会监听注册中心节点的变化,当节点信息改变时,RegistryDirectory中的Inovker列表会自动更新。
源码分析
AbstractDirectory
从上面的继承图我们可以看到StaticDirectory和RegistryDirectory都继承了AbstractDirectory,可以猜到多半又是模板方法模式的实现,确实也是这样,AbstractDirectory提供了获取Invoker的接口,而具体的实现则是子类自行实现的,我们来看看其源码:
public abstract class AbstractDirectory<T> implements Directory<T> {
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
if (destroyed){
throw new RpcException("Directory already destroyed .url: "+ getUrl());
}
// 获取invoker列表,由子类实现
List<Invoker<T>> invokers = doList(invocation);
// 本地路由列表
List<Router> localRouters = this.routers;
if (localRouters != null && localRouters.size() > 0) {
for (Router router: localRouters){
try {
// 是否需要进行路由
if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, true)) {
invokers = router.route(invokers, getConsumerUrl(), invocation);
}
} catch (Throwable t) {
logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
}
}
}
return invokers;
}
protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException ;
// 省略其它方法
}
这个方法逻辑很简单,没什么好说的,其中路由配置可自行了解,本文不打算展开,下面主要看看子类是如何实现doList方法以及如何管理Inovker的。
RegistryDirectory
刚说了RegistryDirectory是动态的目录服务,会监听注册中心的节点变化,并自动刷新Invoker列表,用户可以通过它拿到实时的Invoker列表,下面就主要分析这三部分是如何实现的。
注册监听及自动更新Invoker列表
public void subscribe(URL url) {
setConsumerUrl(url);
registry.subscribe(url, this);
}
注册监听很简单,客户端创建Zookeeper连接时,会添加监听器监听节点变化,该监听器最终会调用到RegistryDirectory的subscribe方法,使得目录也可以监听节点变化,当节点发生变化时,又会触发NotifyListener的notify方法,RegistryDirectory就实现了该方法:
public synchronized void notify(List<URL> urls) {
// 存放provider节点下的url
List<URL> invokerUrls = new ArrayList<URL>();
// 存放router节点下的url
List<URL> routerUrls = new ArrayList<URL>();
// 存放configurator节点下的url
List<URL> configuratorUrls = new ArrayList<URL>();
// 每次传入的url都是节点下所有的url,并非增量
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
// 根据url的协议和分类存放到对应的List中
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 {
// 忽略不支持的url
logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
}
}
// 缓存configurator url
if (configuratorUrls != null && configuratorUrls.size() >0 ){
this.configurators = toConfigurators(configuratorUrls);
}
// 设置路由
if (routerUrls != null && routerUrls.size() >0 ){
List<Router> routers = toRouters(routerUrls);
if(routers != null){ // null - do nothing
setRouters(routers);
}
}
List<Configurator> localConfigurators = this.configurators; // local reference
// 合并override参数
this.overrideDirectoryUrl = directoryUrl;
if (localConfigurators != null && localConfigurators.size() > 0) {
for (Configurator configurator : localConfigurators) {
this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
}
}
// 根据provider节点下的url刷新invoker
refreshInvoker(invokerUrls);
}
这个方法主要缓存各种类型的url以及配置路由,Invoker的刷新主要是在refreshInvoker方法中:
private void refreshInvoker(List<URL> invokerUrls){
if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
// invokerUrls中只有一个url且协议为empty,则禁用所有服务
this.forbidden = true; // 禁止访问
this.methodInvokerMap = null; // 置空列表
destroyAllInvokers(); // 关闭所有Invoker
} else {
this.forbidden = false; // 允许访问
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
if (invokerUrls.size() == 0 && this.cachedInvokerUrls != null){
// invokerUrls为空但缓存中有url,则将缓存中的url添加到invokerUrls
invokerUrls.addAll(this.cachedInvokerUrls);
} else {
// 将invokerUrls中的所有url缓存起来,便于交叉对比
this.cachedInvokerUrls = new HashSet<URL>();
this.cachedInvokerUrls.addAll(invokerUrls);
}
if (invokerUrls.size() ==0 ){
return;
}
// 将URL列表转成Invoker列表,key是url
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
// 换方法名映射Invoker列表
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 ;
}
// 合并多个invoker
this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
this.urlInvokerMap = newUrlInvokerMap;
try{
// 销毁无用的invoker
destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap);
}catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}
该方法首先会判断是否需要禁用服务,若不需要,则将url转化为Invoker,并将方法名映射到对应的Invoker,紧接着将多组Invoker合并后赋值给this.methodInvokerMap变量(该变量会在doList遍历Invoker时用到),最后会销毁掉缓存中已经无用的Invoker,避免调用到已怠机的服务。以上就是Invoker自动刷新的流程,其中各个依赖方法的细节感兴趣的可自行分析,下面就一起来看看如何获取Invoker列表。
获取Invoker列表
public List<Invoker<T>> doList(Invocation invocation) {
// 服务提供者关闭或禁止了服务抛出异常
if (forbidden) {
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "Forbid consumer " + NetUtils.getLocalHost() + " access service " + getInterface().getName() + " from registry " + getUrl().getAddress() + " use dubbo version " + Version.getVersion() + ", Please check registry access list (whitelist/blacklist).");
}
List<Invoker<T>> invokers = null;
// 本地Invoker列表,在refreshInvoker中合并赋值
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())) {
invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // 可根据第一个参数枚举路由
}
// 根据方法名获取Inovker列表
if(invokers == null) {
invokers = localMethodInvokerMap.get(methodName);
}
// 根据“*”获取Invoker列表
if(invokers == null) {
invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
}
// 这里没有什么用处,新版中已经移除
if(invokers == null) {
Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
if (iterator.hasNext()) {
invokers = iterator.next();
}
}
}
return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
}
这个逻辑也非常清晰,就是根据方法名或“*”拿到对应的Invoker列表。以上就是动态服务目录的实现原理,下面再来看看静态服务目录。
StaticDirectory
public class StaticDirectory<T> extends AbstractDirectory<T> {
private final List<Invoker<T>> invokers;
public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
super(url == null && invokers != null && invokers.size() > 0 ? invokers.get(0).getUrl() : url, routers);
if (invokers == null || invokers.size() == 0)
throw new IllegalArgumentException("invokers == null");
this.invokers = invokers;
}
public void destroy() {
if(isDestroyed()) {
return;
}
super.destroy();
for (Invoker<T> invoker : invokers) {
invoker.destroy();
}
invokers.clear();
}
@Override
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
return invokers;
}
}
相比较而言,静态服务目录就简单多了,通过构造器创建,由于没有提供更新的方法,所以一旦创建就不会改变,而读取Inovker列表只需要将自身变量返回即可。
总结
服务目录的原理我们搞清楚了,再结合前面几篇文章,我们能够掌握Dubbo的核心实现原理。现在我们再来看看Dubbo的架构图:
抛开种种繁杂的功能,你会发现这个架构和RMI以及我们之前手写实现的RPC架构没太大区别,所以,大道至简,掌握基础才能更加快速地理解更复杂的框架应用。
至此,Dubbo系列暂时就写到这了,但其本身做为一个优秀的开源框架,发展这么多年,不可能这几篇文章就涵盖完全了,其它的诸如序列化、路由、Monitor以及文中未做详细分析的部分,读者们可自行阅读源码分析。
Dubbo——服务目录的更多相关文章
- K8S(09)交付实战-通过流水线构建dubbo服务
k8s交付实战-流水线构建dubbo服务 目录 k8s交付实战-流水线构建dubbo服务 1 jenkins流水线准备工作 1.1 参数构建要点 1.2 创建流水线 1.2.1 创建流水线 1.2.2 ...
- dubbo源码阅读之服务目录
服务目录 服务目录对应的接口是Directory,这个接口里主要的方法是 List<Invoker<T>> list(Invocation invocation) throws ...
- Dubbo源码(五) - 服务目录
前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 今天,来聊聊Dubbo的服务目录(Directory).下面是官方文档对服务目录的定义: 服务目 ...
- dubbo服务自动化测试搭建
java实现dubbo的消费者服务编写:ruby实现消费者服务的接口测试:通过消费者间接测试dubbo服务接口的逻辑 内容包括:dubbo服务本地调用环境搭建,dubbo服务启动,消费者部署,脚本编写 ...
- Dubbo_创建Dubbo服务并在ZooKeeper注册,然后通过Jar包执行
一.安装ZooKeeper(略) 二.创建Dubbo服务 1.DemoService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...
- 跟我学习dubbo-使用Maven构建Dubbo服务的可执行jar包(4)
Dubbo服务的运行方式: 1.使用Servlet容器运行(Tomcat.Jetty等)----不可取 缺点:增加复杂性(端口.管理) 浪费资源(内存) 官方:服务容器是一个standalone的启动 ...
- Tomcat中部署web应用 ---- Dubbo服务消费者Web应用war包的部署
样例视频:http://www.roncoo.com/course/view/f614343765bc4aac8597c6d8b38f06fd IP: 192.168.2.61 部署容器:apache ...
- dubbo服务运行的三种方式
dubbo服务运行,也就是让生产服务的进程一直启动.如果生产者进程挂掉,也就不存在生产者,消费者不能进行消费. Dubbo服务运行的三种方式如下:1.使用Servlet容器运行(Tomcat.Jett ...
- RPC -dubbo 服务导出实现
在阅读此文章之前,我希望阅读者对Spring 扩展机制的有一定的了解,比如:自定义标签与Spring整合, InitializingBean 接口,ApplicationContextAware,Be ...
随机推荐
- Redis数据迁移同步工具(redis-shake)
前言 最近线上一台自建redis服务的服务器频繁报警,内存使用率有点高,这是一台配置比较简陋(2C8G)的机子了,近期也打算准备抛弃它了.抛弃之前需对原先的数据进行迁移,全量数据,增量数据都需要考虑, ...
- SQL SERVER修改为sa登陆权限报错,233,18456接连出现【抓狂ing】
[记录生活] 今天做作业需要修改sa权限,本人电脑没错误. 同样教程发给朋友,错误百出.... 话不多说,百度很多解决方法,但是都没有解决,贴出解决方法. 0.用Windows身份验证登录,执行SQL ...
- vue-cli2 使用cdn资源
vue-cli2 引入固定cdn资源操作记录 vue引入cdn也不是什么神奇的操作,但是自己之前一直没有尝试过,这次正好项目优化需要,所以,实操一波,特此记录 本次cnd引入了如下资源 vue vue ...
- pyqt5-多线程初步
多线程是实现并发的一个重要手段.在GUI编程中,经常需要将耗费时间较多的任务分离出来成为一个线程,避免对主线程造成影响(造成界面无响应). 在Qt中,最简单的多线程主要通过继承QThread类实现,重 ...
- python 验证码处理
一. 灰度处理,就是把彩色的验证码图片转为灰色的图片. 二值化,是将图片处理为只有黑白两色的图片,利于后面的图像处理和识别 # 自适应阀值二值化 def _get_dynamic_binary_ima ...
- 关于安装Linux-centOS时遇到的问题
1.新建虚拟机实例后倒入centos镜像开机报错.提示不支持 64 位.... 重新下载虚拟机安装包,重新安装. 2.安装到检查光盘镜像的下一步,vm is nor support (or ... c ...
- MongoDB学习(一) 安装与基本使用
链接:https://pan.baidu.com/s/1ogTDFJg3ZZc0CyzaTeswWg 提取码:2k0p 安装 // 将压缩包解压到指定目录 [bigdata@linux backup] ...
- ES6-常用四种数组
1.map 1.1 个人理解 映射 一个对一个 例如:[45,57,138]与[{name:'blue',level:0},{name:'zhangsan',level:99},{name:'lisi ...
- 初窥Ansible playbook
Ansible是一个系列文章,我会尽量以通俗易懂.诙谐幽默的总结方式给大家呈现这些枯燥的知识点,让学习变的有趣一些. Ansible系列博文直达链接:Ansible入门系列 前言 在上一篇文章中说到A ...
- C#中值类型,引用类型,字符串类型的区别(内存图解)
如果用图片来解释值类型,引用类型和字符串类型(引用类型的一种)的区别的话 值类型: 引用类型: string类型: