写点什么

GitHub 关系型数据库垂直分库实践

  • 2021 年 10 月 22 日
  • 本文字数:4092 字

    阅读完需:约 13 分钟

GitHub关系型数据库垂直分库实践

十多年前,与当时的大多数 Web 应用程序一样,GitHub 也是一个使用 Ruby on Rails 开发的网站,它的大部分数据都保存在 MySQL 数据库中。


多年来,这个架构经历了多次迭代,以满足 GitHub 的增长和不断变化的弹性需求。例如,我们单独将某些功能的数据保存在独立的 MySQL 数据库中;我们增加了读副本数量,将读负载分摊到多台机器上;我们还使用了 ProxySQL,减少主 MySQL 实例打开的连接数。


但不管怎样,GitHub 仍然只有一个主数据库集群(我们称之为 mysql1),这个集群保存着 GitHub 核心功能所需的大部分数据,比如用户信息、代码仓库、Issues 和拉取请求。


随着 GitHub 的增长,这种架构难免会面临巨大的挑战。我们努力让数据库系统保持合理的大小,并使用更新、更强大的机器。任何一个影响 mysql1 的故障都会影响所有在这个集群保存数据的功能。


2019 年,为了满足增长和可用性方面的需求,我们启动了一个计划,目标是改进我们对关系型数据库进行分库的工具和能力。正如你所想的那样,这是一项复杂而艰巨的任务,需要引入和创建各种各样的工具。


这样做的结果是,在 2021 年,数据库主机的负载降低了 50%。这极大减少了与数据库相关的故障,并提升了 GitHub 网站的可靠性。

虚拟分库


我们引入的第一个概念叫作数据库模式虚拟分库。在进行真正的数据库分表之前,我们要先确保在应用层面能够将表分开,并且不影响团队开发新功能或修改已有的功能。


为此,我们将数据库表按照领域进行分组,并使用 SQL Linter 来分清领域之间的边界。这样我们才能安全地进行数据分库,避免执行跨分库的查询和事务。

模式领域(Schema Domain)


模式领域是我们用来实现虚拟分库的一个工具。模式领域就是指那些经常一起被用在查询(例如表连接和子查询)和事务中的数据库表的集合。例如,模式领域 gists 包含了与 gists、gist_comments 和 starred_gists 这些功能相关的表。因为它们具有相关性,所以应该被分在一起,它们合在一起被称为一个模式领域。


模式领域之间有清晰的边界,并暴露出各个功能之间模糊的依赖关系。在 Rails 应用程序中,这些信息保存在 db/schema-domains.yml 配置文件中,如下所示:


gists:  - gist_comments  - gists  - starred_gistsrepositories:  - issues  - pull_requests  - repositoriesusers:  - avatars  - gpg_keys  - public_keys  - users
复制代码

SQL Linter


我们基于模式领域构建了两个 Linter,用于确保领域之间具有清晰的虚拟边界。我们在查询语句上添加注解,就可以识别出那些跨越多个模式领域的查询和事务,并可以允许一些例外情况。如果一个领域没有违反这个规则,就可以进行虚拟分库,它们的物理表就可以被迁移到另一个数据库集群中。

Query Linter


Query Linter 用于检查只有属于同一个模式领域的表才能被针对同一个数据库的查询引用。如果它检测到查询中包含来自不同领域的表,就会抛出异常。异常中带有有用的信息,可以帮助开发人员解决问题。


因为 Linter 只在开发和测试环境中启用,开发人员可以在开发过程中发现不合规的查询。另外,在 CI 运行期间,Linter 可以确保不会有新的不合规查询被引入。


Linter 还提供了特殊的/* cross-schema-domain-query-exempted */注释,用它来注解 SQL 查询语句可以允许一些例外情况,将上述的异常忽略掉。


我们还给 ActiveRecord 增加了新方法,这样添加注释就更容易了:


Repository.joins(:owner).annotate("cross-schema-domain-query-exempted")# => SELECT * FROM `repositories` INNER JOIN `users` ON `users`.`id` = `repositories.owner_id` /* cross-schema-domain-query-exempted */
复制代码


将所有查询加上注解,就可以得到需要修改的查询语句的清单。以下是我们用来解决例外情况的常用方法。


有时候,我们只需要把表连接查询拆成单独的查询。例如,用 ActiveRecord 的 preload 方法取代 includes 方法。


另一种比较有挑战性的情况是 has_many :through 关系导致需要连接来自不同模式领域的表。对于这种情况,我们提供了通用解决方案:has_many 新增了 disable_joins 选项,告诉 ActiveRecord 不要执行底层表连接操作,改为执行多次查询,并在查询之间传递主键值。


在应用层进行数据连接,而不是在数据库层,这也是一种常见的解决方案。例如,使用两个单独的查询替代 INNER JOIN,然后在 Ruby 中执行“union”操作(例如,A.pluck(:b_id) & B.where(id:...))。


