Apache Dubbo笔记

Scroll Down

概述

Dubbo构建的分布式系统架构中各个组件服务的作用以及相互关系。

  • Provider为服务提供者集群,服务提供者负责暴露提供的服务,并将服务注册到服务注册中心。
  • Consumer为服务消费者集群,服务消费者通过RPC远程调用服务提供者提供的服务。
  • Registry负责服务注册与发现。
  • Monitor为监控中心,统计服务的调用次数和调用时间。
    以上各个组件的调用关系如下:
  • 服务提供方在启动时会将自己注册到服务注册中心。
  • 服务消费方在启动时会去服务注册中心订阅自己需要的服务的地址列表,然后服务注册中心异步把消费方需要的服务接口的提供者的地址列表返回给服务消费方,服务消费方根据路由规则和设置的负载均衡算法选择一个服务提供者IP进行调用。
  • 监控平台主要用来统计服务的调用次数和调用耗时,即服务消费者和提供者在内存中累计调用服务的次数和耗时,并每分钟定时发送一次统计数据到监控中心,监控中心则使用数据绘制图表来显示。监控平台不是分布式系统必须的,但是这些数据有助于系统的运维和调优。服务提供者和消费者可以直接配置监控平台的地址,也可以通过服务注册中心获取。

Dubbo框架内核原理剖析

Dubbo分层架构概述

  • Service和Config层为API接口层,是为了让Dubbo使用方方便地发布和引用服务;对于服务提供方来说需要实现服务接口,然后使用ServiceConfig API来发布该服务;对于服务消费方来说需要使用ReferenceConfig对服务接口进行代理Dubbo服务发布与引用方可以直接初始化配置类,也可以通过Spring配置自动生成配置类。
  • 其他各层均为SPI(Service Provider Interface,服务提供者接口)层,SPI意味着下面各层都是组件化的,是可以被替换的,这也是Dubbo设计比较好的一点。Dubbo增强了JDK中提供的标准SPI功能,在Dubbo中除了Service和Config层,其他各层都是通过扩展点接口来提供服务的;Dubbo增强了SPI增加了对扩展点IOC和AOP的支持,一个扩展点可以直接使用setter()方法注入其他扩展点,并且不会一次性实例化扩展点的所有实现类,这就避免了当扩展点实现类初始化很耗时,但当前还没用上它的功能时仍进行加载实例化这种浪费资源的情况;增强的SPI是在具体用某一个实现类的时候才对具体实现类进行实例化。
  • Proxy服务代理层:该层主要是对服务消费端使用的接口进行代理,把本地调用透明地转换为远程调用;另外对服务提供方的服务实现类进行代理,把服务实现类转换为Wrapper类,这是为了减少反射的调用。Proxy层的SPI扩展接口为ProxyFactory,Dubbo提供的实现类主要有JavassistProxyFactory(默认使用)和JdkProxyFactory,用户可以实现ProxyFactory SPI接口,自定义代理服务层的实现。
  • Registry服务注册中心层:服务提供者启动时会把服务注册到服务注册中心,消费者启动时会去服务注册中心获取服务提供者的地址列表,Registry层主要功能是封装服务地址的注册于发现逻辑,扩展接口Registry对应的扩展实现为ZookeeperRegistry、RedisRegistry、MulticastRegistry、DubboRegistry等。扩展接口RegistryFactory对应的扩展接口实现为DubboRegistryFactory、RedisRegistryFactory、MulticastRegistryFactory、ZookeeperRegistryFactory。另外,该层扩展接口Directory实现类有RegistryDirectory、StaticDirectory,用来透明地把Invoker列表转换为一个Invoker:用户可以实现该层的一系列扩展接口,自定义该层的服务实现。
  • Cluster路由层:封装多个服务提供者的路由规则、负载均衡、集群容错的实现,并桥接服务注册中心;扩展接口Cluster对应的实现类有FailoverCluster(失败重试)、FailbackCluster(失败自动恢复)、FailfastCluster(快速失败)、FailsafeCluster(失败安全)、ForkingCluster(并行调用)等;负载均衡扩展接口LoadBalance对应的实现类为RandomLoadBalance(随机)、RoundRobinLoadBalance(轮询)、LeastActiveLoadBalance(最小活跃数)、ConsistentHashLoadBalance(一致性Hash)等。用户可以实现该层的一系列扩展接口,自定义集群容错和负载均衡策略。
  • Monitor监控层:用来统计RPC调用次数和调用耗时时间,扩展接口为MonitorFactory,对应的实现类为DubboMonitorFactory。用户可以实现该层的MonitorFactory扩展接口,实现自动逸监控统计策略。
  • Protocol远程调用层:封装RPC调用逻辑,扩展接口为Protocol,对应实现由RegistryProtocol、DubboProtocol、InjvmProtocol等。
  • Exchange信息交换层:封装请求响应模式,同步转异步,扩展接口为Exchanger,对应的扩展实现由HeaderExchanger等。
  • Transport网络传输层:Mina和Netty抽象为统一接口。扩展接口为Channel,对应的实现由NettyChannel(默认)、MinaChannel等;扩展接口为Transporter对应的实现类由GrizzlyTransporter、MinaTransporter、NettyTransporter(默认实现);扩展接口Codec2对应的实现类有DubboCodec、ThriftCodec等。
  • Serialize数据序列化层:提供可以服用的一些工具,扩展接口为Serialization,对应的扩展实现由DubboSerialization、FastJsonSerialization、Hessian2Serialization、JavaSerialization等,扩展接口ThreadPool对应的扩展是现有FixedThreadPool、CacheThreadPool、LimitedThreadPool等。

