RPC(Remote Procedure Call Protocol)即远程过程调用,
允许一台计算机调用另一台计算机上的程序得到结果,
它是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议简言之RPC使得程序能够像访问本地系统资源一样,
而代码中不需要做额外的编程,就像在本地调用一样,去访问远端系统资源。
比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等,注册中心一般为ZooKeeper或者单机直连等
现在互联网应用的量级越来越大,单台计算机的能力有限,需要借助可扩展的计算机集群来完成,分布式的应用可以借助RPC来完成机器之间的调用。
RPC不是一门技术,RPC只是一个概念
直观理解的RPC
1:屏蔽网络编程细节,实现调用远程方法就跟调用本地一样的体验。
2:不需要因为这个方法是远程调用就需要编写很多与业务无关的代码。
这就好比建在小河上的桥一样连接着河的两岸,如果没有小桥,
我们需要通过划船、绕道等其他方式才能到达对面,但是有了小桥之后,
我们就能像在路面上一样行走到达对面,并且跟在路面上行走的体验没有区别
3:屏蔽远程调用跟本地调用的区别, 隐藏底层网络通信的复杂性, 专注业务逻。
具体调用过程:
1、服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;
2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;
3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;
4、服务端存根(server stub)收到消息后进行解码(反序列化操作);
5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;
6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);
7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;
8、客户端存根(client stub)接收到消息,并进行解码(反序列化);
9、服务消费方得到最终结果;
而RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务
对RPC通信流程解
协议格式:明确数据内容格式(json,xml,javabean,其他内容格式)。
如:把数据格式的约定内容叫做xml协议。大多数的协议会分成两部分,分别是数据头和消息体。 数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。
序列化: 网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。
对象是肯定没法直接在网络中传输的. 需要提前把它转成可传输的二进制. 并且要求转换算法是可逆的。 调用方持续地把请求参数序列化成二进制后,经过TCP传输给了服务提供方。服务提供方从TCP 通道里面收到二进制数据,那如何知道一个请求的数据到哪里结束,是一个什么么类型的请求呢?
通信协议:TCP/HTTP/MQ/其他.均建立在TCP之上
反序列化: 根据协议格式,服务提供方就可以正确地从二进制数据中分割出不同的请求来,同时根据请求类型 和序列化类型,把二进制的消息体逆向还原成请求对象。这个过程叫作"反序列化。
服务返回: 服务提供方再根据反序列化出来的请求对象找到对应的实现类,完成真正的方法调用,然后把执行结果序列化后,回写到对应的TCP诵道里面。调用方获取到应答的数据包后,冉反序列化成应答对象,这样调用方就完成了一次RPC调用。
AOP代理:
通过springAOP动态代理的技术,对方法进行拦截增强,以便于增加需要的额外处理逻辑。
由服务提供者给出业务接口声明,在调用方的程序里面,RPC框架根据调用的服务接口提前生成动
态代理实现类,并通过依赖注入等技术注入到声明了该接口的相关业务逻辑里面。该代理实现类会
拦截所有的方法调用,在提供的方法处理逻辑里面完成一整套的远程调用,并把远程调用结果返回
给调用方,这样调用方在调用远程方法的时候就获得了像调用本地接口一样的体验。
RPC为什么要序列化?
例子帮助理解:
比如发快递:
我们要发一个需要自行组装的物件,发件人发之前,会把物件拆开装箱,这就好比序列化;
这时候快递员来了,不能磕碰呀,那就要打包,这就好比将序列化后的数据进行编码,封装成一个固定格式的协议;
过了两天,收件人收到包裹了,就会拆箱将物件拼接好,这就好比是协议解码和反序列化。
因为网络传输的数据必须是二进制数据,
所以在调用中,对入参对象与返回值对象进行序列化与反序列化是一个必须的过程。
序列化:
当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。
反序列化:
当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),
即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用,通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值
有哪些常用的序列化方式
1:JDK原生序列化
2:JSON序列化
3:Hessian序列化
4:Protobuf序列化
5:Kero序列化
RPC框架如何选择序列化?
1:性能和效率(性能越好,序列化及反序列化就越快)
2:空间开销(体积越小,网络传输数据量就小,传输数据就越快)
3:协议的通用性及兼容性(优先级最高,直接关系到服务的可用率和稳定性)
4:协议的安全性(JDK原生序列化存在漏洞)
RPC框架在使用时注意的问题
1:对象构造过于复杂:属性很多,嵌套很多,聚合很多其他对象,依赖过于复杂,
序列化及反序列化的时候越浪费性能,消耗CPU严重影响整体性能,出现问题概率就越高
2:对象过于庞大:如入参对象为大list或大Map,序列化之后字节长度达到上兆字节,严重浪费服务器性能,CPU及带宽
3:使用序列化框架不支持的类作为入参类:hessian不支持linkhashmap,linkhashset,使用第三方集合类,尽量选择原生常用的集合类hashMap,ArrayList
4:对象有复杂的继承关系:大多数序列化框架在序列化对象的时候,都会将对象属性进行序列化,当有继承关系时,会不停地寻找父类遍历属性,越复杂就越浪费性能,出错概率就越高
总结:
1:对象要尽量简单,没有太多依赖关系,属性不要太多,尽量高聚合
2:入参对象与返回值对象不要太大,更不要传太大的集合
3:尽量使用简单的常用的开发语言原生的对象,尤其是集合类
4:对象不要有复杂的继承关系,最好不要有父子类的情况
网络通信模型的选择
RPC是解决进程间通信的一种方式。
一次RPC调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。
服务调用者通过网络发送一条请求消息,服务提供者接收并解析,处理完相关的业务逻辑之后,再发送一条响应消息给服务调用者,服务调用者接收并解析响应消息,处理完相关的响应逻辑,一次RPC调便结束了,网络通信是整个RPC调用流程的基础。
下图属于通信方式属于那种通信IO模型呢?
常见的网络IO模型分为四种:
1:同步阻塞IO(BIO)
2:同步非阻塞IO(NIO)
3:IO多路复用和异步
4:非阻塞IO(AIO)
同步阻塞IO(BIO)
同步阻塞IO是最简单最常见的IO模型,
默认情况下所有的socket都是阻塞的.
操作流程。
1: 应用进程发起IO系统调用后,应用进程被阻塞,转到内核空间处理。
2: 内核开始等待数据,等待到数据之后,再将内核中的数据拷贝到用户内存中,整个处理完毕后返回进程。
3: 应用的进程解除阻塞状态,运行业务逻辑。
这里我们可以看到,系统内核处理IO操作分为两个阶段等待数据和拷贝数据。
而在这2阶段中,应用进程中IO操作的线程会一直都处于阻塞状态,
也就是说,内核准备数据和数据从内核拷贝到用户空间这两个过程都是阻塞的
如果是基于java多线程开发,那么每一个IO操作都要占用线程,直至IO操作结束。
这个流程就好比我们去餐厅吃饭,我们到达餐厅,向服务员点餐,之后要一直在餐厅等待后厨将菜做好,然后服务员会将菜端给我们,我们才能享用。
优点:
能够及时返回数据,无延迟;
调用代码逻辑简单;
缺点:
等待浪费很多时间,影响程序性能
同步非阻塞IO(NIO)
同步非阻塞IO即在IO系统调用的过程中,进程不必阻塞,
而是采用定时轮询(polling)的方式数据是否准备就绪;
在此期间,进程可以处理其他的任务。
1:进程发起read,进行recvfrom系统调用,如果内核中的数据还没有准备好,就立刻返回一个error;
2:调用返回后进程可以进行其他操作,然后再次发起recvfrom系统调用,不断重复;(这个过程称为轮询polling)
3:内核中的数据准备好以后,再次收到recvfrom调用,就将数据拷贝到了用户内存,然后返回;
注意:在数据从内核拷贝到用户内存的过程中,进程仍然是属于阻塞的状态
优点:
能够在IO操作过程中,处理其他的任务。
缺点:
任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,
而任务可能在两次轮询之间的任意时间完成,这会导致整体数据吞吐量的降低。
IO多路复用(IO multiplexing)
I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
常见的select, poll, epoll 都是IO多路复用。
需要注意的是select,poll,epoll本质上都是同步I/O,
因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
Java底层的NIO,redis,nginx底层IO模型就是IO多路复用模型
异步IO(asynchronous IO)
异步IO是事件驱动IO。
用户进程发起IO操作之后,会立即返回,然后可以处理其他任务。
内核会等待数据准备完成,然后将数据拷贝到用户内存。
当这一切都完成之后,内核会给用户进程发送一个信号,通知IO操作完成。
在IO两个阶段,进程都是非阻塞的。目前有很多开源的异步IO库,例如libevent、libev、libuv。
IO多路复用更适合高并发的场景,可以使用较少的进程处理较多的socket的IO请求,如Netty
阻塞IO每处理一个socket的IO请求都会阻塞进程,并发量低,业务逻辑同步进行IO操作,阻塞IO可满足,开销比IO多路复用低
RPC调用在大多数的情况下,是高并发的调用场景 ,故网络通信模型会选择IO多路复用的方式,
最优的选择是基于Reactor模式实现的框架,Java环境.首选Netty框架
Reactor介绍:https://www.cnblogs.com/winner-0715/p/8733787.html
RPC中的动态代理
RPC 框架解决的问题是:像调用本地接口一样调用远程的接口.
那么可使用动态代理去解决:
1:如何组装数据报文
2:经过网络传输发送至服务提供方
3:屏蔽远程接口调用的细节
不需要改动原有代码的前提下,实现非业务逻辑跟业务逻辑的解耦。
通过对字节码进行增强,在方法调用的时候进行拦截,以便于在方法调用前后,增加需要的额外处理逻辑,如屏蔽网络通信过程