所有文章 > API设计 > 长时间运行操作的 API 设计最佳实践:GraphQL 与 REST

长时间运行操作的 API 设计最佳实践:GraphQL 与 REST

我最近读了一篇文章,其中作者指出 GraphQL “不适合长时间运行的操作”。我想证明 GraphQL 可以很好地用于长时间运行的操作。

我们还将看看如何使用传统的 REST API 解决此类问题并比较这两种方法。我们最终会看到的是 GraphQL Schema 从开发人员的角度来看使长时间运行的操作更容易推理。另一方面,REST 方法更容易实现。

现在让我们用一个例子来说明和比较为长时间运行的操作设计 API 的两种方法。

什么是长时间运行的操作?

假设我们正在构建一个使用机器学习来检测博客帖子情绪的 SaaS。您将提交一个指向该博客帖子的链接,该服务将分析该帖子的情绪并将其返回给您。

这个操作可能需要几秒甚至一分钟才能完成。从你自己的角度来看,当你点击网页上的一个按钮时,你看到进度条时会紧张多久?你可能会让服务运行一段时间如果您了解任务的复杂性,只需几秒钟。但是,我们习惯于在不到 5 秒后看到某种进展,至少在我们使用桌面浏览器时是这样。

让我们假设我们的“情感分析”服务需要超过 5 秒才能完成。

如果我们不能在 5 秒内做出响应,我们如何提供良好的用户体验?

如果只有一个加载指示器,我们正在等待请求完成,用户可能会随时取消请求,关闭浏览器,甚至只是离开页面。他们也可能只是点击退出来取消请求和再试一次。

我们需要的是一个理解长时间运行操作概念的 API。我们不应该只调用这个长时间运行的操作并等待同步响应,我们应该设计一个异步 API,以便轻松监控操作的进度甚至取消。

为长时间运行的操作设计同步 REST API

如果我们将我们的 API 设计为同步 API,它可能看起来像这样:

curl -X POST -H "Content-Type: application/json" -d '{"url": "https://www.wundergraph.com/blog/long_running_operations"}' http://localhost:3000/api/v1/analyze_sentiment

这就是响应的样子:

{
"status": "success",
"data": {
"url": "https://www.wundergraph.com/blog/long_running_operations",
"sentiment": "positive"
}
}

但是,正如我们之前所说,此操作可能需要很长时间才能完成,并且用户可能会取消它。

更好的方法是将我们的 API 设计为异步 API

为长时间运行的操作设计异步 REST API

现在让我们将同步 API 转换为异步 API

我们不应立即返回响应,而应返回具有唯一标识符的响应,以便客户端可以轮询服务器以获取结果。

设计此类 API 的正确方法是返回 202 Accepted 状态代码。

因此,在这种情况下,我们的 API 响应可能如下所示。它将是一个带有状态代码 202 和以下正文的响应。

{
"data": {
"url": "https://www.wundergraph.com/blog/long_running_operations",
"id": 1
},
"links": [
{
"rel": "status",
"href": "http://localhost:3000/api/v1/analyze_sentiment/1/status"
},
{
"rel": "cancel",
"href": "http://localhost:3000/api/v1/analyze_sentiment/1/cancel"
}
]
}

我们不是立即返回结果,而是返回一个链接列表,以便 API 的调用者可以获取作业的当前状态或取消它。

客户端然后可以使用以下命令来获取作业的状态:

curl http://localhost:3000/api/v1/analyze_sentiment/1/status

太好了,我们现在已经将 API 设计为异步的。

接下来,我们将看看如何使用 GraphQL 来设计类似的 API

为长时间运行的操作设计异步 GraphQL API

与 REST 类似,我们可以使用 GraphQL 实现异步 API 来轮询服务器的作业状态。但是,GraphQL 不仅支持查询和变更,还支持订阅。这意味着,我们有更好的设计方法我们的 API 而不是强制客户端轮询服务器。

这是我们的 GraphQL 架构:

type Job {
  id: ID!
  url: String!
  status: Status!
  sentiment: Sentiment
}

enum Status {
    queued
    processing
    finished
    cancelled
    failed
}

enum Sentiment {
  positive
  negative
  neutral
}

type Query {
    jobStatus(id: ID!): Job
}

