所有文章 > 日积月累 > gRPC 与 REST:理解 gRPC、OpenAPI 和 REST 以及在 API 设计中何时使用它们
gRPC 与 REST:理解 gRPC、OpenAPI 和 REST 以及在 API 设计中何时使用它们

gRPC 与 REST:理解 gRPC、OpenAPI 和 REST 以及在 API 设计中何时使用它们

大多数软件开发人员无疑都知道,API设计有两个主要模型:RPC和REST。无论采用哪种模型,大多数现代API都以这样或那样的方式映射到相同的HTTP协议上。RPC API设计也变得普遍,它们在保持RPC模型的同时,采用了一两个来自HTTP的理念,这增加了API设计者面临的选择范围。本文试图解释这些选择,并提供如何在它们之间做出选择的指导。

gRPC是一种实现RPC API的技术,它使用HTTP 2.0作为其底层传输协议。你可能会认为gRPC和HTTP是互斥的,因为它们基于相反的概念模型。gRPC基于远程过程调用(RPC)模型,其中可寻址的实体是过程,数据隐藏在过程后面。HTTP的工作方式相反。在HTTP中,可寻址的实体是“数据实体”(在HTTP规范中称为“资源”),行为隐藏在数据后面——系统的行为来自于创建、修改和删除资源。

实际上,谷歌和其他地方创建的许多API以一种有趣的方式结合了RPC和一些来自HTTP的理念。这些API采用了与HTTP相同的面向实体模型,但使用gRPC定义和实现,生成的API可以使用标准的HTTP技术调用。我们将尝试描述它的工作原理,为什么它可能对您有益,以及它可能不适用的地方。

首先,让我们更仔细地看看HTTP通常如何用于API。

用于API的HTTP三种主要方式

大多数公共API和许多私有分布式API都使用HTTP作为传输方式,至少部分原因是组织习惯于处理允许在端口80和443上进行HTTP流量的安全问题。

在我看来,有三种重大且不同的方法来构建使用HTTP的API。它们是:

  1. REST
  2. gRPC(以及Apache Thrift等)
  3. OpenAPI(及其竞争对手)

REST

最不常用的API模型是REST——尽管REST这个词被更广泛地使用(或滥用),但只有一小部分API是这样设计的。这种风格的API的一个标志性特征是客户端不会从其他信息构建URL——它们只是原样使用服务器分发的URL。这就是浏览器的工作方式——它不会从部分构建它使用的URL,也不理解它使用的URL的网站特定格式;它只是盲目地遵循当前页面从服务器接收到的URL,或者从之前页面中保存的书签,或者是用户输入的。浏览器对URL的唯一解析是提取发送HTTP请求所需的信息,浏览器对URL的唯一构造是从相对URL和基础URL形成绝对URL。如果您的API是REST API,那么您的客户端永远不需要理解您的URL格式,这些格式也不是提供给客户端的API规范的一部分1。

REST API可以非常简单。已经发明了许多额外的技术用于与REST API一起使用——例如JSON API、ODATA、HAL、Siren或JSON Hyper-Schema等——但你不需要任何这些技术就能很好地实现REST。

gRPC

第二种使用HTTP进行API的模型由gRPC展示。gRPC在底层使用HTTP/2,但HTTP不对API设计者暴露。gRPC生成的存根和骨架隐藏了HTTP,因此没有人需要担心如何将RPC概念映射到HTTP——他们只需要学习gRPC。

客户端使用gRPC API的方式是通过遵循以下三个步骤:

  1. 决定调用哪个过程
  2. 计算要使用的参数值(如果有)
  3. 使用代码生成的存根进行调用,传递参数值

OpenAPI

使用OpenAPI(以前称为Swagger规范)等规范语言设计使用HTTP的RPC API的最流行方式。

OpenAPI风格的API的一个显著特点是,客户端通过从其他信息构造URL来使用API。客户端使用OpenAPI API的方式遵循以下三个步骤:

  1. 决定使用哪个OpenAPI URL路径模板
  2. 计算要使用的参数值(如果有的话)
  3. 将参数值插入URL路径模板并发送HTTP请求。

应该立即明显的是,以这种方式工作的API不是REST API。OpenAPI使用HTTP的方法要求客户端详细了解它们在请求中使用的URL的格式,并根据其他信息构造符合该格式的URL。这与REST API的工作方式完全相反,REST API中的客户端对它们使用的URL格式一无所知,也永远不需要构造它们。OpenAPI支持的模型非常流行和成功,是API设计者可用的最重要的选项之一——OpenAPI模型不是REST并不减少其实用性或重要性。

