REST 将会是新的 SOAP

来自:开源中国 翻译频道,原文链接

简介

多年前,我所在的一家大型电信公司开发了一个新型信息系统。我们必须通过旧系统或是友商与越来越多的 web 服务进行通讯。

更不用说,我们合理的拥有 SOAP Hell 的份额,玄奥的 WSDL ,不相容的 library ,奇怪的 bug ...所以只要可以,我们就提倡使用简单的远程过程调用协议:XMLRPC 或 JSONRPC 。

我们为这些协议提供的首批服务器与客户端非常基础,单调,脆弱。 但渐渐的,我们改善了它们; 通过几百行额外的代码,我们让所想变成现实:支持不同的方言(例如 Apache 特定的 XMLRPC 扩展),python 异常和分层错误代码之间的内置转换,功能和技术错误的单独处理,后续的自动重审,请求之前或之后的相关日志记录和统计信息,输入数据的彻底验证......

现在,我们只需要几行代码,就能和这样的API建立可靠的连接。

我们也只需要稍微修饰一下,做一些文档更新,就可以暴露一套新的功能给广泛的受众、服务器或者web浏览器。

对于应用间的通信(微服务风格),系统管理员自己就可以完成这些工作;对于软件层面,这几乎是透明的。

用了30分钟集成RPC API后,程序员在休息。

然后,REST出现了。 
表述性状态转移(Representational State Transfer)。

一股复兴浪潮动摇了跨服务通信的根基。

RPC已死,未来是RESTful的:每个资源都有自己的URL,并通过HTTP协议进行操作。

然后,我们必须暴露或调用的API,成为了新的挑战;这简直愚蠢至极。

REST有什么问题呢?

一个简短的例子就值得长篇大论。下面是一个小API,为了可读性删除了数据类型。

createAccount(username, contact_email, password) -> account_id
addSubscription(account_id, subscription_type) -> subscription_id
sendActivationReminderEmail(account_id) -> null
cancelSubscription(subscription_id, reason, immediate=True) -> null
getAccountDetails(account_id) -> {full data tree}

仅添加一个合适文档化的异常层次结构 (InvalidParameterError, MissingParameterError, WorkflowError…),使子类可识别重要的用例(例如AlreadyExistingUsernameError),这样你就可以了。

这些API易于理解、易于使用,并且是健壮的。他们是有精准的状态机支持,但有限的可用操作集使得用户远离无意义的交互(例如修改账户的创建日期)。 

将这个 API 暴露为一个简单的 RPC 服务,估计用时:几个小时。

现在,试试 RESTful。

没有太多的标准和规范,只有一个模糊的“RESTful哲学”,所以容易引起无休止的、形而上学的争论,还催生了许多不优雅的变通方案。

如何将上面明确的功能,映射为简单的 CRUD 操作?发送验证邮件,是更新一下"must_send_activation_reminder_email"属性,还是创建一个"activation_reminder_email resource"资源?如果在宽限期内,订阅仍然有效,或者可能恢复订阅,那用 DELETE 操作执行 cancelSubscription() 是否合理?如何在节点间拆分 getAccountDetails() 的数据树,来使它符合 REST 模型?

为每个资源分配什么 URL?是不难,但也需要实现。

如何使用非常有限的 HTTP 响应码,来表达错误场景的差别?

使用什么序列化、什么格式来描述输入输出?

HTTP 方法、URL、查询参数、负载、请求头、响应码,它们的分界线在哪?

花费了很多时间重复造轮子,甚至造的并不是好轮子。一个不完整的、易碎的轮子,需要通过大量文档来理解它,甚至不知不觉就违反了规范。

为什么 REST 带来了这么多工作(Work)?
这是一个悖论,也是一个双关(译者注:REST 在英文中有休息的意思)。

让我们深入探讨一下这个设计哲学所产生的人为问题。

有趣的 REST

