在今天的文章中,我将深入探讨gRPC和REST,这可能是当今创建API最常用的两种方法。
I will start with a short characteristic of both tools — what they are and what they can offer. Then I will compare them according to seven categories, in my opinion, most crucial for modern-day systems.
分类如下:
- 底层HTTP协议
- 支持的数据格式
- 数据大小
- 吞吐量
- 定义
- 易于采用
- 工具支持
为何选择
当人们听到“API”时,他们可能立刻想到REST API。然而,REST只是构建API的众多方法之一。它并非适用于所有用例的万能解决方案。还有其他方式,其中RPC(远程过程调用)就是其中之一,而gRPC可能是与RPC合作最成功的框架。
尽管gRPC是一种相当成熟且高效的技术,但仍被视为新兴技术。因此,尽管在某些用例中非常方便,其采用率仍低于REST。
我撰写这篇博客的主要目的是推广gRPC,并指出其在哪些用例中能够大放异彩。
什么是REST?
REST,或称表述性状态转移,可能是创建任何类型API应用最常用的方式。它利用HTTP作为底层通信媒介,因此能充分利用HTTP的优势,如缓存。
此外,REST自设计之初就是无状态的,这使得客户端与服务器之间的分离变得容易。客户端仅需了解服务器所暴露的接口即可有效通信,无需依赖服务器的任何实现细节。客户端与服务器的交互基于请求和响应模式,每个请求都是典型的HTTP请求。
REST既不是协议也不是工具(在某种程度上),它是一种构建应用程序的架构方法。遵循REST原则的服务被称为RESTful服务。作为一种架构,它对使用者施加了一些约束,具体包括:
- 客户端-服务器通信
- 无状态通信
- 缓存
- 统一接口
- 分层系统
- 按需代码
REST的两个核心概念是:
- 端点:一个唯一的URL(统一资源定位符),代表特定的资源;可视为通过互联网访问特定操作或数据元素的方式
- 资源:在特定URL下可用的特定数据片段
此外,有一个描述称为Richardson成熟度模型,该模型描述了REST API的“专业化”程度。它将REST API分为3个级别(或4个,取决于是否计算第0级),根据特定API具备的一系列特征进行划分。
其中一项特征是,REST端点应在URL中使用名词,并采用正确的HTTP请求方法来管理其资源。
- 示例:使用DELETE user/1而非GET user/deleteById/1
至于HTTP方法及其对应操作,如下所示:
- GET — 检索特定资源或资源集合
- POST — 创建新资源
- PUT — 整体更新资源
- PATCH — 部分更新特定资源
- DELETE — 按ID删除特定资源
成熟度模型还规定了更多内容,例如,一个名为HyperMedia的概念。HyperMedia将数据呈现与客户端可执行的操作控制紧密结合。
A full description of the maturity model is out of the scope of this blog — you can read more about it here.
注意:本段提及的许多内容比此处描述的更为复杂。REST是一个相当庞大的主题,值得用一系列文章来探讨。尽管如此,这里所述均符合广为人知的REST最佳实践。
什么是gRPC?
这是远程过程调用(RPC)这一相对古老概念的又一实现。由谷歌的团队打造,因此其名称中带有“g”。它或许是当前最为现代化且高效的RPC工具,同时也是CNCF孵化项目。
gRPC采用谷歌的Protocol Buffers作为序列化格式,并利用HTTP/2作为传输媒介,尽管它也能与JSON数据层协同工作。
gRPC的基本构建模块包括:
- 方法:gRPC的基本单元,每个方法都是一次远程过程调用,接收输入并返回输出。它执行一个单一操作,该操作在所选编程语言中进一步实现。截至目前,gRPC支持四种方法类型:
- 一元:经典的请求-响应模式,方法接收输入并返回输出。
- 服务器流式:方法接收一条消息作为输入,返回一系列消息作为输出。gRPC保证单个RPC调用内的消息顺序。
- 客户端流式:方法接收一系列消息作为输入,处理它们直至消息流尽,然后返回一条消息作为输出。与上述类似,gRPC保证单个RPC调用内的消息顺序。
- 双向流式传输:该方法以流作为输入,并以流的形式返回输出,有效地使用了两个读写流。两个流独立运作,且在流级别上保持消息顺序。
- 服务:代表一组方法——每个方法在其服务内必须有唯一的名称。服务还描述了诸如安全性、超时或重试等功能。
- 消息:表示方法的输入或输出的对象。
gRPC API的定义以.proto文件的形式编写,其中包含了上述所有三种基本构建块。此外,gRPC提供了一个协议缓冲编译器,它从我们的.proto文件生成客户端和服务端代码。
我们可以按照自己的方式实现服务器端方法。我们必须遵守API的输入输出契约。
在客户端,有一个称为客户端(或存根)的对象——类似于HTTP客户端。它知道服务器的所有方法,并负责调用远程过程并返回它们的响应。
对比分析
底层HTTP协议
这是第一个类别,可能也是最重要的一个,因为它的影响在其他方面也能显现。
通常,REST是基于请求-响应的,并使用HTTP/1.1作为传输媒介。我们必须使用不同的协议,如WebSocket(更多关于它们的信息在此)或任何类型的流式传输或更持久的连接。
我们还可以在周围实现一些临时的代码,使REST看起来像流式传输。此外,使用HTTP/1.1的REST需要为每次请求-响应交换建立一个连接。这种方法对于长时间运行的请求或网络能力有限的情况下可能会有问题。
当然,我们可以使用HTTP/2来构建类似REST的API;然而,并非所有服务器和库都支持HTTP/2。因此,问题可能会在其他地方出现。
另一方面,gRPC仅使用HTTP/2。它允许通过单个TCP连接发送多个请求-响应对。这种方法可以为我们的应用程序带来相当显著的性能提升。
- 结果: gRPC略胜一筹
支持的数据格式
假设默认情况下REST API使用HTTP/1.1,那么它可以支持多种格式。
REST通常不对消息格式和样式施加任何限制。基本上,任何可以序列化为纯文本的格式都是有效的。我们可以使用在特定场景中最适合我们的任何格式。
在REST应用中发送数据最流行的格式无疑是JSON。由于大量旧版应用程序的存在,XML紧随其后。
但是,当使用REST与HTTP/2时,则仅支持二进制交换格式。在这种情况下,我们可以使用Protobuf或Avro。当然,这种方法可能会有其缺点,但更多内容将在后续点中讨论。
与此同时,gRPC仅支持两种交换数据格式:
- Protobuf — 默认情况下
- JSON — 当您需要与旧API集成
如果您选择使用JSON,那么gRPC将使用JSON作为消息的编码格式,并采用GSON作为消息格式。此外,使用JSON还需要进行一些额外的配置。以下是gRPC文档中关于如何进行此操作的说明。
- 结果:REST在此方面胜出,因为它支持更多格式。
数据大小
默认情况下,gRPC采用二进制数据交换格式,这极大地减少了网络上发送的消息大小:研究表明,数据大小大约减少40-50%,而我从之前的一个项目经验来看,甚至可以达到50-70%的减少。
上述文章提供了一个相对深入的JSON与Protobuff之间的大小比较。作者还提供了一个工具来生成JSON和二进制文件,因此您可以重新运行他的实验并比较结果。
文章中的对象结构相对简单,但一般规律是——JSON中嵌套对象越多,结构越复杂,其相对于Protobuf的体积就会越重。Protobuf相比JSON体积小50%是一个不错的基准。
通过使用二进制交换格式进行REST,差异可以最小化或消除。然而,这不是最常见也不是最佳支持的实现RESTful API的方式,因此可能会出现其他问题。
- 结果:在默认情况下,gRPC胜出;在两者都使用二进制数据格式的情况下,平局。
吞吐量
再次,对于REST,一切都取决于底层的HTTP协议和服务器。
在默认情况下,基于HTTP/1.1的REST,即使是最优性能的服务器也无法超越gRPC的性能,特别是在使用JSON时增加了序列化和反序列化的开销。尽管当我们切换到HTTP/2时,差异似乎有所减小。
至于最大吞吐量,在两种情况下,HTTP作为传输介质,有潜力无限扩展。因此,一切都取决于我们使用的工具以及我们如何精确地操作我们的应用程序,因为设计上没有限制。
- 结果:在默认情况下,gRPC胜出;在两者都使用二进制数据和HTTP/2的情况下,平局或略微胜出gRPC。
定义
在这一部分,我将描述我们如何在两种方法中定义我们的消息和服务。
在大多数REST应用中,我们只是将我们的请求和响应声明为类、对象或特定语言支持的任何结构。然后我们依赖提供的库来序列化和反序列化JSON/XML/YAML,或我们需要的任何格式。
此外,目前已有一些工具旨在根据Swagger的REST API定义,生成用户选择的编程语言代码。然而,这些工具似乎还处于alpha版本,因此可能存在一些bug和小问题,导致使用起来较为困难。
对于REST应用而言,二进制格式与非二进制格式之间的差异不大,因为两者的规则大体相同。对于二进制格式,我们只需按照特定格式的要求定义所有内容。
同时,我们通过底层库或框架的方法或注解定义了REST服务。该工具还负责将其与其他配置一同对外公开。
在gRPC的情况下,Protobuf是默认且实际上唯一的定义编写方式。我们必须声明所有内容:消息、服务和方法在.proto文件中,因此这相当直接明了。
随后,我们使用gRPC提供的工具为我们生成代码,只需实现我们的方法即可。之后,一切应按预期运行。
此外,Protobuf支持导入功能,我们可以相对简单地将设置分散到多个文件中。
- 结果:这里没有明显的胜者,只有描述和我的一点建议:选择最适合你的方法。
易采用性
在这一部分,我将比较现代编程语言中每种方法的库/框架支持情况。
在软件工程师的短暂职业生涯中,我所接触的每种编程语言(如Java、Scala、Python)都至少拥有三个主要的库/框架用于构建REST风格的应用程序,更不用说还有数量相当的库用于将JSON解析为对象/类。
此外,由于REST默认使用人类可读的格式,它对新手来说更易于调试和使用。这不仅能加快新功能的交付,还能帮助你应对代码中出现的错误。
简而言之,对REST风格应用的支持非常良好。
在Scala中,我们甚至有一个名为tapir的工具——我有幸曾担任过一段时间的维护者。Tapir让我们能够抽象HTTP服务器,编写适用于多个服务器的端点。
gRPC自身为超过8种流行的编程语言提供了客户端库。这些库通常包含构建gRPC API所需的一切,足以满足需求。此外,我还了解到有更高级别的抽象库,如Java通过Spring Boot Starter,以及为Scala提供的库。
另一个关键点是,REST如今被视为全球标准及构建服务的入门技术,而RPC尤其是gRPC,尽管已存在一段时间,仍被看作是一种新颖技术。
- 结果:由于REST的广泛采用及其周围丰富的库和框架生态
工具支持
上文已涵盖了库、框架及市场占有率,因此本部分将重点讨论围绕这两种风格的工具支持,包括测试工具、性能/压力测试工具及文档工具。
自动化测试/测试
首先,对于REST,构建自动化测试的工具通常集成在不同的库和框架中,或是专门为此目的设计的独立工具,如REST-assured。
至于gRPC,我们可以生成存根并用于测试。若需更严格控制,可利用生成的客户端作为独立应用,并以此为基础对实际服务进行测试。
至于外部工具对gRPC的支持,我所知的有:
- Postman应用支持gRPC
- JetBrains IDEs中的HTTP客户端在经过简单配置后也能支持gRPC。
- 结果一:REST获胜;然而,gRPC的情况似乎有所改善。
性能测试
在此方面,REST具有显著优势,因为像JMeter或Gatling这样的工具使得对REST API进行压力测试相对容易。
遗憾的是,gRPC并未得到同等支持。我知道Gatling的开发者已经在新版本中包含了gRPC插件,因此情况似乎正在好转。
但直到现在,我们只有一个非官方的插件和库,名为ghz。这些都很好,只是支持程度与REST不在同一水平。
- 结果二:REST再次获胜;然而,gRPC的情况似乎又有所改善,:)
文档
在API文档方面,REST再次胜出,OpenAPI和Swagger在业界广泛采用,成为事实上的标准。几乎所有REST库都能以最小努力或直接开箱即用地提供Swagger文档。
遗憾的是,gRPC并没有类似的支持。
然而,问题在于gRPC是否真的需要这样的工具。gRPC在设计上比REST更具描述性,因此额外的文档工具可能并不必要。通常情况下,我们的API描述使用.proto文件比负责构建REST API的代码更为声明式和简洁,或许gRPC本身就无需更多文档。这个答案留给你自己决定。
- 第三个结果:REST胜出;然而,关于gRPC文档的问题仍然悬而未决。结果三:REST的胜利;不过,gRPC文档的问题依旧开放。
总体结果:
A significant victory for REST
总结
最终的比分表如下所示。

比分在两种风格间平分秋色,各赢三局,有一项未决出明显胜者。
没有万能钥匙:只需考虑哪些类别对你的应用最为关键,然后选择在这些类别中获胜最多的方法——至少这是我的建议。
至于我的偏好,如果可能,我会尝试使用gRPC,因为它在我上一个项目中表现出色。相比传统的REST,它可能是一个更好的选择。
如果你在选择REST还是gRPC时需要帮助,或者遇到其他任何技术问题,随时告诉我。我或许能提供帮助。
感谢你的时间。