Netty 暴露服务

在上一篇文章我们讲解了一下 dubbo 服务暴露过程中的本地暴露。它只是一个开胃小菜,主要是为我们后面讲解远程暴露开个头。下面就来分析一下 dubbo 在远程暴露里面发生了哪些事。因为 dubbo 远程暴露里面的过程还是比较复杂的,所以我就分为三个文章来讲解 dubbo 的远程暴露:

  1. dubbo 远程暴露 – Netty 暴露服务
  2. dubbo 远程暴露 – Zookeeper 连接
  3. dubbo 远程暴露 – Zookeeper 注册 & 订阅

这就篇就是分析 dubbo 服务暴露中通过 Netty 来暴露服务(当然 dubbo 还可以通过 Mina、Grizzly 来暴露服务,默认使用 Netty)。

1、ServiceConfig#doExportUrls

首先通过方法loadRegistries(true)来加载注册中心。在方法checkRegistry()方法中判断如果 xml 里面没有配置注解中心,从 dubbo 的 properties 文件中获取(默认是dubbo.properties)。然后会返回List<URL> 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。URL的格式如下:

  1. registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.0 ...

因为 dubbo 支持多种协议,遍历所有协议分别根据不同的协议把服务export到不同的注册中心上去。

  1. 把配置的信息通过appendParameters提取到 map 中
  2. 判断是否支持泛化调用
  3. 通过协议名称、host、port、contextPath 和第一步提取出来的 map 构造协议的统一数据模型 URL (如:dubbo://169.254.69.197:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider ...)
  4. 循环遍注册中心,把服务暴露在不同的注册中心当中
    a) 如果配置了 monitor,就返回监控统一模型数据 URL,并给以 monitor为 key 添加到生成的 URL中,URL格式如下:
    registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?export=dubbo%3A%2F%2F169.254.69.197%3A20880%2Fcom.alibaba.dubbo.demo.DemoService& ...
    b) 把协议统一模型 URL 以export为 key,添加到注册中心的统一模型 URL中
    c) 根据服务的具体实现、实现的接口以及注册中心统一模型 URL从代理工厂 ProxyFactory(SPI 默认获取到 JavassistProxyFactory)获取 Invoker对象。
    Dubbo源码(五) 服务发布(下) - 图1
    d) 通过 Protocol#export(invoker) 暴露服务,因为注册的协议是 registry 所以生成的 Protocol 对象如下图所示。因为 ProtocolFilterWrapperProtocolFilterWrapper是过滤 registry协议的,所以最终通过 RegistryProtocol来处理暴露过程。
    Dubbo源码(五) 服务发布(下) - 图2

2、RegistryProtocol#export

根据这个类名我们就可以推测出这个类具有的功能,具有 Registry(注册)与 Protocol (协议–服务暴露)在这个方法里面就包括上面提到的三个逻辑:

  • dubbo 远程暴露 – Netty 暴露服务,通过配置的协议根据 SPI 获取到对应的 Protocol对象,这里是 DubboProtocol,对象。
  • dubbo 远程暴露 – Zookeeper 连接 服务注册,通过RegistryFactory根据 SPI 获取对应的 Registry 对象(ZookeeperRegistry),然后注册到注册中心上面去,供 consumer调用
  • dubbo 远程暴露 – Zookeeper 注册 & 订阅,它会把创建2个节点:一个是/dubbo/服务全类名/provider/...节点提供给服务消费方查看节点信息;二是/dubbo/服务全类名/configurators/...节点提供给服务方 watch(监控) dubbo-admin 对于服务的修改。比如:服务权重。

上面粗略的讲了一下服务远程暴露主要干了哪些事,主要是想让大家有一个全局的意识。下面我们就来讲一下 dubbo 服务是如何通过 Netty 来暴露服务。

export()方法中的:ExporterChangeableWrapper exporter = doLocalExport(originInvoker);

  1. getCacheKey(originInvoker),通过 Invoker 对象获取到缓存 key,还记得我们在ServiceConfig#doExportUrls的 4-b 步骤里面吗?它就是把保存在 注册统一模型里面的 export key 获取到协议的统一模型dubbo://169.254.69.197:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider ...,然后再删除 dynamicenabled 参数
  2. Map<String, ExporterChangeableWrapper<?>> bounds缓存中根据上面获取的 key 获取 Exporter 对象,如果获取到直接返回;否则进行服务暴露
  3. 通过 originInvoker获取里面的 URL 获取到协议的统一模型以及originInvoker本身创建 InvokerDelegete
  4. 根据InvokerDelegete暴露服务,因为 URL 协议是 dubbo,所以获取到的实例是 DubboProtocol,而这个对象因为协议不是 registry,所以生成ProtocolListenerWrapper会根据 SPI 机制检测 dubbo 里面配置的 InvokerListener 扩展;而 ProtocolFilterWrapper 会根据 SPI机制检测 dubbo里面配置的 Filter 扩展。所以最终通过 DubboProtocol来处理暴露过程。
    Dubbo源码(五) 服务发布(下) - 图3
  5. 暴露生成的 Exporter 和 传入的 originInvoker 会创建 ExporterChangeableWrapper对象会以步骤 1 生成的 key 缓存在 Map<String, ExporterChangeableWrapper<?>> bounds 当中,并返回结果。

3、DubboProtocol#export

整个DubboProtocol#export的代码如下:

  1. public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
  2. URL url = invoker.getUrl();
  3. // export service.
  4. String key = serviceKey(url);
  5. DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
  6. exporterMap.put(key, exporter);
  7. //export an stub service for dispaching event
  8. Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
  9. Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
  10. if (isStubSupportEvent && !isCallbackservice) {
  11. String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
  12. if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
  13. if (logger.isWarnEnabled()) {
  14. logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
  15. "], has set stubproxy support event ,but no stub methods founded."));
  16. }
  17. } else {
  18. stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
  19. }
  20. }
  21. openServer(url);
  22. return exporter;
  23. }

这断代码主要的操作是:

  1. 根据传入的Invoker中的 URL 通过serviceKey(url)获取到 serviceKey,它的格式为:com.alibaba.dubbo.demo.DemoService:20880.
  2. 以传的Invoker、第 1 步生成的 key 和 Map<String, Exporter<?>> exporterMap 生成 DubboExporter,并以第 1 步生成的 key 为索引,把生成的 DubboExporter添加到Map<String, Exporter<?>> exporterMap
  3. 根据 URL 判断是不是服务端,如果是服务端并且从Map<String, ExchangeServer> serverMap获取到的 ExchangeServer 为空,就通过DubboProtocol#createServer 创建服务,达到服务暴露的目的。返回DubboExporter对象

4、DubboProtocol#createServer

dubbo 远程服务(Provider)暴露最终其实就是创建一个 Netty Serve 服务,然后在 dubbo 在服务引用的时候创建一个 Netty Client 服务。其实 dubbo 远程通信的原理其实就是基于 Socket 的远程通信。下面我们来看一下 dubbo 是如何创建一个 Netty 服务的,下面就是它创建的序列图:
Dubbo源码(五) 服务发布(下) - 图4

它通过传入 URL 与 requestHandler来创建一个 ExchangeServer,通过Netty 基于 NIO的形式通过自定义Channel来接收服务引用方传递过来的信息,以及发送调用远程服务的本地方法后的数据给服务调用者。URL 里面主要包含 IP 地址 与 端口信息用于创建 Socket 连接,而 requestHandler是一个 ExchangeHandler 通过自定义协议来处理 dubbo 的远程通信。

Zookeeper连接

在上一篇文章我们讲解了一下 dubbo 远程服务暴露过程中通过 Netty 进行 Socket 服务暴露。使得远程客户端可以访问这个暴露的服务,这个只是解决了访问之前点到点的服务调用。对于分步式环境当中,越来越多的服务我们如何管理并且治理这些服务是一个问题。因此 dubbo 引入了注册中心这个概念,把服务暴露、服务调用的信息保存到注册中心上面。并且还可以订阅注册中心,实现服务自动发现。