REST 不是 CRUD,它的拥护者们不会让你混淆这两者。然而不久,他们会为 HTTP 已经提供了 CRUD 的语义而欣喜,例如创建(POST)、获取(GET)、更新(PUT/PATCH)和删除(DELETE)。

他们乐于承认这几个动词足以表达任何操作。嗯,当然是这样;就像用几个动词足以表达英语中的任何概念一样:“Today I updated my CarDriverSeat with my body, and created an EngineIgnition, but the FuelTank deleted itself(今天我用我的身体更新了我的汽车座椅,并且创建了一次发动机点火,但是油箱删除了它自己)”;这是不是有点尴尬。除非你是道本语的崇拜者。

追求极简是好事,但起码要做好。你知道为什么从来不在 Web 表单上使用 PUT、PATCH 和 DELETE 吗?因为它们百害而无一利。我们只需要用 GET 来读,用 POST 来写就好了。或者,当我们不需要 HTTP 层缓存时,只用 POST 就好了。其他方法好点的话也许会妨碍你,但最差的情况下会毁了你的一天。

想用 PUT 更新资源?可以,但是一些神圣的规范要求,数据输入必须与 GET 读取到的数据描述一致。那么,如何处理 GET 返回的大量只读参数呢(创建时间、最后更新时间、服务器生成的令牌……)?你准备忽略它们,而违反 PUT 使用原则吗?还是考虑它们,如果它们不符合服务端的值时,抛出"HTTP 409 Conflict"异常(强迫你再调用一次 GET……)?还是你给它们随机值,并祈祷服务端会忽略它们(沉浸在不会报错的喜悦中)?选个死法吧,REST 显然不知道什么是只读属性, 短期内也不会解决这件事。此外,用 GET 来返回密码信息(或信用卡号码)是危险的,之前都使用 POST 或 PUT;处理这些只写参数时,也只能祝君好运了。

我是不是忘了提 PUT 有可能会造成竞态条件,不同的客户端会覆盖其他客户端的变更,尽管它们只是想更新不同的字段。

又想用 PATCH 来更新资源?很好,但是,就像 99% 的人使用这个动词一样,只需要在请求负载中传递资源字段的子集,然后希望服务器能够正确地理解操作意图(和所有副作用);许多资源参数或是紧密联系,又或是相互排斥的(例如:在用户账单信息中,要么是信用卡号,要么是 PayPal 令牌),但是 RESTful 设计原则对这些重要信息避而不谈。不管怎么说,你会再次违反原则:PATCH 不应该只发送一堆需要被更新的参数。相反,应该提供给服务端一些指示信息来应用到资源上(译者注:不应该发一堆参数让服务端来猜,需要给服务一些提示,让服务器理解你的操作意图)。又到你了,拿上你的纸板和咖啡杯,你必须决定如何来表达这些指示信息。通常需要自定义规范,因为没有标准就是 REST 世界里的事实标准。(编辑注:REST 倡导者在这个问题上有所让步,提出了 Json Merge Patch,这是一个 Json Patch 的候选方案)

想用 DELETE 删除资源?好,但我希望你,不需要提供大量的上下文数据;就像用户对 PDF 扫描的终止请求。DELETE 不允许包含有效负载。这一点,REST 架构师经常忽略掉,因为大多数 Web 服务器不会对收到的请求强制执行这个规则。如果一个 DELETE 请求携带一个 2MB 的 Base64 字符串,怎么兼容规范呢?(编辑注:RFC 2616 指明了没有语义的负载应该被忽略,但现在已经废弃了)

REST 爱好者通常信奉“人们都做错了”,他们的 API“并不是真正的 RESTful”的。例如,许多开发者使用 PUT 直接在最终 URL 上创建资源(/myresourcebase/myresourceid), 然而,“正确的用法”(编辑注:根据很多人)应该是,在上一层URL(/myresourcebase)使用 POST 来创建资源,然后服务器通过 HTTP 的"Location"头来指明新资源的 URL(编辑注:不过,这不是 HTTP 重定向)。好消息是:这没关系。这些严格的准则就像是高位优先 vs 低位优先,它们让哲学家们费劲脑汁,但对现实没有什么影响,换言之,把事情做好就可以了。

