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

原创 吴就业 114 0 2019-12-14

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/1f0729f6d70846039840c027869cd62e

作者:吴就业
链接:https://wujiuye.com/article/1f0729f6d70846039840c027869cd62e
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2019年12月14日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。

服务注册与发现是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只是将服务的注册与订阅抽象为接口,由注册中心自己去实现。因此,我们需要先了解注册中心的接口定义,以及契约。

public interface Registry extends Node, RegistryService {
}

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

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的实现类。

@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方法继续完成服务的导出,该方法的代码很长,为了便于分析,我将去掉不关心的代码。

.....
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。

img

详细的url如下

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方法。

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]是什么样的。

img

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

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是如何调度完成服务的注册与订阅的。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Dubbo RPC远程调用过程源码分析(服务提供者)

在前面分析Dubbo注册中心层源码的文章中,我们知道,服务的导出与引入由RegistryProtocol调度完成。对于服务提供者,服务是先导出再注册到注册中心;对于服务消费者,先将自己注册到注册中心,再订阅事件,由RegistryDirectory将所有服务提供者转为Invoker。

Dubbo RPC远程调用过程源码分析(服务消费者)

本篇继续分析服务提供者发起一个远程RPC调用的全过程,也是跳过信息交换层和传输层,但发起请求的逻辑会复杂些,包括负载均衡和失败重试的过程,以及当消费端配置与每个服务提供端保持多个长连接时的处理逻辑。

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

由于我在实际项目中并未使用Redis作为服务注册中心,所以一直没有关注这个话题。那么,使用Redis作为服务注册中心有哪些缺点,希望本篇文章能给你答案。

缓存雪崩、穿透如何解决,如何确保Redis只缓存热点数据?

缓存雪崩如何解决?缓存穿透如何解决?如何确保Redis缓存的都是热点数据?如何更新缓存数据?如何处理请求倾斜?实际业务场景下,如何选择缓存数据结构。

线上RPC远程调用频繁超时问题排查,大功臣Arthas

项目不断新增需求,难免不会出现问题,特别是近期新增的增加请求处理耗时的需求。以及一些配置的修改而忽略掉的问题,如dubbo工作线程数调增时,忽略了redis连接池的修改。由于redis可用连接远小于工作线程数,就会出现多个线程竞争redis连接,影响性能。

如何让JedisCluster支持Pipeline

pipeline提升性能的关键,一是RTT,节省往返时间,二是I/O系统调用,read系统调用,需要从用户态,切换到内核态。