前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dubbo分层架构之服务注册中心层的源码分析(上)

Dubbo分层架构之服务注册中心层的源码分析(上)

作者头像
吴就业
发布2020-07-13 15:51:46
5220
发布2020-07-13 15:51:46
举报
文章被收录于专栏:Java艺术Java艺术
服务注册与发现是Dubbo核心的一个模块,假如没有注册中心,我们要调用远程服务,就必须要预先配置,就像调用第三方http接口一样,需要知道接口的域名或者IP、端口号才能调用。虽然手动配置麻烦了点,但也没多大事。

手动配置会有很多问题,比如服务提供者挂了而服务消费者不知道,依旧调用。解决办法可以是在多次调用失败后将其剔除。但是服务提供者又重启恢复了呢,也可以解决,后台开一个任务定期检测剔除,如果服务可用了就恢复,这很像AWS的ELB负载均衡服务。但是,如果服务提供者是换了台机器重启呢,或者是新增加节点呢?修改配置重启,亦或是手动一台服务器一台服务器的修改配置文件,在代码中定时检查配置文件是否有改动?所以注册中心是为了解决这些问题而诞生的。

Dubbo服务注册与发现流程

源码在dubbo-registry模块,核心在dubbo-registry-api模块。dubbo目前提供了redis、zookeeper、nacos、etcd3这几个生产级的注册中心的实现。其中,redis不推荐使用,但本文会从redis的实现分析Dubbo的服务注册与订阅,没别的原因,只是因为我本地只装了redis。

了解dubbo注册中心的实现,并不是说一定要去自己实现一个注册中心,而是当你想用某个注册中心,但是dubbo不提供的时候,可以自己去实现支持。或者是你想在注册中心修改某项配置,但不知道怎么修改,网上也找不到教程,如果了解源码,就不用浪费这种时间。

Dubbo有很多种协议(Protocol),比如序列化协议(HessianProtocol)、注册中心协议(RegistryProtocol)、远程调用协议(DubboProtocol),而本篇重点要关注的是RegistryProtocol。服务注册与发现都是由RegistryProtocol调度完成的。

服务提供者,在服务导出(export)时,由RegistryProtocol从注册工厂获取注册器Registry(RegistryService),具体使用哪种注册中心由配置文件指定,通过SPI获取。一般注册中心都不会直接实现RegisterService接口,而是通过继承FailbackRegistry获得失败重试的支持。注册中心只需要实现doRegister方法实现具体的注册逻辑。在服务注册到注册中心之后,由RegistryProtocol调用subscriber方法开启订阅。对zookeeper注册中心而言,就是监听节点的改变事件,对redis注册中心而言,就是订阅某个key的事件。

服务注册流程图

服务消费者,在引入服务(refer)时,也需要先将自己注册到注册中心,也是由RegistryProtocol取得具体的注册器Registry(RegistryService),与服务提供者的区别在于事件订阅。在服务注册到注册中心之前,会先创建一个本地服务目录,即RegistryDirectory,用于缓存服务提供者信息,每个接口对应一个RegistryDirectory,同时RegistryDirectory也是一个NotifyListener,当订阅到事件时会更新缓存的服务提供者信息。

服务发现流程图

服务消费者会订阅配置(configurators)改变、路由(routers)改变、服务提供者(providers)改变事件,而服务提供者只订阅配置(configurators)改变事件,这并不是说服务提供者并不能订阅其它事件,这只是一种契约,在服务导入与导出时通过在服务的URL上绑定一个参数category约定。因为服务提供者也不并关心路由(routers)改变、服务提供者(providers)改变事件,所以定义这一契约。

服务注册与订阅流程源码分析

本篇只分析Redis注册中心的实现。无论是服务提供者,还是服务消费者,在注册完成后都会发布一个事件,因为Redis注册中心是通过发布/订阅事件实现的,Redis无法感知服务注册,只能由服务自己发布事件。这是与其它注册中心不同的地方,实际上每种注册中心的实现都不一样,所以Dubbo只是将服务的注册与订阅抽象为接口,由注册中心自己去实现。因此,我们需要先了解注册中心的接口定义,以及契约。

代码语言:javascript
复制
public interface Registry extends Node, RegistryService {
}

Registry接口继承Node、RegistryService,Node接口可以忽略,实际上是没有必要的,虽然URL是Dubbo串起所有分层模块的桥梁,但这里的URL是注册中心的URL。所以,我们只需关注RegistryService,说到注册中心,其实说的就是RegistryService。

代码语言:javascript
复制
public interface RegistryService {
    void register(URL url);
    void unregister(URL url);
    void subscribe(URL url, NotifyListener listener);
    void unsubscribe(URL url, NotifyListener listener);
    List<URL> lookup(URL url);
}

关于契约,可以查阅官方文档/SPI扩展实现/注册中心扩展。RegistryService接口定义了五个方法,分别是注册(register)、注销(unregister)、订阅(subscribe)、取消订阅(unsubscribe),以及查询注册列表(lookup)。很多情况下unregister、unsubscribe方法都不会被调用,比如进程kill -9结束的,所以本篇不做具体分析,如果想自己实现注册中心,就要实现这两个方法,并且保证即时这两个方法不会被调用也不会影响正常使用。lookup方法其实是一个可以废弃的方法,redis注册中心、zookeeper注册中心并没有去重写实现它,因此我们也不必要关心。那么,我们重点关注的就是register与subscribe方法的实现。