你可能做出的第二个观察是,使用OpenAPI API的客户端模型与使用gRPC API的客户端模型非常相似。当一个gRPC客户端选择一个要调用的程序时,一个OpenAPI客户端选择一个URL路径模板来使用。gRPC和OpenAPI客户端都会计算参数值。gRPC客户端使用存根程序将参数与程序签名结合起来并发起调用,而OpenAPI客户端将参数值插入到URL路径模板中并发出HTTP请求。细节不同,但总体模型非常相似。OpenAPI还包括一些工具,这些工具可以可选地生成客户端编程语言中的客户端存根程序,隐藏这些细节,使得两种客户端体验更加相似。

解释gRPC和OpenAPI客户端模型之间密切相似性的一个方法是将OpenAPI视为一种用于指定经典RPC API的语言,它具有自定义映射到HTTP请求。如果你接受这个观点,那么gRPC和OpenAPI都是RPC接口定义语言(IDLs),它们之间的基本区别在于OpenAPI向客户端暴露了底层HTTP传输的细节,并允许API设计者控制映射,而gRPC使用预定义的映射隐藏了所有HTTP细节。

即使你不接受OpenAPI使用的基本API模型只是传统的RPC这一观点,也难以否认两者之间存在一些明显的相似之处,并且它们都与REST截然不同。不管怎样,我认为这些类比有助于激发接下来更详细的比较。

这些方法各有利弊——我们将探索所有三种,并给你一些关于如何决定哪一种最适合你的应用的想法。

看看RPC有多简单和明显!

这是一个来自一个流行博客文章的例子,该文章赞扬了RPC的优点(我们稍后会回到这篇博客文章):

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}

博主说,很多人发现为这个问题定义一个RPC API很容易,但用HTTP解决同样的问题却很挣扎,浪费了大量的时间和精力,而没有为他们的项目带来任何好处。我同意。一个原因是,在HTTP之上设计API是一项需要学习的技能,而且有很多选择。

使用REST模型也同样简单和明显

因为我们已经使用REST设计了许多API,所以对我们来说,用REST表达这个例子也同样明显。以下是我会怎么做的:

POST /accounts <headers> (username, contact_email, password)> -> account_URL
POST /subscriptions <headers> (account_URL, subscription_type) -> subscription_URL
POST /activation-reminder-outbox <headers> (account_URL) -> email_URL
POST /cancellations <headers> (subscription_URL, reason, immediate=True) -> cancellaton_URL
GET {account_URL} -> {full data tree}

客户端提供给服务器的用户名、联系邮箱、密码、account_URL以及其他数据都只是请求正文中的简单JSON名称/值对。我省略了请求头的细节以及返回结果的方式,因为这些都在HTTP规范中有详细解释——实际上没有选择或决策要做。

客户端和服务器之间在两个方向上传递的所有标识符都是URL——API中没有不是URL的标识符。每当一个资源包含对另一个资源的引用时,这个引用都是使用另一个资源的URL来表达的。这种技术被称为超文本或超媒体——如果你的API不使用URL这种方式,那么它就不是在使用REST模型,因为超文本链接是REST区别于其他模型的一个标志性特征。RPC API也通过在一个实体中包含另一个实体的标识符来表达实体之间的关系,但这些标识符不是可以直接使用的URL,它们需要额外的信息。

REST的优势

REST所声称的优势基本上就是万维网本身的优势,比如稳定性、一致性和普遍性。这些优势在其他地方有所记录,而且REST无论如何都是少数人的兴趣,所以我们不会在这里过多地讨论它们。一个例外是HTTP/REST模型固有的实体导向特性。这个特性特别值得关注,因为它已经被非REST模型的支持者广泛讨论和采用,比如gRPC和OpenAPI。 根据我的经验,实体导向模型比简单的RPC模型更简单、更规则、更容易理解,并且随着时间的推移更稳定。RPC API倾向于随着一个接一个的程序添加而有机地增长,每个程序都实现了系统可以执行的一个动作。

