文章目录
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
优点:支持网络集群
缺点:稳定性受限于ZookeeperRedis
优点:性能高
缺点:对服务器环境要求较高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.xxxMETA-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原理
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
返回服务提供者地址列表给消费者
消费者从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用