在 REST API 中添加批处理或批量处理接口

在 REST API 中添加批处理或批量处理接口
原文标题:Adding batch or bulk endpoints to your REST API
原文链接:https://www.codementor.io/blog/batch-endpoints-6olbjay1hd
作者:Gareth Dwyer

REST API 简介

大约二十年前,罗伊·菲尔丁(Roy Fielding)提出了 REST API 的概念,这个想法很快就变得非常流行。与允许用户以编程方式与互联网应用程序交互的现有方法(如 SOAP 和 RPC)相比,REST 提供了一种结构良好且易于理解的模式,用于解决各种需求。与面向对象编程(OOP)和关系型数据库类似,REST 允许程序员考虑一组相关联的资源,类似于 OOP 中的对象或数据库表,可以使用一组有限的标准 HTTP 进行修改,这可以看作是 OOP 中的方法或数据库中的 CRUD(增删改查)操作的对应方式。

如今,REST API 的资源导向设计仍然非常受欢迎,但它也有一些局限性和易错点。在这篇文章中,我们将特别关注 REST API 上的批量操作,讨论它为什么是必要的,并比较不同的实现方法。

值得注意的是,有些人会对批量处理(将相同的操作应用于多个条目)和批处理(将可能不同的操作应用于不同条目)进行了严格划分。尽管这样划分是有道理的,但实际上这两个概念经常混淆使用。“批处理(Batch)”通常被视为更通用的术语(处理请求或数据批次),而“批量(Bulk)”则是“批处理”的一个子集(批量处理数据,但不处理操作)。

要学习本教程,首先要了解 REST API 设计的基础知识,在文中不会过多解释。我们将重点关注为什么批处理端点(endpoints)有用,以及如何将它们添加到现有的 REST API 中。

一个 REST API 示例

我们来看一个非常简单的 REST API 示例。假设这是 Stripe 的支付处理 API 的一个子集。我们只考虑 /customers 端点,该端点用于检索现有客户或创建新客户。从文档中我们知道有以下几个可用选项:

  POST         /v1/customers
   GET         /v1/customers/:id
  POST         /v1/customers/:id
DELETE         /v1/customers/:id
   GET         /v1/customers

我们可以马上看出 REST API 的一个核心优势。如果你以前使用过 REST API,那么即使没有 Stripe 的文档,你也可能已经猜到了。

要创建客户,我们对 /v1/customers 发出 POST 请求并检索客户,我们使用相同的端点,但改用 GET 请求。

要检索、修改或删除现有客户,我们仍然使用 /customers 端点,但同时我们添加我们感兴趣的特定客户的 :id