实体导向模型为系统的行为提供了一个整体的组织结构。例如,我们大家都熟悉在线购物的实体模型,它有产品、购物车、订单、账户等。如果这种能力仅用RPC程序来表达,将会导致一个长长的、无结构的程序列表,用于浏览产品目录、将它们添加到购物车、结账、跟踪送货和退货。

这个列表很快就变得令人不知所措,并且在程序定义之间很难实现一致性。给列表带来结构和秩序的一种方式是,使用一套标准的程序来模拟每种实体类型的所有行为。HTTP 本身是以实体为中心的,但你同样可以将实体导向添加到 RPC 中,稍后会讨论这一点。按实体类型对过程进行分组也是面向对象语言的关键思想之一。

你如何使用 OpenAPI

在 OpenAPI 中,你定义了一些称为路径的东西。OpenAPI 路径在 YAML 中看起来像这样:

paths:
/pets/{petId}:
get:
operationId: getPetById
parameters:
- name: petId
in: path
required: true
description: 要检索的宠物的 ID
schema:
type: string

定义了像这些路径的 API 将 {petId} 的值暴露给客户端,并要求客户端使用适当的路径定义,以便将 {petId} 值(和其他值)转换成可用于 HTTP 请求的 URL。

以这种方式表达和使用 ID 是 REST 标志性的超文本链接的替代方案。

OpenAPI 将这些路径中的变量称为“参数”,路径和 HTTP 方法的组合称为“操作”——与 RPC 系统的术语相似。

**OpenAPI 使用带有参数的 URL 模板可以被视为一种用自定义映射到 HTTP 的方式来表达类似 RPC 的概念。

OpenAPI 的优点和缺点

在我看来,OpenAPI 有两个基本特征可以解释其成功。首先,OpenAPI 模型与传统的 RPC 模型相似,大多数程序员对此都很熟悉和舒适。该模型还非常适合他们使用的编程语言的概念。第二个原因是它允许程序员定义这些RPC概念到HTTP请求的自定义映射。这第二个特性既带来了好处也带来了问题。主要好处是客户端可以使用仅标准HTTP技术访问API。这对于公共API尤其重要,因为这意味着API几乎可以从所有编程语言和环境中访问,而不需要客户端采用任何额外的技术。一个缺点是它可能需要大量的努力来设计HTTP细节——看看网上所有关于你应该做什么和不应该做什么的指导,其中很多是相互矛盾的——以及消费者学习它的进一步努力。

什么时候gRPC可能是比OpenAPI更好的选择?

你用OpenAPI描述的API的设计挑战是定义URL路径和HTTP方法的组合来表示你的“操作”和它们的“参数”。这可能是一项棘手的工作,因为有很多选项。对于大多数项目来说,这并不一定是时间和努力的好用途。由于这种方法导致的挫折感在Pascal Chambon的博客文章中充满激情地描述了,我们之前提到过,这篇文章还提供了我们开头提到的RPC示例。Chambon的文章包含了一些错误信息和误解,而且对他的帖子的反应大多集中在纠正这些问题上,但Chambon的错误实际上为他的主要观点提供了支持,即设计你自己的将RPC类概念映射到HTTP上是相当复杂和困难的。

对Chambon博客文章的回应中提出的大多数建议都提倡将REST作为Chambon和大多数人熟悉的RPC模型的替代品。这当然是一个选项——我们在文章开头描述的简单的REST示例是一个极简主义者对如何做到这一点的看法。

Chambon的另一个选择是保留他的基本RPC模型,但使用gRPC而不是OpenAPI来表达它。这避免了定义自定义API到HTTP的映射的复杂性。RPC模型的持久受欢迎程度超过了任何替代品,如果API设计者无论如何都要使用RPC类模型,那么他们应该权衡所有可用的技术来实现这一点。

gRPC的优势

gRPC用接口描述语言(IDL)表达一个RPC API,这种语言从DCE IDL、Corba IDL等RPC IDL的悠久传统中受益。与OpenAPI使用URL路径、它们的参数和与之一起使用的HTTP方法的方法相比,gRPC的IDL提供了一种更简单、更直接的方式来定义远程过程。gRPC 在底层使用 HTTP/2,但 gRPC 并未将任何 HTTP/2 暴露给 API 设计者或 API 用户。gRPC 已经做出了所有关于如何在 HTTP 上层实现 RPC 模型的决策,因此您无需再做这些决策——这些决策已经内置在 gRPC 软件和生成的代码中。这使得 API 设计者和客户端的生活更简单。相比之下,OpenAPI 要求 API 设计者指定他们的特定 API 在 HTTP 上如何表达 RPC 模型的细节,API 的客户端必须了解这些细节。OpenAPI 方法的一个重要优势是它允许 API 客户端使用标准的 HTTP 工具和技术,对于许多 API 设计者来说,这证明了付出的努力是值得的。

