对 GitHub 的关系数据库进行拆分

对 GitHub 的关系数据库进行分区以处理规模 | GitHub 博客

十多年前,GitHub.com 与当时的许多其他 Web 应用程序一样,最初是基于 Ruby on Rails 构建的,使用单个 MySQL 数据库来存储大部分数据。

多年来,该架构经历了多次迭代,以支持 GitHub 的增长和不断变化的弹性需求。 例如,我们开始存储某些功能的数据 (例如 statuses) 在单独的 MySQL 数据库中,我们添加了只读副本以将负载分散到多台计算机上,并且我们开始使用 ProxySQL 来减少针对主 MySQL 实例打开的连接数量。

然而,从本质上讲,GitHub.com 仍然围绕一个主数据库集群(称为 mysql1 )构建,该数据库集群容纳了 GitHub 核心功能(例如用户配置文件、存储库、问题和拉取请求)所使用的大部分数据。

随着 GitHub 的发展,这不可避免地带来了挑战。 我们努力保持我们的数据库系统足够大,总是迁移到更新更大的机器来扩展。 任何对 mysql1 产生负面影响的事件都会影响在此集群上存储数据的所有功能。

2019 年,为了应对我们面临的增长和可用性挑战,我们制定了一项计划,以改进我们的工具和关系数据库分区的能力。 正如您可以想象的那样,这是一项复杂的挑战,需要引入和创建如下所述的各种工具。

我们在 2021 年看到的结果是,曾经位于mysql1 上的存储数据的数据库主机的负载减少了 50%。 这极大地减少了数据库相关事件的数量,并提高了 GitHub.com 对所有用户的可靠性。

虚拟分区


我们引入的第一个概念是数据库模式的虚拟分区。 在物理移动数据库表之前,我们必须确保它们在应用程序层中_虚拟_分离,并且必须在不影响开发新功能或现有功能的团队的情况下进行。

为此,我们将属于模式域的数据库表分组,并使用 SQL linter 强制执行域之间的边界。 这使我们能够稍后安全地对数据进行分区,而不会因跨分区的查询和事务而结束。

架构域

架构域是我们为实现虚拟分区而提出的工具。 架构域描述一组紧密耦合的数据库表,这些表经常在查询(例如,使用表联接或子查询时)和事务中一起使用。 例如,gists 架构域包含支持 GitHub Gist 功能的所有表,例如 gistsgist_commentsstarred_gists 表。 既然是一体,就应该在一起。 架构域是对其进行编码的第一步。

架构域设置了明确的边界,并暴露了功能之间有时隐藏的依赖关系。 在 Rails 应用程序中,信息存储在位于 db/schema-domains.yml 的简单 YAML 配置中。 这是说明该文件内容的示例:

要点:

  • gist_comments

  • gists

  • starred_gists

存储库:

  • issues

  • pull_requests

  • repositories

用户:

  • avatars

  • gpg_keys

  • public_keys

  • users

语法检查确保该文件中的表列表与我们的数据库模式匹配。 反过来,同一个语法检查强制为每个表分配架构域。

SQL 语法检查

两个新的 SQL 语法检查构建在架构域之上,强制执行域之间的虚拟边界。 他们通过添加查询注释并将其视为豁免来识别跨架构域的任何违规查询和事务。 如果域没有违规,则它已被虚拟分区并准备好物理移动到另一个数据库集群。

查询语法检查

查询语法检查验证在同一数据库查询中只能引用属于同一架构域的表。 如果它检测到来自不同域的表,它会抛出异常并提供有用的消息,以帮助开发人员避免出现问题。

由于语法检查仅在开发和测试环境中启用,因此开发人员在开发过程的早期就会遇到违规错误。 此外,在 CI 运行期间,语法检查可确保不会意外引入新的违规行为。

语法检查有一种方法可以通过使用特殊注释来注释 SQL 查询来抑制异常: /* 跨架构域查询豁免 */

