billyma

Coursera 的 GraphQL 之旅

原文链接: dev-blog.apollodata.com

Coursera 的 GraphQL 之旅

为 REST 和微服务后端添加 GraphQL

Coursera 的客户端开发人员钟情于 GraphQL 的灵活性,类型安全性和良好的社区支持,我们对 GraphQL 的喜爱~~~。然而,我们并没有过多讨论后端开发人员是如何看待 GraphQL 的,因为他们大多数实际上并不需要考虑 GraphQL。

在过去一年中,我们构建了一系列的工具来将所有的 REST API 转换为 GraphQL,在我们的后端开发人员继续编写他们熟悉的 API 的同时,让客户端开发人员可以通过 GraphQL 访问所有数据。

在这篇文章中,我们将介绍一下我们引入 GraphQL 的历程,并且会着重介绍我们实践过程中成功和失败的点。

初步调研

Coursera 使用 REST API 构建基于资源的 API(比如课程 API,教师 API,成绩 API等)。这些都很容易进行构建和测试,并且对后端提供了很好的关注点分离。然而,随着我们的产品规模和 API 数量的增长,我们开始面对许多关于性能,文档和通用性的问题。在许多页面上,我们不得不执行四五次后端请求来获取渲染页面需要的数据。

我还记得当 Facebook 首次推出 GraphQL 时,我们团队都兴奋不已——我们当即意识到 GraphQL 可以解决我们的很多问题,让我们可以在单次的请求中获取所有数据,并为我们的 API 提供结构化文档。然而,尽管我们很想开始为所有资源编写 GraphQL,不再在客户端上使用 REST,但这不切实际,因为:

  • 彼时,Coursera 项目拥有超过 1,000 个不同的 REST 端点(现在更多)——即使我们想完全停止使用 REST,GraphQL 的迁移成本也将是天文数字。

  • 所有后端服务都使用 REST API 进行服务间通信,我们经常会在后台服务和前端客户端使用相同的 API。

  • 我们有三个不同的客户端(Web,iOS 和 Android),希望能够平滑的升级。

经过一番调研,我们找到了一个很好的方式来帮助我们使用 GraphQL——我们决定在我们的 REST API 之上添加一个 GraphQL 代理层。这种方式实际上很常见,并且~~记录~详实,所以这里我不再赘述。

在生产环境应用 GraphQL

封装 REST API 的过程很简单——我们构建了一些实用程序来执行下游的 REST 请求,从而在解析器中获取数据,并制定了一些关于如何将现有模型转换为 GraphQL 的规则。

首先,我们构建了少量的 GraphQL 解析器,然后在生产环境中启动一个 GraphQL 服务器,以调用下游 REST 接口请求我们的资源。一旦我们完成了这项工作(使用 GraphiQL 验证所有内容),我们就会在提前准备的演示页面上展示这些数据,几天之内 GraphQL 就能暂时调用成功了。

短暂的庆祝

如果我从这个项目中得到什么教训,那就是不要高兴得太早了。

我们的 GraphQL 服务器完美工作了好几天。但是突然之间,就在我们即将给团队演示这个 demo 之前,每一个 GraphQL 查询都开始执行失败。这让我们没有一点点防备,因为上一次确认它能正常工作之后,我们并未对 GraphQL 服务器做过任何更改。

经过一番调查,我们才发现由于一个无关的 bug,导致我们下游的课程目录服务接口被回滚到了以前的版本,而我们在 GraphQL 服务中构建的 schema 现在已经不同步了。我们本可以手动更新 schema 并修复我们的 demo,但是我们很快意识到,由于我们的 GraphQL schema 扩展了1,000多个不同的资源,由50多个服务提供支持,手动同步所有的更新是不可能的。 如果你在微服务架构中有多个数据源,那么问题就在于它们何时同步,而不是是否会同步。

自动化处理

