💧 Posted on 

RPC vs REST

RPC

RPC 是指远程服务调用(Remote Procedure Call) 也就是说两台服务器 A 和B。一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数 / 方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

最终解决的问题:让分布式或者微服务系统中不同服务之间的调用像本地调用一样简单。

RPC 要解决的三个基本问题:

  1. 如何表示数据:这里数据包括了传递给方法的参数,以及方法执行后的返回值。

进程内的方法调用,使用自定义的数据类型,就很容易解决数据表示问题,远程方法调用则完全可能面临交互双方各自使用不同程序语言的情况;

即使只支持一种程序语言的 RPC 协议,在不同硬件指令集、不同操作系统下,同样的数据类型也完全可能有不一样表现细节,譬如数据宽度、字节序的差异等等。

有效的做法是交互双方约定一种序列化和反序列化协议,将所涉及的数据转换为某种事先约定好的中立数据流格式来进行传输。

每种 RPC 协议都应该要有对应的序列化协议

  1. 如何传输数据:准确地说,是指如何通过网络,在两个服务的 Endpoint 之间相互操作、交换数据。

这里“交换数据”通常指的是应用层协议,实际传输一般是基于标准的 TCP、UDP 等标准的传输层协议来完成的。

两个服务交互不是只扔个序列化数据流来表示参数和结果就行的,许多在此之外信息,譬如异常、超时、安全、认证、授权、事务,等等,都可能产生双方需要交换信息的需求。

如果要求足够简单,双方都是 HTTP Endpoint,直接使用 HTTP 协议也是可以的

  1. 如何确定方法:这在本地方法调用中并不是太大的问题,编译器或者解释器会根据语言规范,将调用的方法签名转换为进程空间中子过程入口位置的指针。

不过一旦要考虑不同语言,事情又立刻麻烦起来,每门语言的方法签名都可能有所差别,所以“如何表示同一个方法”,“如何找到对应的方法”还是得弄个跨语言的统一的标准才行。

为什么用 RPC,不用 HTTP

RPC 是一种设计,就是为了解决不同服务之间的调用问题,完整的 RPC 实现一般会包含有 传输协议 和 序列化协议 这两个。

而 HTTP 是一种传输协议,RPC 框架完全可以使用 HTTP 作为传输协议,也可以直接使用 TCP,使用不同的协议一般也是为了适应不同的场景。

使用 TCP 和使用 HTTP 各有优势:

传输效率:

  • TCP,通常自定义上层协议,可以让请求报文体积更小
  • HTTP:如果是基于HTTP 1.1 的协议,请求中会包含很多无用的内容

性能消耗,主要在于序列化和反序列化的耗时

  • TCP,可以基于各种序列化框架进行,效率比较高
  • HTTP,大部分是通过 json 来实现的,字节大小和序列化耗时都要更消耗性能

跨平台:

  • TCP:通常要求客户端和服务器为统一平台
  • HTTP:可以在各种异构系统上运行

REST

REST 无论是在思想上、概念上,还是使用范围上,与 RPC 都不尽相同,充其量只能算是有一些相似,应用会有一部分重合之处,但本质上并不是同一类型的东西。

REST 与 RPC 在思想上差异的核心是抽象的目标不一样,即面向资源的编程思想与面向过程的编程思想两者之间的区别。

概念上的不同是指 REST 并不是一种远程服务调用协议(它不是一种协议, 更像是一种设计风格)。协议都带有一定的规范性和强制性,最起码也该有个规约文档,譬如 JSON-RPC,它哪怕再简单,也要有个《JSON-RPC Specification》来规定协议的格式细节、异常、响应码等信息,但是 REST 并没有定义这些内容,尽管有一些指导原则,但实际上并不受任何强制的约束。

REST,即“表征状态转移”的缩写。

下面通过一个具体事例来理解什么是“表征”以及 REST 中其他关键概念:

  • 资源(Resource):譬如你现在正在阅读一篇名为《REST 设计风格》的文章,这篇文章的内容本身称之为“资源”。无论你是购买的书籍、是在浏览器看的网页、是打印出来看的文稿、是在电脑屏幕上阅读抑或是手机上浏览,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。

  • 表征(Representation):当你通过电脑浏览器阅读此文章时,浏览器向服务端发出请求“我需要这个资源的 HTML 格式”,服务端向浏览器返回的这个 HTML 就被称之为“表征”,你可能通过其他方式拿到本文的 PDF、Markdown、RSS 等其他形式的版本,它们也同样是一个资源的多种表征。

  • 状态(State):当你读完了这篇文章,想看后面是什么内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息即被称为“状态”。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务器要完成“取下一篇”的请求,要么自己记住用户的状态:这个用户现在阅读的是哪一篇文章,这称为有状态;要么客户端来记住状态,在请求的时候明确告诉服务器:我正在阅读某某文章,现在要读它的下一篇,这称为无状态。

  • 转移(Transfer):无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。

RESTful 的系统

一套理想的、完全满足 REST 风格的系统应该满足以下六大原则。

  1. 服务端与客户端分离(Client-Server)

  2. 无状态(Stateless)