我们甚至构建并上传了 ActiveRecord 的新方法 以便更轻松地添加这样的注释:

Repository.joins(:owner).annotate("跨架构域查询豁免")

=> SELECT FROM repositories INNER JOIN users ON users.id = repositories.owner_id / 跨架构域查询豁免 */

通过注释所有导致失败的查询,可以构建需要修改的查询的积压。 以下是我们经常用来消除豁免的几种方法:

  1. 有时,可以通过触发单独的查询而不是连接表来轻松解决豁免问题。 一个例子是使用 ActiveRecordpreload 方法而不是 includes.

另一个挑战是 "has_many :through "关系,这种关系会导致跨不同模式域的表的 "JOIN"。为此,我们开发了一个通用解决方案,该解决方案也被上传到了 Rails: has_many "现在有一个 "disable_joins "选项,可以让 Active Record 不在底层表之间进行任何 "JOIN "查询。取而代之的是,它会运行几个传递主键值的查询。

  1. 另一种常见的解决方案是在应用程序中而不是在数据库中加入数据。例如,用两个单独的查询来代替 INNER JOIN 语句,并在 Ruby 中执行 "union "操作(例如,A.p pluck(:b_id) & B.where(id:...))。

在某些情况下,这会带来令人惊讶的性能提升。根据数据结构和卡入度的不同,MySQL 的查询规划器有时会创建次优的查询执行计划,而应用侧连接的性能成本则更稳定。

与几乎所有与可靠性和性能相关的变更一样,我们在 科学家实验 后面发布了这些变更,这些实验针对部分请求执行了新旧两种实现,从而使我们能够评估每种变更对性能的影响。

事务衬垫

除了查询,事务也是一个值得关注的问题。现有的应用代码是根据特定的数据库模式编写的。MySQL 事务保证了数据库中表之间的一致性。如果事务包括对将移动到不同数据库的表的查询,那么它将不再能保证一致性。

为了了解哪些事务需要审查,我们引入了事务筛选器。与查询衬垫类似,它可以验证在给定事务中一起使用的所有表是否属于同一模式域。

在生产环境中运行时,为了将对性能的影响降到最低,会进行大量的抽样检查。通过收集和分析线程结果,我们可以了解大多数跨域事务发生在哪里,从而决定更新某些代码路径或调整数据模型。

在事务一致性保证至关重要的情况下,我们会将数据提取到属于同一模式域的新表中。这样就能确保它们位于同一个数据库集群上,从而继续保持事务一致性。这种情况经常发生在存放来自不同模式域的数据的多态表中(例如,「反应」表存储不同功能的记录,如问题、合并请求、讨论等)。

不停机移动数据

虚拟隔离的模式域可以物理移动到另一个数据库群集。我们使用两种不同的方法来移动表: Vitess 和自定义写入切换过程。

Vitess

Vitess 是MySQL之上的一个扩展层,有助于满足分片需求。我们使用它的 垂直分片功能在生产中将表集移动到一起,而无需停机。

为此,我们在 Kubernetes 集群中部署了 Vitess 的 VTGate。这些 VTGate 进程成为应用程序连接的端点,而不是直接连接到 MySQL。它们执行相同的 MySQL 协议,与应用程序端没有区别。

VTGate 进程知道 Vitess 设置的当前状态,并通过另一个 Vitess 组件与 MySQL 实例对话: VTTablet。在幕后,Vitess 的表格移动功能由 VReplication 提供支持,它可以在数据库群集之间复制数据。

写切换过程

由于在2020年初,Vitess的采用仍处于早期阶段,我们开发了一种替代方法,一次性移动大量表。这有助于减轻依赖单一解决方案以确保GitHub.com持续可用性的风险。

我们使用 MySQL 的定期复制功能将数据提供给另一个集群。 最初,新集群被添加到旧集群的复制树中。 然后,脚本快速执行一系列更改以实现切换。