所以我们从头开始,试图找到一个更精确的方案来实现单一数据源——我们将 REST API 视为数据源是有依据的,因为我们的 GraphQL 的 schema 是基于它们构建的。为此,我们需要自动且准确地构建我们的 GraphQL 层,以正确反映当前运行在我们的架构中的业务资源,而不是我们之前做的那样。

幸运的是(或许还带有一点远见),我们的 REST 框架能给我们建立自动化层所需的一切:

  • 我们架构中的每项服务均能够动态地为我们提供其运行的 REST 资源列表

  • 对于单个资源,我们可以内省端点列表和参数(比如课程端点可以通过 id 获取,也可以通过教师查找)

  • 另外,我们能够接收到由我们的 Courier 模式语言为每个返回的模型定义的 Pegasus Schemas

一旦我们发现不同步的地方,就会触发构建一个 GraphQL schema,我们在 GraphQL 服务器上设置了一个定时任务,每五分钟 ping 一次下游服务,并请求所有资源信息。 然后,我们就可以在 Pegasus Schemas 和 GraphQL 类型之间编写1:1转换层

接下来,我们利用之前解析器的大部分逻辑,简单地定义了 GraphQL 查询和 REST 请求之间的转换,并且能够生成一个功能完善的 GraphQL 服务器,时间不超过五分钟。

关联资源

我们采用 GraphQL 的主要原因之一就是希望能在单次服务器往返中获取我们的页面需要的所有数据。但是,我们最初的方案仅提供了 REST API 返回的模型与 GraphQL 返回的模型之间的一对一映射。这样并没有将我们的资源真正地链接在一起,我们仍然会使用尽可能多的 GraphQL 查询来获取数据,就像使用 REST API 一样。尽管使用 GraphQL 替代 REST 获取用户数据能带来极致的开发体验,但如果在获取更多数据之前必须等待前一个查询返回,实际上并不会获得性能的提升。

我们的 REST API 每一个都相互独立 ——它们不需要知道任何其他 API 的存在。然而,使用 GraphQL,则模型和资源之间需要相互关联。

自动建立资源之间的链接并不可行,所以我们定义了一个简单的注解,开发人员可以添加资源来指定它们之间的关系。例如,一个课程资源应该有一个教师字段代表教授该门课程的教师。为了获取这些数据,我们可以通过 id 来查询教师信息,这里的 id 可以使用课程中已经提供的 InstructorIds 字段。我们将其称为“向前关联”,因为我们可以通过 id 获取的数据知道确切的教师信息。

当我们想要从某个资源跳转到另一个没有明确链接的资源的情况下,我们增加了通过反向查询获取数据的功能——例如,通过课程信息获取用户的注册信息,我们可以调用 byCourseId 来查找 userEnrollments.v1 的资源,这将会在指定的课程资源中返回匹配的用户注册数据。

代码的语法如这般:

courseAPI.addRelation(
  "instructors" -> ReverseRelation(
    resourceName = "instructors.v1",
    finderName = "byCourseId",
    arguments = Map("courseId" -> "$id", "version" -> "$version"))

一旦这些联系就位,我们的 GraphQL schema 就会开始合并在一起——它们并非是能通过 GraphQL 获取的多块小数据,而是由所有 Coursera 的数据和资源组成的网络。

结论

我们的 GraphQL 服务器已经在 Coursera 生产环境上运行了6个多月 ,尽管并非一帆风顺,但我们切身感受到了 GraphQL 带来的诸多好处。得益于 GraphQL 额外提供的类型安全检查,开发人员更容易检测数据和编写查询,我们的站点更加可靠,并且使用 GraphQL 加载数据的页面运行得更快。

还有比较重要的一点,迁移到 GraphQL 的过程并不会大幅降低开发人员的生产力。尽管我们的前端开发人员不得不学习如何使用 GraphQL,但我们不需要重写任何后端 API 或运行复杂的迁移流程才能使用 GraphQL——开发人员创建新的项目可以直接使用。

总的来说,我们很高兴 GraphQL 为我们的开发人员(也是我们的终极用户)提供了很大的帮助,并且对 GraphQL 生态的未来感到非常兴奋。