写点什么

我们怎样用 GraphQL 分阶段重构后端系统?

  • 2020-12-10
  • 本文字数:2559 字

    阅读完需:约 8 分钟

我们怎样用GraphQL分阶段重构后端系统?

之前,我写过一篇博文,用 Go 语言重写可汗学院有 10 年历史的单体 Python 2 后端系统。这篇博文带来的一些反馈是这样的“突然大规模重写会带来巨大的风险”。当然,我非常同意。本文,我要说的是我们的做法基本上和突然大规模重写相反。

联盟的 GraphQL


我们新架构的中心是基于GraphQL联盟的。基于 REST 的服务已经有了“API网关”很多年了。当你的系统是基于 GraphQL 时,联盟提供了相同的功能。这两者之间很大的区别是:


  1. GraphQL 为后端系统能提供的所有数据提供了一个单一的类型化的模板。

  2. REST API 网关通常知道如何将一个请求定向到单个后端服务。 GraphQL 网关生成一个查询计划,来构建一个包含来自多个后端服务的数据的响应。


从较高的层次来看,我们的系统是这样的:



graphql-gateway 服务负责创建查询计划并向我们的其它服务(包括我们的单体应用)发出 GraphQL 请求。我们的 Go 服务有自己的 GraphQL 模板,因此我们使用gqlgen来响应请求。


我将重点介绍我们是如何定制Apollo GraphQL服务器,来使我们能够安全地从 Python2 单体应用迁移到这些新服务。


通过 GraphQL 联盟,每个服务提供了整个 GraphQL 模板的一部分,网关将所有这些单独的模板合并为一个。我们的单体应用像这个配置中的其它服务一样工作。


并行测试


GraphQL 基于各种类型。知道如何查询某一个字段的值的代码称为解析器。下面是一个简单的例子,我们将用它来讨论我们的方案:


type Assignment {    createdDate: Time}
复制代码


在 Assignments 中涉及更多其它字段,但是我们的方案对所有字段都是相同的。


假设我们想要将这个字段从单体应用转移到我们的新 Go 服务中。我们如何能够确信新服务会返回与单体应用相同的数据?我们使用了一个类似 GitHub 的Scientist的库的方案:同时查询单体应用和新服务,比较两者结果,但是只返回其中之一。


步骤 1:控制单体应用


当一个用户请求 createdDate 字段,graphql-gateway 会向 Python 单体应用发起一个请求。



我们的第一步是进行配置,以便可以在 Go 编写的 assignments 服务中添加这个字段。这个 assignments 服务会有一个类似.graphql文件的东西:


extend type Assignment @key(fields: "id") {    id: ID! @external     createdDate: Time @migrate(from: "python", state: "manual")}
复制代码


这是使用联盟来表示,这个服务正在将 createdDate 字段添加到 Assignment 类型,而且可以使用 id 查找该字段。我们添加的秘方是 @migrate 指令。我们已经编写了能够理解这些指令的代码来构建多个模板,graphql-gateway 在决定如何路由一个查询时会使用这些模板。


在“手动”状态下,查询只会转到 Python 代码。我们在编写一个新服务时使用这个状态。我们的工程师仍然可以直接查询服务来获取 createdDate 字段,或者可以向 graphql-gateway 请求“手动”模板,但是对于该模板是否真的能正常工作是不确定的。


步骤 2:并行状态


一旦我们为 createdDate 字段构建了解析器代码,我们将这个字段切换到 side-by-side 状态。


extend type Assignment @key(fields: "id") {    id: ID! @external     createdDate: Time @migrate(from: "python", state: "side-by-side")}
复制代码


在这种状态下,graphql-gateway 将同时调用 Python 代码和新的 Go 代码。它将比较两者的结果,记录存在差异的实例,并将 Python 结果返回给用户。


这种模式实际上是建立信任迁移将正常工作的关键。我们有数百万用户和多年的数据。通过了解这些代码的实际运行效果,我们可以验证即使是奇怪的边缘情况也能得到一致且正确的处理。


下面是一份示例报告,展示了并行差异的统计数量:



我们确实会遇到这样的情况:非确定性排序的 Python 集合产生的结果与 Go 代码不同。随着时间的推移,我们学会了发现这些用户不可见类型的问题。


当在开发服务器工作时,我们还有一些工具,提供了颜色高亮的差异,让任何变化都易于发现和理解。


突变怎么处理?


你可能发现了一个难题:如果我们在 Python 和 Go 中运行相同的代码,那么更改数据而不仅仅是查询数据会怎么样?在 GraphQL 术语中,这些被称为突变。


我们的并行测试不包含突变。我们曾考虑过一些方案来实现这一点,但它们太复杂了,我们认为不值得那么做。但我们确实想到一个方案来帮助解决这个难题。


步骤 2.5:Canary


如果我们有一个字段或突变,我们认为已经准备就绪,但我们仍然想要做一些产品测试并验证结果,我们对此有一个“canary”模式。


extend type Assignment @key(fields: "id") {    id: ID! @external     createdDate: Time @migrate(from: "python", state: "canary")}
复制代码


“canary”状态下的字段和突变会以运行时可配置的用户百分比发送到 Go 服务。另外,可汗学院的内部用户将得到 canary 模式。这为我们提供了一种相当低风险的方法来测试更加复杂的变化,因为如果某些部分工作不正常,我们可以快速关闭 canary 模式。