无论您的 API 如何使用 HTTP,您可能都希望为程序员使用各种语言创建客户端编程库。这些编程库将采取程序的形式(可能被称为函数或方法,这取决于编程语言)。gRPC 最吸引人的特性之一是它非常擅长生成直观且高效的客户端编程库供程序员使用和执行。OpenAPI 也可以生成客户端编程库,但我发现 gRPC 版本更简单、更明显,可能是因为其 IDL 只需要表达 RPC 概念,而不需要同时描述这些概念到 HTTP 的映射。在gRPC中指定的API在服务器端也很容易实现。由于gRPC提供的框架、库和代码生成工具,创建gRPC方法的服务器实现可能比编写一个解析传入请求并调用正确实现函数的标准HTTP请求处理器更简单,尽管有许多框架旨在帮助完成这项工作。

gRPC的另一个特点是性能良好。gRPC使用二进制负载,创建和解析都非常高效,并且利用HTTP/2高效管理连接。当然,你也可以不使用gRPC直接使用二进制负载和HTTP/2,但这需要你和你的客户端掌握更多的技术。

gRPC还避免了即使是最好的基于HTTP的API也没有实现整个HTTP协议的问题,这要求API提供者和客户端弄清楚如何指定和学习特定API支持的HTTP的哪个子集。这对于REST和OpenAPI API都是一个问题。gRPC通过要求客户端和服务器都采用实现完整gRPC协议的特殊软件来避免这个问题。我们希望gRPC能够成功地保持该协议至少25年的稳定,就像HTTP所做的那样,以便在服务器升级和客户端升级时客户端不会中断。

如何将面向实体的模型与RPC结合起来?

不管你是使用gRPC还是OpenAPI,以面向实体的方式使用RPC的关键在于将RPC方法定义限制为那些能够轻松映射到每种资源类型的标准实体操作(创建、检索、更新和删除,通常称为CRUD3,加上列表)。

要使用面向实体风格的RPC,你需要颠倒通常的RPC思考过程——不是从程序定义开始,而是首先定义你的资源类型,然后为这些类型上的常见实体操作以及任何你发现必要的额外操作制作RPC方法定义。

使用面向实体风格的RPC依赖于教导人们一种受限的使用模式。在实践中,我们发现这样设计的API有时是面向实体和面向过程概念的混合体,这削弱了一些好处。

那么gRPC的缺点是什么呢?

每种技术都有其缺点和局限性。我们已经讨论了一些OpenAPI的。

HTTP API的一个流行特性是客户端可以使用它们,服务器也可以仅使用通用和广泛可用的技术来实现它们。API调用可以简单地通过在浏览器中输入URL,或在终端窗口或bash脚本中发出cURL命令来轻松完成。程序员可以使用基本的HTTP库访问或实现HTTP API。相比之下,gRPC需要在客户端和服务器上都使用特殊软件。gRPC生成的代码必须被整合到客户端和服务器的构建过程中——这对一些人来说可能是繁琐的,特别是那些习惯于在像JavaScript或Python这样的动态语言中工作的人,因为在开发机器上,构建过程可能根本不存在。Google Cloud Endpoints产品使得可以通过HTTP和JSON访问gRPC API,而无需特殊软件,这恢复了许多客户端的选项,但并非每个人都愿意或能够使用Cloud Endpoints或找到或构建一个等效的产品。

编写一个爬取整个REST API的机器人是很简单的,就像浏览器或网络爬虫可以爬取整个HTML网络一样。你不能对RPC风格的API这样做,无论它是使用gRPC还是OpenAPI描述的,因为RPC为每种实体类型提供了不同的API,需要定制软件或元数据才能使用它。实际上,通常并不关键需要能够编写通用的API客户端,尽管这可能是有用的。

HTTP API通常被代理以添加安全功能,执行输入验证,映射数据格式,并解决许多其他问题。这通常需要添加、移除或修改头部,以及解析甚至修改正文。代理使用标准和自定义头部的组合来实现这一点。这些功能通常使用像 Apigee Edge 这样的产品来实现,这些产品不需要传统的编程技能或可以轻松集成 gRPC 的那种软件开发环境。我认为为 gRPC 进行这种代理操作会更加困难,并且我不清楚它是否被普遍实施。

