本文要点
表征状态转移(REST)已经成为微服务通信事实上的标准。作者认为,这不是一件好事——事实上,这是一件非常糟糕的事,尤其是对于微服务通信来说。
REST 是基于 HTTP 实现的。使用 REST 的一个常见理由是,它很容易调试,因为它是“人类可读的”。不容易阅读是工具问题。
在微服务通信协议设计中,我们需要的部分特性包括二进制序列化、双向通信、多路复用以及元数据交换能力。
工程师希望能够在数据到来时进行处理——他们希望能够流化数据。对于通过流发送的数据,需要应用程序流控制。
我们需要一种现代化的方式来代替 HTTP,用于创建现代化的服务。开源项目 RSocket 是为服务而设计的。它是面向连接的、消息驱动的协议,内置了应用程序级的流控制。
表征状态转移(REST)已经成为微服务通信事实上的标准。作者认为,这不是一件好事——事实上,这是一件非常糟糕的事。这种情况是如何出现的呢?在 REST 出现的时候,还有更糟糕的选择。在 2000 年,Roy Fielding提出“REST”的时候,在众多味道差得多的三明治中,“REST”是羽衣甘蓝三明治。
人们在使用 SOAP、RMI、CORBA 和 EJB。JSON 从 XML 那里获得了不错的喘息之机。使用 URL 输出一些文本很容易。此外,JavaScript 开始真正在浏览器中流行起来,它比 SOAP 更容易处理 REST。与最近的微服务趋势不同,大多数应用程序都是传统的单体三层应用。与它们交互的大多数外部流量都来自于浏览器,所以当它们需要生成一些东西时,REST 是一个简单的选择。许多人开始从比较大的商业产品(如 WebSphere)转向 Jetty 和 Tomcat。它们甚至没有处理 EJB 的工具,所以 REST 是一个方便的选择。
这和微服务有什么关系呢?早期的微服务先驱者转向微服务的原因与今天的人们不同。他们转向微服务是因为他们必须应对大规模的应用程序。他们开始拥有如此多的用户,以至于他们无法在一个单体应用中提供所有的服务。与当今许多企业不同的是,成本不是激励因素——时间才是。他们昨天就需要把服务准备好。当越来越多的用户使用他们的软件时,他们的单体应用难以为继,所以他们把应用程序分割成更小的部分。他们可以将这些应用程序部署到数千台服务器上,然后是虚拟机。
此外,他们可以非常迅速地部署他们的应用程序。采用这种模式的公司能够生存下来。然而,在这场竞赛中,他们没有太多时间去考虑他们在做什么。这些早期的先驱者必须应对用户的指数级增长和竞争,所以他们选择战术解决方案是有道理的。其中之一就是使用 REST 进行服务通信。
为什么 REST 对微服务而言很糟糕?
在编写应用程序时,编程语言最终会以机器码的形式存在。这一点是显而易见的。甚至像 Java 或 JavaScript 这样的“解释型”语言也是如此。它们不直接编译成机器代码,而是使用 JIT 或即时编译器。在某些情况下,即时编译的代码可能比工程师手工编写和调试的代码还要快——VM 确实是现代计算机科学的精品。
那我们为什么要浪费这个精品呢?我们发送的不是针对机器进行过优化的二进制消息,而是在针对服务进行过优化的协议上发送针对人类进行过优化的消息。我们使用为发送图书而设计的协议发送 JSON 和 XML 这样的东西。想想这是多么可笑!你有一个二进制程序,它把一个二进制结构转换成文本,通过网络以文本的形式发送给一台机器,后者再解析并把它转换成二进制结构,然后在应用程序中进行处理。
避免现代 CPU 的缓存未命中是非常关键的。遗憾的是,解析大量 JSON 和字符串将导致缓存未命中!
使用 REST 的一个常见理由是,它很容易调试,因为它是“人类可读的”。不容易阅读是工具问题。JSON 文本只是人类可读的,因为有一些工具让你可以阅读它——否则,它只是网络上的字节。此外,发送出去的数据有一半被压缩或加密——在这两种情况下,都是人类不可读的。此外,其中有多少是人们可以通过阅读“调试”的?如果你有一个平均每秒 10 个请求的服务,它的 JSON 大小为 1KB,即相当于每天 860MB 的数据,或者每天 250 本《战争与和平》。没有人能读懂,所以你只是在浪费钱。
然后,你有时候需要分发二进制数据,或者希望使用二进制格式而不是 JSON。为此,必须对数据进行 Base64 编码。这意味着,你实际上将数据序列化两次——同样,这不是使用现代硬件的有效方法。
最后,REST 基于 HTTP 实现。HTTP 被用作在服务之间发送传输数据。HTTP 被设计用于在互联网上搬运图书。它不应该用于服务之间的通信。相反,使用一种针对应用程序进行过优化的格式——它负责处理所有的数据。
什么适合于微服务通信?
如果我们暂时假设 REST 不是服务之间通信的最佳选择,那么什么才是呢?让我们来看一下,我们希望在一个为微服务通信而设计的协议中包含的一些内容。
首先,我们希望它是双向的。这是 REST 的一个大问题——客户端只能调用服务器。当双方具有对等的相互调用能力时,你就可以用一种自然的方式创建应用程序之间的交互。否则,你将不得不设计一些笨拙的变通方案,例如长轮询,以模拟服务器发起的调用。你可以使用 HTTP/2 部分地解决这个问题,但是调用仍然需要由客户端发起。你需要的是客户端和服务器在必要时能够自由地相互调用。
另一个要求是,服务之间的连接必须支持同一连接上的多请求,而且是同时。这叫做多路复用。现在,对于单个连接,需要有某种方法来区分一个请求和另一个请求。这不同于 HTTP,一个请求开始,另一个请求结束。使用多路复用,你需要跟踪不同的请求。用二进制帧来表示每个请求是一个不错的方法。每个帧都可以保存请求以及和请求有关的元数据。然后,可以用它把帧发送到正确的位置。
当通过单个连接发送数据时,需要分割请求的能力。单个连接上的大型请求会阻塞它后面的所有其他请求,也就是排头阻塞。相反,我们需要的是将请求分割成较小的部分,并通过网络发送它们。由于数据是按帧发送的,所以可以把它分解成更小的帧片段,然后在另一边重新组装。通过这种方式,请求可以相互交织。大请求不会再阻塞小请求。这样就可以创建一个响应更快的系统。
此外,交换连接元数据的能力也很有用。有时候,需要发送的数据并不一定是业务事务的一部分——比如配置整体跟踪级别或交换基于字典的压缩信息。这些都与业务逻辑无关,但可以在连接级进行控制。交换元数据的能力可以为此提供支持。
在应用程序代码中,经常会调用一个函数或方法,该函数或方法接受一个列表,返回一个列表,或者两者兼而有之。这在微服务中也经常发生。REST 不能很好地处理这些情况,这会导致各种各样的问题和复杂性。
所需要的是一个能够轻松自然地处理迭代数据的协议——就像你在应用程序中所做的那样。读取整个数据列表,对其进行处理,然后在处理完所有数据之后返回数据列表,这是没有意义的。你需要的是处理数据的能力。你希望能够流化数据。如果有一长串数据,你不希望在数据处理时等待——你希望在数据可用时将其发送出去,并在响应出现时获取它。
这样可以创建一个响应更快的系统。它可以用于各种各样的事情,从从文件中读取字节并在网络上以流的方式传输,到返回数据库查询结果,再到向后端提供浏览器点击流数据。如果协议中提供了一等的流支持,则不需要包含其他系统(如 Spark)来进行流处理。也没有必要包括 Kafka 之类的东西,除非你想要存储数据。
对于通过流发送的数据,接下来需要的是应用程序流控制。字节级的流控制对于 TCP 之类的东西是有效的,因为从网卡的角度来看,所有东西的大小都是相同的,并且一般来说,处理的成本是相同的。然而,在应用程序中,并不是所有东西的成本都是相同的。一个 10KB 的消息可能需要 10 毫秒来处理,但另一个 10 字节的消息需要 10 秒。
在微服务中发现的另一个场景是,下游服务处理数据的速度比它能够达到的速度慢。这意味着,TCP 缓冲区永远不会满。也需要有某种方法来控制流量,以避免淹没下游服务,以保持它们的响应性。
应用程序必须能够控制消息流速率,而不受底层网络字节影响。对于应用程序开发人员来说,很难推断一条消息在不同语言之间有多少字节。另一方面,对于开发人员来说,推断他们发送了多少消息是很简单的。通过这种方式,服务可以在网络流控制和应用程序流控制之间进行寻租。有时,应用程序处理数据的速度比网络快,有时,网络处理数据的速度比应用程序快。应用程序流控制可以确保尾延迟稳定——又回到了创建响应性应用程序。它还可以防止对无界队列的需求,这是在其他应用程序中发现的一种危险的黑客行为。
如上所述,RESTful Web 服务有一个巨大的缺陷,它们实际上是基于文本实现的。要发送任何二进制数据,你需要对数据进行 Base64 编码,并将所有内容序列化两次。你真正想要的是二进制的东西——因为它可以表示任何东西——包括文本。此外,与文本(尤其是数值)相比,应用程序处理二进制数据的效率要高得多。此外,它们天生更紧凑——它们没有额外的大括号、花括号或尖括号。最后,如果数据是二进制的,根据格式的不同,也有可能实现零拷贝序列化和反序列化。这超出了本文的讨论范围,但读者可以看下简单二进制编码(SBE)和Flatbuffers。它们比使用 JSON 快得多。
最后,你希望能够通过不同的传输协议来发送请求。RESTful Web 服务通常使用 HTTP,而它仅使用 TCP。你真正想要的是一种将网络抽象出来的方法,这样,你只要按照规范进行编程就可以,而不必担心传输问题。同时,如果它与浏览器对话,你的应用程序应该能够基于 WebSocket 运行。不应该因为想要更改应用程序部署的位置就必须切换到新的网络工具包,传输协议的切换应该可以在不修改应用程序的情况下轻松完成。
什么协议符合要求?
有人认为 REST 和 HTTP/2 更合适,HTTP/2 优于 HTTP/1,但是,如果你阅读下规范,就会发现,它唯一目的是创建更好的 Web 浏览器协议。它从来就不是为微服务而设计的,也没打算用于微服务。它就是应该用于服务器 HTML 到 Web 浏览器。同样,它从来没有打算用于微服务通信。此外,你仍然必须处理 URL 并将不同的 HTTP 方法匹配到你的应用程序——这些方法从来就没有真得打算用于服务器到服务器的通信。
HTTP/2 确实提供了流,但它提供流只是为了服务器推送。因此,使用基于 HTTP/2 的 REST 需要从客户端发起请求,然后将数据推送到服务器。HTTP/2 的流控制是基于字节的流控制。对 Web 浏览器而言,这很好,但对应用程序来说,就不太好了。目前还无法像在应用程序上所做的那样来控制应用程序流。
最近,围绕使用 gRPC 有很多争论。gRPC 在概念上与 SOAP 非常类似。它没有使用 XML 来定义服务,而是使用了 Protobuf。像 SOAP 一样,它结合了 URL 和 Header“幻数(magic)”——这次使用了 HTTP/2。这意味着,gRPC 明确地绑定到了 HTTP/2,这是一种为 Web 浏览器设计的协议。更糟糕的是,Web 浏览器不支持它。
因此,你必须使用一个代理将 gRPC 调用转成 REST 调用,违背其初衷来使用它。这凸显了 gRPC 的设计有多么糟糕。为什么要为了一种协议而使用 HTTP/2,而且不能确保它在浏览器中有效?你将永远受限于它最初的目的,却不能把它用在应该使用它的地方。这引出了我的下一个观点:REST 的最大限制是它与 HTTP 绑定。
你需要的是为服务到服务的通信而设计的协议。使用专门为服务之间的通信而设计的协议使你可以创建明显更简单、更可靠的应用程序。不会有任何非法用法、变通方案或阻抗不匹配。
建筑材料是一个很好的类比。木头是建造小桥梁的好材料。你可以用它跨越一条小溪,这不是问题。
当工程师开始使用它来跨越更大的距离时,事情就变得复杂起来。
像这样的木桥很有用。但是,与使用更好的材料建造的现代桥梁相比,它们的故障率非常高。它们也非常复杂,建造起来要花很长时间。这就是为什么我们现在使用钢筋和混凝土。它们更容易维护,建造成本更低,寿命更长,而且可以跨越更远的距离。
我们需要一种现代化的材料来代替 HTTP 用于创建现代化的服务。开源项目RSocket就是为服务而设计的。它是面向连接的、消息驱动的协议,内置了应用程序级的流控制。它在浏览器中和在服务器上一样工作。事实上,Web 浏览器可以服务于后端微服务的流量。它也是二进制的。它可以同样好地处理文本和二进制数据,并且可以分解有效工作负载。它将应用程序中的所有交互建模为网络原语。这意味着,你可以流化数据或执行发布/订阅,而无需设置应用程序队列。
在合理的情况下,REST 是一个不错的解决方案。其中一个不合理的地方就是微服务。分布式系统本身就非常困难。我们最不需要的就是,使用不为它们设计的东西使它们变得更加复杂。
作者简介
Robert Roeser 是 Netifi 的联合创始人兼 CEO。他在分布式实时系统领域有 10 年的经验。他在 Netflix 和耐克公司领导大规模技术项目。
查看英文原文:Give REST a Rest with RSocket
评论 8 条评论