【Dubbo】Dubbo 源码分析
Dubbo 框架设计
该图描述了服务注册中心、服务提供方、服务消费方、服务监控中心之间的调用关系:
- 服务提供者(Provider):暴露服务的服务提供方。服务提供者在启动时完成以下步骤进行服务暴露:
- 本地暴露:创建Netty服务端NettyServer,启动并监听配置文件中指定的Dubbo服务端口20880,等待消费者客户端发送远程调用当前服务的请求;
- 注册服务信息到本地缓存:将服务信息注册到提供者注册表(本地缓存
Map
)中; - 注册服务信息到注册中心:向注册中心注册自己提供的服务信息,例如在ZooKeeper的Dubbo节点下创建提供的服务节点,该节点包含该服务的接口名、Provider服务端(通常为NettyServer)的URL等信息;服务信息将被保存成
Map
结构:服务名 : List<URL>
。
- 服务消费者(Consumer): 调用远程服务的服务消费方。服务消费者在启动时,完成以下步骤进行服务引用:
- 获取注册中心地址并据此创建ZooKeeper注册中心类
registry
; - 根据服务名称从注册中心
registry
订阅服务的URL列表List<URL>
(只订阅当前工程引用的服务,其他服务不订阅); - 获取到服务URL信息后,创建
NettyClient
客户端与其进行通讯,并创建invoker
(包含了远程服务的信息,后续使用其进行远程服务调用); - 将
invoker
保存到消费者的本地缓存Map
中,这样即使ZooKeeper宕机本地工程也能使用缓存中的invoker
调用远程服务。
- 获取注册中心地址并据此创建ZooKeeper注册中心类
- 服务消费者在进行服务调用时将进行以下步骤:
- 使用服务的代理对象调用方法时将逐层调用其内嵌套的多个不同功能的
invoker
,基于软负载均衡算法,从服务URL列表中选择其中的一台提供者进行远程调用; - 多层
invoker
调用后将创建出Netty客户端NettyClient
与服务端NettyServer
进行通讯; - 将要调用的接口名、方法名和方法参数等信息经过编码序列化后发送给
NettyServer
,服务端收到该请求后创建目标服务对应的代理对象执行服务方法,待其执行完毕后再将方法的返回结果发送给客户端; - 远程调用原理:选择其中的某一个URL作为目标服务端
NettyServer
,创建NettyClient
与其进行通讯,将要执行的服务接口名、方法名、方法参数类型列表与方法值列表发送给NettyServer,令其创建代理对象调用该方法,从而实现远程调用目标方法。如果选中的服务器调用失败,再选另一台调用。
- 使用服务的代理对象调用方法时将逐层调用其内嵌套的多个不同功能的
- 注册中心(Registry):
- 注册中心负责保存服务提供者发来的服务信息,同时在消费者发来订阅请求时返回服务提供者地址列表(包含ip/port等信息)给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 当注册中心检测到有Dubbo提供者宕机时,将从节点中删除该提供者信息(超时检测机制),同时通知所有消费者节点信息发生改变,修改消费者本地缓存中的服务数据(使用ZooKeeper里的监听机制,在消费者从注册中心订阅时就绑定监听了注册中心服务节点信息改变事件)
- 监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
- 框架容器(Container):Dubbo容器(Spring容器)。
图中线条代表含义:
- 紫色虚线代表Dubbo容器启动时执行的步骤,先后为:
0.start
:启动Dubbo容器1.register
:服务提供者在注册中心内注册信息(服务端的ip地址和端口号等信息)2.subscribe
:服务消费者向注册中心订阅所有服务提供者的信息
- 蓝色虚线代表异步执行,当注册中心发现服务提供者发生改变时,会通知服务消费者该变化。服务提供者和服务消费者会定期向监控中心发送数据;
- 蓝色实线代表服务消费者同步执行服务提供者的方法。
Dubbo 整体设计图:
Dubbo 组件标签解析
两种使用Dubbo的方式:
- 基于xml配置文件方式
- 基于注解方式(和Spring Boot整合)
基于 xml 配置文件方式
Spring容器在启动时将创建DubboNamespaceHandler组件,并调用其init()
方法。该方法内将创建Dubbo组件的定义解析器registerBeanDefinitionParser,该类用于解析.xml文件中定义的所有Dubbo组件的信息,即根据配置文件中每个组件所赋予的属性值创建出相应的Dubbo组件。
上图中registerBeanDefinitionParser解析的组件在xml文件中对应如下标签:
1 | <!--1、指定当前服务/应用的名字(同样的服务名字相同,不要和别的服务同名)--> |
其中registerBeanDefinitionParser解析Dubbo标签,创建对应组件的具体代码:
依次解析每个Dubbo标签,创建出对应的组件并注入到容器中,即完成了Dubbo组件的初始化工作。
基于注解方式(和Spring Boot整合)
若采用Spring Boot整合的方式,则无需手动配置xml文件。
使用方式:直接导入dubbo-starter
,在application.properties
配置属性,使用 @Service暴露服务,使用 @Reference 引用服务。
此时Dubbo组件的注册将交由Spring Boot的自动配置机制实现,具体原理见【Spring Boot】Spring Boot2 源码分析
服务暴露流程
服务暴露功能由ServiceBean组件实现,该类实现了Spring的多个接口:
1 | public class ServiceBean<T> extends ServiceConfig<T> implements |
其中较为重要的接口作用:
- InitializingBean:在
ServiceBean
组件对象创建完毕后将调用其实现的InitializingBean接口的afterPropertiesSet()
方法 - ApplicationListener<ContextRefreshedEvent>:Spring的监听器,负责监听IoC容器刷新完成事件。当IoC容器刷新完成,即容器中所有组件都创建完成后(此时已创建了Dubbo的其他配置类组件)调用其
onApplicationEvent()
方法
afterPropertiesSet() 方法
InitializingBean接口的 afterPropertiesSet()
方法用于将Dubbo相关的组件保存到当前ServiceBean
组件里,例如Provider
,Application
,Module
,Registries
,Monitor
等。这样ServiceBean
就可以在后续获取到注册中心、服务协议等信息。
onApplicationEvent() 方法
ApplicationListener<ContextRefreshedEvent> 接口的 onApplicationEvent() 方法会在IoC容器完成刷新后调用(响应ContextRefreshedEvent
事件),其内调用了 export() 方法进行服务暴露,即将服务信息(如接口名,方法名,方法参数列表等)暴露给注册中心ZooKeeper:
该方法经过多层方法栈后将调用 doExportUrls() 方法暴露URL到注册中心:
该方法的作用:
- 读取注册中心(ZooKeeper)的URL地址(可能有多个注册中心组成集群)
- 使用配置文件中指定的协议暴露当前服务(可能有多个协议,即可以配置多个
dubbo_protocol
标签)。例如将当前服务的接口名、方法名等信息注册到注册中心ZooKeeper中。
上述注册中心和协议分别为我们在配置文件中指定的内容:
1 | <!--2、指定注册中心的位置,可以有多个注册中心组成集群--> |
进入 doExportUrlsFor1Protocol() 方法后:
首先将要暴露的服务实现类、接口和注册中心的URL等信息包装成一个执行器invoker
,该执行器内含有该服务的接口名,方法名,方法参数列表等参数。后续在接收到客户端调用服务时即可使用该执行器即可调用该服务的内容。
接着再使用协议对象protocol
暴露该执行器并保存生成的导出器,该export()
暴露方法共做三件事:
- 本地暴露:创建Netty服务端NettyServer,启动并监听配置文件中指定的Dubbo服务端口20880,等待消费者客户端发送远程调用当前服务的请求;
- 注册服务信息到本地缓存:将服务信息注册到提供者注册表(本地缓存
Map
)中; - 注册服务信息到注册中心:向注册中心注册自己提供的服务信息,例如在ZooKeeper的Dubbo节点下创建提供的服务节点,该节点包含该服务的接口名、Provider服务端(通常为NettyServer)的URL等信息;服务信息将被保存成
Map
结构:服务名 : List<URL>
;
Dubbo支持的所有协议:
Dubbo基于自己独特的SPI机制获取到当前项目满足的协议对象protocol
,其将根据当前工程导入的依赖判断使用哪种协议:
默认情况下上文中的protocol
对象为RegistryProtocol类型(注册协议),该协议的作用为:将服务信息注册到注册中心。
同时该协议内部又调用了DubboProtocol协议的export()
方法,该协议的作用为:在底层创建Netty服务端NettyServer,启动并监听配置文件中指定的端口20880。待确定)这样消费者客户端就可以通过Netty连接到提供者的服务端口,发送要调用的服务信息,从而远程调用服务端的代理对象invoker执行方法
即注册协议RegistryProtocol默认会调用Dubbo协议DubboProtocol,先创建NettyServer绑定端口并开启监听,再将服务信息注册到注册中心。
进入protocol
对象(RegistryProtocol类型)的 export() 方法:
下面分别介绍上述三个关键方法:
1. doLocalExport():启动Netty服务端并绑定服务端口
该方法内将首先调用 doLocalExport() 方法进行本地暴露,即使用DubboProtocol协议的 export() 方法在底层创建Netty服务端NettyServer,启动并监听配置文件中指定的Dubbo服务端口20880。
1 | <!--3、指定通信规则(通信协议? 服务端口),可以有多个协议、端口号--> |
doLocalExport() 方法内将调用DubboProtocol协议的 export() 方法:
2. registerProvider() :注册服务信息到本地缓存
该方法用于将服务信息注册到本地缓存的Map
中。进入 registerProvider() 方法:
- 首先将执行器invoker(在上文中包装得到,包含要暴露的服务实现类、接口和注册中心的URL等信息)、注册URL和服务URL等信息包装成一个ProviderInvokerWrapper对象
wrapperInvoker
; - 接着从本地缓存的执行器包装类集合providerInvokers(
Map
类型)里取出当前服务对应的执行器包装类Set
集合(该Map
的存储结构为服务名称:服务执行器包装类集合); - 若返回的执行器包装类
Set
集合为空,说明该服务还未注册,则在Map
中新建一组数据,存储当前服务名与其对应的服务执行器包装类wrapperInvoker
; - 若返回的执行器包装类
Set
集合不为空,则获取该服务对应的Set集合,将当前的wrapperInvoker
添加进去
providerInvokers:
其中存储的服务名称包含以下信息:
- 接口名
- Group
- Version
3. register():注册服务信息到注册中心
register() 方法用于将服务URL信息(包含暴露的Netty服务端口号、接口名、方法参数等信息)注册到注册中心:
- 根据注册中心URL获取到对应的注册器(例如
ZooKeeperRegistry
) - 调用该注册器的
register()
方法,将提供者URL信息注册到注册中心
此时注册中心就记录了每个服务与其对应的URL(该服务所在的Netty服务端地址和端口号),后续消费者访问该URL即可访问到了绑定该服务的Netty服务端。
其中URL类内包含以下信息:
URL示例:dubbo://192.168.1.101:20880/com.zhao.gmall.service.UserService?anyhost=true&application=order-service-consumer&…version=1.0…
总结:服务暴露的整个流程图:
服务引用流程
服务引用功能由ReferenceBean组件实现,该类实现了Spring的多个接口:
其getObject()
方法中的get()
方法用于获取引用对象的代理对象ref
(基于Java JDK 动态代理机制),该代理对象内包含了能够远程调用当前服务的执行器invoker
(invoker
创建过程见下文分析):
在上图中的init()
方法中将根据map
对象创建出对应的代理对象ref
:
该map
对象保存了服务的信息,例如:
side=consumer
:代表是消费者端register.ip=192.168.1.101
:消费者的ip地址(用于向注册中心注册消费者的ip地址,从而在后续监听到服务信息变更事件后通知消费者)method=getUserAddressList
:引用服务的方法名default.check=false
:代表当前所有的服务都不启动时检查pid=47980
:进程idinterface=com.zhao.gmall.service.UserService
:服务的接口名version=*
:服务版本号
createProxy(map)
方法内将调用refprotocol
远程引用服务:
使用refprotocol.ref()
进行服务引用的流程与在服务暴露中使用protocol.export()
进行服务暴露的流程相似,共有以下主要流程:
- 获取注册中心地址并据此创建ZooKeeper注册中心类
registry
- 根据注册中心地址订阅当前服务的信息(只订阅当前工程引用的服务,其他服务不订阅),获取到服务URL信息后,创建
NettyClient
客户端与其进行通讯,并创建invoker
(包含了远程服务的信息,后续使用其进行远程服务调用) - 将
invoker
保存到消费者的本地缓存Map
中,这样即使ZooKeeper宕机本地工程也能使用缓存中的invoker
调用远程服务
下面逐一分析上述过程的具体代码:
进入refprotocol.ref()
方法,此时调用的是RegistryProtocol类型(与服务暴露时顺序相同):
doRefer()
方法内(仍为RegistryProtocol协议的方法):
在subscribe()
订阅时将调用DubboProtocol的refer()
方法:
该方法内将创建一个NettyClient
与远程服务的NettyServer
进行连接,并返回一个invoker
(包含目标服务的URL等信息)。后续的服务调用将使用该invoker
对象进行服务的远程调用。
同时该invoker
将被保存到消费者的本地缓存Map
中,这样即使ZooKeeper宕机本地工程也能使用缓存中的invoker
调用远程服务。
总结:服务引用的整个流程:
服务调用流程
1 | public class ConsumerApplication { |
客户端进行服务调用时,将使用Java JDK的动态代理机制创建出的目标服务类的代理对象orderService
(创建过程见服务引用流程)。调用该代理对象,逐层调用其内嵌套的多个不同功能的invoker
,直至创建出Netty客户端NettyClient
与服务端NettyServer
进行通讯,将要调用的接口名、方法名和方法参数等信息经过编码序列化后发送给NettyServer
,服务端收到该请求后创建目标服务对应的代理对象执行服务方法,待其执行完毕后再将方法返回结果发送给客户端,从而完成远程调用服务方法。
上图中的invoker
为MockClusterInvoker类型内又嵌套了许多其他类型的invoker
:
该invoker
将逐层调用其内嵌套的每个invoker
,执行其相应的功能。
整个服务调用流程图:
下面按照从下到上的顺序(客户端到服务端)分析上图:
- 首先客户端获取到服务的代理对象,并调用其
invoke()
方法: - 如果配置了容错缓存等功能则经过一层过滤器
Filter
- 从
Cluster
中选出一个Invoker
(Cluster
内封装了多个Invoker
):如果有多个Invoker
,则使用负载均衡机制选择其中的一个Invoker
,若其执行失败(例如超时),则再换一个Invoker
(Invoker
中保存了从ZooKeeper注册中心Registry
中获取到的服务URL信息) - 选出
Invoker
后再经过一些Filter
,例如计数、监控等 - 调用Dubbo协议的
Invoker
,其底层创建了一个NettyClient
,该客户端连接服务端NettyServer
,将要调用的接口名、方法名、方法参数、版本号等信息经过编码和序列化后发送给NettyServer
- 服务端收到该请求后创建目标服务对应的代理对象执行服务方法,待其执行完毕后再基于
NettyChannel
将方法返回结果发送给客户端,从而完成远程调用服务方法。