dubbo框架自己理解

RPC是远程调用过程的简写,是一个协议,处于网络通信协议的第五层:会话层,其下就是TCP/IP协议,在建立在其基础上的通信会话协议。RPC定义了交互的模式,而应用程序使用这些模式,来访问其他服务器的方法,并不需要关系具体的网络上的细节。     

     一、RPC基础知识   

     1.RPC模式

     RPC采用C/S模式,客户端发送请求,服务端响应。

     基于底层的协议,比如TCP/IP模式。

     2.设计目的

     ①通过固定的协议,调用非本机的方法

     ②实现不同程序语言之间的通信

     ③不需要了解底层协议,像本地方法一样调。它完全封装了网络传输,以及其他细节。

     二、RPC过程详解

          

                                                图一   RPC调用过程

     从RPC的角度看,应该有服务的提供方,即生产者;还有服务的调用方,即消费者。

     对消费者来时,在RPC调用过程中,使用第1步、第2步、第3步、第4步是透明的,其他的都是使用RPC框架去封装这些事情。当应用开始调用PRC的方式时,就会去容器中去取Bean对象,所以我们应该首先注册Bean对象到容器中,我们通过Java的动态代理,将代理过程封装到代理对象中,代理对象实现接口,创建实例到容器中。相应的,在调用远程对象的对象方法时,就会调用动态代理中的方法,这就是代理层的作用。

     代理对象在获取到请求方法、接口和参数时,就会用序列化层,将这些信息封装成一个请求报文,再让通信层向服务端传送报文的内容,然后就到了生产者这块。

     相应的服务必须有个监听器,来监听来自其他服务的请求,一般都会用容器做消息的监听,就会调用对应的Bean对象的方法,去处理响应的请求。当然,RPC框架不会让容器中的每一个框架都会被调用,所以只有注册了的Bean才会被RPC的请求调用到。然后,通过请求中的类、方法、参数,反射调用对应的Bean,拿到结果之后,通过序列化层,封装好结果报文,服务端的通信层将报文反馈给调用方,调用方解析到返回值,动态代理类返回结果,调用结束。

     这样,一个完整的RPC调用反馈链条就完成了。

     1.消费者设计

               

                                               图二  消费者设计

     ①代理层:

     消费者将对应的接口,通过RPC框架的代理来生成一个对象到Spring容器中。代理层将代理接口生成该接口的对象,该对象处理调用时传过来的对象、方法、参数,通过序列化层封装好,调用网络层。

     ②序列化层:

     将请求的参数序列化成报文;将返回的报文反序列化成对象;

     ③网络层:

     将报文与服务端通信;接收返回结果。

     2.生产者设计

            

                                              图三  生产者设计 

     ①代理层:

     一个应用提供服务,必须由一个网络监听的模块,这个模块大多有开源的容器来处理网络上的监听;服务需要注册,只有注册了的服务才可以被调用;注册的服务需要被我们发射调用到,来进行相应的处理。

     ②序列化层:

     就是相应的做请求的反序列化和结果的序列化。

     ③网络层:

     接收客户端报文;将序列化的结果返回给客户端。

     三、RPC模式总结

                     

                                                 图三  RPC模式总结

     1.Proxy代理层

     用于对象的代理;对象的反射调用;RPC流程的控制。

     2.Serialize序列化层

     将请求序列化和结果反序列化。

     3.Invoke网络模块

     主要用于网络通信的相关处理。

     4.Container容器组件

     这层主要用于代理层监听网络请求。

(1)什么是RPC

RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。那么我们至少从这样的描述中挖掘出几个要点:

  • RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。这里要说明一下,目前技术的发展趋势来看,实现了RPC协议的应用工具往往都会附加其他重要功能,例如Dubbo还包括了服务管理、访问权限管理等功能。

  • 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。既然网络协议对其透明,那么调用过程中,使用的是哪一种网络IO模型调用者也不需要关心。

  • 信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。

  • 应该有跨语言能力:为什么这样说呢?因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。

那么上面的描述情况可以用下图表示:

这里写图片描述

 

(2)RPC要素