使用面向实体的方法与 gRPC 一起使用,主要适用于新构建的项目——你不会发现它很容易地被改造到现有的 RPC API 中。

gRPC 没有定义一个标准机制来防止两个客户端同时尝试更新同一资源时数据丢失,因此如果你使用 gRPC,你可能需要自己发明一个。HTTP 为此目的定义了标准的 Etag 和 If-Match 头部——我们设计的大多数 HTTP API 都使用这些头部。

同样,gRPC 也没有定义进行部分更新的机制,因此你可能需要自己发明一个。HTTP 定义了一个方法——PATCH——用于部分更新,但没有说明补丁应该是什么样子或如何应用它。有两个额外的 IETF 标准填补了 JSON 的这一空白:JSON 合并补丁和 JSON 补丁。前者使用起来更简单,但并不处理所有情况,特别是数组的更新;后者处理更多情况,但使用起来更复杂。我最近构建的大多数HTTP API都实现了两种标准,并让客户端选择;Kubernetes API也是这样工作的。

结论

有一些API使用了与HTML Web相同的REST超文本模型。它们的目标是继承HTML Web的核心特性,比如稳定性、一致性和普遍性。如果你已经知道如何以这种方式设计API,或者有动力去学习,那么这是一个不错的选择。这是我自己的偏好。

用OpenAPI描述的API基于与RPC类似的概念,但有一个自定义的映射到HTTP。这种方法允许客户端仅使用常见的HTTP技术访问生成的API,但它也为这些API增加了额外的设计选择,这可能使它们更难设计和构建,也更难学习。

如果你正在考虑为API使用OpenAPI,你还应该考虑使用gRPC来设计和实现它的选项。两者的基本API模型是可比的,而gRPC避免了发明自己的映射到HTTP的需要。

不管你是使用gRPC还是OpenAPI来构建你的API,如果你以面向实体的风格组织API,标准化你的程序名称(例如,坚持使用动词create、retrieve、update、delete和list),并施加其他命名约定,你可以获得一些,但不是全部的REST API的好处。gRPC将带来一些它自己的好处。使用gRPC特别有吸引力的情况,如果以下情况之一成立:

  • 您可以使用像Cloud Endpoints这样的产品,这样您的客户端就不会因为您使用了gRPC技术而被迫采用gRPC技术。
  • API是内部的,您可以控制所有客户端以及服务器的技术选择。

如果您选择用gRPC替代OpenAPI或REST,您至少应该意识到在代理中增强或修复API行为的机会要有限得多,特别是那些使用Apigee Edge或其竞争对手实现的API管理工具。这取决于您打算如何以及在哪里使用gRPC,这可能是也可能不是一个问题。

和大多数设计挑战一样,有很多因素需要考虑,也有很多权衡要做。希望这次讨论有助于解释HTTP和RPC风格的API如何相互匹配。

特别感谢Nandan Sridhar和Marsh Gardiner对这篇文章的贡献。


  1. 一些REST API允许客户端在基础URL后附加一个查询,这种情况下,客户端需要理解服务器在URL查询部分支持的查询语法,尽管他们不需要知道URL其余部分的格式。一些REST API设计者允许在URL路径中编码查询,这种情况下,他们的查询URL开始看起来像下面讨论的OpenAPI风格的URL。 2.一些REST评论家表示,为了声称符合REST模型,你还需要实现类似于HTML表单的JSON版本,但几乎所有人都会同意,如果超文本链接不是API的一个显著特征,那么它就不是REST。
  2. 即使在严格面向实体的API中,我们有时也会遇到需要一个我们认为是“翻译”或“转换”的第五个操作。翻译接收一个实体并产生另一个实体,而不创建持久资源。HTTP没有为这个操作提供特殊方法,所以我们不得不使用POST来同时处理“创建”和“翻译”。有时我们也会使用POST来检索,以绕过URL长度限制,通常是对于包含查询的URL。
  3. 通过在服务器上做一些额外的工作,你可以让浏览器本身爬取REST API。

原文地址: https://cloud.google.com/blog/products/api-management/understanding-grpc-openapi-and-rest-and-when-to-use-them

#你可能也喜欢这些API文章!