执行写切换过程之前的 MySQL 数据库集群设置

在运行脚本之前,我们准备应用程序和数据库复制,以便名为 cluster_b 的目标集群成为现有 cluster_a. ProxySQL 的子集群。用于多路复用客户端连接 到 MySQL 主数据库。 cluster_b 上的 ProxySQL 实例配置为将所有流量路由到 cluster_a 主节点。 使用 ProxySQL 使我们能够快速更改数据库流量路由,并且对数据库客户端(在我们的例子中是 Rails 应用程序)的影响最小。

使用这个设置,我们可以将数据库连接移至 cluster_b,而无需拆分任何内容。所有读流量仍然流向从 cluster_a 主节点复制的主机。所有写流量仍然留在 cluster_a 主节点。

在这种情况下,我们运行一个切换脚本执行以下步骤:

  1. cluster_a 主节点启用只读模式。此时,阻止对 cluster_acluster_b 的所有写入。所有尝试写入这些数据库主节点的网络请求都会失败并导致 500 错误。

  2. cluster_a 主节点读取最后执行的 MySQL GTID

  3. 轮询 cluster_b 主节点以验证最后执行的 GTID 是否已到达。

  4. cluster_b 上停止从 cluster_a 复制的复制。

  5. 更新 cluster_b 上的 ProxySQL 路由配置,将流量引导到 cluster_b 主节点。

  6. cluster_acluster_b 主节点禁用只读模式。

  7. 庆祝!

经过充分的准备和练习,我们了解到对于最繁忙的数据库表,这六个步骤的执行时间仅为几十毫秒。由于我们在一天中流量最低的时间执行这样的切换,因此由于写操作失败,我们只会导致少量面向用户的错误。这种方法的结果比我们预期的要好。

学习

写入切换过程用于拆分 mysql1,即我们原来的主数据库集群。我们一次移动了130个最繁忙的表——那些支持GitHub核心功能的表:存储库、问题和拉取请求。这一过程是作为一种风险缓解策略而创建的,我们可以使用多种独立的工具。此外,由于部署拓扑和读写支持等因素,我们并没有在每种情况下都选择Vitess作为移动数据库表的工具。不过,我们预计将来有机会在大多数数据迁移中使用它。

结果


在介绍中提到的主数据库集群 mysql1 ,承载了GitHub许多重要功能所使用的大部分数据,如用户、仓库、问题和拉取请求。自2019年以来,我们已经成功实现了对这个关系型数据库的扩展,取得了以下成果:

  • 2019年, mysql1 平均每秒响应 950,000 个查询,在副本上每秒响应 900,000 个查询,在主服务器上每秒响应 50,000 个查询。

  • 2021 年的今天,相同的数据库表分布在多个集群中。在接下来的两年里,它们经历了持续增长,并且逐年加速。 这些集群的所有主机平均每秒应答 1,200,000 个查询(副本上每秒 1,125,000 个查询,主服务器上每秒 75,000 个查询)。 与此同时,每台主机的平均负载减半。

负载的减少对于降低与数据库相关的故障数量发挥了重要作用,并提高了GitHub.com对于所有用户的可靠性。

更多的分区


除了垂直分区来移动数据库表,我们还使用水平分区(又名分片)。这允许我们跨多个集群拆分数据库表,从而实现更可持续的增长。我们将在以后的博客文章中详细介绍与此相关的工具、脚本和Rails改进。

结论


在过去的10年里,GitHub一直在学习根据需求进行扩展。我们经常选择利用“无聊”的技术,这些技术已经被证明在我们的规模下有效,因为可靠性仍然是首要考虑的问题。但是,将经过行业验证的工具与对生产代码及其依赖项的简单更改相结合,为我们的数据库在未来的持续增长提供了一条道路。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://github.blog/2021-09-27-partition...

译文地址:https://learnku.com/mysql/t/84824

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!