当然,上图是作为RPC的调用者所观察到的现象(而实际情况是客户端或多或少的还是需要知道一些调用RPC的细节)。但是我们是要讲解RPC的基本概念,所以RPC协议内部是怎么回事就要说清楚:

这里写图片描述

  • Client:RPC协议的调用方。就像上文所描述的那样,最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用。但实际情况来说Client或多或少的都需要指定RPC框架的一些细节。

  • Server:在RPC规范中,这个Server并不是提供RPC服务器IP、端口监听的模块。而是远程服务方法的具体实现(在JAVA中就是RPC服务接口的具体实现)。其中的代码是最普通的和业务相关的代码,甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用。

  • Stub/Proxy:RPC代理存在于客户端,因为要实现客户端对RPC框架“透明”调用,那么客户端不可能自行去管理消息格式、不可能自己去管理网络传输协议,也不可能自己去判断调用过程是否有异常。这一切工作在客户端都是交给RPC框架中的“代理”层来处理的。

  • Message Protocol:在上文我们已经说到,一次完整的client-server的交互肯定是携带某种两端都能识别的,共同约定的消息格式。RPC的消息管理层专门对网络传输所承载的消息信息进行编号和解码操作。目前流行的技术趋势是不同的RPC实现,为了加强自身框架的效率都有一套(或者几套)私有的消息格式。例如前文所讲到的RMI框架使用的消息协议为JRMP;后文我们将详细讲解的RPC框架Thrift也有私有的消息协议,“- Transfer/Network Protocol”(当然它还支持一些通用的消息格式,如JSON)。

  • Transfer/Network Protocol:传输协议层负责管理RPC框架所使用的网络协议、网络IO模型。例如Hessian的传输协议基于HTTP(应用层协议);而Thrift的传输协议基于TCP(传输层协议)。传输层还需要统一RPC客户端和RPC服务端所使用的IO模型;常用的IO模型在之前已经详细讲解过了(可参见我之前的博文《架构设计:系统间通信(3)——IO通信模型和JAVA实践 上篇》)

  • Selector/Processor:存在于RPC服务端,由于服务器端某一个RPC接口的实现的特性(它并不知道自己是一个将要被RPC提供给第三方系统调用的服务)。所以在RPC框架中应该有一种“负责执行RPC接口实现”的角色。它负责了包括:管理RPC接口的注册、判断客户端的请求权限、控制接口实现类的执行在内的各种工作。

  • IDL:实际上IDL(接口定义语言)并不是RPC实现中所必须的。但是需要跨语言的RPC框架一定会有IDL部分的存在。这是因为要找到一个各种语言能够理解的消息结构、接口定义的描述形式。如果您的RPC实现没有考虑跨语言性,那么IDL部分就不需要包括,例如JAVA RMI因为就是为了在JAVA语言间进行使用,所以JAVA RMI就没有相应的IDL。

  • 一定要说明一点,不同的RPC框架实现都有一定设计差异。例如生成Stub的方式不一样,IDL描述语言不一样、服务注册的管理方式不一样、运行服务实现的方式不一样、采用的消息格式封装不一样、采用的网络协议不一样。但是基本的思路都是一样的,上图中的所列出的要素也都是具有的。

2、RPC框架的性能依据

这里写图片描述