dubbo 支持以下几种注册中心

注册中心 成熟度 优点 问题 建议
Zookeeper注册中心 Stable 支持基于网络的集群方式,有广泛周边开源产品,建议使用dubbo-2.3.3以上版本(推荐使用) 依赖于Zookeeper的稳定性 可用于生产环境
Redis注册中心 Stable 支持基于客户端双写的集群方式,性能高 要求服务器时间同步,用于检查心跳过期脏数据 可用于生产环境
Multicast注册中心 Tested 去中心化,不需要安装注册中心 依赖于网络拓普和路由,跨机房有风险 小规模应用或开发测试环境
Simple注册中心 Tested Dogfooding,注册中心本身也是一个标准的RPC服务 没有集群支持,可能单点故障 试用

官方推荐使用 zookeeper 为注册中心。在我们分析一下 dubbo 中是如何集成 zookeeper 之前,我们先来回顾一下 dubbo 服务暴露里面的主要步骤。

1、RegistryProtocol#export

下面就是 dubbo 暴露的核心步骤的代码,可能由于版本的原因(下面的代码基于 2.6.1)代码会有所差异但是核心思想不变。

  1. public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
  2. //export invoker
  3. final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
  4. URL registryUrl = getRegistryUrl(originInvoker);
  5. //registry provider
  6. final Registry registry = getRegistry(originInvoker);
  7. final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
  8. //to judge to delay publish whether or not
  9. boolean register = registedProviderUrl.getParameter("register", true);
  10. ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);
  11. if (register) {
  12. register(registryUrl, registedProviderUrl);
  13. ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
  14. }
  15. // Subscribe the override data
  16. // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover.
  17. final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
  18. final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
  19. overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
  20. registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
  21. //Ensure that a new exporter instance is returned every time export
  22. return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registedProviderUrl);
  23. }

之前我们分析了第一步:ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);dubbo 基于 socket 的本地暴露提供服务给远程客户端调用。下面我们就来分析服务远程暴露的注册服务到 zookeeper 这个注册中心上面来实现高可用的。

2、RegistryProtocol#getRegistry

1、把 URL 里面的 protocol 设置成 <dubbo:registry address="zookeeper://127.0.0.1:2181"里面设置的 zookeeper。
2、通过 RegistryFactory 的 SPI 接口 RegistryFactory$Adaptive根据 URL 里面的 protocol 获取 zookeeper 的注册工厂调用 getRegistry获取 zookeeper 注册中心 – ZookeeperRegistry。

以下是由 dubbo 字节码服务生成的 RegistryFactory$Adaptive

  1. public class RegistryFactory$Adaptive implements com.alibaba.dubbo.registry.RegistryFactory {
  2. public com.alibaba.dubbo.registry.Registry getRegistry(com.alibaba.dubbo.common.URL arg0) {
  3. if (arg0 == null) throw new IllegalArgumentException("url == null");
  4. com.alibaba.dubbo.common.URL url = arg0;
  5. String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
  6. if (extName == null)
  7. throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.registry.RegistryFactory) name from url(" + url.toString() + ") use keys([protocol])");
  8. com.alibaba.dubbo.registry.RegistryFactory extension = (com.alibaba.dubbo.registry.RegistryFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.registry.RegistryFactory.class).getExtension(extName);
  9. return extension.getRegistry(arg0);
  10. }
  11. }

3、AbstractRegistryFactory.getRegistry

1、URL 添加 参数interfacecom.alibaba.dubbo.registry.RegistryService,并去掉 export 参数。
2、ZookeeperRegistryFactory#createRegistry创建 ZookeeperRegistry 实例
3、AbstractRegistry#init()中调用loadProperties(),在以下目录中保存注册信息(以window为例)。

  1. C:\Users\Carl\.dubbo\dubbo-registry-127.0.0.1.cache
  2. 1