为提高效率,只有一个 canary 模式。实际上,任何时候都不会有太多字段或突变处于 canary 状态,因此这应该不是个问题。这是一个很好的折中方案,因为这个模板非常大(超过 5000 个字段),而且网关实例需要将 primary、manual、canary 状态的模板保存在内存中。


步骤 3:已迁移


下一步是将 createdDate 字段改变成“migrated”状态:


extend type Assignment @key(fields: "id") {    id: ID! @external     createdDate: Time @migrate(from: "python", state: "migrated")}
复制代码


在这种状态下,网关只会将流量发送到 Go 服务。Python 代码仍然存在,准备好响应。这使得分阶段进行我们的部署并在出问题时进行回滚更容易。


步骤 4:移除 Python 代码


最后,我们完成了 Python 代码的迁移,可以完全删除 @migrate 指令:


extend type Assignment @key(fields: "id") {    id: ID! @external     createdDate: Time}
复制代码


在这时,Assignment.createdDate 字段只有网关知道来自 assignments Go 服务。


进展如何?


今年,我们建立了并行测试基础设施,它允许我们安全地一步步地迁移大量代码到 Go。过去一年中,流量显著增长且通过对后端、web 前端和移动端应用程序进行大量改动,我们保持了高可用性。截至本文撰写时,大约 40%的我们的 GraphQL 字段现在由 Go 服务提供,因此这项技术在这次迁移中得到了验证。


即使在 Goliath 项目完成之后,我们仍然可以继续使用这项技术来保护我们的前端,使其免受随着时间的推移而对后端服务进行的不可避免的改动的影响。


原文链接:


https://blog.khanacademy.org/incremental-rewrites-with-graphql/

2020-12-10 14:354324
用户头像

发布了 165 篇内容, 共 84.1 次阅读, 收获喜欢 343 次。

关注

评论

发布
暂无评论
发现更多内容

浅谈数据中台

数据社

大数据 数据中台 数据仓库

微服务架构深度解析与最佳实践 - 第六部分

kimmking

微服务 最佳实践 深度解析 高可用

亚马逊云 AWS LightSail 搭建高性能 LNMP 环境并安全部署 Wordpress

SnowHide雪诺海德

nginx Wordpress 部署 SELinux 安全上下文配置 亚马逊云 AWS Lightsail 安全

黄金思维圈,养成透过现象看本质的能力

非著名程序员

读书笔记 程序员 程序人生 提升认知

程序员职业生涯的八点感想

池建强

程序员 职业

归去来兮:递归

曲镇

算法

两边夹的应用

孙苏勇

算法 积水问题 两边夹

凡事必先骑上虎背

Steve

学习 态度 方法论

平均响应1000ms到200ms,PHP和Go那家强?

拖地先生

php 架构 性能优化 后台开发 运维

【译】Rust 开发者的2019

WasmEdge

程序员 rust

数据分析师应该了解的数据湖

数据社

大数据 数据仓库 数据湖 数据分析

微服务架构深度解析与最佳实践(全篇汇总)

kimmking

微服务 最佳实践 深度解析 高可用

一个独立开发者,他是如何做到月入 20 万的?

非著名程序员

程序员 独立开发者 副业赚钱 开发者 程序人生

你不是迷茫,只是缺乏目标

Steve

学习 身心健康 方法 自我管理

微服务架构深度解析与最佳实践 - 第三部分

kimmking

微服务 最佳实践 深度解析 高可用

聊聊:Python

谢烟客

Python 人工智能 编程

微服务架构深度解析与最佳实践 - 第七部分

kimmking

微服务 最佳实践 深度解析 高可用

阿里笔记之数据模型

迹_Jason

大数据

申请鲲鹏920测试机试水+编译nginx

草宝虫

nginx 鲲鹏920 centos7 armv8

小程序的当下和未来可能 | GMTC.2019深圳站演讲文稿

崔红保

小程序 uni-app

微服务架构深度解析与最佳实践 - 第四部分

kimmking

微服务 最佳实践 深度解析 高可用

微服务架构深度解析与最佳实践-第二部分

kimmking

微服务 最佳实践 深度解析 高可用

求稳不得

孙苏勇

职业 发展 职场

一个运营经理人的工作两周年总结

霍太稳@极客邦科技

高效工作 身心健康 项目管理 自我管理

2019 年

贾献华

2020 2019 总结 日历 计划

越是困难,越是要做有分析判断能力的人

霍太稳@极客邦科技

创业 团队管理 个人成长

一文讲清楚 MySQL 事务隔离级别和实现原理,开发人员必备知识点

古时的风筝

MySQL 数据库 事务隔离级别 mysql事务 数据库事务

两边夹的应用二

孙苏勇

算法 两边夹 重排序 函数式接口 Lambda

微服务架构深度解析与最佳实践 - 第五部分

kimmking

微服务 最佳实践 深度解析 高可用

最近看了两本书:The Rules of Life 和 Make Big Happen

霍太稳@极客邦科技

创业 团队管理 自我管理

微服务架构深度解析与最佳实践-第一部分

kimmking

微服务 最佳实践 深度解析 高可用

我们怎样用GraphQL分阶段重构后端系统?_架构_Kevin Dangoor_InfoQ精选文章