综上可知,Dubbo的分层架构使得Dubbo每层的功能都是可被替换的,这使得Dubbo的扩展性极强。

Dubbo远程调用细节

服务提供者暴露一个服务的概要过程

首先,ServiceConfig类引用对外提供服务的实现类ref,然后通过ProxyFactory接口的扩展实现类的getInvoker()方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成了具体服务到Invoker的转化。接下来就是Invoker转换到Exporter的过程。Dubbo协议的Invoker转为Exporter发生在DubboProtocol类的export()方法中,Dubbo处理服务暴露的关键就在Invoker转换到Exporter的过程中,在这个过程中会先启动Netty Server监听服务连接,然后将服务注册到服务注册中心。

服务消费者消费一个服务的概要过程

首先,ReferenceConfig类的init()方法调用Protocol扩展接口实现类的refer()方法生成Invoker实例,这是服务消费的关键。接下来Invoker转换为客户端需要的接口。
Dubbo协议的Invoker转换为客户端需要的接口,发生在ProxyFactory接口的扩展实现类的getProxy()方法中,它主要是使用代理对服务接口的调用转换为对Invoker的调用。

Dubbo的适配器原理

Dubbo为每个功能点提供了一个SPI扩展接口,Dubbo框架在使用扩展点功能的时候对接口进行依赖的,而一个扩展接口对应了一系列的扩展实现类。那么如何选择使用哪一个扩展接口的实现类呢?其实这是使用适配器模式来做的。
Dubbo会使用动态编译技术为接口Protocol生成一个适配器类Protocol$Adaptive的对象实例,在Dubbo框架中需要使用Protocol的实例时,实际上就是使用Protocol$Adaptive的对象实例来获取具体的SPI实现类的。
需要注意的是,在Dubbo中URL是一个核心概念,Dubbo框架把所需的参数都拼接到了URL对象里,总结一下就是,适配器类Protocol$Adaptive会根据传递的协议参数的不同,加载不同的Protocol的SPI实现。
其实在Dubbo框架中,框架会给每个SPI扩展接口动态生成一个对应的适配器类,并根据参数来使用增强SPI以选择不同的SPI实现。比如扩展接口ProxyFactory的适配器类为ProxyFactory$Adaaptive,其根据参数proxy来选择使用JdkProxyFactory还是使用JavassistProxyFactory做代理工厂;扩展接口Registry的适配器类Registry$Adaptive则根据参数register来决定使用ZookeeperRegistry、RedisRegistry、MulticastRegistry、DubboRegistry中的哪一个作为服务注册中心等。