type Mutation {
  createJob(url: String!): Job
  cancelJob(id: ID!): Job
}

type Subscription {
  jobStatus(id: ID!): Job
}

创建作业将如下所示:

mutation ($url: String!) {
  createJob(url: $url) {
    id
    url
    status
  }
}

一旦我们得到了 id,我们就可以订阅 Job 的变化:

subscription ($id: ID!) {
  jobStatus(id: $id) {
    id
    url
    status
    sentiment
  }
}

这是一个好的开始,但我们甚至可以进一步改进此架构。在当前状态下,我们必须使“情感”可为空,因为该字段只有在作业完成后才有值。

让我们的 API 更直观:

interface Job {
  id: ID!
  url: String!
}

type SuccessfulJob implements Job {
  id: ID!
  url: String!
  sentiment: Sentiment!
}

type QueuedJob implements Job {
  id: ID!
  url: String!
}

type FailedJob implements Job {
  id: ID!
  url: String!
  reason: String!
}

type CancelledJob implements Job {
  id: ID!
  url: String!
  time: Time!
}

enum Sentiment {
  positive
  negative
  neutral
}

type Query {
    jobStatus(id: ID!): Job
}

type Mutation {
  createJob(url: String!): Job
  cancelJob(id: ID!): Job
}

type Subscription {
  jobStatus(id: ID!): Job
}

将 Job 变成一个接口使我们的 API 更加明确。我们现在可以使用以下订阅来订阅作业状态:

subscription ($id: ID!) {
  jobStatus(id: $id) {
    __typename
    ... on SuccessfulJob {
      id
      url
      sentiment
    }
    ... on QueuedJob {
      id
      url
    }
    ... on FailedJob {
      id
      url
      reason
    }
    ... on CancelledJob {
      id
      url
      time
    }
  }
}

只有当 __typename 字段设置为“SuccessfulJob”时才会返回 sentiment 字段。

比较 REST 和 GraphQL 的长时间运行操作

从上面的例子我们可以看出,REST 和 GraphQL 都可以用来设计异步 API。现在让我们开始讨论每种方法的优缺点。

首先,我要说的是,异步 API 的两种方法都比同步 API 更好。无论您选择使用 REST 还是 GraphQL,当一个操作需要超过几秒钟才能完成时,我总是建议您以异步方式设计 API。

现在,让我们看看造成不同的微小细节。

Hypermedia vs Graphql 类型定义

REST 方法的一个巨大好处是一切都是资源,我们可以利用超媒体控件(Hypermedia)。让我将这个“行话”翻译成简单的词: REST API 的核心概念之一是每个“事物”都可以通过唯一的 URL 访问。如果您向 API 提交作业,您将得到一个 URL,您可以使用该 URL 检查作业的状态。

相比之下,GraphQL 只有一个端点。如果你通过 GraphQL mutation 提交作业,你得到的是一个 ID! 类型的 id。如果你想检查作业的状态,你必须使用 id 作为Query 或 Subscription 类型的正确根字段的参数。作为开发人员,您如何知道 Job id 与 Query 或 Subscription 类型的根字段之间的关系?不幸的是,您不知道!

如果你想在设计你的 GraphQL schema 时做得很好,你可以将这些信息放入字段的描述中。但是,GraphQL 规范不允许我们像在 REST 中那样使这些“链接”显式。

相同的规则适用于作业的取消。使用 REST API,您可以向客户端返回一个 URL,可以调用该 URL 来取消作业。

使用 GraphQL,您必须知道必须使用 cancelJob mutation 来取消作业并将 id 作为参数传递。

这听起来可能有点夸张,因为我们使用的是一个非常小的 schema,但想象一下,如果我们的 Query 和 Mutation 类型上都有几百个根字段。可能很难找到正确的根字段来使用。

REST API 可以有一个Schema,GraphQL API 必须有一个 schema

缺乏资源和唯一 URL 似乎是 GraphQL 的弱点。但是,我们也可以反过来论证。

可以在 REST API 中返回带有操作的链接。也就是说,我们从提交作业中返回的链接并不明显。此外,我们也不知道例如是否应该使用 POST 或 GET 调用取消 URL。或者我们应该只删除作业?

