1.定义:
RPC 的全称是 Remote Procedure Call 是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的,本质上编写的调用代码基本相同。
2.功能目标:
RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。
3.结构:
RPC 的程序包括 5 个部分:User、User-stub、RPCRuntime、 Server-stub、Server
RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的RpcInvoker 通过连接器RpcConnector 去维持与服务端的通道RpcChannel,并使用RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用RpcProtocol 执行协议解码(decode)。解码后的调用信息传递给RpcProcessor 去控制处理调用过程,最后再委托调用给RpcInvoker 去实际执行并返回调用结果。
4.调用分类:
(1)同步调用 : 客户方等待调用执行完成并返回结果。
(2)异步调用 : 客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。 若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。
异步和同步的区分在于是否等待服务端执行完成并返回结果。
5.组件分类:
(1) RpcServer 负责导出(export)远程接口
(2) RpcClient 负责导入(import)远程接口的代理实现
(3)RpcProxy 远程接口的代理实现
(4) RpcInvoker 客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回 服务方实现:负责调用服务端接口的具体实现并返回调用结果
(5) RpcProtocol 负责协议编/解码
(6) RpcConnector 负责维持客户方和服务方的连接通道和发送数据到服务方
(7)RpcAcceptor 负责接收客户方请求并返回请求结果
(8)RpcProcessor 负责在服务方控制调用过程,包括管理调用线程池、超时时间等
(9)RpcChannel 数据传输通道
6.导出远程接口 :
导出远程接口的意思是指只有导出的接口可以供远程调用,而未导出的接口则不能。
7.导入远程接口与客户端代理 :
导入相对于导出远程接口,客户端代码为了能够发起调用必须要获得远程接口的方法或过程定义。目前,大部分跨语言平台 RPC 框架采用根据 IDL 定义通过 code generator 去生成 stub 代码,这种方式下实际导入的过程就是通过代码生成器在编译期完成的。我所使用过的一些跨语言平台 RPC 框架如 CORBAR、WebService、ICE、Thrift 均是此类方式。 代码生成的方式对跨语言平台 RPC 框架而言是必然的选择,而对于同一语言平台的 RPC 则可以通过共享接口定义来实现。
8.协议编解码 :
客户端代理在发起调用前需要对调用信息进行编码,这就要考虑需要编码些什么信息并以什么格式传输到服务端才能让服务端完成调用。出于效率考虑,编码的信息越少越好(传输数据少),编码的规则越简单越好(执行效率高)。需要的编码信息: 调用编码 -- 接口方法(接口名、方法名)方法参数(参数类型、参数值)调用属性(调用属性信息,例如调用附件隐式参数、调用超时时间等 -- 返回编码 -- 返回结果(接口方法中定义的返回值)返回码(异常返回码)返回异常信息(调用异常信息)除了以上这些必须的调用信息,我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。这样我们的编码消息里面就分成了两部分,一部分是元信息、另一部分是调用的必要信息。如果设计一种 RPC 协议消息的话,元信息我们把它放在协议消息头中,而必要信息放在协议消息体中。下面给出一种概念上的 RPC 协议消息设计格式:
-- 消息头 -- magic: 协议魔数,为解码设计 ; header size: 协议头长度,为扩展设计 ;version : 协议版本,为兼容设计;st : 消息体序列化类型 ;hb : 心跳消息标记,为长连接传输层心跳设计 ; ow : 单向消息标记; rp : 响应消息标记,不置位默认是请求消息 ;status code: 响应消息状态码; reserved : 为字节对齐保留;message id : 消息 id;body size : 消息体长度 ;
-- 消息体 -- 采用序列化编码,常见有以下格式 xml: 如 webservie soap;json: 如 JSON-RPC ;binary: 如 thrift; hession; kryo 等。格式确定后编解码就简单了,由于头长度一定所以我们比较关心的就是消息体的序列化方式。序列化我们关心三个方面:(1)序列化和反序列化的效率,越快越好。(2)序列化后的字节长度,越小越好。(3) 序列化和反序列化的兼容性,接口参数对象若增加了字段,是否兼容。
9. 传输服务:
协议编码之后,自然就是需要将编码后的 RPC 请求消息传输到服务方,服务方执行后返回结果消息或确认消息给客户方。RPC 的应用场景实质是一种可靠的请求应答消息流,和 HTTP 类似。因此选择长连接方式的 TCP 协议会更高效,与 HTTP 不同的是在协议层面我们定义了每个消息的唯一 id,因此可以更容易的复用连接。 既然使用长连接,那么第一个问题是到底 client 和 server 之间需要多少根连接?实际上单连接和多连接在使用上没有区别,对于数据传输量较小的应用类型,单连接基本足够。单连接和多连接最大的区别在于,每根连接都有自己私有的发送和接收缓冲区,因此大数据量传输时分散在不同的连接缓冲区会得到更好的吞吐效率。所以,如果你的数据传输量不足以让单连接的缓冲区一直处于饱和状态的话,那么使用多连接并不会产生任何明显的提升,反而会增加连接管理的开销。 连接是由 client 端发起建立并维持。如果 client 和 server 之间是直连的,那么连接一般不会中断(当然物理链路故障除外)。如果 client 和 server 连接经过一些负载中转设备,有可能连接一段时间不活跃时会被这些中间设备中断。为了保持连接有必要定时为每个连接发送心跳数据以维持连接不中断。心跳消息是 RPC 框架库使用的内部消息,在前文协议头结构中也有一个专门的心跳位,就是用来标记心跳消息的,它对业务应用透明。
10.执行调用:
client stub 所做的事情仅仅是编码消息并传输给服务方,而真正调用过程发生在服务方。server stub 从前文的结构拆解中我们细分了 RpcProcessor 和 RpcInvoker 两个组件,一个负责控制调用过程,一个负责真正调用。这里我们还是以 java 中实现这两个组件为例来分析下它们到底需要做什么? java 中实现代码的动态接口调用目前一般通过反射调用。除了原生的 jdk 自带的反射,一些第三方库也提供了性能更优的反射调用,因此 RpcInvoker 就是封装了反射调用的实现细节。
11.RPC 异常处理
无论 RPC 怎样努力把远程调用伪装的像本地调用,但它们依然有很大的不同点,而且有一些异常情况是在本地调用时绝对不会碰到的。在说异常处理之前,我们先比较下本地调用和 RPC 调用的一些差异:(1)本地调用一定会执行,而远程调用则不一定,调用消息可能因为网络原因并未发送到服务方。(2) 本地调用只会抛出接口声明的异常,而远程调用还会跑出 RPC 框架运行时的其他异常。(3) 本地调用和远程调用的性能可能差距很大,这取决于 RPC 固有消耗所占的比重。 正是这些区别决定了使用 RPC 时需要更多考量。当调用远程接口抛出异常时,异常可能是一个业务异常,也可能是 RPC 框架抛出的运行时异常(如:网络中断等)。业务异常表明服务方已经执行了调用,可能因为某些原因导致未能正常执行,而 RPC 运行时异常则有可能服务方根本没有执行,对调用方而言的异常处理策略自然需要区分。由于 RPC 固有的消耗相对本地调用高出几个数量级,本地调用的固有消耗是纳秒级,而 RPC 的固有消耗是在毫秒级。那么对于过于轻量的计算任务就并不合适导出远程接口由独立的进程提供服务,只有花在计算任务上时间远远高于 RPC 的固有消耗才值得导出为远程接口提供服务。