RPC 解决什么问题
远程服务调用(Remote Procedure Call, RPC)是分布式系统架构的基础,但作为一项拥有四十年历史的计算机技术,它仍备受关注、持续更迭,不可谓不是一个奇观。
RPC 在解决什么问题、如何解决这些问题?本文先讨论 RPC 解决什么问题。
进程间通信
分布式系统在当下应用很广泛,但它的起源并非是解决高并发问题。在原始分布式时代,计算机的单机计算能力有限,为了完成一个复杂计算,计算机科学家提出了分布式系统,使多台单机共同执行计算。
由此,也产生了计算机之间的远程方法调用问题,RPC 的提出正是为了让计算机能够跟调用本地方法一样去调用远程方法。
首先,看一下本地方法调用的几个步骤:
- 传递方法参数
- 确定方法版本
- 执行被调方法
- 返回执行结果
若远程方法调用与之相似,面临两个主要问题:
- 第 1、4 两步传递参数、传回结果都依赖于栈内存,而计算机之间的不同进程无法拥有相同的栈内存;
- 第 2 步的方法版本选择,依赖编程语言的规则定义,若两端语言不一致,那么方法版本选择就是不可知的行为。
讨论下第一个问题,两个进程之间如何交换数据?这是便是计算机科学中的进程间通信(Inter-Process Communication, IPC)。
- 消息队列(Message Queue)用于进程间数据量较多的通信:进程可以向队列添加消息,被赋予读权限的进程则可以从队列消费消息;
- 共享内存(Shared Memory):允许多个进程访问同一块公共的内存空间,这是效率最高的进程间通信形式。
消息队列和共享内存只适合单机多进程间的通信,套接字接口(Socket)是更为普适的进程间通信机制,可用于不同机器之间的进程通信。
忽略通信成本的八宗罪
由于 Socket 是网络栈的统一接口,它也理所当然地能支持基于网络的跨机器的进程间通信。
因此,早期分布式系统的 RPC 就是奔着透明调用的方向去做的(作为 IPC 的一种形式),把远程方法调用的通信细节隐藏在操作系统底层,从应用层面上看来可以做到远程调用与本地的进程间通信在编码上完全一致这。
但是,这种透明的调用形式却反而造成了程序员误以为 Socket 编程的通信是无成本的假象,因而被滥用以致于显著降低了分布式系统的性能;而且,这也增加了程序员工作的复杂度。
后来,几位计算机科学家共同总结了通过网络进行分布式运算的八宗罪 (8 Fallacies of Distributed Computing):
- The network is reliable —— 网络是可靠的。
- Latency is zero —— 延迟是不存在的。
- Bandwidth is infinite —— 带宽是无限的。
- The network is secure —— 网络是安全的。
- Topology doesn’t change —— 拓扑结构是一成不变的。
- There is one administrator —— 总会有一个管理员。
- Transport cost is zero —— 不必考虑传输成本。
- The network is homogeneous —— 网络是同质化的。
如果 RPC 以透明的方式实现,则程序员很容易酿成以上的罪过,并为之买单。
至此,也确定了一种基本的观点:RPC 应该是一种高层次的或者说语言层次的特征,而不是像 IPC 那样,是低层次的或者说操作系统层次的特征。
RPC 的三个基本问题
一般仍认为 RPC 概念的定义最早是由施乐公司首次提出的,也是完全符合以上结论的:
Remote procedure call is the synchronous language-level transfer of control between programs in disjoint address spaces whose primary communication medium is a narrow channel.
远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的 方式使用带宽有限的信道来传输程序控制信息。
—— Bruce Jay Nelson,Remote Procedure Call ,Xerox PARC,1981
几十年来,流行过的、当下流行的 RPC 协议不外乎地在解决三个基本问题:
- 如何表示数据
- 如何传递数据
- 如何表示方法
如何表示数据
数据包括了传递给方法的参数,以及方法执行后的返回值。无论是将参数传递给另外一个进程,还是从另外一个进程中取回执行结果,都涉及到它们应该如何表示。
RPC 交互双方可能各自使用不同的程序语言,或者同样的数据类型在不同硬件指令集、不同操作系统下,也可能存在数据宽度、字节序等等的差异。
有效的做法是将交互双方所涉及的数据转换为某种事先约定好的中立数据流格式来进行传输,将数据流转换回不同语言中对应的数据类型来进行使用,这就是序列化与反序列化。
每种 RPC 协议都应该要有对应的序列化协议:
- Java RMI 的 Java Object Serialization Stream Protocol.
- gRPC 的 Protocol Buffers.
- Web Service 的 XML Serialization.
- 众多轻量级 RPC 支持的 JSON Serialization.
如何传递数据
指如何通过网络,在两个服务的 Endpoint 之间相互操作、交换数据。
“交换数据”通常指的是应用层协议,实际传输一般是基于标准的 TCP、UDP 等标准的传输层协议来完成的。RPC 双方信息交换的需求除了序列化的数据,还包括异常、超时、安全、认证、授权、事务等。
“Wire Protocol” 来用于表示两个 Endpoint 之间交换这类数据的行为,常见的协议:
- Java RMI 的 Java Remote Message Protocol(JRMP)
- Web Service 的 Simple Object Access Protocol(SOAP)
- 若要求足够简单,双方都是 HTTP Endpoint,直接使用 HTTP 协议即可(如 JSON-RPC)
如何表示方法
语言无关的接口描述语言 (Interface Description Language,IDL),是许多 RPC 参考或依赖的基础。
- Android 的Android Interface Definition Language (AIDL)
- Web Service 的Web Service Description Language (WSDL)
- JSON-RPC 的JSON Web Service Protocol (JSON-WSP)
尽管 RPC 早已不再追求实现成与本地方法调用完全一致,但其设计思路仍然带有本地方法调用的深刻烙印,抓住两者间的联系来类比,对我们更深刻地理解 RPC 的本质会很有好处。
参考
- 周志明,《凤凰架构》
(正文完)