深度剖析Apache Dubbo核心技术内幕(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-26 23:20:42

点击下载

作者:翟陆续(加多)

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深度剖析Apache Dubbo核心技术内幕

深度剖析Apache Dubbo核心技术内幕试读:

前言

在单体应用时,不同业务模块部署在同一个JVM进程内,这时通过本地调用就可以解决不同业务模块之间的相互引用;但在多体应用时,不同业务模块大多部署到不同的机器上,这时一个高效、稳定的RPC框架就显得特别重要了。Apache Dubbo作为阿里巴巴开源的分布式RPC框架,是众多RPC框架中比较优秀的一个,在进入Apache孵化器项目后现已毕业,相信在开源社区的不断贡献下,它会成为RPC框架中的佼佼者。

为何要研究Apache Dubbo的实现原理

引用笔者在《Java并发编程之美》一书前言中的论述:研究开源框架,特别是优秀的开源框架的实现原理,可以开拓我们的技术视野,提高我们的架构能力,减少由于使用不当导致的线上故障的发生。而在微服务大行其道的今天,RPC框架作为微服务之间通信的一种手段,其在微服务架构中占有一席之地,Apache Dubbo(后面简称Dubbo)则是RPC框架中比较优秀的代表,为了更好地使用它,其实现原理自然值得我们去探究。

下面我们具体谈谈通过研究Dubbo框架的实现原理,到底能学到什么。

我们首先可以学习和深刻体会到分层架构带来的好处。Dubbo框架从整体上分为了业务(Business)层、RPC层和远程调用(Remoting)层,其中业务层提供API,让使用者方便地发布与引用服务;RPC层则是对服务注册与发现、服务代理、路由、负载均衡等功能的封装,该层又可以被划分为很多层;远程调用层则是对网络传输与请求数据序列/反序列化等的抽象。使用分层架构可以保证下层的改变对上层不可见,并且可以实现关注点分离,比如使用者使用Dubbo时只关心如何使用业务层的API来发布与引用服务,而不需要关心RPC层的实现,当新版本Dubbo升级了RPC层的逻辑时,使用者只需要升级Dubbo的版本就可以了,这是因为RPC层的修改对业务层使用者来说是透明的。

我们也可以学习到好的框架应该具有可扩展性。Dubbo就是一个扩展性极强的框架,其RPC层中的所有组件都是基于SPI扩展接口实现的,每个组件都可以被替换;Dubbo增强了JDK中提供的标准SPI功能,并且增加了对扩展接口的IoC(一个扩展接口可以直接使用setter()方法注入其他扩展接口)和AOP的支持(可以使用Wrapper类对扩展接口进行功能增强);增强SPI不会一次性实例化扩展点的所有实现类,从而避免了当扩展点实现类初始化很耗时但当前还没用上它的功能时,仍进行加载实例化这种浪费资源的情况;增强的SPI是在具体用到某个实现类时才对具体实现类进行实例化。

作为高可用分布式RPC框架,其自身必须具有容错能力,以便提高系统的可用性。Dubbo框架则提供了分布式系统中常见的集群容错策略,并且提供了扩展接口,让使用方方便地定制自己的集群容错策略,通过研究Dubbo框架提供的集群容错策略,可以让我们对分布式系统中的容错技术有深入的理解。

在分布式系统中,每个微服务都是以集群的方式部署的,那么当我们访问一个具体服务时到底访问哪一台机器提供的服务呢?这就是分布式系统中负载均衡器与路由规则要做的事情,作为分布式RPC框架,其自身也必须具有负载均衡的能力。Dubbo框架提供了分布式系统中常见的负载均衡策略,并且提供了扩展接口,让使用者方便地定制自己的负载均衡策略;另外,路由规则提供了服务治理的一种策略,在Dubbo中我们可以通过管理控制台来配置路由规则,让消费者只可访问那些服务提供者。通过研究Dubbo框架提供的负载均衡与路由策略,可以让我们对分布式系统中的负载均衡技术与路由规则有深入的理解。

在分布式系统中,当我们要消费某个服务时,如何找到其地址是一个要解决的问题。在分布式RPC中,一个通用解决方案是引入服务注册中心,当服务提供者启动时,会自动把自己的服务注册到服务注册中心;当消费者启动时,会去服务注册中心订阅自己感兴趣的服务的地址列表。在Dubbo框架中,提供了扩展接口来方便地让我们使用ZooKeeper、Redis等作为服务注册中心,通过研究Dubbo原理,我们可以深刻理解服务提供方到底是如何把服务注册到服务注册中心的,以及服务消费端是如何动态地感知服务提供方地址列表变化的。

所有RPC框架要解决的一个问题是,如何让使用者无感知地发起远程过程调用,也就是让使用者在发起远程调用时有和本地调用一样的体验。Dubbo框架和其他RPC框架一样,采用代理来实现该功能。在Dubbo框架中扩展接口Proxy就是专门来做代理使用的,并且其提供了扩展接口的JDK动态代理与Cglib的实现。研究Dubbo的原理,我们可以学习到消费端如何对服务接口进行代理以实现透明调用,服务提供端如何使用代理与JavaAssist技术来减少反射调用开销。

在Dubbo的分层架构中,Transport网络传输层把Mina 和Netty抽象为统一接口,并且在默认情况下使用Netty作为底层网络通信。通过研究Dubbo,我们可以学习到Dubbo的网络协议帧是如何设计的;服务消费端是如何启动Netty客户端的,是如何把RPC请求封装为协议帧并序列化然后通过Netty客户端发起网络请求的;服务提供端又是如何启动Netty服务器进行服务监听的,是如何处理经典的半包、粘包问题的,是如何把接收到的二进制包转换为Dubbo协议帧并反序列化为POJO对象的。另外,使用Netty时都说不要在ChannelHandler中做阻塞的事情,以免阻塞了I/O线程,使其他请求得不到及时处理,那么这到底是什么意思呢?研究完Dubbo的线程模型后,你就会明白了。

对于网络请求来说,同步调用是比较直截了当的,但是同步调用意味着当前发起请求的调用线程在远端机器返回结果前必须阻塞等待,这显然很浪费资源。好的做法是发起请求的调用线程发起请求后,注册一个回调函数,然后马上返回去做其他事情,当远端把结果返回后再使用I/O线程执行回调函数,也就是发起方实现了异步调用,调用线程不会被阻塞。Dubbo则基于Netty的异步非阻塞能力和JDK 8中的CompletableFuture轻松地实现RPC请求的异步调用,提高了资源利用率。通过研究Dubbo的实现原理,我们可以对异步编程带来的好处以及实现原理有深刻的体会。

……

总之,研究透彻Dubbo框架原理实现后,你会对分布式系统中的很多技术点有深入的理解。而笔者坚信分布式系统是应用的发展方向,因为随着业务规模的增大,为了保障系统的可伸缩性、高可用性,系统必然朝着分布式方向发展。所以,掌握一些分布式系统中的优秀RPC框架的原理及实现细节,无论现在还是将来都将成为区别于他人的核心竞争力。

如何阅读本书

本书分为三部分:第一部分为

基础篇

,首先从整体上讲解使用Dubbo搭建的系统由哪些模块组成,各模块相互之间的调用关系是怎么样的,然后基于本书的Demo讲解如何使用Dubbo;第二部分为高级篇,主要讲解Dubbo框架内部的实现原理,包含支撑Dubbo框架的适配器类原理、动态编译原理、增强SPI原理、消费端的泛化调用实现原理、消费端异步调用与服务提供端的异步执行、Dubbo框架的线程模型、消费端负载均衡策略、消费端集群容错策略、并发控制原理、Dubbo网络协议等;第三部分为实践篇,主要探讨如何使用Arthas和一些Demo来为研究Dubbo框架原理提供方便,并且讲解如何基于CompletableFuture和Netty模拟RPC同步与纯异步调用。

读者可以在博文视点官方网站(http://www.broadview.com.cn)下载本书的Demo源码。基础篇

基础篇简单地讲解Dubbo如何使用以及本书中的Demo实例,建议读者先阅读基础篇再进入后面的章节,因为后面的章节基本上是基于本章的Demo进行讲解的。

第1章 Dubbo基础

1.1 初识Dubbo

整体来说,一个公司业务系统的演进流程基本上都是从单体应用到多体应用。业务系统为单体应用时,不同业务模块的相互调用直接在本地JVM 进程内就可以完成;而变为多个应用时,相互之间进行通信的方式就不是简单地进行本地调用了,因为不同业务模块部署到了不同的JVM进程里,更常见的情况是部署到了不同的机器中,这时候,一个高效、稳定的RPC远程调用框架就变得非常重要。

Dubbo 是阿里巴巴开发的一个开源的高性能的RPC调用框架,是一个致力于提供高性能和透明化的RPC远程调用服务解决方案。作为阿里巴巴SOA服务化治理方案的核心框架,目前它已从Apache孵化器项目毕业,其前景可谓无限光明。

本节概要介绍使用Dubbo构建的分布式系统架构中各个组件服务的作用以及相互关系,其中各个组件之间的关系如图1.1所示。

图1.1是Dubbo的架构图,其中:

· Provider为服务提供者集群,服务提供者负责暴露提供的服务,并将服务注册到服务注册中心。

· Consumer为服务消费者集群,服务消费者通过RPC远程调用服务提供者提供的服务。

· Registry负责服务注册与发现。

· Monitor为监控中心,统计服务的调用次数和调用时间。图1.1

以上各个组件的调用关系如下:

· 服务提供方在启动时会将自己提供的服务注册到服务注册中心。

· 服务消费方在启动时会去服务注册中心订阅自己需要的服务的地址列表,然后服务注册中心异步把消费方需要的服务接口的提供者的地址列表返回给服务消费方,服务消费方根据路由规则和设置的负载均衡算法选择一个服务提供者IP进行调用。

· 监控平台主要用来统计服务的调用次数和调用耗时,即服务消费者和提供者在内存中累计调用服务的次数和耗时,并每分钟定时发送一次统计数据到监控中心,监控中心则使用数据绘制图表来显示。监控平台不是分布式系统必需的,但是这些数据有助于系统的运维和调优。服务提供者和消费者可以直接配置监控平台的地址,也可以通过服务注册中心获取。

1.2 本书Demo详解

1.2.1 Demo结构说明

首先讲解本书使用的Demo的结构,Demo使用Maven聚合功能构建,里面有三个模块,目录如图1.2所示。图1.2

其中:

· Consumer 模块为服务消费者相关模块,本书中所有与消费端有关的Demo都在该模块中,包含普通调用、各种异步调用、泛化调用、基于扩展接口实现的自定义负载均衡策略、集群容错策略等。

· Provider 模块为服务提供者相关模块,本书中所有与服务提供端有关的Demo都在该模块中,包含服务接口的实现类、服务提供方的同步处理请求、各种异步处理请求的实现等。

· SDK模块是一个二方包,用来存放服务接口,这是为了代码复用,在服务提供者和消费者(泛化调用除外)的模块里需要引入这个二方包。

在Demo根目录下执行mvn clean install命令会将这些模块的Jar安装到本地仓库,要想在其他模块里引入这些模块,必须先执行这个安装步骤。

本书的Demo使用ZooKeeper(ZooKeeper 3.4.13)作为服务注册中心,大家可以在其官网下载,并做相应的配置后启动。另外,由于在编写本书时Dubbo最新的版本为2.7.1,所以本书基于Dubbo 2.7.1版本做讲解。

下面我们详细讲解Demo中的每个模块,这里假设ZooKeeper已经启动,并且地址为127.0.0.1:2181。

1.2.2 SDK模块

考虑到代码复用,SDK模块主要用来存放服务接口的定义与POJO类,下面我们看看其中的内容。

· GreetingService接口类主要用来演示同步调用,如下代码就定义了两个方法:

· GrettingServiceAsync接口类主要用来演示基于定义CompletableFuture签名的接口如何实现异步执行,如下代码就定义了一个返回值类型为CompletableFuture的方法:

· GrettingServiceRpcContext接口类主要用来演示AsyncContext如何实现异步执行,如下代码只有一个方法:

· PoJo类,一个简单的POJO(Plain Ordinary Java Object,简单Java对象)对象,用于演示泛化调用时的参数转换:

· Result类,POJO的返回值类型,用于演示泛化调用时的参数转换,定义如下:

1.2.3 同步发布与调用服务

首先我们看看服务提供端是如何使用Dubbo API发布服务的。在了解具体如何发布之前,先看看服务提供端针对SDK中com.books.dubbo.demo.api.GreetingService接口的实现类GreetingServiceImpl的代码:

上面的代码很简单,sayHello()方法休眠1s后返回拼接的字符串。其中RpcContext.getContext().getAttachment("company")获取调用方在上下文对象中附加的company变量的值,如果调用方在调用前没进行设置,则返回null。testGeneric()方法则把传入的poJo对象转换为字符串后返回。

下面我们看看ApiProvider类是如何发布服务的:

代码1创建了ServiceConfig实例,其泛型参数为GreetingService接口;代码2配置应用程序属性;代码3设置服务注册中心地址,可知服务注册中心使用了ZooKeeper,并且ZooKeeper的地址为127.0.0.1,启动端口为2181,服务提供方会将服务注册到该中心。

代码4将接口与实现类设置到ServiceConfig实例;代码5设置服务分组与版本,在Dubbo中,“服务接口+服务分组+服务版本”唯一地确定一个服务,同一个服务接口可以有不同的版本以便服务升级等,另外每个服务接口可以属于不同分组,所以当调用方消费服务时一定要设置正确的分组与版本。

代码7导出服务,启动NettyServer监听链接请求,并将服务注册到服务注册中心;代码8挂起线程,避免服务停止。

下面我们看看Consumer模块是如果同步调用服务的,APiConsumer类的代码如下:

代码9创建服务引用对象实例,其中泛型参数为GreetingService;代码10创建应用程序配置对象;代码11设置服务注册中心地址,可知服务注册中心使用了ZooKeeper,并且ZooKeeper的地址为127.0.0.1,启动端口为2181,服务消费端启动后会从该中心获取服务提供者地址列表。

代码12设置服务接口和超时时间;代码13设置自定义负载均衡策略与集群容错策略,后面会具体讲解;代码14设置服务分组与版本,需要注意的是分组与版本要与服务提供者的分组与版本一致;代码15引用服务;代码16设置隐式参数,然后服务提供者就可以在服务实现类方法里获取参数值;代码17同步发起远程调用,然后当前线程会被阻塞直到服务提供方把结果返回。

1.2.4 服务消费端异步调用服务

上一节我们讲解的服务消费端是同步调用的,也就是说调用线程在服务提供方结果返回前需要被阻塞,异步调用则是说消费端发起调用后会马上返回。本节我们将介绍两种异步调用方式。

1.Dubbo 2.6.*版本提供的异步调用

首先我们看看第一种异步调用方式,也就是Consumer模块中的APiAsyncConsumer类:

代码1创建引用实例,并设置属性;代码2设置调用为异步;代码3进行服务引用并且调用sayHello()方法,由于是异步调用,所以该方法马上返回null;代码4使用RpcContext.getContext().getFuture()获取future对象,然后在需要获取真实响应结果的地方调用future.get()来获取响应结果(调用future.get()会阻塞调用线程直到结果返回)。

上面介绍的基于从返回的future对象调用get()方法实现异步的缺点是当业务线程调用get()方法后业务线程会被阻塞,这不是我们想要的,所以Dubbo提供了在future对象上设置回调函数的方式,让我们实现真正的异步调用。下面是Consumer模块的APiAsyncConsumerForCallBack类:

上面的代码不同之处在于代码4,在((FutureAdapter)RpcContext.getContext().getFuture()).getFuture()获取的future对象上可以调用setCallback()方法设置一个回调函数,该回调函数有两个方法,当远端正常返回响应结果后,会回调done()方法,其参数response就是响应结果值;当发起远程调用发生错误时会回调caught()方法以打印错误信息。

设置回调的这种方式不会阻塞业务调用线程,这是借助了Netty的异步通信机制,Netty底层的I/O线程会在接收到响应后自动回调注册的回调函数,不需要业务线程干预。

2.Dubbo 2.7.*版本提供的异步调用

上面我们介绍了Dubbo 2.7.0版本前提供的异步调用方式,Future方式只支持阻塞式的get()接口获取结果。虽然通过获取内置的ResponseFuture接口可以设置回调,但获取ResponseFuture的API使用起来不方便,并且无法满足让多个Future协同工作的场景,功能比较单一。下面我们讲解Dubbo 2.7.0版本提供的基于CompletableFuture的异步调用。

下面是Consumer模块的APiAsyncConsumerForCompletableFuture2类:

代码4直接使用RpcContext.getContext().getCompletableFuture()获取CompletableFuture类型的future,然后就可以基于CompletableFuture的能力做一系列操作,这里通过调用whenComplete()方法设置了回调函数,作用是当服务提供端产生响应结果后调用设置的回调函数,函数内判断如果异常t不为null,则打印异常信息,否则打印响应结果。

1.2.5 服务提供端异步执行

1.基于定义CompletableFuture签名的接口实现异步执行

在Provider模块中,基于CompletableFuture签名的接口实现异步执行的接口实现类为GrettingServiceAsyncImpl,其代码如下:

通过上面的代码可知,基于定义CompletableFuture签名的接口实现异步执行需要接口方法的返回值为CompletableFuture,并且方法内部使用CompletableFuture.supplyAsync让本该由Dubbo内部线程池中的线程处理的服务,转换为由业务自定义线程池中的线程来处理,CompletableFuture.supplyAsync()方法会马上返回一个CompletableFuture对象(所以Dubbo内部线程池中的线程会得到及时释放),传递的业务函数则由业务线程池bizThreadpool执行。

需要注意的是,调用sayHello()方法的线程是Dubbo线程模型线程池中的线程,而业务处理是由bizThreadpool中的线程处理的,所以代码2.1保存了RPC上下文对象(ThreadLocal变量),以便在业务处理线程中使用。

然后,ApiProviderForAsync类用来发布服务,其代码如下:

上面的代码比较简单,这里就不再讲解了。服务发布后我们看看服务调用端如何调用。我们看看Consumer模块的APiConsumerForProviderAsync类:

上面代码的不同之处在于代码4,即调用greetingService.sayHello("world")直接返回了CompletableFuture对象,并在其上设置了回调函数。

2.使用AsyncContext实现异步执行

在Provider模块中,GrettingServiceAsyncContextImpl使用了AsyncContext实现异步执行,具体代码如下:

代码1创建了一个自定义线程池用来执行业务处理;代码2.1调用RpcContext.startAsync()方法开启服务异步执行,返回一个asyncContext,然后把服务处理任务提交到业务线程池后sayHello()方法就直接返回了null;异步任务内首先执行代码2.2切换任务的上下文,然后休眠500ms充当任务执行;最后,代码2.3把任务执行结果写入异步上下文。

这里由于具体执行业务处理的逻辑不在sayHello()方法所在的Dubbo内部线程池的线程里,所以Dubbo框架的线程不会被阻塞。

1.2.6 服务消费端泛化调用

前面我们讲到,当基于Dubbo API搭建Dubbo服务时,服务消费端需要引入一个SDK二方包,其中存放着服务提供端提供的所有接口类。

泛化接口调用方式主要在服务消费端没有API接口类及模型类元(比如入参和出参的POJO 类)的情况下使用,其参数及返回值中没有对应的POJO 类,所以所有POJO参数均转换为Map表示。使用泛化调用时,服务消费模块不再需要引入SDK二方包。

在Dubbo中,根据序列化方式的不同,分为三种泛化调用,分别为true、bean和nativejava。

1.generic=true方式

在Consumer模块的APiGenericConsumerForTrue类中演示了这种方式,其代码如下:代码1创建引用实例,这里需要注意的是,在泛型调用时,泛型参数固定为GenericService;代码2设置泛化调用类型为true;代码3获取引用,注意泛化值类型固定为GenericService。

代码4调用greetingService.$invoke()方法,其中第一个参数为sayHello,说明要调用sayHello()方法,第二个参数为sayHello的参数类型,第三个参数为sayHello参数的值。

代码5和代码6是对方法testGeneric()发起泛型调用,这里的第二个参数为testGeneric入参的POJO类型;第三个参数为入参的值,这里由于为POJO类,所以把POJO类的属性转换到了Map里,并且Map中固定有一个key为class的属性,其对应的value为第二个参数的包名加类名,这是为了在服务提供端进行反射使用。

另外,由于这里的testGeneric()方法返回值是Result类型,是POJO类,所以返回值也会被转换为Map。

2.generic=bean方式

在Consumer模块的APiGenericConsumerForBean类中演示了这种方式,其代码如下:

上面代码的不同之处在于代码2设置了泛型类型为bean,这意味着对参数使用JavaBean方式进行序列化,如代码4使用JavaBeanSerializeUtil.serialize把参数进行序列化,然后把序列化结果作为第三个参数,最后执行泛化调用。

代码5对响应结果使用JavaBeanSerializeUtil.deserialize进行反序

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载