无状态是 REST 的一条核心原则。

REST 希望服务器不要去负责维护状态,每一次从客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。

客户端承担状态维护职责以后,会产生一些新的问题,譬如身份认证、授权等可信问题。

但必须承认的现状是,目前大多数的系统都达不到这个要求,往往越复杂、越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的好处,但大型系统的上下文状态数量完全可能膨胀到让客户端在每次请求时提供变得不切实际的程度,在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种是事实上存在,并将长期存在、被广泛使用的主流的方案。

  1. 可缓存(Cacheability)

无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。

“降低网络性”的通俗解释是某个功能如果使用有状态的设计只需要一次(或少量)请求就能完成,使用无状态的设计则可能会需要多次请求,或者在请求中带有额外冗余的信息。

为了缓解这个矛盾,REST 希望软件系统能够如同万维网一样,允许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。

当然,为了缓存能够正确地运作,服务端的应答中必须明确地或者间接地表明本身是否可以进行缓存、可以缓存多长时间,以避免客户端在将来进行请求的时候得到过时的数据。

运作良好的缓存机制可以减少客户端、服务器之间的交互,甚至有些场景中可以完全避免交互,这就进一步提了高性能。

  1. 分层系统(Layered System)

这里所指的并不是表示层、服务层、持久层这种意义上的分层。而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。

中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。

该原则的典型的应用是内容分发网络(Content Distribution Network,CDN)。如果你是通过网站浏览到这篇文章的话,你所发出的请求一般并不是直接访问位于 GitHub Pages 的源服务器,而是访问了位于国内的 CDN 服务器,但作为用户,你完全不需要感知到这一点。

  1. 统一接口(Uniform Interface)

这是 REST 的另一条核心原则。REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。

这条原则你可以类比计算机中对文件管理的操作来理解,管理文件可能会进行创建、修改、删除、移动等操作,这些操作数量是可数的,而且对所有文件都是固定的、统一的。如果面向资源来设计系统,同样会具有类似的操作特征,由于 REST 并没有设计新的协议,所以这些操作都借用了 HTTP 协议中固有的操作命令来完成。

  1. 按需代码

REST 的优势

REST 的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。

在 REST 提出以前,人们设计分布式系统服务的唯一方案就只有 RPC,RPC 是将本地的方法调用思路迁移到远程方法调用上,开发者是围绕着“远程方法”去设计两个系统间交互的。

这样做的坏处不仅是“如何在异构系统间表示一个方法”、“如何获得接口能够提供的方法清单”都成了需要专门协议去解决的问题(RPC 的三大基本问题之一),更在于服务的每个方法都是完全独立的,服务使用者必须逐个学习才能正确地使用它们。

REST 提出以资源为主体进行服务设计的风格,能为它带来不少好处,譬如:

  • 降低的服务接口的学习成本。统一接口(Uniform Interface)是 REST 的重要标志,将对资源的标准操作都映射到了标准的 HTTP 方法上去,这些方法对于每个资源的用法都是一致的,语义都是类似的,不需要刻意去学习,更不需要有什么 Interface Description Language 之类的协议存在。
  • 资源天然具有集合与层次结构。以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。
  • REST 绑定于 HTTP 协议。面向资源编程不是必须构筑在 HTTP 之上,但 REST 是,这是缺点,也是优点。

因为 HTTP 本来就是面向资源而设计的网络协议,纯粹只用 HTTP(而不是 SOAP over HTTP 那样在再构筑协议)带来的好处是 RPC 中的 Wire Protocol 问题就无需再多考虑了,REST 将复用 HTTP 协议中已经定义的概念和相关基础支持来解决问题。HTTP 协议已经有效运作了三十年,其相关的技术基础设施已是千锤百炼,无比成熟。而坏处自然是,当你想去考虑那些 HTTP 不提供的特性时,便会彻底地束手无策。

REST 的不足

  • 面向资源的编程思想只适合做 CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑
  • REST 与 HTTP 完全绑定,不适合应用于要求高性能传输的场景中
  • REST 没有传输可靠性支持
  • REST 缺乏对资源进行“部分”和“批量”的处理能力

RMM 成熟度模型

《RESTful Web APIs》和《RESTful Web Services》的作者 Leonard Richardson 曾提出过一个衡量“服务有多么 REST”的 Richardson 成熟度模型(Richardson Maturity Model),便于那些原本不使用 REST 的系统,能够逐步地导入 REST。Richardson 将服务接口“REST 的程度”从低到高,分为 0 至 3 级:

  • The Swamp of Plain Old XML:完全不 REST。另外,关于 Plain Old XML 这说法,SOAP 表示感觉有被冒犯到。
  • Resources:开始引入资源的概念。
  • HTTP Verbs:引入统一接口,映射到 HTTP 协议的方法上。
  • Hypermedia Controls:超媒体控制在本文里面的说法是“超文本驱动”,在 Fielding 论文里的说法是“Hypertext As The Engine Of Application State,HATEOAS”,其实都是指同一件事情。