写点什么

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

2020 年 12 月 10 日

我们怎样用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:351110
用户头像

发布了 125 篇内容, 共 40.9 次阅读, 收获喜欢 224 次。

关注

评论

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

streamlit:算法工程师快速编写demo的利器

行者AI

算法

架构实战营模块二命题作业

Geek_993581

架构实战营

上线半天下载量破100W!美团大佬的Java性能调优实战手册,超详细

Crud的程序员

Java 编程 架构

Service Mesh 从“趋势”走向“无聊”

阿里巴巴云原生

Java 运维 云原生 dubbo 中间件

腾讯司晓:区块链如何在数字世界中重塑所有权?

CECBC区块链专委会

跨链技术如何破解区块链的可扩展性难题?

CECBC区块链专委会

区块链

如何避免团队里出现搭便车现象

石云升

团队建设 28天写作 职场经验 管理经验 4月日更

【架构实战营】第二模块作业

hiqian

架构实战营

Flutter 学习笔记(一) Text 组件

U+2647

flutter 四月日更

IPFS矿机可以挖多少年?IPFS矿机一年大约挖多少FIL?

投资矿机v:IPFS1234

IPFS矿机一年挖多少FIL IPFS矿机可以挖多少年

马丁量化策略系统搭建,量化交易软件开发

13823153121

美国指责俄罗斯SVR发动网络攻击并对其进行制裁 俄方表示必将回击

Machine Gun

网络安全 信息安全 美国

让孩子爱上阅读(一)

箭上有毒

读书笔记 【4 月日更】

浪潮云说丨打造网络安全“铜墙铁壁”

浪潮云

云计算

混沌工程缓存实战系列一Redis

心远

缓存 混沌工程

8421的数学含义

山@支

2021最新发布:Java面试突击大全 带你摸熟20+互联网公司面试考点

比伯

Java 编程 架构 程序人生 计算机

【架构实战营】第二模块总结

hiqian

架构实战营

用APICloud开发仿微信聊天App制作经验分享

APICloud

小程序云开发 前端开发 web开发 APP开发 APICloud

【得物技术】得物前端性能监控实践

得物技术

前端 体验 监控 用户体验 实践

话题讨论|华为首款汽车公开试乘,互联网造车再起波澜

程序员架构进阶

自动驾驶 华为 话题讨论 28天写作 4月日更

微服务架构框架SpringBoot初探:SpringBoot中Servlet容器的自动配置原理

攻城狮Chova

微服务架构 SpringBoot 2 【4 月日更】

第二课作业

杰语

微服务中台技术解析之sso登录实践

小江

Java 架构设计 后端开发 SSO

「前端初学者、硬件爱好者、编程自学者」微信小程序开发很简单!

智能物联实验室

前端 前端开发 前端进阶 硬件设计 硬件研发

跨省通办,海淀在全国率先推出“区块链+”服务新模式

CECBC区块链专委会

架构实战业命题二学习总结

Geek_993581

架构实战营

推荐!看完全面掌握,最详细的 Docker 学习笔记总结(2021最新版)

民工哥

Docker 程序员 容器 DevOps 运维

分布式系统当中的CAP理论

五分钟学大数据

分布式 CAP理论 4月日更

我把这个软件,推荐给了总监

yes的练级攻略

爬虫实例:爬取中国大学排名Top20

Bob

Python python 爬虫 四月日更

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

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