当你在构建一个分布式系统时,势必需要考虑的一个问题是:如何实现服务与服务之间高效调用?当然,你可以使用Dubbo或Spring Cloud等分布式服务框架来完成这个目标,这些框架帮助我们封装了技术实现的复杂性。那么,假如没有这些框架,而需要自己来实现远程调用,你应该怎么做的?这就需要引入今天讨论的话题:RPC架构。
RPC架构的基本结构
RPC的英文全称为Remote Procedure Call,也就是远程过程调用。我们通常把发生交互关系的两个服务分别称为服务的提供者(Provider)和消费者(Consumer)。简单来说,RPC就是用来实现服务的消费者向提供者发起远程调用的过程,这是RPC最简单的一种表现形式。
接下来,如果我们把上图做一些展开。如果想要实现服务提供者和消费者之间的有效交互,那么两者之间就需要确立与网络通信相关的网络协议以及通信通道。同时,服务的提供者需要把自己的服务调用入口暴露出来,并时刻准备接收来自消费者的请求。这样,RPC架构就演变成这个样子。
在上图中,我们把通信通道和网络协议分别命名为RpcChannel和RpcProtocol,而把提供者接收请求的组件称为RpcAcceptor,把消费者发起请求的组件称为RpcConnector。
然后,对于服务提供者和消费者而言,为了双方能够正常识别所发送的请求和所接收到的响应结果,需要定义统一的契约。我们把这种契约称为远程API,以与本地API进行区别。基于同一套远程API的定义,RPC架构就具备了根据业务来定义通信契约的能力。
类似的,为了我们更好的区分RPC架构中的角色,我们把真正提供业务服务的组件称为RpcServer,而把发起真实客户端请求的组件称为RpcClient。这样,RpcServer负责实现远程API,而RpcClient负责调用远程API。
当然,对于远程API而言,服务提供者和消费者的处理方式显然是不一样的。提供者需要根据消费者的请求来调用RpcServer的具体实现并返回结果,这部分的工作由RpcInvoker来执行,而消费者通过RpcCaller组件对请求进行编码之后发送到服务方并等待结果。
最后,为了降低开发人员的开发难度,让远程调用的执行过程看上去就先在执行本地方法一样,在主流的RPC实现方法中通常都会在客户端添加代理机制的实现组件RpcProxy,从而提供远程服务本地化访问的入口。另一方面,在服务器端,为了更好地控制业务方法执行过程,通常也会引入具备线程管理、超时控制等机制的RpcProcessor组件。
这样,我们对整个RPC架构的演进过程做了详细的描述。可以看到RPC架构有左右对称的两大部分构成,分别代表了一个远程过程调用的客户端和服务器端组件。客户端组件与职责包括:
- RpcClient,负责导入(import)由RpcProxy提供的远程接口的代理实现
- RpcProxy,远程接口的代理实现,提供远程服务本地化访问的入口
- RpcCaller,负责编码和发送调用请求到服务方并等待结果
- RpcConnector,负责维持客户端和服务端连接通道和发送数据到服务端
服务端组件与职责包括:
- RpcServer,负责导出(export)远程接口
- RpcInvoker,负责调用服务端接口的具体实现并返回结果
- RpcAcceptor,负责接收客户方请求并返回请求结果
- RpcProcessor,负责在服务方控制调用过程,包括管理调用线程池、超时时间等
而客户端和服务器端所共有的组件包括:
- RpcProtocol,负责网络传输协议的编码和解码
- RpcChannel,网络数据传输通道
这样,我们对一个典型RPC架构中的基本结构和组件已经都了解了,接下来我们再来重点分析想要实现这个架构中所应该具备的技术体系。
RPC架构的技术体系
从RPC架构的基本结构和组件出发,我们可以进一步梳理想要实现RPC架构的技术体系,包括网络通信、序列化、传输协议和远程调用。
网络通信
首当其中的无疑是网络通信。网络通信涉及面很广,RPC架构中的网络通信关注于网络连接、IO模型和可靠性设计。
基于TCP协议的网络连接有两种基本方式,也就是通常所说的长连接和短连接。长连接和短连接的产生在于客户端和服务器端采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。在RPC框架实现过程中,考虑到性能和服务治理等因素,通常使用长连接进行通信,典型的实现框架就是Dubbo。
关于IO模型,最简单、最基础的就是阻塞式IO(Blocking IO,BIO),BIO要求客户端请求数与服务端线程数一一对应,显然服务端可以创建的线程数会成为系统的瓶颈。因此,在RPC架构中,我们通常都会使用非阻塞IO(Non-blocking IO,NIO)技术来提供性能。
由于存在网络闪断、超时等网络状态相关的不稳定性以及业务系统本身的故障,网络之间的通信必须在发生上述问题时能够快速感知并修复。常见的网络通信保障手段包括链路有效性检测以及断线之后的重连处理等
序列化
想要在网络上传输数据,就需要用到数据序列化技术。序列化的方式有很多,常见的有文本和二进制两大类。XML和JSON是文本类序列化方式的代表,而二进制实现的方案包括Google的Protocol Buffer和Facebook的Thrift等。
性能可能是我们在序列化工具选择过程中最看重的一个指标。性能指标主要包括序列化之后码流大小、序列化/反序列化速度和CPU/内存资源占用。下表中我们列举了目前主流的一些序列化技术:
序列化时间 | 反序列化时间 | 大小 | 压缩后大小 | |
Java | 8654 | 43787 | 889 | 541 |
hessian | 6725 | 10460 | 501 | 313 |
protocol buffer | 2964 | 1745 | 239 | 149 |
thrift | 3177 | 1949 | 349 | 197 |
json-lib | 45788 | 149741 | 485 | 263 |
jackson | 3052 | 4161 | 503 | 271 |
fastjson | 2595 | 1472 | 468 | 251 |
可以看到在序列化和反序列化时间维度上Alibaba的fastjson具有一定优势,而从空间维度上看,相较其他技术我们可以优先选择Protocol Buffer。
传输协议
在ISO/OSI的7层网络模型中,RPC架构的设计和实现通常会涉及传输层及以上各个层次的相关协议,通常所说的TCP协议就属于传输层,而HTTP协议则位于应用层。传输协议的消息包括消息头和消息体两部分,消息体表示需要传输的业务数据,而消息头用于进行传输控制。图
图7
可以看到每个层次都从上层取得数据,加上消息头信息形成新的数据单元,并将新的数据单元传递给下一层次。
我们可以使用TCP协议和HTTP协议等公共协议作为基本的传输协议构建RPC架构,也可以使用基于HTTP协议的Web Service和RESTful风格设计更加强大和友好的数据传输方式。但大部分RPC框架内部往往使用私有协议进行通信,这样做的主要目的是对共有协议进行精简,从而提升性能。另一方面,出于扩展性的考虑,具备高度定制化的私有协议也比公共协议更加容易实现扩展。这方面典型的示例还是Dubbo框架,它提供了完全自定义的Dubbo协议。
远程调用
RPC本质也是一种服务调用,而服务调用存在两种基本方式,即单向(One Way)模式和请求应答(Request-Response)模式,前者体现为异步操作,而后者一般执行同步操作。
同步调用会造成业务线程阻塞,但开发和管理相对简单。同步调用时序图如下所示,我们可以看到服务线程发送请求到IO线程之后就一直处于等待阶段,直到IO线程完成与网络的读写操作之后被主动唤醒。
使用异步调用的目的在于获取高性能。在实现异步调用过程中,我们通常都会使用到Java中所提供的了Future机制。Future调用可以进一步细分成两种模式,Future-Get模式和Future-Listener模式。Future-Get模式参考下图:
可以看到这种模式下通过主动get结果的方式获取Future结果,而这个get过程是串行的,会造成执行get方法的线程形成阻塞。而Future-Listener模式则不同,在Future-Listener模式中需要创建Listener,当Future结果生成时会唤醒注册到该Future上的Listener对象,从而形成异步回调机制。
除了同步和异步调用之外,还存在并行(Parallel)调用和泛化(Generic)调用等调用方法,虽然也有其特定的应用场景,但对于RPC架构而言并不是主流的调用方式,这里不做具体展开。
可以说,RPC是分布式系统中一项基础设施类的技术体系,但凡涉及到服务与服务之间的交互就需要使用到RPC架构。当你在使用一个分布式框架时,可以尝试使用今天介绍的RPC架构的基本结构和技术体系进行分析,从而加深对这项技术体系的理解。