顺便提一下……设计 URL 很有意思。你知道在构建 REST URL 时,有多少 urlencode() 的正确实现吗?如果没有,就等着接受 SSRF/CSRF 攻击吧。


当你忘记在 30 个 URL 中的其中一个对用户名进行 urlencode 时……

REST错误处理中的乐趣

每一个编码者都可以使“名义上的用例”工作。错误处理就是这些功能里的一种,它将决定你的代码是否是健壮软件,还是一大推火柴棍。

HTTP提供了一系列开箱即用的错误码列表。好极了,让我们了解下。

使用“HTTP 404 Not Found”来通知那些不存在的资源,看起来很符合REST风格,难道不是嘛?太糟糕了:你的nginx被错误配置了一个小时,所以你的API消费者仅获得了404错误,并清除了数百个账户,他们认为这些账户已经被删除了... 

我们的客户,在我们因为错误而删除其数G字节的重要镜像之后。

当用户对一个第三方服务没有访问权限时,使用"HTTP 401 Unauthorized",这听起来是可以接受的,不是吗?但是,如果你在 Safari 浏览器的 Ajax 调用中获得此错误码,它会弹出一个密码输入框,这会让你的客户很吃惊[几年前,确实是这样的,你可能有不同意见]。

HTTP 比 RESTful 历史长得多,Web 生态系统充满了关于错误码含义约定俗成的东西。用 HTTP 状态码来表示应用错误,就像是用牛奶瓶装剧毒废物一样:总有一天会遇到麻烦。

一些标准的 HTTP 错误码是 WebDAV 专用的,另一些是微软专用的,剩下的有一些定义模糊,没有太大用处。最后,和大多数 REST 用户一样,你可能开始随意使用 HTTP 状态码,比如“HTTP 418 I’m a teapot”或未分配的数字,以用来表达应用程序中的特定异常。或者,厚着脸皮用“HTTP 400 Bad Request”来表示所有功能错误,然后用布尔、整型代码、slug 和翻译信息整合成笨重的错误格式,填充到负载中。或者,完全放弃正确的错误处理;只返回一个用自然语言描述的普通消息,并希望调用者能够分析问题,并采取行动。当与自治系统的这些 API 交互时,只能祝君好运了。

有趣的 REST 概念

REST 靠吹嘘概念和原则为生,但是,这些概念是任何头脑正常的架构师早就熟知的,这些原则连它自己都不遵守。下面,我从高权重的网页中摘录了一些:

REST 是 client-server 架构。客户端和服务端都有不同的关注点。这真是软件世界的独家新闻。

REST 提供了组件之间的统一接口。好吧,当在整个服务生态系统中,强迫使用一种通用语言时,任何其他协议都可以做到这一点。

REST 是一个分层系统。各个组件不能跨越直接相关层看到对方。这听起来像是,任何设计良好、松散耦合的架构的自然结果吧;不可思议。

REST 是很棒,因为它是无状态的。是的,Web 服务器后面可能有一个巨大的数据库,但它不记录客户端的状态。或者,是的,事实上它会记住客户端的身份认证会话、访问权限……尽管如此,它确实是无状态的。或者,更精确的说,与任何基于 HTTP 的协议一样无状态,就像前面提到的简单 RPC 一样。

