目录
前言
一、何为RPC(Remote Procedure Call Protocol)
二、RPC架构组件及调用过程
三、RPC的实现基础
四、RPC的实现原理架构图
五、Dubbox简介
六、Dubbox与Dubbo区别
七、Dubbo的架构及调用过程
八、Dubbo的使用配置
九、Dubbo的SPI机制
9.1 Dubbo的SPI机制约定
9.2 Dubbo的SPI加载顺序
9.3 Dubbo的SPI简单使用
十、Dubbo的集群容错和负载均衡
10.1 集群模块
10.2 集群容错
10.3 负载均衡(LoadBalance)
首先申明,dubbox是在dubbo的基础上衍生出来的,主要是因为阿里巴巴的dubbo团队解散之后dubbo不再维护了,因此当当网的大佬们对dubbo进行了一些功能的扩展,造就了dubbox,但是其保留了原dubbo的所有功能,因此本文很多dubbox的介绍其实就是dubbo的功能介绍。在具体学习之前,首先会讲一下rpc知识作为入门,毕竟dubbo就是rpc框架中的一类。
RPC即远程过程调用协议,它是一种通过网络调用远程计算机程序上的服务,而不需要了解网络底层技术的协议。通俗点来讲,rpc使得程序调用远程机子上的服务像调用本地机器上的服务一样简单。其诞生之初主要为了应对在分布式架构下,不同的服务提供者可能会做一些重复的工作,例如:序列化、反序列化、网络框架、超时处理等等,所以迫切需要一个统一的分布式服务调用框架,RPC应运而生。当然有时候仅仅是因为通过调用本地上的服务并不能完成需求,需要跨进程或跨机子通信,此时选用RPC框架也是一个选择。
一个基本的RPC框架应该至少包含4个组件:
1、客户端(Client):服务调用方,又称服务消费者
2、客户端存根(Client Stub):存放服务端的地址信息,用于将客户端所传的参数进行打包成网络消息,并通过网络传输将请求发送到服务端
3、服务端存根(Server Stub):接收客户端发送过来的网络消息,并对其请求数据进行解包,然后调用本地服务进行处理
4、服务端(Server):服务提供者
具体调用过程如下:
下面大致介绍下详细过程,步骤序号与上图并未一一对应:
1、客户端像调用本地服务一样调用提供者提供的服务
2、客户端存根将请求参数进行序列化操作封装成一个网络消息,然后找到服务端的地址,通过网络将消息发往服务端存根
3、服务端存根接收到客户端传来的消息后,进行反序列化操作,将二进制信息转成内存中的表达式,然后调用本地的服务
4、服务端进行一系列的业务逻辑操作之后,返回处理结果给服务端存根
5、服务端存根将结果进行序列化操作传回客户端存根
6、客户端存根接收到服务端传回的结果后,进行反序列化操作,并将调用结果返回给客户端
7、客户端得到此次调用结果,一个完整过程结束
RPC框架的主要作用就是封装了上面的2~6过程,使得客户端在调用服务时感受不到底层的调用过程,像调用本地服务一样简单
1、需要有非常高效的网络通信,比如一般选用Netty作为网络通信框架
2、需要有较高效的序列化框架,比如谷歌的Protobuf序列化框架
3、需要可靠的寻址服务,即注册中心,比如Zookeeper
4、如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能
换言之,RPC框架实现大致需要的技术为:动态代理、序列化/反序列化、NIO通信、服务注册中心等
简单的再次总结,大致包括四个步骤,
1、建立通信:主要是通过在客户端和服务端之间建立起TCP通信连接,一切数据交换均在该连接中实现,该连接可以是长连接也可以是按需连接,多个远程过程调用共享同一个连接。
2、寻址服务:即一个可靠的服务注册中心,一般过程往往是服务提供者先注册到注册中心,然后服务消费者从注册中心获取提供者地址,进而完成服务调用,大致过程图如下:
3、网络传输:由于网络协议是基于二进制形式的,所以框架中一般都使用序列化操作将请求数据转成二进制流,然后才能在网络中传输,为了提高并发性能,一般都是采用异步IO通信,即NIO
4、服务调用:服务端存根一般采用动态代理的形式调用本地服务,得到结果反序列化之后再通过网络传输回客户端
可以认为是加强版的Dubbo,是一款开源的RPC框架,主要面向服务化治理,上手容易且对应用无较大侵入,Dubbo采用Spring-bean的方式管理及配置实例,一般情况下我们也都是采用这种方式,到了Dubbox之后,支持完全基于java代码的Dubbo配置,接下来看下Dubbox在Dubbo基础上主要新增的几个功能
以下区别摘自Dubbox官方,地址:https://github.com/dangdangdotcom/dubbox
1、支持REST风格远程调用(HTTP + JSON/XML):基于非常成熟的JBoss RestEasy框架,在dubbo中实现了REST风格(HTTP + JSON/XML)的远程调用,以显著简化企业内部的跨语言交互,同时显著简化企业对外的Open API、无线API甚至AJAX服务端等等的开发。事实上,这个REST调用也使得Dubbo可以对当今特别流行的“微服务”架构提供基础性支持。 另外,REST调用也达到了比较高的性能,在基准测试下,HTTP + JSON与Dubbo 2.x默认的RPC协议(即TCP + Hessian2二进制序列化)之间只有1.5倍左右的差距,详见文档中的基准测试报告。
2、支持基于Kryo和FST的Java高效序列化实现:基于当今比较知名的Kryo和FST高性能序列化库,为Dubbo默认的RPC协议添加新的序列化实现,并优化调整了其序列化体系,比较显著的提高了Dubbo RPC的性能,详见文档中的基准测试报告。
3、支持基于Jackson的JSON序列化:基于业界应用最广泛的Jackson序列化库,为Dubbo默认的RPC协议添加新的JSON序列化实现。
4、支持基于嵌入式Tomcat的HTTP remoting体系:基于嵌入式tomcat实现dubbo的HTTP remoting体系(即dubbo-remoting-http),用以逐步取代Dubbo中旧版本的嵌入式Jetty,可以显著的提高REST等的远程调用性能,并将Servlet API的支持从2.5升级到3.1。(注:除了REST,dubbo中的WebServices、Hessian、HTTP Invoker等协议都基于这个HTTP remoting体系)。
5、升级Spring:将dubbo中Spring由2.x升级到目前最常用的3.x版本,减少版本冲突带来的麻烦。
6、升级ZooKeeper客户端:将dubbo中的zookeeper客户端升级到最新的版本,以修正老版本中包含的bug。
7、支持完全基于Java代码的Dubbo配置:基于Spring的Java Config,实现完全无XML的纯Java代码方式来配置dubbo
8、调整Demo应用:暂时将dubbo的demo应用调整并改写以主要演示REST功能、Dubbo协议的新序列化方式、基于Java代码的Spring配置等等。
9、修正了dubbo的bug:包括配置、序列化、管理界面等等的bug。
Dubbo的运行时架构图如下,如果理解不够深刻建议自己画几遍,边画边思考其意义会好很多:
节点 | 角色说明 |
---|---|
Provider |
暴露服务的服务提供方 |
Consumer |
调用远程服务的服务消费方 |
Registry |
服务注册与发现的注册中心 |
Monitor |
统计服务的调用次数和调用时间的监控中心 |
Container |
服务运行容器 |
以架构图中的序号为步骤,详解调用过程:
0:服务容器负责启动加载服务提供者(初始化操作)
1:服务提供者在启动时向注册中心注册自己所提供的服务(初始化操作)
2:服务消费者在启动时向注册中心订阅所需的服务列表(初始化操作)
3:注册中心返回服务提供者列表给消费者,如果有变更,将基于TCP长连接推送变更数据给消费者(异步实现)
4:服务消费者从提供者地址列表中,基于软负载均衡算法,选取一台机器进行调用,如果调用失败,则选取另一台(同步实现)
5:服务提供者和消费者,在内存中累计调用次数和调用时间,定时每分钟发送数据给监控中心(异步实现)
这一块其实没什么好写的,无非就是一些spring-bean配置,可对着Dubbo官方文档进行配置,Spring框架下使用Dubbo的demo请走我很久以前写的一个demo:基于注解的spring+dubbo发布一个简单的helloWord服务及调用
这是Dubbo框架的重中之重,Dubbo的所有组件均由SPI进行加载,且正是由于SPI机制,Dubbo的扩展性非常强,几乎所有组件均可由第三方替换实现。然鹅,Dubbo的SPI机制并不等同于jdk中的SPI机制,在其基础上进行了一系列增强,比如:
getName()
获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。如果你对jdk中的SPI机制及其约定实现不了解,可先走:Java的SPI机制实现
大部分约定与jdk的SPI类似,就是Dubbo的配置文件需放在META-INF/dubbo/接口全限定名,文件内容也不是简单的目标接口实现类的全路径,而是以键值对的方式进行存储,实现类有多个时换行展示,即形如:
xxx=com.alibaba.xxx.XxxProtocol
该设计是为了解决jdk的SPI会一次性实例化扩展点所有实现的问题,使用键值对可实现按需加载
当你以上述约定实现一个新的扩展点之后,例如上图中的扩展协议,你可在配置模块中指定使用该扩展,在Dubbo配置中,每个扩展点均有其对应配置属性或标签,可通过配置指定使用哪个扩展,例如协议扩展配置如下:
只有在接口上打上@SPI注解,Dubbo才会去寻找该接口的扩展点实现,会依次这几个地方进行加载:
META-INF/dubbo/internal/ //dubbo内部实现的各种扩展都放在了这个目录了
META-INF/dubbo/
META-INF/services/
我们以protocol协议接口为例,看下其源码:
@SPI("dubbo")
public interface Protocol {
/**
* 获取缺省端口,当用户没有配置端口时使用。
*
* @return 缺省端口
*/
int getDefaultPort();
可以发现该接口上已打上有@SPI注解,且其扩展点默认名称为dubbo,查询该getDefaultPort()方法的实现,我们可以发现存在多个实现类,并且在我们自己的项目中运用SPI机制也能轻易扩展
看这一节前最好先看下上文中jdk的SPI机制简单demo,两者可简单对比下,以上文中的demo为例:
还是熟悉的People、Boy、Girl类,这里需要注意的是目标接口上须加上Dubbo的@SPI注解
@SPI
public interface People {
void say();
}
public class Boy implements People {
@Override
public void say() {
System.out.println("i'm a boy");
}
}
public class Girl implements People {
@Override
public void say() {
System.out.println("i'm a girl");
}
}
配置文件记得放在resource/META-INFO/dubbo路径下,文件名为People的全限定名,文件内容如下:
boy=com.alibaba.dubbo.examples.spi.Boy
girl=com.alibaba.dubbo.examples.spi.Girl
测试类代码:
public class SpiDemo {
@Test
public void test() {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(People.class);
// 根据配置文件中的key获取扩展类对象
People boy = extensionLoader.getExtension("boy");
boy.say();
People girl = extensionLoader.getExtension("girl");
girl.say();
}
}
输出如下:
i'm a boy
i'm a girl
当然这个demo只是为了加深对Dubbo SPI机制的理解,有了上面的介绍,在我们项目中想要自定义实现Dubbo的扩展也就很简单啦
这是Dubbo在集群模式下的两种处理措施,在分布式环境下,如果多台机子同时提供服务,那么消费者该调用哪台机子的服务呢?当调用失败时该如何处理呢?是重试呢,还是抛出异常,亦或是仅仅打印异常,这就是接下来会介绍的。
为了处理这些问题,Dubbo 定义了集群接口 Cluster 以及 Cluster Invoker。集群 Cluster 用途是将多个服务提供者合并为一个 Cluster Invoker,并将这个 Invoker 暴露给服务消费者。这样一来,服务消费者只需通过这个 Invoker 进行远程调用即可,至于具体调用哪个服务提供者,以及调用失败后如何处理等问题,现在都交给集群模块去处理。集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是集群的作用。
在集群调用失败时,Dubbo提供了多种容错方案,如下所示:
失败自动切换,当出现失败,重试其它服务器。(缺省)
通常用于读操作,但重试会带来更长延迟。
可通过retries=“2”来设置重试次数(不含第一次)。
快速失败,只发起一次调用,失败立即报错。
通常用于非幂等性的写操作,比如新增记录。
失败安全,出现异常时,直接忽略。
通常用于写入审计日志等操作。
失败自动恢复,后台记录失败请求,定时重发。
通常用于消息通知操作。
并行调用多个服务器,只要一个成功即返回。
通常用于实时性要求较高的读操作,但需要浪费更多服务资源。
可通过forks=“2”来设置最大并行数。
广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持)
通常用于通知所有提供者更新缓存或日志等本地资源信息。
负载均衡通俗点来讲就是将网络请求,或者其他形式的负载均摊到多个服务器上,避免集群中部分服务器压力过大,而另一些服务器空闲的情况。通过负载均衡,可以使每个服务器都获取到适合自己处理能力的负载,在为高负载服务器分流的同时,还能避免资源的浪费,一举两得。对应到Dubbo中,服务提供者就类比于服务器,Dubbo的负载均衡策略也是为了避免部分提供者压力过大的情况。Dubbo的负载均衡主要分为四种方式,如下:
假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。
活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提供者。在具体实现中,每个服务提供者对应一个活跃数 active。初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最小活跃数负载均衡算法的基本思想。
除了最小活跃数,LeastActiveLoadBalance 在实现上还引入了权重值。所以准确的来说,LeastActiveLoadBalance 是基于加权最小活跃数算法实现的。举个例子说明一下,在一个服务提供者集群中,有两个性能优异的服务提供者。某一时刻它们的活跃数相同,此时 Dubbo 会根据它们的权重去分配请求,权重越大,获取到新请求的概率就越大。如果两个服务提供者权重相同,此时随机选择一个即可。
一致性 hash 算法由麻省理工学院的 Karger 及其合作者于1997年提出的,算法提出之初是用于大规模缓存系统的负载均衡。它的工作过程是这样的,首先根据 ip 或者其他的信息为缓存节点生成一个 hash,并将这个 hash 投射到 [0, 232 - 1] 的圆环上。当有查询或写入请求时,则为缓存项的 key 生成一个 hash 值。然后查找第一个大于或等于该 hash 值的缓存节点,并到这个节点中查询或写入缓存项。如果当前节点挂了,则在下一次查询或写入缓存时,为缓存项查找另一个大于其 hash 值的缓存节点即可。大致效果如下图所示,每个缓存节点在圆环上占据一个位置。如果缓存项的 key 的 hash 值小于缓存节点 hash 值,则到该缓存节点中存储或读取缓存项。比如下面绿色点对应的缓存项将会被存储到 cache-2 节点中。由于 cache-3 挂了,原本应该存到该节点中的缓存项最终会存储到 cache-4 节点中。
下面来看看一致性 hash 在 Dubbo 中的应用。我们把上图的缓存节点替换成 Dubbo 的服务提供者,于是得到了下图:
这里相同颜色的节点均属于同一个服务提供者,比如 Invoker1-1,Invoker1-2,……, Invoker1-160。这样做的目的是通过引入虚拟节点,让 Invoker 在圆环上分散开来,避免数据倾斜问题。所谓数据倾斜是指,由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到了少量请求的情况。比如:
如上,由于 Invoker-1 和 Invoker-2 在圆环上分布不均,导致系统中75%的请求都会落到 Invoker-1 上,只有 25% 的请求会落到 Invoker-2 上。解决这个问题办法是引入虚拟节点,通过虚拟节点均衡各个节点的请求量。
先从最简单的轮询开始讲起,所谓轮询是指将请求轮流分配给每台服务器。举个例子,我们有三台服务器 A、B、C。我们将第一个请求分配给服务器 A,第二个请求分配给服务器 B,第三个请求分配给服务器 C,第四个请求再次分配给服务器 A。这个过程就叫做轮询。轮询是一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。但现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。
配置如下:
或:
或:
或: