REST —— HTTP 背后的架构风格(下)
接口设计不是开发过程的一部分,而是设计过程的一部分,把接口设计提升到架构设计的高度,才能体会到 REST 架构风格蕴含的魅力

HTTP 接口设计
如今我们提到 REST,通常是跟 HTTP 接口相关的。如果我们新设计开发一个系统,那么在设计阶段,接口设计一定是重要的环节。很多人把接口设计视为开发工作的一部分,这是错误的。接口设计其实是架构设计的一部分,所谓的接口不过是架构设计的一种表现形式而已。所以,我们一定要把接口设计,放到架构设计的高度去重视。
那么对于本文一开始提出的两种接口设计风格,我们再用 REST 的理论来重新分析一下孰好孰坏,同时也来看看 REST 的理论是如何应用到具体实践的。

状态码
首先,我们设计的是 HTTP 接口,而 HTTP 是 REST 的一种实现,那么 HTTP 本身就是符合 REST 风格的,这一点是被很多人忽略的事情。在上图 RPC 风格中,我们如果想判断一次请求是否成功,则需要判断两件事:
- 响应状态码是否为 200;
- 如果是,消息体中的 errorCode 字段是否为 0。
这显然是复杂的,因为我们想要知道请求成功失败,竟然要去解析消息体。我们前面提到过分层系统这个架构风格,如今的互联网软件系统几乎都应用了这个架构风格。当你发出一个 HTTP 请求时,很少会直接请求到应用服务,而一定会经过 SLB、网关、Nginx 等中间层,甚至这些中间层中还会有缓存层。
一方面,中间层会做一些事情,比如,请求成功了会缓存,请求失败了会报警。那么中间层如何判断请求的成功失败呢,就是通过 HTTP 的响应状态码。HTTP 状态码位于 HTTP 整个响应信息的最开始的位置,读取极为方便。如果还需要解析消息体来判断请求的成功失败,那这些中间层的性能就会大打折扣了。
另一方面,像 errorCode 这样的字段,一定是在业务系统实现的,而中间层我们使用的都是成熟的组件,没法适应业务系统对消息体的要求。最终,为了兼顾业务系统与中间层不同的规范,从使用方的角度来看,接口规范一定会变得更加复杂。
有些人会说,HTTP 的状态码表示的是网络请求的成功失败,而消息体里的 errorCode 表示的是业务行为的成功失败。那这就犯了另一个错误,把 HTTP 当做传输层协议了。HTTP 全称是 HyperText Transfer Protocol,这里的 Transfer 如果你去查字典会发现它更多的是“转移”的意思,指的是应用状态的转移,而非信息传输。HTTP 本身也不是传输层协议,而是应用层协议,它底层的传输层协议是 TCP。如何在网络层面上把一个请求发送出去,再接收到响应,这是 TCP 协议来保障的。而 HTTP 协议是应用层协议,它就是要来表达业务含义的。再说了,如果在网络层面上都请求失败了,又何来响应,何来状态码呢?
如果我们去看 HTTP 协议中定义的所有状态码,就会发现,它除了成功和失败外,还有很多很多的含义。比如:
| 状态码 | 含义 |
|---|---|
| 201 | 已创建 |
| 202 | 已接收 |
| 401 | 未通过身份认证 |
| 403 | 没有访问权限 |
| 404 | 未找到资源 |
这些都是应用层的语义,都是可以与业务含义相对应的。以 200 为例,当一个请求响应 200 Ok 时,它表示的含义应该是,客户端发起这个请求的目的已经达到了,客户端要做的事已经做成了,而不仅仅是完整地收发了一个网络请求。如果我们只把 200 当做是网络请求成功,那 HTTP 设计这么多 2xx 的状态码也就没有什么意义了。
所以说,通过状态码来表达请求结果,并让分层系统中的每一层都能很容易地获取到,这种 REST 风格,才是更合理的设计。而这对应的架构属性就是可见性(Visibility)。
可见性
可见性:一个组件对于其他两个组件之间的交互进行监视或进行中间斡旋的能力。
在 HTTP 中,除了状态码外,所有的头部内容也都体现了可见性。在实际设计中,我们通常是把与业务数据无关的信息放在头部,这样可以剥离非业务逻辑,以便把更核心的资源放在处理业务逻辑上。
比如最常用的媒体类型头部,Content-Type 和 Accept。这两个头部分别表示消息体内容和期望响应时的消息体内容是什么媒体类型,它与我们的业务领域无关,只是表示了传输信息的格式。这样我们收发请求时,可以快速的判断内容格式是否符合预期,我们的系统是否可以处理,只有在一切正常的前提下,才去用合适的方式解析消息体,而在执行业务逻辑之前的一切,甚至可以委托给中间层去做。这就对业务与非业务的解偶提供了支持。
再比如,当我们做链路追踪时,也一定会把一次请求链的标识放在头部,如果放到了消息体里,那这系统的效率和可扩展性可就低得可怜了。
所以,当我们设计接口时,负责表达认证、鉴权、监控、缓存等业务无关信息时,应该把它们放置在头部。这种头部与消息体分离,业务与非业务解耦,也体现了 HTTP 的另一个架构风格——统一接口。
统一接口
我们在开发之前,之所以设计接口,编写接口文档,是为了在多人、多团队、多系统开发中,事先定义好交互的规则,才能更高效的让所有相关者达成一致。这一架构风格,已经是我们日常开发中最最常用的手段了。
我们回看前面提到的 RPC 接口风格,errorCode 的设计,实际上已经破坏了 HTTP 的统一接口。HTTP 的统一接口,是所有 HTTP 使用者达成的一致。前面我说过,如今的互联网系统都是分层系统,都有各种中间层,只要他们之间使用 HTTP 进行交互,他们就是 HTTP 的使用者,他们就遵循了 HTTP 的统一接口。而如果你的业务系统使用了 errorCode,那么你对请求成功失败的判断规则,就与其他所有中间层组件不一样了。
具体来说,errorCode 实际上已经属于一种基于 HTTP 消息体之上的项目私有协议了,只有我们的业务系统可以理解它,而我们使用的中间件,比如 Nginx,显然是不知道我们自己定义的私有协议的,而开发团队也显然不会为了这个私有协议去修改所有的中间件。这样就会产生系统间规范的不一致,会给系统开发带来额外的复杂度。比如中间层无法准确监控或统计请求异常,通用的自动化测试工具无法适配我们的接口等。
RPC
我一直在用 RPC 和 REST 做对比,来说明 REST 风格更好,但这种比较是有失公平的。因为我比较的前提是 REST 架构风格的前提,即论文中提到的“基于网络的应用软件架构”,RPC 客场作战显然不占优势。RPC 有它自己的适用场景,比如对性能要求较高的内网环境,它始终有着不错的表现。但是至少,我们不要像本文举例的那样,在 HTTP 消息体的基础上去设计私有的 RPC 协议,因为这样做 RPC 的性能优势也就荡然无存了。
如今比较流行的 gRPC 虽然构建于 HTTP 之上,但人家用的是 HTTP/2,在数据传输层面已经做了大量的优化。而且并未破坏 HTTP 的原有语义,与本文举的 RPC 反例的设计大有不同,这是很多人没能意识到的。这或许也是我们滥用归纳法,而引发的认知偏差吧。
再想一个问题,HTTP 是 REST 的实现,那么 RPC 是不是也可以是 REST 的实现呢?
例外
最后,再展示一张表,是我之前对各个大厂接口设计的调研(于4年前)。

我们平时看的关于 RESTful 接口设计的文章,经常会提到,使用合理的请求方法,使用名词来做URL,合理的状态码等,所以这里也统计了这些信息。
绝大多数开放接口的设计都是符合 REST 风格的,唯独阿里系的接口有着浓郁的 RPC 风格。阿里的技术实力大家还是认可的,但是他们为什么不用 REST 风格呢?我认为这更多是历史原因导致的,阿里的体系已经非常庞大了,很难整体的转换风格。
我听说阿里内部 RPC 风格也很浓重,但是有一点我们要知道,阿里是有能力自己设计开发中间件的,他们可以为了自己的风格,去设计开发符合自己需求的各种组件,而对于绝大多数公司来讲,这是不应该做的。而且,阿里的新项目,从公开文档看,也都在向 REST 风格转变。如果说有什么原因导致我们无法使用 REST 风格来接口设计,那么“历史包袱”应该是唯一的原因。
我们如今的开发大量依赖开源框架,而且几乎都是国外的开源框架。如果说这些开源框架,它们是认认真真遵守符合 REST 风格的 HTTP 协议的,那么你,作为这些开源框架的使用者,又要作何选择呢?去看看那些 Web 框架的源码吧,去看看它们为了遵循 HTTP 协议都做了哪些实现。
结语
REST 常被误解为是一种接口规范,而它背后的架构思想却鲜为人知。跟作为接口规范的 REST 相比,它背后的架构思想比较抽象,学习门槛更高一些。我写这篇文章就是希望能够通过一个大家熟悉的切入点,通过一些更容易理解的方式,把 REST 作为架构风格的一面展现出来。
如果你也是那个误解过 REST 的人,我相信这篇文章对你未来的接口设计会有一些帮助,甚至有很大的帮助。但是如果你想真正搞明白 REST 架构风格,我的这篇入门文章显然表述的还不够深入,还希望你能继续钻研,不断精进。
REST 真正要解决的是架构设计的问题,而不仅仅是接口,它关注的是如何让一个系统在长期的运行中去不断适应变化,适应意外,所以你可以认为它是一种长期主义。但在实际工作中,并非每个项目都是长期主义,很多人热衷于挣快钱,热衷于急功近利,这在商业世界这也未必是错,不过对于我们程序员来说,快对提高我们的技术未必有什么帮助。
真正让我们的成长的,是在一个快速增长的系统中,修复一个又一个的 Bug,应对一波又一波的流量,是在真实的应用场景中,不断地寻找恰当的解决方案。如果你有幸经历过这一切,再回过头来,就能体会到什么是架构了。
这篇文章偏于理论,想进一步学习 REST,从接口设计入手也是一个不错的选择,关键是我们要不断思考,每一条 RESTful 接口规范背后的原理,而不仅仅是遵守。而说到 RESTful 接口设计,不得不提的就是 REST 成熟度模型,下一篇文章,我们从 REST 成熟度模型讲起,来聊聊 HTTP 接口设计中需要遵循的一些规则。
参考文献:
《REST 实战》: https://book.douban.com/subject/6854551/
论文导读: https://www.infoq.cn/article/doctor-fielding-article-review/
论文原文: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
论文译文: https://www.infoq.cn/article/web-based-apps-archit-design/