通过 REST,可以使用 HTTP 缓存。好吧,这至少是个结论:GET 请求及其缓存控制头确实对 Web 缓存非常友好。尽管如此,本地缓存(Memcached等)不足以满足 99% 的 Web 服务吗?失控的缓存是危险的;有多少人愿意用明文暴露他们的 API,以至于中间的 Varnish 或代理,在资源被更新或删除很久之后,还继续提供过期的内容?又或者,一旦配置错误,甚至"永远”不会更新? 默认情况下,系统必须是安全的。我完全赞同,一些高负载的系统希望通过 HTTP 缓存来减轻压力,但是,为了大量的只读交互,公开一小部分 GET 端点,要比将所有操作全部切换为 REST、并且不稳定的异常处理,成本要低得多。

因为这些,REST 具有高性能。我们能确定吗?任何 API 设计人员都知道:本地的 API,最好是细粒度的,功能强大;远程的 API,最好是粗粒度的,能够降低网络影响。这也是 REST 不幸失败的领域。数据在“资源”上的隔离(每个实例位于其自己的端点上)自然会导致 N+1 查询问题。为了获取用户的完整数据(账户、订阅、账单信息……),必须发出同样多的 HTTP 请求;而且,你还无法并行处理这些,因为你事先并不知道依赖资源的唯一 ID。这再加上不能只获取部分资源对象,自然会造成严重的瓶颈。 

REST 提供了更好的兼容性。是吗?为什么那么多 REST Web 服务在它们的基本 URL 中有“/v2/” 或者 “/v3/”?使用高级语言,只需要在添加或废弃参数时遵循简单的规则,就不难实现向前和向后兼容。据我所知,REST 并没有给这个问题带来任何新的东西。

REST 简单,因为大家都知道 HTTP!好吧,每个人也都知道卵石,然而在建造房子时,人们更乐于使用更好的砖。同样的,XML 是一种元语言,HTTP 是一种元协议。定义真正的应用层协议(比如“方言”是 XML),你需要指定许多东西;最后,你将得到另一个 RPC 协议,就好像 RPC 协议还不够多。

REST 太简单了,甚至在 shell 中用 CURL 来查询!好吧,事实上,任何基于 HTTP 的协议都可以用 CURL 来查询。甚至是 SOAP。当然,发送 GET 请求非常简单,但是手写 JSON 或 XML 的 POST 时,只能祝你好运了;人们通常使用 fixture 文件,或者直接在他们最喜欢的语言的命令行接口中实现更方便、更成熟的 API 客户端。 

“客户端不用事先了解服务,就能使用它”。这是到目前为止,我最喜欢的一句话。我发现它很多次了,以不同的形式出现,尤其是当流行词 HATEOAS 出现时;有时后面会谨慎地(但不够)跟一些“例外”短语。不过,我不知道这些人生活在哪个幻想世界里,但在这个世界里,客户端并不是一群蚂蚁;它不会随机浏览远程 API,然后根据模式识别或黑魔法来决定如何最好的处理它们(译者注:作者类比蚁群算法思想)。恰恰相反,客户端对为什么把这个字段用这个值通过 PUT 请求到这个 URL 上有强烈的期望,而服务端最好尊重在集成的时候约定的语义,否则将万劫不复。

当你问 HATEOAS 应该如何工作的时候。

如何正确、快速地使用 REST?

忘记“正确”这部分吧。REST 就像是一种宗教,没有一个凡人能够拥有天赋,更别提“正确使用”了。

所有正确的问题应该是:如果你被迫以类 RESTful 方式暴露或调用 Web 服务时,如何快速完成这项工作,并尽快切换到更有建设性的任务上?

更新:实际上有很多关于 REST 的“标准”和工业化成果,虽然我从来没有亲身体验过它们(也许是因为很少人使用它们?)。更多信息请参考我的后续文章

如何工业化地完成服务端暴露?

每个 Web 框架都有自己的方式来定义 URL 端点。因此,希望有一些大的依赖项,或者一层好的封装,来将现有的 API 作为一组 REST 端点,嵌入到你最喜欢的服务器上。