Dubbo的动态编译原理

众所周知,Java程序要想运行首先需要使用javac把源代码编译为class字节码文件,然后使用JVM把class字节码文件加载到内存创建Class对象后,使用Class对象创建对象实例。正常情况下我们是把所有源文件静态编译为字节码文件,然后由JVM统一加载,而动态编译则是在JVM进程运行时把源文件编译为字节码文件,然后使用字节码文件创建对象实例。
在Dubbo框架中框架会把每个SPI扩展接口动态生成一个对应的适配器类,使用了动态编译技术,在Dubbo中提供了一个Compiler的SPI。
Dubbo提供的Compiler的实现有JavassistCompiler(默认实现)和JdkCompiler两种。
总结一下就是,Dubbo框架会为每个扩展接口生成其对应的适配器类的源码,然后选择具体的动态编译类的扩展实现对源码进行编译以生成适配器类的Class对象,然后就可以调用Class对象的newInstance()方法生成扩展接口对应的适配器类的实例。

Dubbo增强SPI

JDK标准SPI

Dubbo增强的SPI功能是从JDK标准SPI演化而来的,所以有必要先讲讲标准的SPI的原理。
JDK中的SPI是面向接口编程的,服务规则提供者会在JRE的核心API里提供服务访问接口,而具体的实现则是由开发商提供。
Java核心API(比如rt.jar包)是使用Bootstrap ClassLoader类加载器加载的,而用户提供的Jar包是由AppClassLoader加载的。如果一个类由类加载器加载,那么这个类依赖的类也是由相同的类加载器加载的。
用来搜索开发商提供的SPI扩展实现类的API类(ServiceLoader)是使用Bootstrap ClassLoader加载的,那么ServiceLoader里面依赖的类应该也是由Bootstrap ClassLoader加载的。而上面说了用户提供的包含SPI实现类的Jar包是由AppClassLoader加载的,所以这就需要一种违反双亲委派模型的方法,线程上下文加载器ContextClassLoader就是用来解决这个问题的。

增强SPI原理

Dubbo的扩展点加载机制是基于JDK标准的SPI扩展机制增强而来的,Dubbo解决了JDK标准SPI的以下问题:

  • JDK标准的SPI会一次性实例化扩展点的所有实现,如果有些扩展实现初始化很耗时,但又没用上,那么加载就很浪费资源。
  • 如果扩展点加载失败,是不会友好地向用户通知具体异常的。比如:对于JDK标准的ScriptEngine类加载失败,那么这个加载失败原因就被隐藏了,当用户执行致Ruby脚本时,会报空指针异常,而不是报Ruby ScriptEngine不存在。
  • 增加了对扩展点IOC和AOP的支持,一个扩展点可以直接使用setter()方法注入其他扩展点,也可以对扩展点使用Wrapper类进行功能增强。

远程服务发布与引用流程剖析

Dubbo服务发布端启动流程剖析

服务提供方需要使用ServiceConfig API发布服务,具体来说就是执行export()方法来激活发布服务。Dubbo的延迟发布是通过使用ScheduledExecutorService来实现的,可以通过调用ServiceConfig的setDelay(Integer delay)方法来设置延迟发布时间。如果没有设置延迟时间,则直接调用doExport()方法发布服务;如果设置了延迟发布,则等时间过期后调用doExport()方法来发布服务。
然后根据ServiceConfig里面的属性进行合法性检查,通过调用loadRegistries()方法加载所有的服务注册中心对象,在Dubbo中,一个服务可以被注册到多个服务注册中心。
在doExportUrlsFor1Protocol()方法内部首先把参数封装为URL(在Dubbo里会把所有参数封装到一个URL里),然后具体执行服务导出。
这里首先把服务实现类转换为Wrapper类,是为了减少反射的使用,这里返回的是AbstractProxyInvoker对象,其内部重写了doInvoker()方法,并委托给Wrapper实现具体功能。
另外,当执行protocol.export(wrapperInvoker)方法的时候,实际调用了Protocol的适配器类Protocol$Adaptive的export()方法。如果为远程服务暴露,则其内部根据URL中Protocol的实现类RegistryProtocol。
注册的时候,首先是获取当前机器地址信息并作为key,然后判断是否为服务提供端。如果是,则以此key为key查看缓存serverMap中是否有对应的Server,如果没有则调用createServer()方法来创建,否则返回缓存中的value。由于机器的ip:port是唯一的,所以多个不同服务启动时只有第一个会被创建,后面的服务都是直接从缓存中返回的。