在物理服务器性能相同的情况下,以下几个因素会对一款RPC框架的性能产生直接影响:

  • 所支持的网络IO模型:您的RPC服务器可以只支持传统的阻塞式同步IO,也可以做一些改进让您的RPC服务器支持非阻塞式同步IO,或者在您的服务器上实现对多路IO模型的支持。这样的RPC服务器的性能在高并发状态下,会有很大的差别。特别是单位处理性能下对内存、CPU资源的使用率。

  • 基于的网络协议:一般来说您可以选择让您的RPC使用应用层协议,例如HTTP或者之前我们提到的HTTP/2协议,或者使用TCP协议,让您的RPC框架工作在传输层。工作在哪一层网络上会对RPC框架的工作性能产生一定的影响,但是对RPC最终的性能影响并不大。但是至少从各种主流的RPC实现来看,没有采用UDP协议做为主要的传输协议的。

  • 选择的消息封装格式:选择或者定义一种消息格式的封装,要考虑的问题包括:消息的易读性、描述单位内容时的消息体大小、编码难度、解码难度、解决半包/粘包问题的难易度。当然如果您只是想定义一种RPC专用的消息格式,那么消息的易读性可能不是最需要考虑的。消息封装格式的设计是目前各种RPC框架性能差异的最重要原因,这就是为什么几乎所有主流的RPC框架都会设计私有的消息封装格式的原因。

  • 实现的服务处理管理方式:在高并发请求下,如何管理注册的服务也是一个性能影响点。您可以让RPC的Selector/Processor使用单个线程运行服务的具体实现(这意味着上一个客户端的请求没有处理完,下一个客户端的请求就需要等待)、您也可以为每一个RPC具体服务的实现开启一个独立的线程运行(可以一次处理多个请求,但是操作系统对于“可运行的最大线程数”是有限制的)、您也可以线程池来运行RPC具体的服务实现(目前看来,在单个服务节点的情况下,这种方式是比较好的)、您还可以通过注册代理的方式让多个服务节点来运行具体的RPC服务实现。

在上文中的传输管理层分析源代码,基本原理如下:
  1. client一个线程调用远程接口,生成一个唯一的ID(比如一段随机字符串,UUID等),Dubbo是使用AtomicLong从0开始累计数字的
  2. 将打包的方法调用信息(如调用的接口名称,方法名称,参数值列表等),和处理结果的回调对象callback,全部封装在一起,组成一个对象object
  3. 向专门存放调用信息的全局ConcurrentHashMap里面put(ID, object)
  4. 将ID和打包的方法调用信息封装成一对象connRequest,使用IoSession.write(connRequest)异步发送出去
  5. 当前线程再使用callback的get()方法试图获取远程返回的结果,在get()内部,则使用synchronized获取回调对象callback的锁, 再先检测是否已经获取到结果,如果没有,然后调用callback的wait()方法,释放callback上的锁,让当前线程处于等待状态。
  6. 服务端接收到请求并处理后,将结果(此结果中包含了前面的ID,即回传)发送给客户端,客户端socket连接上专门监听消息的线程收到消息,分析结果,取到ID,再从前面的ConcurrentHashMap里面get(ID),从而找到callback,将方法调用结果设置到callback对象里。
  7. 监听线程接着使用synchronized获取回调对象callback的锁(因为前面调用过wait(),那个线程已释放callback的锁了),再notifyAll(),唤醒前面处于等待状态的线程继续执行(callback的get()方法继续执行就能拿到调用结果了),至此,整个过程结束。

使用NIO设计RPC调用分析

前面提到,由于NIO的SocketChannel是非阻塞的,所以不再需要连接池,使用一个连接就够了。

NIO单一长连接RPC线程模型

但是如果真的使用NIO来进行RPC调用的话,会有数据和调用方对应不上的问题,如下图:

NIO异步顺序问题

如上图所示,如果多个线程共用一个连接,那么每个线程调用之后返回的顺序是不可控的,所以有可能先发出数据的反而后得到返回值,这就使得数据对应不上了。个人觉得因为这一点,NIO及其适合聊天室类型的设计,因为每个聊天方都是一个单独的SocketChannel连接,而此时并没有顺序问题。

但是对RPC调用来说,每次调用的返回值必须与调用方对应上,为此,Dubbo的设计是给每个请求设计一个请求id,在发送请求与发送返回值时都带上这个id。详细思路如下图:

NIO单一长连接的RPC设计

业务线程在发出请求之前,需要存储一个请求对象,同时挂起相应的业务线程(挂起不会被任务调度,所以不存在线程切换消耗),这个请求对象包含了此次请求的id,然后在获取服务端返回的数据的时候,解析出这个id,通过这个id取出请求对象,并唤醒对应的线程。



作者:峡客
链接:https://www.jianshu.com/p/13bef2795c44
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自blog.csdn.net/weixin_36098377/article/details/82885162