【Dubbo】Dubbo 源码分析

Dubbo 框架设计

image-20210826221319345

该图描述了服务注册中心、服务提供方、服务消费方、服务监控中心之间的调用关系:

  • 服务提供者(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调用远程服务。
  • 服务消费者在进行服务调用时将进行以下步骤:
    • 使用服务的代理对象调用方法时将逐层调用其内嵌套的多个不同功能的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:服务消费者向注册中心订阅所有服务提供者的信息
  • 蓝色虚线代表异步执行,当注册中心发现服务提供者发生改变时,会通知服务消费者该变化。服务提供者和服务消费者会定期向监控中心发送数据;
  • 蓝色实线代表服务消费者同步执行服务提供者的方法。

image-20210827133552495

https://dubbo.apache.org/zh/docsv2.7/dev/design/

Dubbo 整体设计图:

/dev-guide/images/dubbo-framework.jpg

Dubbo 组件标签解析

两种使用Dubbo的方式:

  • 基于xml配置文件方式
  • 基于注解方式(和Spring Boot整合)

基于 xml 配置文件方式

Spring容器在启动时将创建DubboNamespaceHandler组件,并调用其init()方法。该方法内将创建Dubbo组件的定义解析器registerBeanDefinitionParser,该类用于解析.xml文件中定义的所有Dubbo组件的信息,即根据配置文件中每个组件所赋予的属性值创建出相应的Dubbo组件。

image-20210825102509354

上图中registerBeanDefinitionParser解析的组件在xml文件中对应如下标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--1、指定当前服务/应用的名字(同样的服务名字相同,不要和别的服务同名)-->
<dubbo:application name="user-service-provider"></dubbo:application>

<!--2、指定注册中心的位置-->
<!--<dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry>-->
<dubbo:registry protocol="zookeeper" address="127.0.0.1:2181"></dubbo:registry>

<!--3、指定通信规则(通信协议? 服务端口):服务可以通过什么协议被消费者调用-->
<dubbo:protocol name="dubbo" port="20880"></dubbo:protocol>

<!--4、暴露服务让其他模块调用,ref指向服务的真正实现对象-->
<dubbo:service interface="com.zhao.gmail.service.UserService" ref="userServiceImpl"></dubbo:service>

<!--服务的实现-->
<bean id="userServiceImpl" class="com.zhao.gmail.service.impl.UserServiceImpl"></bean>

<!--dubbo-monitor-simple监控中心发现的配置-->
<!--使用registry协议,去注册中心自动监控-->
<dubbo:monitor protocol="registry"></dubbo:monitor>
<!--<dubbo:monitor address="127.0.0.1:7070"></dubbo:monitor>-->

<!--配置当前消费者的统一规则,当前所有的服务都不启动时检查-->
<dubbo:consumer check="false"></dubbo:consumer>

其中registerBeanDefinitionParser解析Dubbo标签,创建对应组件的具体代码:

image-20210825104559439

依次解析每个Dubbo标签,创建出对应的组件并注入到容器中,即完成了Dubbo组件的初始化工作。

基于注解方式(和Spring Boot整合)

若采用Spring Boot整合的方式,则无需手动配置xml文件。

使用方式:直接导入dubbo-starter,在application.properties配置属性,使用 @Service暴露服务,使用 @Reference 引用服务。

此时Dubbo组件的注册将交由Spring Boot的自动配置机制实现,具体原理见【Spring Boot】Spring Boot2 源码分析

服务暴露流程

服务暴露功能由ServiceBean组件实现,该类实现了Spring的多个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ServiceBean<T> extends ServiceConfig<T> implements 
InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware {

private static final long serialVersionUID = 213195494150089726L;
private static transient ApplicationContext SPRING_CONTEXT;
private final transient Service service;
private transient ApplicationContext applicationContext;
private transient String beanName;
private transient boolean supportedApplicationListener;

public ServiceBean() {
super();
this.service = null;
}

public ServiceBean(Service service) {
super(service);
this.service = service;
}
}

其中较为重要的接口作用:

  • InitializingBean:在ServiceBean组件对象创建完毕后将调用其实现的InitializingBean接口的 afterPropertiesSet() 方法
  • ApplicationListener<ContextRefreshedEvent>:Spring的监听器,负责监听IoC容器刷新完成事件。当IoC容器刷新完成,即容器中所有组件都创建完成后(此时已创建了Dubbo的其他配置类组件)调用其 onApplicationEvent() 方法

afterPropertiesSet() 方法

InitializingBean接口的 afterPropertiesSet() 方法用于将Dubbo相关的组件保存到当前ServiceBean组件里,例如ProviderApplicationModuleRegistriesMonitor等。这样ServiceBean就可以在后续获取到注册中心、服务协议等信息。

image-20210825135725282

onApplicationEvent() 方法

ApplicationListener<ContextRefreshedEvent> 接口的 onApplicationEvent() 方法会在IoC容器完成刷新后调用(响应ContextRefreshedEvent事件),其内调用了 export() 方法进行服务暴露,即将服务信息(如接口名,方法名,方法参数列表等)暴露给注册中心ZooKeeper:

image-20210825135831748

该方法经过多层方法栈后将调用 doExportUrls() 方法暴露URL到注册中心:

image-20210825140131870

该方法的作用:

  • 读取注册中心(ZooKeeper)的URL地址(可能有多个注册中心组成集群)
  • 使用配置文件中指定的协议暴露当前服务(可能有多个协议,即可以配置多个dubbo_protocol标签)。例如将当前服务的接口名、方法名等信息注册到注册中心ZooKeeper中。

image-20210825140558697

上述注册中心和协议分别为我们在配置文件中指定的内容:

1
2
3
4
5
6
<!--2、指定注册中心的位置,可以有多个注册中心组成集群-->
<!--<dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry>-->
<dubbo:registry protocol="zookeeper" address="127.0.0.1:2181"></dubbo:registry>

<!--3、指定通信规则(通信协议? 服务端口):服务可以通过什么协议被消费者调用。可以有多个协议、端口号-->
<dubbo:protocol name="dubbo" port="20880"></dubbo:protocol>

进入 doExportUrlsFor1Protocol() 方法后:

image-20210825161725034

首先将要暴露的服务实现类接口注册中心的URL等信息包装成一个执行器invoker,该执行器内含有该服务的接口名,方法名,方法参数列表等参数。后续在接收到客户端调用服务时即可使用该执行器即可调用该服务的内容。

接着再使用协议对象protocol暴露该执行器并保存生成的导出器,该export()暴露方法共做三件事:

  • 本地暴露:创建Netty服务端NettyServer,启动并监听配置文件中指定的Dubbo服务端口20880,等待消费者客户端发送远程调用当前服务的请求;
  • 注册服务信息到本地缓存:将服务信息注册到提供者注册表(本地缓存Map)中;
  • 注册服务信息到注册中心:向注册中心注册自己提供的服务信息,例如在ZooKeeper的Dubbo节点下创建提供的服务节点,该节点包含该服务的接口名、Provider服务端(通常为NettyServer)的URL等信息;服务信息将被保存成Map结构:服务名 : List<URL>

Dubbo支持的所有协议:

image-20210825152318380

Dubbo基于自己独特的SPI机制获取到当前项目满足的协议对象protocol,其将根据当前工程导入的依赖判断使用哪种协议:

image-20210825163420576

默认情况下上文中的protocol对象为RegistryProtocol类型(注册协议),该协议的作用为:将服务信息注册到注册中心。

同时该协议内部又调用了DubboProtocol协议的export()方法,该协议的作用为:在底层创建Netty服务端NettyServer,启动并监听配置文件中指定的端口20880。待确定)这样消费者客户端就可以通过Netty连接到提供者的服务端口,发送要调用的服务信息,从而远程调用服务端的代理对象invoker执行方法