有时候,这样做会带来性能上的极大提升。根据数据结构和数据集势的不同,MySQL 的查询计划器有时会生成性能较差的查询执行计划,而应用层的数据连接可以获得较稳定的性能。


与大多数与稳定性和性能相关的变更一样,这些都用 Scientist 库做过实验。我们对新旧两种实现进行了实验对比,可以客观地评估每一个变更的性能。

Transaction Linter


除了查询语句之外,事务也是我们的一个关注点。现有的应用程序代码都是基于一定的数据库模式。MySQL 事务可以保证同一数据库不同表之间的一致性。如果事务中的查询所涉及的表被移到其他数据库中,那就无法保证一致性。


为了弄清楚需要检查哪些事务,我们引入了 Transaction Linter。与 Query Linter 类似,它可以确保一个事务所涉及的表都属于同一个模式领域。


这个 Linter 运行在生产环境中,进行大量的采样,并将对性能的影响降到最低。结果被收集起来,用于分析哪些地方存在跨领域事务,这样我们就可以决定是否要更新某些代码或修改我们的数据模型。


对于那些对事务一致性要求很高的地方,我们将数据抽取到同属一个模式领域的新表中。这样可以确保它们位于同一个数据库集群中,继续享有事务一致性保证。这种情况多发生在“多态性”表上,这些表的数据来自不同的模式领域(例如,reactions 表保存了来自多个不同功能的数据,如 Issues、拉取请求、讨论等)。

不停机迁移数据


模式领域在经过虚拟分拆之后,就可以进行物理表迁移。为了进行数据迁移,我们采用了两种不同的方法:Vitess 和写切换(Write-Cutover)。

Vitess


Vitess 是一个建立在 MySQL 之上的伸缩层,用于满足数据分片需求。我们用了它的垂直分片特性,在不停机的情况下将一些表迁移到一起。


我们在 Kubernetes 集群上部署了 Vitess 的 VTGate。应用程序连接到这些 VTGate 端点上,而不是直接连接到 MySQL。VTGate 实现了同样的 MySQL 协议,对于应用程序来说与 MySQL 没有什么两样。


VTGate 进程通过 Vitess 的另一个组件 VTTablet 与 MySQL 实例发生交互。Vitess 的数据表迁移特性是通过 VReplication 来实现的,这个组件负责在数据库集群之间复制数据。

写切换


在 2020 年初,Vitess 的采用还处在早期阶段。除此之外,我们还采用了另一种迁移大规模数据表的方法。这样可以降低依赖单一解决方案所带来的风险,确保 GitHub 网站的持续可用性。


我们利用 MySQL 的常规复制特性将数据迁移到另一个集群。在一开始,新集群被加到旧集群的复制树中,然后再用一个脚本快速执行一些变更来实现切换。


在进行写切换之前的 MySQL 集群


在运行脚本之前,我们先调整应用程序和数据库复制结构,将目标集群 cluster_b 作为现有集群 cluster_a 的子集群。我们用 ProxySQL 实现 MySQL 主实例之间的多路客户端连接。cluster_b 上的 ProxySQL 将流量路由到 cluster_a 的主实例上。有了 ProxySQL,我们可以快速改变数据库的流量路由,将对客户端(也就是我们的 Rails 应用程序)的影响降到最低。


基于这样的结构,我们可以很自然地将数据库连接迁移到 cluster_b。所有的读流量都流向复制了 cluster_a 主实例数据的主机,所有的写流量仍然流向 cluster_a 主实例。


随后,我们开始执行切换脚本:


  • 开启 cluster_a 主实例的只读模式。这个时候,所有向 cluster_a 和 cluster_b 的写入操作都是不允许的。所有尝试向数据库执行写入操作的 Web 请求都会失败,并返回 500 错误。

  • 从 cluster_a 主实例读取最后执行的 MySQL GTID。

  • 轮询 cluster_b 主实例,确认最后执行的 GTID 已达到。

  • 停止从 cluster_a 到 cluster_b 的复制。

  • 更新 cluster_b 的 ProxySQL 配置,将流量重定向到 cluster_b 主实例。

  • 关闭 cluster_a 和 cluster_b 主实例的只读模式。

  • 大功告成!


经过精心的准备和调整,我们发现,即使是我们最繁忙的数据库表,执行完以上 6 个步骤也只需要几十毫秒。由于我们是在一天内流量最不繁忙的时间进行切换,因写入失败而导致的用户可感知错误非常少。这样的结果已经超出了我们的预期。

发现


我们通过写切换来拆分 mysql1——我们最初的数据库主集群。我们一次性迁移了 130 张最繁忙的数据库表,它们为 GitHub 的核心功能提供支撑:代码仓库、Issues 和拉取请求。写切换是我们用来降低迁移风险的一种策略,让我们可以使用多种独立的工具。另外,因为部署拓扑问题和需要提供读己之所写(Read-Your-Write)支持,我们并没有在所有地方都使用 Vitess 作为迁移数据库表的工具,但我们预计在未来会将它作为数据迁移的主要工具。