一些第三方库,像 Django-Rest-Framework,在 SQL/NoSQL 上封装了一层所谓的数据中心,以用于自动创建 REST 的 API。如果只是想“通过 HTTP 实现 CRUD”,这一般就够了。但是,如果想暴露常见的“为我所用”的 API,这些 API 包含工作流、约束、复杂的数据影响等,这样你就很难在这些 REST 框架上做出变通,来满足自己的需求。

那就做好准备吧,将每个端点的 HTTP 方法,一个接一个地连接到正确的方法调用;还要手动处理相当一部分的异常,将需要传递的异常翻译为相关的错误码和负载。

如何工业化地完成客户端集成?

从经验来看,我的猜测是:做不到。

对于每个 API 的集成,你不得不翻看冗长的文档,然后理解这 N 个可能的操作,每一个是怎么执行的。

你必须手工创建 URL,编写序列化器和反序列化器,并且学会如何解决 API 的歧义性。在驯服野兽之前,估计会有大量的试错。

你知道 Web 服务提供商如何弥补这一问题,并简化使用的吗?

简单来说,他们编写自己的官方客户端实现。

为每一个主流的语言和平台实现客户端。

我最近用过一个订阅管理系统。他们提供了 PHP、Ruby、Python、.NET、Android、Java……官方客户端。还有外部捐赠的 Go 和 NodeJS 客户端。

每个客户端都有自己的 GitHub 仓库。每个都有自己的一大串提交、缺陷跟踪和合并请求。每个都有自己的使用示例。每个在 ActiveRecord 和 RPC 代理之间,都有自己难以处理的架构。

这太令人震惊了。我们花了多少时间来开发这些奇怪的封装,而不是改进真正的、有价值的、完善的 WEB 服务?

Sisyphus 为他的 API 开发了另一个客户端。

结语

几十年来,几乎每一种编程语言都具有相同的工作流程:将输入发送到可调用的程序,并得到结果或错误作为输出。效果很好,相当好。

REST 呢?这变成了一种“鸡同鸭讲”的愚蠢工作,并且前脚赞扬 HTTP 规范,后脚就违反。

在一个微服务越来越普遍的时代,为什么如此简单的任务 —— 通过网络把各个库连起来 —— 仍然如此做作和繁琐?

我不怀疑,一些聪明的人会提出 REST 适合的案例;他们会展示他们自制的基于 REST 的协议,允许在任意对象树上发现并执行 CRUD 操作,这多亏了超链接;他们会解释 REST 设计是多么的绝妙,而我只是没有阅读足够多的,关于它概念的文章和论文。

我不管这些。什么树结什么果。我用简单的 RPC,只需要花费我几个小时来编码,就可以非常健壮地工作。现在,需要数周的时间,而且并不能阻止出现新的错误,也不能实现预期。开发变成了修补。

几乎透明的远程过程调用(RPC),是 99% 的人真正需要的,现有的协议尽管不完美,却很好用。这种寻求 Web 最小公分母的偏执,造成了时间和脑力的巨大浪费。

REST 提倡简单,其实复杂。
REST 提倡稳定,其实脆弱。
REST 提倡互操作性,却带来了异构性。
REST 就是新的 SOAP。

后记

未来可能是光明的。仍然有大量优秀的协议可供使用,二进制或文本格式的,模式或非模式的,有利用 HTTP2 新功能的……所以,让我们继续前进。我们不能永远停留在 WEB 服务的石器时代。

编辑:许多人问这些替代协议,这些应该另起文章讨论。但人们可以看看 XMLRPC 和 JSONRPC(简单却很有意义),或者 JSONWSP(包括 schema),或者内部使用的话,可以看看语言层面提供的,像 Pyro 或 RMI,或者提供公开 API 的话,可以看看这个圈里的新成员,像 GraphQL 和 gRPC……

推荐↓↓↓
Web开发
上一篇:Chrome 70 即将发布,数千个网站或因安全证书受影响​​​​​​​ 下一篇:美图高性能twemproxy的改造之路