Directory目录与Router路由服务

Directory目录

Directory代表了多个invoker(对于消费端来说,每个invoker代表了一个服务提供者),其内部维护着一个List,并且这个List的内容是动态变化的,比如当服务提供者集群新增或者减少机器时,服务注册中心就会推送当前服务提供者的地址列表,然后Directory中的List就会根据服务提供者地址列表相应变化。
在Dubbo中,接口Directory的实现有RegistryDirectory和StaticDirectory两种,其中前者管理的invoker列表是根据服务注册中心的推送变化而变化的,而后者是当消费端使用了多个注册中心时,其把所有服务注册中心的invoker列表汇集到了一个invoker列表中。

RegistryDirectory的创建

RegistryDirectory是在服务消费端启动时创建的。
ReferenceConfig代表了一个要消费的服务的配置对象,调用ReferenceConfig的get()方法,就意味着要创建一个对服务提供方的远程调用代理。
在创建对远程服务提供者的代理时,第一步是调用RegistryProtocol类的refer()方法,由于RegistryProtocol是一个SPI,所以这里通过其适配器类的Protocol$Adaptive进行间接调用的。另外,这里的ProtocolListenerWrapper、QosProtocolWrapper和ProtocolFilterWrapper是对RegistryProtocol类的功能的增强。

RegistryDirectory中invoker列表的更新

创建完RegistryDirectory后,调用了其subscribe()方法,这里假设使用的服务注册中心为Zookeeper,这样就会去Zookeeper订阅需要调用的服务提供者的地址列表,然后添加了一个监听器,当Zookeeper服务端发现服务提供者地址列表发生变化后,会将地址列表推送到服务消费端,然后zkClient会回调该监听器的notify()方法。
在设置完监听器后,同步返回了订阅的服务地址列表、路由规则、配置信息等,然后同步调用了RegistryDirectory的notify()方法;
从Zookeeper返回的服务提供者的信息里获取对应的路由规则,并保存到RouterChain里,这个路由规则是通过管理控制台进行配置的。
根据服务降级信息,重写URL并保存到overrideDirectoryUrl中,然后把服务提供者的URL列表转换为invoker列表,并保存到RouterChain里:
另外,RouterChain里也保存了可用服务提供者对应的invokers列表和路由规则信息,当服务消费者的集群容错策略要获取可用服务提供者对应的invoker列表时,会调用RouterChain的route()方法,其内部根据路由规则信息和invokers列表来提供服务。
总结一下就是,在服务消费端应用中,每个需要消费的服务都被包装为ReferenceConfig,在应用启动时会调用每个服务对应的ReferenceConfig的get()方法,然后会为每个服务创建一个自己的RegistryDirectory对象,每个RegistryDirectory管理该服务提供者的地址列表、路由规则、动态配置等信息,当服务提供者的信息发生变化时,RegistryDirectory会动态地得到变化通知,并自动更新。

Dubbo消费端服务mock与服务降级策略原理

当服务消费者启动时,会订阅子树中的信息,比如服务提供者列表、Routers、Configurators等信息。
当服务消费者发起远程调用时,会看是否设置了force:return降级策略,如果设置了则不发起远程调用并直接返回mock值,否则发起远程调用。当远程调用接口OK时,直接返回远程调用返回的结果;如果远程调用失败了,则看当前是否设置了fail:return的降级策略,如果设置了,则直接返回mock值,否则返回调用远程服务失败的具体原因。