结果


在文章简介里所提到的 mysql1,也就是我们的数据库主集群,它保存着 GitHub 核心功能的大部分数据,比如用户、代码仓库、Issues 和拉取请求。从 2019 年开始,我们逐渐具备了对这个关系型数据库进行伸缩的能力,并获得了如下结果:


  • 在 2019 年,mysql1 平均每秒处理 95 万个查询,其中 90 万个查询发生在副本上,5 万个发生在主实例上。


  • 现在,也就是在 2021 年,同样是这些表,它们分布在不同的集群中。在两年之内,它们见证了持续的增长,而且一年比一年快。所有这些集群的服务器加在一起,平均每秒处理 120 万个查询,其中 112 万 5 千个查询发生在副本上,7 万 5 千个发生在主实例上。与此同时,每台主机的平均负载减少了一半。


这极大减少了与数据库相关的故障,并提升了 GitHub 网站的可靠性。

更多的分库策略


除了垂直分库,我们也进行水平分库(也就是分片)。我们可以将数据库表拆分到多个集群中,为可持续的增长提供支持。我们将在后续文章中分享更多与之相关的工具、Linter 和 Rails 改进的细节内容。

结论


在过去的十多年,GitHub 学会了如何通过伸缩数据库来满足不断增长的需求。我们通常选择的是“普通”的技术,这些技术被证明很适合我们的规模,因为对于我们来说,可靠性是最为重要的。与此同时,我们也使用一些被业界证明可行的工具,有了这些工具,我们只需要对代码做简单的修改,它们为我们的数据库在未来增长铺平了道路。


原文链接:https://github.blog/2021-09-27-partitioning-githubs-relational-databases-scale/

2021 年 10 月 22 日 16:425020

评论 2 条评论

发布
用户头像
迁移的方案和实施居然不谋而合
2021 年 10 月 25 日 12:36
回复
用户头像
难得的干货,赞👍
2021 年 10 月 22 日 17:09
回复
没有更多了
发现更多内容

LeetCode题解:50. Pow(x, n),暴力法,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

了解HashMap数据结构,超详细!

程序员的时光

面试 hashmap HashMap底层原理

华为云如何赋能无人车飞驰?从这群AI热血少年谈起

华为云开发者社区

人工智能 无人驾驶

开发一个交易所需要多少费用?币币交易系统

135深圳3055源中瑞8032

笔记本中的“全优生”,英特尔Evo产品上市首日秒光!

新闻科技资讯

架构师训练营第 1 期第五周总结

Leo乐

极客大学架构师训练营

不会java的人能不能读《Head First设计模式》?

Nydia

架构师训练营培训第一周总结

lakers

极客大学架构师训练营

1分钟带你入门 React SCU、memo、pureCom

Leo

react.js 前端 React 前端进阶训练营

作为一名Java程序员,技术栈的广度深度都不够还想要高薪?请先把这些技术掌握再说。

Java架构之路

Java 程序员 架构 面试 编程语言

第五周学习代码技术选型总结

三板斧

极客大学架构师训练营

我从高级开发者身上学到的19条编码原则

Java架构师迁哥

合约跟单软件开发,一键跟单系统搭建

135深圳3055源中瑞8032

一周信创舆情观察(10.12~10.18)

统小信uos

架构师训练营 - 第一周学习总结

joshuamai

区块链多币种钱包开发服务商,多币种钱包APP

135深圳3055源中瑞8032

USDT承兑商支付系统开发,区块链跨境支付源码

135深圳3055源中瑞8032

极客时间架构师训练营第一周学习总结

爱码士

课程总结

大数据上手实战!训练营“9营齐开”第二季限时免费报名啦

Apache Flink

大数据

2020,国产数据库崭露峥嵘的发轫之年

墨天轮

数据库 阿里云 华为云 SQL优化 活动专区

为什么说容器的崛起预示着云原生时代到来?

华为云开发者社区

容器 云原生

两个程序员老友的会面

Philips

敏捷开发

Spring 5.2.7和SpringBoot 2.3.3中文翻译发布啦!!!

青年IT男

spring springboot

阿里18道常见的MySQL面试题,含解析

Java架构师迁哥

第一周学习总结

Griffenliu

技术体系的构成

异想的芦苇

技术 技术管理 研发体系

数字货币可能重构全球货币体系

Geek_987812

数字货币 金融

区块链技术本质再认知

Geek_987812

区块链 大数据

技术都是相通的

小黄鱼

极客大学架构师训练营

网易:Flink + Iceberg 数据湖探索与实践

Apache Flink

flink 数据湖

Java高并发编程的一本百科全书《Java高并发编程详解:多线程与架构设计》,把Java语言中最为晦涩的知识点都详解出来了!

Java架构之路

Java 程序员 架构 并发编程 编程语言

基于英特尔x86平台构建AI软件生态系统

基于英特尔x86平台构建AI软件生态系统

GitHub关系型数据库垂直分库实践-InfoQ