4、FailbackRegistry#init()中故障回复注册类中创建线程池 ScheduledExecutorService 检测并连接注册中心,如果失败就就调用 retry()进行重连,高可用。
5、zookeeperTransporter#connect()由于 ZookeeperTransporter 是一个 @SPI 接口并且 @Adaptive,所以会生成一个 ZookeeperTransporter$Adaptive,并且是由RegistryFactory这个 SPI 接口创建的时候通过 SPI 依赖注入创建 ZookeeperRegistryFactory 对象的时候依赖注入的。然后通过ZookeeperRegistryFactory#createRegistry()把这个接口传入的。而且因为 我的这个版本使用的是 dubbo-2.6.1 版本所以默认使用的是 Curator 这个 Zookeeper 客户端,之前低版本默认使用的是 Zkclient 这个默认客户端。如果大家对 dubbo 里面的 SPI 机制不太了解可以看之前的 dubbo源码分析 之 内核SPI实现 (见上)

Dubbo源码(五) 服务发布(下) - 图5

不知道大家细心观察没有:Curator 和 Zkclient 在连接 zookeeper 的时候代码风格不太一样。而 dubbo 在进行 zookeeper 连接的时候通过 ZkClientWrapper这个包装类使得 Zkclient 与 Curator 的代码风格一致。(统一调用应该是这个机制,通过定义抽象类)
image.png

1、Curator – CuratorZookeeperClient#init()

  1. // 构造连接参数
  2. CuratorFramework client = builder.build();
  3. // 进行连接操作
  4. client.start();

2、Zkclient – ZkclientZookeeperClient#init()

  1. // 构造连接参数
  2. ZkClientWrapper client = = new ZkClientWrapper(url.getBackupAddress(), 30000);
  3. // 进行连接操作
  4. client.start();

这个就是 dubbo 平等对待第三方框架,而且把 zk 的两种不同的客户端的代码风格统一了起来。

Zookeeper 注册 & 订阅

1、RegistryProtocol#export

下面就是 dubbo 暴露的核心步骤的代码,可能由于版本的原因(下面的代码基于 2.6.1)代码会有所差异但是核心思想不变。

  1. public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
  2. //export invoker
  3. final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
  4. URL registryUrl = getRegistryUrl(originInvoker);
  5. //registry provider
  6. final Registry registry = getRegistry(originInvoker);
  7. final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
  8. //to judge to delay publish whether or not
  9. boolean register = registedProviderUrl.getParameter("register", true);
  10. ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);
  11. if (register) {
  12. register(registryUrl, registedProviderUrl);
  13. ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
  14. }
  15. // Subscribe the override data
  16. // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover.
  17. final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
  18. final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
  19. overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
  20. registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
  21. //Ensure that a new exporter instance is returned every time export
  22. return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registedProviderUrl);
  23. }

在之前的文章中分析了 dubbo 通过 netty 暴露服务,然后获取到 zookeeper 注册中心 — ZookeeperRegistry。下面就是把 Provider (服务提供者)的信息注册到注册中心上。这样 Consumer (服务消费者)就可以通过 Registry (注册中心)获取到 Provider 的信息。这样就解耦了 Provider 与 Consumer,并且可以通过 Registry 来修改路由规则、权重等控制。这样就达到的服务的治理。下面我们就来讲解一下,服务的注册与订阅。

2、ZookeeperRegistry#register

这个就是把服务信息注册到 Zookeeper 上面去。

1、AbstractRegistry#register添加注册Set<URL> registered(已注册的URL),用于失败重试。
2、ZookeeperRegistry#doRegister通过上篇 blog 讲解的获取到的 ZookeeperClient 把服务信息注册到 Zookeeper 上。此时的 URL 为:

  1. dubbo://192.168.75.1:20880/com.alibaba.dubbo.demo.DemoService?
  2. anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&
  3. interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=2076&
  4. side=provider&timestamp=1522237459900

dubbo 会基于这个 URL 生成一个新的URL,生成规则为:

  1. "/dubbo/" + url里面的interface的值 + "/providers/" + URL.encode(url)