在使用 REST API 时,关于单数和复数的思考方式需要特别关注。端点称为“/customers”,在这个端点上调用 GET 会返回许多客户的信息数组(例如 [{customer1...},{customer2...}])。但是,如果要创建新的客户,我们需要使用 POST 请求并发送一个客户的信息(例如 {customer1...}。这是需要注意的细节之一,因为不同的 REST API 可能实现方式不一样。

为什么需要批处理?

从上面的例子中我们可以看出,当我们同时处理多个客户时存在不对称性。如果我们想要一次性从所有客户处检索信息,只需简单调用 GET /v1/customers,就可以获得所需的所有数据。但是如果存在一个数量可能达到数千名的客户集合,需要将他们每个人都添加到 Stripe 中,我们就需要逐个创建,要对每个客户都调用一次 POST /v1/customers。为了解决这个问题,我们需要批处理端点,它允许我们一次性处理多个客户。

网络,或者更确切地说,我们需要调用的数量,往往是现代应用程序的瓶颈。采用公共云(如 AWS)使我们能够很容易地扩展应用程序的处理能力、内存或存储容量,但是每次网络调用仍需要在由计算机、路由器、交换机和协议(如 TCP)组成的复杂而不可靠的全球网络中协商,这会大量增加每次调用的开销。因此,更好的做法是更多的数据发出更少的请求(如多个客户),而不是用更少的数据发出更多的请求(如一个客户)。

假设我们已经创建了上面所示的客户 API,现在想要允许 API 用户一次性创建多个客户。这该怎么实现呢?我们可以简单地修改 POST /customers 来接受客户数组而不是单个客户。这样做将匹配 GET /customers 端点的返回结果。然而,这将是对我们 API 的突破性变化,但对于每次只想添加一个客户的用户来说是非常烦人的。他们总是需要记住在发送之前添加客户或数组,并处理返回的创建 ID 数组。很可能在大多数情况下,我们的用户只想一次添加一个用户。

Stripe 并没有提供一种一次性创建多个客户的方法,但是让我们来看看其他 API 是如何批处理请求的。

批处理 API 的真实示例

当遇到不确定该如何解决的设计问题时,第一步最好是看看其他人是如何解决这个问题。幸运的是,并不缺少带有公共文档的 REST API。大多数 API 选择实现一个可以将不同请求批处理到单个调用中的端点,或者实现一些或全部端点的批量化版本,可以在单个调用中接受多个资源。我们将从 Google Drive 开始,这是前者的示例,然后再看 ZenDesk,这是后者的示例。

Google Drive - 批处理请求

Google Drive 以云存储服务而闻名,类似于 Dropbox,但它还有一个功能强大的 API,用于创建、修改和检索各种文档。

Google 实现了一个复杂但灵活的批处理端点。它不是接受多个资源的端点,而是接受多个请求的端点。这些本质上是 “meta” HTTP请求,其中主请求包含不同的子请求。

这种方法的优点是非常灵活。Google 可以在单次调用中接受不同类型的 POST 请求,每个请求都包含不同的数据,并在服务器端并行处理,从而减少他们 API 代理需要处理的调用数量。

这种方法的主要缺点就是构建类似的 POST 请求相当困难。对于用户来说,操纵他们通过请求传递的数据很容易(例如,如果他们已经知道如何传递单个客户,就可以开始使用数组传递用户),但将不同的 API 请求批处理在一起并发送到新端点却复杂得多。即使在以高级语言提供 API 包装器的情况下,用户仍然需要思考何时对不同请求进行批处理、组合他们的最佳方式是什么以及请求如何相互交互。

在 Google 的示例中,如下所示,我们发送了一个批处理的 POST 请求,其中包含两个子 POST 请求。每个内部 POST 请求都创建对特定文件的权限:第一个请求提供写入特定文件的电子邮件地址的权限,第二个请求提供对整个域读取特定文件的权限。

对于每个嵌入式请求,都有一个--END_OF_PART标记。注意会有重复的部分,例如,AuthorizationContent-Type字段是对每个子请求重复的,尽管这些字段不太可能是不同的。一旦这个较大的多部分批处理请求被交付给 Google,他们的服务器将简单地将其分开,并处理每个部分,就像是单独到达的一样。

POST https://www.googleapis.com/batch/drive/v3
Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963


--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8


{
  "emailAddress":"example@appsrocks.com",
  "role":"writer",
  "type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8


{
   "domain":"appsrocks.com",
   "role":"reader",
   "type":"domain"
}
--END_OF_PART--

ZenDesk - 批处理资源

ZenDesk 是一个客户支持和工单系统。他们在实现批处理 API 时采用了略有不同的方法。你可以阅读他们的博文,了解他们为什么要增加批处理端点,并看看他们的 API 文档,看看具体的实现方式。

与 Google Drive API 类似,ZenDesk 有一个不同的端点来批处理请求,但与 Google 不同的是,每个资源类型有一个批处理端点,而不是所有资源都有一个批处理端点。例如,你可以使用以下端点创建多个用户:

POST /api/v2/users/create_or_update_many.json

你会看到,用户仍然包含在上面的端点中,而不是像 Google Drive 那样,必须在 POST 请求的子部分的数据中指定我们想要的每个端点。他们用来展示如何一次性传递多个用户的示例是如预期那样的:我们创建一个用户数组,指定每个用户的信息。需要注意的是,ZenDesk 文档中的示例假定你使用的是他们的一个 API 库,而不是创建原始的 POST 请求,所以它看起来确实比 Google 的示例整洁。JSON 数据最终还是会被编码到 POST 的 body 中,并在发送之前添加 Content-LengthContent-Type和其他 headers。

'{"users": [{"name": "Roger Wilco", "email": "roge@example.org", "role": "agent"}, {"external_id": "account_54321", "name": "Woger Rilco", "email": "woge@example.org", "role": "admin"}]}'

这种方法的优点是使用简单。如果我们的 API 用户知道如何创建单个资源,并且因为需要一次创建多个资源而遇到性能问题,他们可以轻松修改现有代码以传递数组,而不是单个资源。

缺点是它不如通用的批处理端点灵活。如果我们想同时创建多个用户、组织和工单,我们仍然需要进行至少三次调用。而使用 Google 的设计,我们只需要通过批处理这些不同的端点来进行一次请求。

寻找其他方法

虽然我们只看了两个示例,但你会看到很多公司都在使用这两种模式。只要在互联网上搜索“API 文档”,再加上一个大型科技公司的关键词,就可以很容易地找到更多的例子。你会发现 API 文档的质量天差地别。例如,Stripe 投入了大量的时间和金钱来确保他们的 API 文档设计良好、准确且易于使用。Salesforce 和 Oracle 这种早期公司的文档通常不够完整且难以理解。

有趣的是,不同的地方对批量处理和批处理请求的实现方式略有不同(如果有的话)。同样有趣的是,许多确实存在的批量处理或批处理端点都被标记为 "实验性 ",或者是在比核心端点晚得多的阶段才添加的。显然,批处理和批量处理并不适合 REST 或面向资源的 API 的核心设计原则。也就是说,这是一个足够复杂的话题,在意识到需要时盲目地将端点添加到 API 之前,有必要对不同的选项进行一些思考。

批处理的其他注意事项

我们查看了一些批处理 API 的示例,并对批处理和批量处理进行了区分。除了我们提到的问题外,在实现批处理或批量处理时,你还需要考虑一些问题:

  • 错误处理:如果单个请求或批处理请求中的资源失败会怎样?整个批处理应该失败,还是应该尽可能地处理更多的请求?
  • 批处理大小限制:许多端点指定了在特定时间窗口内可以调用每个端点的次数的速率限制。如果你添加了批处理的端点,你的用户可能会发送大量的数据进行处理,这时可能难以跟上。

从上面的示例可以看出,批处理和批量处理往往是在发现网络瓶颈后才被添加到 REST API 中的。

无论你是刚刚开始设计 API,还是在扩展到真实用户后发现了对批处理的需求,了解 REST API 中批处理的不同实现方式以及发挥的优劣势是很好的。

或许你还可以看看:

订阅
qrcode

订阅

随时随地获取 Apifox 最新动态