文章目录
- 1 Dubbo原理
- 1.1 Dubbo核心功能
- 1.2 Dubbo架构图
- 1.2.1 Dubbo基本理解
- 1.2.2 架构内角色说明
- 1.2.3 运行原理
- 1.3 底层调用真正原理
- 1.3.1 底层调用原理
- 1.2.2 调用相关问题
- 1.2.2.1 怎么让当前线程暂停
- 1.2.2.2 服务提供者能实现失效踢出的原理
- 1.2.2.3 为什么要通过代理对象通信
- 1.2.3 调用支持协议
- 1.2.4 调用支持注册中心
- 1.2.5 provider和consumer的invoke
- 1.2.6 服务暴露和消费详细过程
- 1.2.6.1 provider暴露服务详细过程
- 1.2.6.2 consumer消费服务过程
- 1.4 Dubbo中设计模式
- 1.5 Dubbo SPI机制
- 1.5.1 简介
- 1.5.2 Dubbo的SPI和JAVA的SPI区别
- 1.6 Dubbo集群相关
- 1.6.1 集群负载均衡策略
- 1.6.1.1 加权随机
- 1.6.1.2 最小活跃数
- 1.6.1.3 一致性hash
- 1.6.1.4 加权轮询
- 1.6.2 集群容错方式
- 1.7 Dubbo分层
- 2 dubbo和zookeeper的关系
1 Dubbo原理
Dubbo
:一个分布式、高性能、透明化的RPC
服务框架
作用:提供服务自动注册
、自动发现
等高效服务治理方案
1.1 Dubbo核心功能
-
远程通讯
:dubbo-remoting模块, 提供对多种基于长连接的NIO
框架抽象封装,包括多种线程模型,序列化,以及请求-响应
模式的信息交换方式。 -
集群容错
: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 -
自动发现
: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器
1.2 Dubbo架构图
1.2.1 Dubbo基本理解
Dubbo
:一个分布式、高性能、透明化的RPC
服务框架
作用:提供服务自动注册、自动发现等高效服务治理方案
1.2.2 架构内角色说明
-
Provider
:提供者,服务发布方. -
Consumer
:消费者, 调用服务方 -
Container
:Dubbo
容器.依赖于Spring
容器. -
Registry
: 注册中心.当Container
启动时把所有可以提供的服务列表上Registry
中进行注册,作用:告诉Consumer
提供了什么服务和服务方在哪里 -
Monitor
:监听器 -
虚线
都是异步访问,实线
都是同步访问 -
蓝色虚线
:在启动时完成的功能 -
红色虚线(实线)
都是程序运行过程中执行的功能 - 所有的角色都是可以在单独的服务器上,所以必须遵守特定的协议
- 上图中,除了监视器
monitor
这里是短连接,其他连接都是长连接
1.2.3 运行原理
-
启动容器
,相当于在启动Dubbo
的Provider
- 启动后会去注册中心进行注册,注册所有可以提供的服务列表
- 在
Consumer
启动后会去Registry
中获取服务列表和Provider
的地址,进行订阅. - 当
Provider
有修改后,注册中心会把消息推送给Consummer
,使用了观察者设计模式(又叫发布/订阅设计模式) - 根据获取到的
Provider
地址,真实调用Provider
中功能,在Consumer
方使用了代理设计模式,创建一个Provider
方类的一个代理对象,通过代理对象获取Provider
中真实功能,起到保护Provider
真实功能的作用. -
Consumer
和Provider
每隔1
分钟向Monitor
发送统计信息,统计信息包含,访问次数,频率等
1.3 底层调用真正原理
Dubbo
底层采用Socket
进行通信,Dubbo
缺省协议采用单一长连接和NIO
异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况
http1.1默认长连接,http1.0默认短连接
长连接
长连接
意味着进行一次数据传输后,不关闭连接,长期保持连通状态。如果两个应用程序之间有新的数据需要传输,则直接复用这个连接,无需再建立一个新的连接短连接
短连接意味着每一次的数据传输都需要建立一个新的连接,用完再马上关闭它。下次再用的时候重新建立一个新的连接,如此反复- 并行通信
一组信息(通常是字节)的各位数据被同时传送的通信方法称为并行通信
。并行通信依靠并行I/O
接口实现。并行通信速度快,但传输线根数多,只适用于近距离
(相距数公尺)的通信。 - 串行通信
一组信息的各位数据被逐位顺序传送的通信方式称为串行通信
。串行通信可通过串行接口来实现。串行通信速度慢
,但传输线少,适宜长距离
通信
1.3.1 底层调用原理
分析源代码,基本原理如下:
- 消费端
Proxy
持有一个Invoker
对象,使用Invoker
调用,之后通过Cluster
进行负载容错,失败重试,调用Directory
获取远程服务的Invoker
列表 - 负载均衡
用户配置了路由规则,则根据路由规则过滤获取到的Invoker
列表
用户没有配置路由规则或配置路由后还有很多节点,则使用LoadBalance
方法做负载均衡,选用一个可以调用的Invoker
- 经过一个一个过滤器链,通常是处理上下文、限流、计数等
- 会使用
Client
做数据传输
-
Client
一个线程调用远程接口,生成一个唯一的ID
(比如一段随机字符串,UUID
等),Dubbo
是使用AtomicLong
从0
开始累计数字的 - 将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象
callback
,全部封装在一起,组成一个对象object
- 向专门存放调用信息的全局
ConcurrentHashMap
里面put(ID, object)
- 将
ID
和打包的方法调用信息封装成一对象connRequest
,使用IoSession.write(connRequest)
异步发送出去 - 当前线程再使用
callback
的get()
方法试图获取远程返回的结果,在get()
内部,则使用synchronized
获取回调对象callback
的锁, 再先检测是否已经获取到结果,如果没有,然后调用callback
的wait()
方法,释放callback
上的锁,让当前线程处于等待状态
- 私有化协议的构造(
Codec
) - 进行序列化
- 服务端收到这个
Request
请求,将其分配到ThreadPool
中进行处理 -
Server
来处理这些Request
,根据请求查找对应的Exporter
,之后经过一个服务提供者端的过滤器链 - 然后找到接口实现并真正的调用,将请求结果返回
- 服务端接收到请求并处理后,将结果(此结果中包含了前面的
ID
,即回传)发送给客户端,客户端socket
连接上专门监听消息的线程收到消息,分析结果,取到ID
,再从前面的ConcurrentHashMap
里面get(ID)
,从而找到callback
,将方法调用结果设置到callback
对象里。 - 监听线程接着使用
synchronized
获取回调对象callback
的锁(因为前面调用过wait()
,那个线程已释放callback
的锁了),再notifyAll()
,唤醒前面处于等待状态的线程继续执行(callback
的get()
方法继续执行就能拿到调用结果了),至此,整个过程结束
1.2.2 调用相关问题
1.2.2.1 怎么让当前线程暂停
当前线程怎么让它暂停
,等结果回来后,再向后执行?
答:先生成一个对象obj
,在一个全局map
里put(ID,obj)
存放起来,再用synchronized
获取obj
锁,再调用obj.wait()
让当前线程处于等待状态,然后另一消息监听线程等到服 务端结果来了后,再map.get(ID)
找到obj
,再用synchronized
获取obj
锁,再调用obj.notifyAll()
唤醒前面处于等待状态的线程。
正如前面所说,Socket
通信是一个全双工的方式,如果有多个线程同时进行远程方法调用,这时建立在client server
之间的socket
连接上会有很多双方发送的消息传递,前后顺序也可能是乱七八糟的,server
处理完结果后,将结果消息发送给client
,client
收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的?
答:使用一个ID
,让其唯一,然后传递给服务端,再服务端又回传回来,这样就知道结果是原先哪个线程的了
1.2.2.2 服务提供者能实现失效踢出的原理
服务失效踢出基于Zookeeper
的临时节点原理。Zookeeper
中节点是有生命周期的,具体的生命周期取决于节点的类型,节点主要分为持久节点(Persistent
)和临时节点(Ephemeral
)
1.2.2.3 为什么要通过代理对象通信
其实主要就是为了将调用细节封装起来,将调用远程方法变得和调用本地方法一样简单,还可以做一些其他方面的增强,比如负载均衡,容错机制,过滤操作,调用数据的统计
1.2.3 调用支持协议
-
Dubbo
:缺省协议,使用基于mina1.1.7+hessian3.2.1
的tbremoting
交互。
单一长连接和NIO
异步通讯,适合大并发小数据量的服务调用建议小于100K
),以及消费者远大于提供者。传输协议TCP
,异步,Hessian
序列化 -
rmi
:采用JDK
标准的rmi
协议实现,传输参数和返回参数对象需要实现Serializable
接口,使用java
标准序列化机制,使用阻塞式短连接
,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议TCP
。
多个短连接,TCP
协议传输,同步传输,适用常规的远程服务调用和rmi
互 操作。在依赖低版本的Common-Collections
包,java
序列化存在安全漏洞 -
webservice
:基于WebService
的远程调用协议,集成CXF
实现,提供和原生WebService
的互操作。多个短连接,基于HTTP
传输,同步传输,适用系统集成和跨语言调用; -
http
:基于Http
表单提交的远程调用协议,使用Spring
的HttpInvoke
实 现。多个短连接,传输协议HTTP
,传入参数大小混合,提供者个数多于消 费者,需要给应用程序和浏览器JS
调用 -
hessian
:集成Hessian
服务,基于HTTP
通讯,采用Servlet
暴露服务,Dubbo
内嵌Jetty
作为服务器时默认实现,提供与Hession
服务互操作。多个短连接,同步HTTP
传输,Hessian
序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件; -
memcache
:基于memcached
实现的RPC
协议 -
redis
:基于redis
实现的RPC
协议 1
1.2.4 调用支持注册中心
-
Zookeeper
优点:支持网络集群
缺点:稳定性受限于Zookeeper -
Redis
优点:性能高
缺点:对服务器环境要求较高 -
Multicast
优点:面中心化,不需要额外安装软件
缺点:建议同机房(局域网)内使用 -
Simple
适用于测试环境.不支持集群
1.2.5 provider和consumer的invoke
下面我们用一个精简的图来说明最重要的两种Invoker
:服务提供Invoker
和服务消费Invoker
:
1.2.6 服务暴露和消费详细过程
1.2.6.1 provider暴露服务详细过程
服务提供者暴露服务的主过程:
首先通过ServiceConfig
解析标签,创建dubbo
标签解析器来解析dubbo
的标签,容器创建完成之后,触发ContextRefreshEvent
事件回调开始暴露服务。
ServiceConfig
类拿到对外提供服务的实际类ref
(如:HelloWorldImpl
),然后通过ProxyFactory
类的getInvoker
方法使用ref
生成一个AbstractProxyInvoker
实例(通过proxyFactory.getInvoker
方法并利用javassist或DdkProxyFactory
来进行动态代理,将服务暴露接口封装成invoker
对象),到这一步就完成具体服务到Invoker
的转化。
再通过DubboProtocol
的实现把包装后的invoker
转换成exporter
的过程。
Dubbo
处理服务暴露的关键就在Invoker
转换到Exporter
的过程(如上图中的红色部分),下面我们以Dubbo
和RMI
这两种典型协议的实现来进行说明:
-
Dubbo
的实现:Dubbo
协议的Invoker
转为Exporter
发生在DubboProtocol
类的export
方法,它主要是打开socket
侦听服务,并接收客户端发来的各种请求,通讯细节由Dubbo
自己实现。 -
RMI
的实现:RMI
,(Remote Method Invoke
远程方法调用,只支持java
)协议的Invoker
转为Exporter
发生在RmiProtocol
类的export
方法,
它通过Spring
或Dubbo
或JDK
来实现RMI
服务,通讯细节这一块由JDK
底层来实现,这就省了不少工作量。
启动服务器server,监听端口,最后RegistryProtocol
保存URL
地址和invoker
的映射关系,同时注册到服务中心
1.2.6.2 consumer消费服务过程
服务消费的主过程:
首先ReferenceConfig
类的init
方法调用Protocol
的refer
方法生成Invoker
实例(如上图中的红色部分),这是服务消费的关键。
接下来,然后通过ProxyFactory
类的getProxy
方法把Invoker
转换为客户端需要的接口(如:HelloWorld
)或者
首先客户端根据配置文件信息从注册中心订阅服务,首次会全量缓存到本地,后续的更新会监听动态更新到本地。之后DubboProtocol
根据provider
的地址和接口信息连接到服务端server
,开启客户端client
,然后创建invoker
之后通过invoker
为服务接口生成代理对象,这个代理对象用于远程调用provider
,至此完成了服务引用
1.4 Dubbo中设计模式
- 责任链模式
责任链模式
在Dubbo
中发挥的作用举足轻重,就像是Dubbo
框架的骨架。Dubbo
的调用链组织是用责任链模式串连起来的。责任链中的每个节点实现Filter
接口,然后由ProtocolFilterWrapper
,将所有Filter
串连起来。Dubbo
的许多功能都是通过Filter
扩展实现的,比如监控、日志、缓存、安全、telnet
以及RPC
本身都是 - 观察者模式
Dubbo
中使用观察者模式最典型的例子是RegistryService
。消费者在初始化的时候会调用subscribe
方法,注册一个观察者,如果观察者引用的服务地址列表发生改变,就会通过NotifyListener
通知消费者。此外,Dubbo
的InvokerListener
、ExporterListener
也实现了观察者模式,只要实现该接口,并注册,就可以接收到consumer
端调用refer
和provider
端调用export
的通知。 - 修饰器模式
Dubbo
中还大量用到了修饰器模式。比如ProtocolFilterWrapper
类是对Protocol
类的修饰。在export
和refer
方法中,配合责任链模式,把Filter
组装成责任链,实现对Protocol
功能的修饰。其他还有ProtocolListenerWrapper
、ListenerInvokerWrapper
、InvokerWrapper
等。 - 工厂方法模式
CacheFactory
的实现采用的是工厂方法模式。CacheFactory
接口定义getCache
方法,然后定义一个AbstractCacheFactory
抽象类实现CacheFactory
,并将实际创建cache
的createCache
方法分离出来,并设置为抽象方法。这样具体cache
的创建工作就留给具体的子类去完成。 - 抽象工厂模式
ProxyFactory
及其子类是Dubbo
中使用抽象工厂模式的典型例子。ProxyFactory
提供两个方法,分别用来生产Proxy
和Invoker
(这两个方法签名看起来有些矛盾,因为getProxy
方法需要传入一个Invoker
对象,而getInvoker
方法需要传入一个Proxy
对象,看起来会形成循环依赖,但其实两个方式使用的场景不一样)。AbstractProxyFactory
实现了ProxyFactory
接口,作为具体实现类的抽象父类。然后定义了JdkProxyFactory
和JavassistProxyFactory
两个具体类,分别用来生产基于jdk代理机制和基于javassist代理机制的Proxy和Invoker。 - 适配器模式
为了让用户根据自己的需求选择日志组件,Dubbo
自定义了自己的Logger
接口,并为常见的日志组件(包括jcl, jdk, log4j, slf4j
)提供相应的适配器。并且利用简单工厂模式提供一个LoggerFactory
,客户可以创建抽象的Dubbo
自定义Logger
,而无需关心实际使用的日志组件类型。在LoggerFactory
初始化时,客户通过设置系统变量的方式选择自己所用的日志组件,这样提供了很大的灵活性。 - 代理模式
Dubbo consumer
使用Proxy
类创建远程服务的本地代理,本地代理实现和远程服务一样的接口,并且屏蔽了网络通信的细节,使得用户在使用本地代理的时候,感觉和使用本地服务一样
1.5 Dubbo SPI机制
1.5.1 简介
SPI
(Service Provider Interface
),是一种服务发现机制,其实就是将结构的实现类写入配置当中,在服务加载的时候将配置文件独处,加载实现类,这样就可以在运行的时候,动态的帮助接口替换实现类
Dubbo
的SPI
其实是对java
的SPI
进行了一种增强,可以按需加载实现类之外,增加了 IOC
和 AOP
的特性,还有自适应扩展机制。SPI
在dubbo
应用很多,包括协议扩展、集群扩展、路由扩展、序列化扩展等等。Dubbo
对于文件目录的配置分为了三类。
-
META-INF/services/
目录:该目录下的SPI
配置文件是为了用来兼容Java SPI
-
META-INF/dubbo/
目录:该目录存放用户自定义的SPI
配置文件
key=com.xxx.xxx -
META-INF/dubbo/internal/
目录:该目录存放Dubbo
内部使用的SPI
配置文件
1.5.2 Dubbo的SPI和JAVA的SPI区别
-
Java SPI
Java SPI
在查找扩展实现类的时候遍历SPI
的配置文件并且将实现类全部实例化 -
Dubbo Spi
对Dubbo
进行扩展,不需要改动Dubbo
的源码
延迟加载,可以一次只加载自己想要加载的扩展实现
增加了对扩展点IOC
和AOP
的支持,一个扩展点可以直接setter
注入其它扩展点Dubbo
的扩展机制能很好的支持第三方IOC
容器,默认支持Spring Bean
1.6 Dubbo集群相关
1.6.1 集群负载均衡策略
1.6.1.1 加权随机
比如我们有三台服务器[A, B, C],给他们设置权重为[4, 5, 6],然后把这三个数平铺在水平线上,和为15。
然后在15以内生成一个随机数,0~4是服务器A,4~9是服务器B,9~15是服务器C
1.6.1.2 最小活跃数
每个服务提供者对应一个活跃数 active
,初始情况下,所有服务提供者活跃数均为0
。每收到一个请求,活跃数加1
,完成请求后则将活跃数减1
。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。
1.6.1.3 一致性hash
首先求出memcached
服务器(节点)的哈希值,并将其配置到0~2
的32次方
的圆(continuum)上。
然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
然后从数据映射到的位置开始 顺时针查找
,将数据保存到找到的第一个服务器上。如果超过2的32次方仍然找不到服务器,就会保存到第一台memcached
服务器上。
1.6.1.4 加权轮询
比如我们有三台服务器[A, B, C],给他们设置权重为[4, 5, 6],那么假如总共有15次请求,那么会有4次落在A服务器,5次落在B服务器,6次落在C服务器。
1.6.2 集群容错方式
-
Failover Cluster
失败自动切换:dubbo
的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可通过引用服务的时候配置,默认重试次数为1是只调用一次。 -
Failback Cluster
失败自动恢复:在调用失败,记录日志和调用信息,然后返回空结果给consumer
,并且通过定时任务每隔5秒
对失败的调用进行重试 -
Failfast Cluster
快速失败:只会调用一次,失败后立刻抛出异常 -
Failsafe Cluster
失败安全:调用出现异常,记录日志不抛出,返回空结果 -
Forking Cluster
并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider
,结果保存到阻塞队列,只要有一个provider
成功返回了结果,就会立刻返回结果 -
Broadcast Cluster
广播模式:逐个调用每个provider
,如果其中一台报错,在循环调用结束后,抛出异常
1.7 Dubbo分层
Dubbo
大致上分为三层,分别是:
- 业务层:业务逻辑层由我们自己来提供接口和实现还有一些配置信息
-
RPC
层:就是真正的RPC
调用的核心层,封装整个RPC
的调用过程、负载均衡、集群容错、代理 -
Remoting
层:是对网络传输协议和数据转换的封装
Service
和Config
两层可以认为是API
层,主要提供给API
使用者,使用者只需要配置和完成业务代码就可以了。
后面所有的层级是SPI
层,主要提供给扩展者使用主要是用来做Dubbo
的二次开发扩展功能。
2 dubbo和zookeeper的关系
对于注册中心的选择,我们一般用Zookeeper
,那么zookeeper
和dubbo
的关系是怎么样的
zookeeper
的数据模型跟我们windows
系统下的文件模型相似,都是树形结构的;windows
下的文件系统有文件夹和文件两种,文件夹
只是路径
,文件
才是存储体
;而zookeeper
的数据模型也是树结构的,每个节点叫做znode
,每个znode
既可以存储数据也可以当做路径
树形结构的节点都是唯一的,而上面这个图上的绿色圆点都是zookeeper
中的一个znode
,每个znode
都有自己的路径和自己的值,存储着我们dubbo
注册的service
信息,而上面这张图的znode
分为4级(root、service、type、url
):
- 一级节点
root
内存储着dubbo
,代表这个znode
下的所有znode
都是dubbo
相关的 - 二级节点
service
存储着我们dubbo
注册到zk
中的service
名称,每多注册一个service
服务,就会在dubbo
这个znode
下添加一个新的service
节点 - 三级节点
type
存储着service
类型,是提供者还是消费者 - 四级节点
url
存储着我们所注册的服务的具体地址
dubbo
就是通过这一层层的节点找到我们需要调用的url
然后进行调用的。zookeeper
作为dubbo
的注册中心的角色使用,我们把提供者和消费者通过dubbo
注册到zookeeper
这个注册中心里,zookeeper
中存储的是服务的url
的列表
通过消费者调用提供者服务的时候,会根据接口的名称类型通过dubbo
到zookeeper
中找到对应的服务的url
列表,zookeeper
返回服务提供者地址列表给消费者
消费者从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
- 通信协议参考链接 ↩︎