有额外的工具可以帮助解决这个问题。Kevin Swiber 的 Siren(https://github.com/kevinswiber/siren) 就是这样的工具/规范。

如果你想设计好的 REST API,你绝对应该研究像 Siren 这样的解决方案。

也就是说,考虑到 REST API 是占主导地位的 API 风格这一事实,Siren 已有 5 年多的历史并且只有 1.2k 星表明存在问题。

对我来说,好的 (REST) API 设计似乎是可选的。大多数开发人员构建简单的 CRUD 风格的 API,而不是利用超媒体的强大功能。

另一方面,由于缺乏资源,GraphQL 不允许您构建超媒体 API。但是,Schema 在 GraphQL 中是强制性的,迫使开发人员使他们的 CRUD 风格的 API 更加明确和类型安全。

在我个人看来,超媒体 API 比 CRUD 风格的 API 强大得多,但这种强大是有代价的,并且增加了复杂性。正是这种复杂性使 GraphQL 成为大多数开发人员更好的选择。

作为 REST API 的开发者,你“可以”使用 Siren,但大多数开发者就是不在乎。作为 GraphQL API 的开发者,你“必须”有一个 Schema,没有办法绕过它。

如果你看看我们的 GraphQL 模式的第二个版本,接口的使用帮助我们使 API 非常明确和类型安全。它并不完美,我们仍然缺乏“链接”,但这是一个很好的权衡。

轮询 vs 订阅

订阅作业的状态比轮询状态更优雅,这是显而易见的。从 API 用户的心智模型来看,订阅事件流比轮询状态更直观。

也就是说,没有什么是免费的。

为了能够使用订阅,您通常必须使用 WebSockets。WebSockets 是有状态的,它是有代价的。

将 WebSockets 添加到您的堆栈中也意味着 API 后端更加复杂。您的托管服务提供商是否支持 WebSockets?一些无服务器环境不允许长时间运行的操作或只是拒绝 HTTP 升级请求。WebSocket 连接的扩展方式也不同于短期连接HTTP 连接。

WebSockets 也是 HTTP 1.1 功能,这意味着您不能将它们与 HTTP/2 一起使用。如果您的网站对所有端点都使用 HTTP/2,则客户端必须为 WebSocket 打开另一个 TCP 连接。

此外,WebSockets 可能无法在所有环境中工作,例如如果您使用反向代理或使用负载均衡器。

另一方面,HTTP 轮询是一种非常简单而乏味的解决方案。因此,虽然 GraphQL 订阅为 API 用户提供了一种更简单的心智模型,但它们在实施方面带来了巨大的成本。

请记住,您不会被迫将订阅与 GraphQL 一起使用。您仍然可以通过使用查询而不是订阅来使用 HTTP 轮询作业的状态。

总结

REST 和 GraphQL API 样式都是设计同步和异步 API 的绝佳工具。它们各有优缺点。

GraphQL 对模式及其类型系统更加明确。另一方面,由于独特的 URL 和超媒体控件,REST 可以更加强大。

我个人非常喜欢 Siren 的方法。但是,REST API 缺乏明确的架构给普通开发人员留下了太多的解释空间。

借助正确的工具和良好的 API 治理,您应该能够设计出色的 REST API

有人可能会争辩说 GraphQL 具有更多开箱即用的功能并且需要更少的治理,但我认为这不是真的。正如您从我们的 GraphQL 模式的两个版本中看到的那样,在设计方面有很大的灵活性GraphQL Schema。即使是 Schema 的第二个版本也可以进一步改进。

最后,我看不出一种解决方案比另一种解决方案好多少。在 API 设计上投入精力比在 REST 或 GraphQL 之间进行选择要重要得多。

与您的用户交谈并弄清楚他们想如何使用您的 API。他们是习惯 REST API 还是 GraphQL API?他们会从 Subscriptions 而不是 WebSockets 中受益,还是他们更喜欢简单无聊的轮询?

也许你甚至不必在 REST 和 GraphQL 之间做出选择。如果你可以构建一个很棒的 REST API,你可以轻松地用 GraphQL 包装它,或者反过来。这样,你可以为你的用户提供两种 API 样式,如果这能给他们带来价值。

你的收获应该是良好的 API 设计和与你的用户交流比选择酷炫的技术重要得多。

文章转自微信公众号@云原生AI视界

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