架构系列二十(RPC 还是 REST上)

181 阅读9分钟

一旦我们选择分布式系统架构设计,必然会绕不开系统间调用,交互的问题。像下面这样

image.png 我们看到

  • 订单是一个独立的进程,有自己的内存地址空间
  • 用户是一个独立的进程,有自己的内存地址空间

那么它们之间的交互,肯定不会像进程内本地方法调用这么简单!首先我们来尝试梳理一下,进程间通信(IPC)有哪些方式或着说手段

  • 管道:你还记得这个命令吗?ps -ef | grep java ,通过管道符|,将ps命令的标准输出,连接到grep命令的标准输入
  • 信号:同样你一定记得命令 kill -9 pid ,通过shell进程向指定pid的进程发送sigkill信号
  • 信号量:wait(),notify()
  • 共享内存:操作系统允许多个进程可以访问同一块内存空间(共享内存),这是进程间效率最高的通信方式
  • Socket套接字:不但能支持单机多进程通信,还能实现跨机器多进程的通信
  • 消息队列:典型的生产者,消费者通信模型,将消息队列作为媒介,实现信息的交互共享

我们原本聊的是RPC 和 REST,为什么要先说一说IPC呢?因为RPC最早被看成是IPC的一种特例。在简单了解IPC以后,正式进入主题。

首先我们需要搞清楚以下问题

  • 什么是RPC
  • RPC需要解决什么问题,以及解决了什么问题
  • 都有哪些主流的RPC框架

什么是RPC?

从字面意思理解RPC(Remote Procedure Call),即远程过程调用,是一种协议。它最初是被当成IPC的一种特例来看待的,用于实现跨进程之间的相互通信。

而且还提出了一个非常美好的愿景:让计算机调用远程方法,拥有调用本地方法一致的体验! 画外音,远程调用和本地调用无差别化!至少对于开发人员来是这样的,透明化。

那么等等,简单梳理一下本地方法调用需要考虑些什么问题?

public class HelloWorld{
    
    public static void main(String[] args){
        System.out.println("hello world!");
    }
    
}

你看到了,这是你入门java的第一个程序:hello world。这个程序中,通过main方法调用了println方法,这里面都涉及到了哪些元素呢?

  • Caller调用者:main
  • Callee被调用者:println
  • CallSite调用点:发生方法调用的指令位置
  • Parameter参数:调用者传递给被调用者的数据
  • Return返回值:被调用者传递给调用者的数据

真实运行过程是怎么样的呢?

  • 传递方法参数:将参数hello world 压栈
  • 确定方法版本:通过编译器静态解析或者运行期动态分派,确定要调用的方法和版本
  • 执行被调用方法:执行被调用方法的逻辑(有压栈的过程)
  • 返回执行结果:将执行结果压栈,返回到指令流位置,继续执行后续代码

本地方法调用,依赖进程自生的栈内存,因为不管是参数,还是返回值,甚至被调用者都需要压栈。但是当我们考虑跨进程调用,不同进程之间都是独立的进程空间。该如何解决这个问题?这个时候,IPC通信的各种方式就能派上用处。

这个问题还算能解决。另外一个问题:如何实现远程方法调用,拥有本地方法调用一致的体验?无差别化,透明化。

这里有意无意的忽视了一个问题:网络通信没有成本! 事实真是这样的吗?答案是否定的。一方面系统间的调用透明化,其实是增加了开发人员工作的复杂度,不信你尝试想想RPC中的各种概念,比如:IDL,Skeleton,Stub;另外一方面,以下对网络情况假设的可行性

  • 网络是可靠的
  • 网络是安全的
  • 网络是同质化的
  • 带宽是无限的
  • 延迟是不存在的
  • 不需要考虑传输成本
  • 拓扑结构是一成不变的
  • 总会有一个管理员

这些假设,其实是著名的通过网络进行分布式计算的八宗罪!为什么是八宗罪呢?因为它们是你我他在网络编程中,最容易经常会忽略的八大问题。因此如果要把远程服务调用实现透明化,就不得不为这些罪过买单。

RPC需要解决的问题?

既然设想了让RPC调用,拥有本地方法调用的体验,必然有固有的问题需要去解决,比如说

  • 如何表示数据
  • 如何传递数据
  • 如何表示方法

方法之间的调用,不管是本地方法,还是远程方法,都需要考虑参数怎么传递给对方,又如何拿到执行后的结果。这就涉及到了如何表示数据?

如果是本地方法调用,因为是同一种编程语言,使用编程语言支持的数据类型,即可完成数据的表示。但是远程方法调用,很有可能交互的双方本身就技术异构性,使用了不同的编程语言。哪怕双方使用相同的编程语言,在跨进程的情况下,不同的硬件设备在指令集,操作系统上也是有差异的

一种行之有效的方法是,发送数据的一方将通信双方交互的数据,转换成事先约定好的,中立的数据格式进行传输;接收数据的一方将收到的数据,转换回自生支持的数据类型进行处理。得,这不就是系列化反序列化吗?

数据的表示通过序列化反序列化得到了解决,那么要如何传递呢? 服务与服务之间,服务的Endpoint(端点)之间如何实现信息的交互呢?

这里不是简单的考虑参数和返回结果的正常交互,还需要考虑诸如安全,认证授权,超时,异常等情况。因此需要一系列的协议来支持,比如Web Service的简单对象访问协议SOAP,比如JSON-RPC来进行数据传递标准的约定。

最后还有如何表示方法这个问题需要解决。毕竟远程服务与服务之间,我怎么知道要调用哪个方法?调用方法的哪个版本?这样的问题,该如何回答?

在本地方法调用中,这不是一个问题,有编译器或者解释器做好这个事情,只要将调用的目标方法,转换成进程地址空间中的方法入口指针即可。

但是远程方法调用就麻烦得多了!可能不同得服务之间,本身就不是同一门编程语言(技术异构性),不同得进程地址空间。要解决这个问题,就不得不提到熟悉得IDL(Interface Definition Language)接口定义语言,来解决如何找到方法这个问题。比如还是Web Service 中的WSDL,或者JSON-RPC中的JSON-WSP。

主流的RPC框架选择?

最早有机会一统RPC细分领域的是CORBA,因为它跨系统,跨语言,实现了语言中立,平台中立。但是CORBA本身的设计太过于繁琐,最终失去了这样的机会,再次印证了复杂度高的东西,总是不具有普适性。

在CORBA后,Web Service来了。我的从业生涯中没有机会用过CORBA,WebService倒是用过不少,早些年做电信相关系统的时候,用的比较多,你可能也很熟悉,比如说Apache Axis,和CXF。

可以说WebService一出现,直接导致了CORBA的溃败!进入了博物馆!WebService采用XML作为远程过程调用的序列化、接口描述、服务发现编码的载体。从而解决了表示数据、传递数据、表示方法,这些RPC中需要解决的问题。

但是WebService在接口和数据的定义上过于严格,采用XML双方面带来了性能问题。

XML的特定是结构良好,自描述性好。但是事物总有两面性,XML对于信息描述的密度非常低,且非常消耗空间,导致每次数据交互冗余信息多,性能差。不说要和二进制序列化协议对比,单说与JSON相比较,都处于劣势的位置上。

不但如此,你说性能差点就差点吧,可是WebService偏偏还挺贪心!它想要一站式解决分布式中所有的问题,于是你看到了SOAP、WSDL、UDDI,以及一堆WS-*的子功能协议,用于解决分布式系统中的安全、事物、一致性等相关问题。

最终还是走到复杂性的路上,最终还是被广大开发者抛弃!最终进入了博物馆!

当然今天,其实还有一些老系统在用着WebService进行RPC交互,去年我负责的某重构系统中,就还用着WebService。当然我们重构完后,就没有WebService什么事了。

于是我们看到了。面向透明、简单的RPC协议,有依赖操作系统,依赖于特定语言的约束;面向通用、普适性的RPC协议,虽然满足平台中立,语言中立,有性能的问题,和复杂性的问题。

真难伺候!这是一个鱼和熊掌,不可兼得的问题!那么就从,简单、普适性、高性能中选一个吧。

于是你看到了,如今百花齐放,各家争鸣的RPC世界!

国内国外,当前有代表性的RPC框架

  • RMI:Sun/Oracle,远古时代的东西了
  • Dubbo:阿里巴巴,已经贡献给Apache了
  • Motan:来自于新浪,业界用的比较少
  • gRPC:高性能RPC框架,Google出品,必属精品
  • Thrift:媲美gRPC,来自于Facebook,已经贡献给Apache

等等,还有更多......每一个RPC框架,都不再追求完美,转而是找到一个主要发展方向的点进行深耕。比如说

  • 面向分布式对象的发展方向
  • 面向性能的发展方向,比如gRPC、Thrift
  • 面向简单易用的发展方向,比如JSON-RPC

最后我们发现,在RPC领域至少短时间内,很难有一招鲜吃遍天的存在,在选择上难以完美,必须要做取舍!当然普通开发者不需要这么为难,这是架构师的活,交给你们公司的架构师就可以,拿着那么高的工资不受点罪怎么好意思!