即注册协议RegistryProtocol默认会调用Dubbo协议DubboProtocol,先创建NettyServer绑定端口并开启监听,再将服务信息注册到注册中心。


进入protocol对象(RegistryProtocol类型)的 export() 方法:

image-20210826203228308

下面分别介绍上述三个关键方法:

1. doLocalExport():启动Netty服务端并绑定服务端口

该方法内将首先调用 doLocalExport() 方法进行本地暴露,即使用DubboProtocol协议的 export() 方法在底层创建Netty服务端NettyServer,启动并监听配置文件中指定的Dubbo服务端口20880。

1
2
<!--3、指定通信规则(通信协议? 服务端口),可以有多个协议、端口号-->
<dubbo:protocol name="dubbo" port="20880"></dubbo:protocol>

doLocalExport() 方法内将调用DubboProtocol协议的 export() 方法:

image-20210825164256973

image-20210825165344016

2. registerProvider() :注册服务信息到本地缓存

该方法用于将服务信息注册到本地缓存的Map中。进入 registerProvider() 方法:

image-20210826171112848

  1. 首先将执行器invoker(在上文中包装得到,包含要暴露的服务实现类接口注册中心的URL等信息)、注册URL和服务URL等信息包装成一个ProviderInvokerWrapper对象wrapperInvoker
  2. 接着从本地缓存的执行器包装类集合providerInvokersMap类型)里取出当前服务对应的执行器包装类Set集合(该Map的存储结构为服务名称:服务执行器包装类集合);
  3. 若返回的执行器包装类Set集合为空,说明该服务还未注册,则在Map中新建一组数据,存储当前服务名与其对应的服务执行器包装类wrapperInvoker
  4. 若返回的执行器包装类Set集合不为空,则获取该服务对应的Set集合,将当前的wrapperInvoker添加进去

providerInvokers

image-20210826172358632

其中存储的服务名称包含以下信息:

  • 接口名
  • Group
  • Version

image-20210826172946074


3. register():注册服务信息到注册中心

register() 方法用于将服务URL信息(包含暴露的Netty服务端口号、接口名、方法参数等信息)注册到注册中心:

image-20210826195943484

  • 根据注册中心URL获取到对应的注册器(例如ZooKeeperRegistry
  • 调用该注册器的register()方法,将提供者URL信息注册到注册中心

此时注册中心就记录了每个服务与其对应的URL(该服务所在的Netty服务端地址和端口号),后续消费者访问该URL即可访问到了绑定该服务的Netty服务端。


其中URL类内包含以下信息:

image-20210826200137647

image-20210826200443713

URL示例:dubbo://192.168.1.101:20880/com.zhao.gmall.service.UserService?anyhost=true&application=order-service-consumer&…version=1.0…


总结:服务暴露的整个流程图:

dubbo-服务暴露

服务引用流程

服务引用功能由ReferenceBean组件实现,该类实现了Spring的多个接口:

image-20210827100909250

getObject()方法中的get()方法用于获取引用对象的代理对象ref(基于Java JDK 动态代理机制),该代理对象内包含了能够远程调用当前服务的执行器invokerinvoker创建过程见下文分析):

image-20210827101104100

image-20210827101027069

在上图中的init()方法中将根据map对象创建出对应的代理对象ref

image-20210827101320120

map对象保存了服务的信息,例如:

  • side=consumer:代表是消费者端
  • register.ip=192.168.1.101:消费者的ip地址(用于向注册中心注册消费者的ip地址,从而在后续监听到服务信息变更事件后通知消费者)
  • method=getUserAddressList:引用服务的方法名
  • default.check=false:代表当前所有的服务都不启动时检查
  • pid=47980:进程id
  • interface=com.zhao.gmall.service.UserService:服务的接口名
  • version=*:服务版本号

createProxy(map)方法内将调用refprotocol远程引用服务:

image-20210827102408840

使用refprotocol.ref()进行服务引用的流程与在服务暴露中使用protocol.export()进行服务暴露的流程相似,共有以下主要流程:

  • 获取注册中心地址并据此创建ZooKeeper注册中心类registry
  • 根据注册中心地址订阅当前服务的信息(只订阅当前工程引用的服务,其他服务不订阅),获取到服务URL信息后,创建NettyClient客户端与其进行通讯,并创建invoker(包含了远程服务的信息,后续使用其进行远程服务调用
  • invoker保存到消费者的本地缓存Map中,这样即使ZooKeeper宕机本地工程也能使用缓存中的invoker调用远程服务

下面逐一分析上述过程的具体代码:

进入refprotocol.ref()方法,此时调用的是RegistryProtocol类型(与服务暴露时顺序相同):

image-20210827104447523

doRefer() 方法内(仍为RegistryProtocol协议的方法):

image-20210827110250618

subscribe()订阅时将调用DubboProtocolrefer()方法:

image-20210827105657596

该方法内将创建一个NettyClient与远程服务的NettyServer进行连接,并返回一个invoker(包含目标服务的URL等信息)。后续的服务调用将使用该invoker对象进行服务的远程调用。

同时该invoker将被保存到消费者的本地缓存Map中,这样即使ZooKeeper宕机本地工程也能使用缓存中的invoker调用远程服务。

总结:服务引用的整个流程:

dubbo-服务引用

服务调用流程

1
2
3
4
5
6
7
8
9
10
11
public class ConsumerApplication {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("consumer.xml");
OrderService orderService = applicationContext.getBean(OrderService.class);

//调用方法查询出数据
orderService.initOrder("1");
System.out.println("调用完成...");
System.in.read();
}
}

客户端进行服务调用时,将使用Java JDK的动态代理机制创建出的目标服务类的代理对象orderService(创建过程见服务引用流程)。调用该代理对象,逐层调用其内嵌套的多个不同功能的invoker,直至创建出Netty客户端NettyClient与服务端NettyServer进行通讯,将要调用的接口名、方法名和方法参数等信息经过编码序列化后发送给NettyServer,服务端收到该请求后创建目标服务对应的代理对象执行服务方法,待其执行完毕后再将方法返回结果发送给客户端,从而完成远程调用服务方法。

image-20210826222218361

上图中的invokerMockClusterInvoker类型内又嵌套了许多其他类型的invoker

image-20210826223808769

invoker将逐层调用其内嵌套的每个invoker,执行其相应的功能。

整个服务调用流程图:

/dev-guide/images/dubbo-extension.jpg

下面按照从下到上的顺序(客户端到服务端)分析上图:

  • 首先客户端获取到服务的代理对象,并调用其invoke()方法:
  • 如果配置了容错缓存等功能则经过一层过滤器Filter
  • Cluster中选出一个InvokerCluster内封装了多个Invoker):如果有多个Invoker,则使用负载均衡机制选择其中的一个Invoker,若其执行失败(例如超时),则再换一个InvokerInvoker中保存了从ZooKeeper注册中心Registry中获取到的服务URL信息)
  • 选出Invoker后再经过一些Filter,例如计数、监控等
  • 调用Dubbo协议的Invoker,其底层创建了一个NettyClient,该客户端连接服务端NettyServer,将要调用的接口名、方法名、方法参数、版本号等信息经过编码和序列化后发送给NettyServer
  • 服务端收到该请求后创建目标服务对应的代理对象执行服务方法,待其执行完毕后再基于NettyChannel将方法返回结果发送给客户端,从而完成远程调用服务方法。