生成后的值 :

  1. /dubbo/com.alibaba.dubbo.demo.DemoService/providers/
  2. dubbo%3A%2F%2F192.168.75.1%3A20880%2Fcom.alibaba.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26dubbo%3D2.0.0%26generic%3Dfalse%26interface%3Dcom.alibaba.dubbo.demo.DemoService%26methods%3DsayHello%26pid%3D2076%26side%3Dprovider%26timestamp%3D1522237459900

然后在 Zookeeper 上面把 /dubbo/com.alibaba.dubbo.demo.DemoService/providers/ 创建成持久化节点,而后面部分的 URL 就会创建成临时节点。

把服务提供者信息创建成临时节点的好处就是如果当前服务挂掉,这个节点就会自动删除。这样失效服务就可以自动剔除。

Zookeeper 持久化节点 和临时节点有什么区别? 持久化节点:一旦被创建,触发主动删除掉,否则就一直存储在ZK里面。 临时节点:与客户端会话绑定,一旦客户端会话失效,这个客户端端所创建的所有临时节点都会被删除。

3、ZookeeperRegistry#subscribe

通过订阅 Zookeeper 上面的的节点信息变更, 可以通过 dubbo-admin来修改服务路由规则、权重等。

1、创建一个 NotifyListener 实例 OverrideListener, 当收到服务变更通知时触发。
2、在 Zookeeper 注册中心创建持久化节点/dubbo/com.alibaba.dubbo.demo.DemoService/configurators,用于接收 dubbo-admin这个客户端上对于集群的服务治理。

  1. ZookeeperRegistry#doSubscribe {
  2. zkClient.create(path, false)
  3. }

3、启动加入订阅/dubbo/com.alibaba.dubbo.demo.DemoService/configurators,如果该节点信息发生改变,就会交给FailbackRegistry.notify处理。

  1. ZookeeperRegistry#doSubscribe {
  2. zkClient.addChildListener(path, zkListener)
  3. }

4、FailbackRegistry#notify

1、把服务端的注册 url 信息更新到本地缓存(AbstractRegistry#saveProperties),以Window为例:

  1. C:\Users\Carl\.dubbo\dubbo-registry-10.65.209.12.cache

2、调用传入的OverrideListener#notify,如果修改了 URL 信息,调用RegistryProtocol.this.doChangeLocalExport(originInvoker, newUrl)重新暴露当前服务。

前面二篇加上这一篇就是 dubbo 远程服务暴露的整个过程。下面这张图片就是 dubbo 进行服务暴露、服务远程调用添加 dubbo monitor 后 Zookeeper 上面 dubbo 节点的结构图。

Dubbo源码(五) 服务发布(下) - 图7

关于 zookeeper在Dubbo中扮演了一个什么角色,起到了什么作用啊?,(见下)大家可以看一下这篇知乎上面的问答。

流程:

1.服务提供者启动时向/dubbo/com.foo.BarService/providers目录下写入URL 2.服务消费者启动时订阅/dubbo/com.foo.BarService/providers目录下的URL向/dubbo/com.foo.BarService/consumers目录下写入自己的URL

3.监控中心启动时订阅/dubbo/com.foo.BarService目录下的所有提供者和消费者URL

支持以下功能:

1.当提供者出现断电等异常停机时,注册中心能自动删除提供者信息。

2.当注册中心重启时,能自动恢复注册数据,以及订阅请求。

3.当会话过期时,能自动恢复注册数据,以及订阅请求。

4.当设置时,记录失败注册和订阅请求,后台定时重试。

5.可通过设置zookeeper登录信息。

6.可通过设置zookeeper的根节点,不设置将使用无根树。

7.支持号通配符<dubbo:reference group=”“ version=”*” />,可订阅服务的所有分组和所有版本的提供者。

注意的是阿里内部并没有采用Zookeeper做为注册中心,而是使用自己实现的基于数据库的注册中心,即:Zookeeper注册中心并没有在阿里内部长时间运行的可靠性保障,此Zookeeper桥接实现只为开源版本提供,其可靠性依赖于Zookeeper本身的可靠性。