前面提到RegistryProtocol是服务注册与订阅的调度者,无论是服务提供者,还是服务消费者,都是由RegistryProtocol调度服务注册与引用的。RegistryProtocol是Protocol的实现类。

代码语言:javascript
复制
@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}

Protocol的两个方法都注释了@Adaptive注解,说明这两个方法都是自适应扩展点,只有在运行时由URL中的protocol决定加载具体的实现类,RegistryProtocol是它的实现类,自然也是通过自适应扩展点机制获取的,对应“registry”这一协议。那么RegistryProtocol的export与refer这两个方法是何时被调用的呢?

回想下我们前面分析Dubbo与Spring整合如何导出服务与引入服务那篇文章。是通过ServiceBean与ReferenceBean分别实现导出与引入的。

01

服务提供者的导出流程可从ServiceBean的onApplicationEvent方法开始追溯,即在Spring初始化完成之后导出服务。最先在ServiceConfig的doExportUrls方法中调用AbstractInterfaceConfig的loadRegistries获取注册中心配置,再调用ServiceConfig的doExportUrlsFor1Protocol方法继续完成服务的导出,该方法的代码很长,为了便于分析,我将去掉不关心的代码。

代码语言:javascript
复制

.....
for (URL registryURL : registryURLs) {
      // For providers, this is used to enable custom proxy to generate invoker
      String proxy = url.getParameter(PROXY_KEY);
      if (StringUtils.isNotEmpty(proxy)) {
               registryURL = registryURL.addParameter(PROXY_KEY, proxy);
       }
       Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
       DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
       Exporter<?> exporter = protocol.export(wrapperInvoker);
       exporters.add(exporter);
  }
......

遍历我们配置的注册中心的url,一般情况下我们只会使用一个注册中心,所以for循环只会执行一遍。protocol.export(wrapperInvoker)就是我们要找的入口,这里看着是与RegistryProtocol不着边,interfaceClass是要导出的服务没有错,但是这里的url是包了一层注册中心的,这是注册中心的url。

详细的url如下

代码语言:javascript
复制
registry://127.0.0.1:6379/org.apache.dubbo.registry.RegistryService?
application=dubbo-demo-annotation-provider&dubbo=2.0.2
&export=dubbo%3A%2F%2F10.1.0.164%3A20880%2Forg.apache.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-demo-annotation-provider%26bean.name%3DServiceBean%3Aorg.apache.dubbo.demo.DemoService%26bind.ip%3D10.1.0.164%26bind.port%3D20880%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.DemoService%26methods%3DsayHello%26pid%3D81526%26register%3Dtrue%26release%3D%26side%3Dprovider%26timestamp%3D1576260341338&pid=81526&registry=redis&timestamp=1576260336326

Protocol是使用了自适应扩展点机制的,因此此处的protocol并不是RegistryProtocol,也不是DubboProtocol,只是一个动态的使用javasist生成的Protocol接口的一个代理类。因此,只有了解Dubbo的SPI自适应扩展点机制,我们才能看得懂。由于Protocol的自适应扩展是通过从URL中拿到协议再获取具体实现类的,而此处Invoker的url是注册中心的url,协议自然是“registry”,也就等同于protocol.export(wrapperInvoker)的protocol就是RegistryProtocol。

02

服务消费者的引入我们可以从ReferenceBean的getObject方法开始追溯,因为ReferenceBean是一个FactoryBean。一直往下跟踪,最先跟到ReferenceConfig的createProxy方法。

代码语言:javascript
复制
checkRegistry();
List<URL> us = loadRegistries(false);
if (CollectionUtils.isNotEmpty(us)) {
       for (URL u : us) {
            urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
       }
}
.....
 if (urls.size() == 1) {
      invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
  }

与服务提供者的导出有点相似,先是获取注册中心的配置,在只有一个注册中心的情况下,顺利进入到REF_PROTOCOL.refer方法的调用。同样,我们先debug看下此时的urls[0]是什么样的。

我把详细的url拷贝出来,内容如下

代码语言:javascript
复制
registry://127.0.0.1:6379/org.apache.dubbo.registry.RegistryService?
application=dubbo-demo-annotation-consumer&dubbo=2.0.2&pid=81543
&refer=application%3Ddubbo-demo-annotation-consumer%26check%3Dfalse%26dubbo%3D2.0.2%26interface%3Dorg.apache.dubbo.demo.DemoService%26lazy%3Dfalse%26methods%3DsayHello%26mock%3Dorg.apache.dubbo.demo.consumer.mock.DemoMock%26pid%3D81543%26register.ip%3D10.1.0.164%26sayHello.async%3Dtrue%26sayHello.return%3Dtrue%26side%3Dconsumer%26sticky%3Dfalse%26timestamp%3D1576262437682&registry=redis&timestamp=1576262454637

REF_PROTOCOL跟前面服务导出的分析是一样的,只是这里调用的是refer方法。REF_PROTOCOL是通过javassist动态生成的Protocol接口的一个代理类,实现了Protocol接口,在refer方法中,调用getUrl方法获取URL,并从URL中获取"://"之前的字符串代表协议,根据协议拿到Protocol的实现类,也正是RegistryProtocol。

自此,我们已经知道了RegistryProtocol是何时被调用的,而具体的注册与订阅逻辑则是由RegistryProtocol调度实现的,由于文章篇幅问题,为减轻大家的阅读负担,我将在下篇文章继续给大家分析RegistryProtocol是如何调度完成服务的注册与订阅的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档