GitHub 数据库升级过程
最近 GitHub 官方博客更新了一篇文章,讲述了将 GitHub 网站依赖的 1200+ 台 MySQL 主机升级到 8.0 版本的过程。
先前,Oracle 官方宣布,在 2023 年 10 月 21 日之后,MySQL 5.7 将达到其生命周期的终点,这意味着 Oracle 将不再为 MySQL 5.7 提供官方更新、错误修复或安全补丁,正是在这一背景下,才有了 GitHub 此次的升级。
根据 GitHub 官方介绍,GitHub 使用 MySQL 来存储大量关系数据,因此对于数据库这种底层的基础设施的升级绝非易事,为了升级到 MySQL 8.0,他们规划、测试和升级本身总共花费了一年多的时间,并且需要 GitHub 内部多个团队的协作。因此对于 GitHub 此次这种大规模的底层数据库的升级,我本身是抱有极大的兴趣,也想了解一下如此大规模的 MySQL 的集群采用了什么样的升级方案,所以我去阅读了 GitHub 官方博客,并且将原文翻译如下,以下是原文翻译全部内容。
15 年前,GitHub 开始于一个使用 Ruby on Rails(cczywyc 注:一个基于 Ruby 语言的 web 开发框架)框架构建的应用,使用单体的 MySQL 数据库,从那时起,Github 逐渐改进 MySQL 结构,以满足平台的扩展和弹性需求,这其中包括构建高可用、实现自动化测试和数据分区的要求。如今,MySQL 仍热是 GitHub 基础设施的核心部分,并且也是我们选择关系型数据库的核心部分。
下面就是我们将我们的 1200 多个 MySQL 主机升级到 8.0 版本的过程。在不影响我们的服务水平目标(SLO)的前提下升级 MySQL 版本可谓是一项不小的成就,在这个过程中,升级规划、测试和升级本身就花费了我们一年多的时间,并且还依赖于 GitHub 内部多个团队的协作。
升级的动机
为什么我们要升级到 MySQL8.0 版本呢?随着 MySQL 5.7 临近生命周期的尾声,我们将我们 MySQL 集群升级到了下一个主要的版本 – MySQL8.0。我们不仅希望使用这个能够获得最新的安全补丁、错误修复和性能增强功能的 MySQL 版本,我们还希望能够使用到 8.0 版本中的一些新功能,这包括 Instant DDLs、不可见索引和压缩二进制日志等。
GitHub 的 MySQL 的基础设施
在我们深入了解升级过程之前,让我们先来全面了解一下我们的 MySQL 基础设施:
- 我们的 MySQL 集群包括 1200 多台主机,它们包括在数据中心的 Azure 虚拟机和裸金属主机。
- 我们数据库存储了超过 300 TB 的数据,并且在 50 多个数据库集群中每秒处理 550 万次的查询。
- 每个集群都带有主集群加副本集群的配置,用来实现高可用性。
- 我们做到了数据分区。我们利用水平和垂直分片来扩展 MySQL 集群,针对特定的领域,我们有特定的 MySQL 集群来存储这些特定领域的数据,同时我们还为大区域提供了水平分片的 Vitess 集群,这些区域的增长超出了单-主 MySQL 集群的规模。
- 我们拥有一个庞大的工具生态系统,包括 Percona Toolkit、gh-ost、orchestrator、freno 以及内部自动化,用于运营我们的平台。
上面所有的这些组合成一个多样化且复杂的部署,需要在保持我们的 SLOs 的同时进行升级。
准备阶段
作为 GitHub 主要的数据存储,我们对可用性设定了很高的标准。由于我们的数据库集群的规模和 MySQL 基础设施的关键性,我们对升级过程确定了一下的要求:
- 我们必须在遵守我们的服务水平目标(SLO)和服务水平协议(SLA)的同时升级每个 MySQL 数据库。
- 我们无法在测试和验证阶段考虑所有的故障模式,因此,为了保持在服务水平目标(SLO)范围内,(在发生升级故障时)我们需要能够在不中断服务的情况下可以回滚到先前的 MySQL 5.7 版本。
- 我们的 MySQL 集群负载非常多样化,为了降低风险,我们需要对每个数据库集群进行原子升级,并在其他重大变更周围安排计划,这意味着升级过程会很长,因此我们从一开始就预见我们需要能够维持运行混合版本的环境。
升级准备工作开始于 2022 年 7 月,甚至在升级任何一个生产数据库之前,我们就设定了几个关键里程碑。
准备升级的基础设施
我们需要确定 MySQL 8.0 的适当默认值,并进行一些基准性能测试。因为我们需要操作两个版本的 MySQL,我们的工具和自动化需要能够处理混合版本,并且能够识别在 5.7 和 8.0 之间的新的、不同的或已弃用的语法。
确保应用兼容性
我们将 MySQL 8.0 添加到所有使用 MySQL 的应用程序的持续集成(CI)中。我们在 CI 中并行运行 MySQL 5.7 和 8.0,以确保在长时间的升级过程中不会出现回归。我们检测到 CI 中的各种错误和不兼容性,帮助我们删除任何不受支持的配置或功能,并转义任何新的保留关键字。
为帮助应用程序开发人员过渡到 MySQL 8.0,我们还在 GitHub Codespacees 中启动了选择 MySQL 8.0 预构建容器的选项,以便进行调试,并为额外的预生产测试提供了 MySQL 8.0 开发集群。
沟通和透明度
我们使用 GitHub Projects 创建了一个 滚动日历,以便在内部进行升级计划的沟通和跟踪。我们创建了 issue 模版,用于跟踪应用团队和数据库团队的 checklist,以协调升级工作。
升级计划
为了满足我们的可用性标准,我们采用了一种渐进式升级策略,允许在整个过程中设置检查点和回滚。
步骤一:滚动副本升级
我们首先升级了单个副本,并在其离线状态下进行监控,以确保基本功能稳定。然后我们启用了生产流量,并继续监控查询延迟、系统指标和应用程序指标。接着,我们逐步将 8.0 副本上线,直到升级了整个数据中心,然后遍历其他数据中心。同时,我们还保留了足够的 5.7 版本的副本以便进行回滚,但我们禁用了它们的生产流量,开始通过 8.0 服务器提供所有的读取流量。
步骤二:更新复制拓扑
在所有的只读流量通过 8.0 副本提供服务后,我们调整了复制拓扑如下:
- 配置一个 8.0 的主备候选节点,直接在当前的 5.7 主节点下进行复制。
- 在那个 8.0 副本的下游创建了两个复制链:
- 一组仅包含 5.7 版本的副本(不提供流量,但已准备好以防回滚)
- 一组仅包含 8.0 版本的副本(服务流量)
- 拓扑结构在这个状态下只持续了很短的时间(最多几个小时),然后我们就进入了下一步
步骤三:将 MySQL 主机升级为主节点
我们选择不直接在主数据库主机上进行升级。相反,我们将通过 Orchestrator 执行的优雅故障切换将一个 MySQL 8.0 副本升级为主数据库。在那时,复制拓扑结构包括一个8.0 主数据库,连接到它的有两个复制链:一个用于回滚的离线的 5.7 副本集和一个用于服务的 8.0 副本集。
Orchestrator 还能将 5.7 版本的数据库主机作为潜在的故障切换候选者列入黑名单,以防止在计划外的故障切换时发生意外回滚。
步骤四: 升级内部实例类型
我们在内部还拥有用于备份或非生产工作负载的辅助服务器,我们对这些服务器也进行了升级以确保一致性。
步骤五:清理
一旦我们确认这些集群无需再回滚,并已成功升级至 8.0 版本,我们就移除 5.7 版本的服务器。然后验证包括至少完成一轮完整的 24 小时流量循环,以确保在流量高峰期间没有问题。
回滚的能力
保持我们升级策略的一个核心部分就是能够回滚到之前的 MySQL 5.7 版本的能力。对于只读副本,我们确保保持足够的 5.7 副本在线以提供生产流量负载,并且如果 8.0 副本运行不良时,通过禁用这些 8.0 副本来启动回滚操作;对于主节点来说,为了在不丢失数据或中断服务的情况下进行回滚,我们就需要能够在 8.0 和 5.7 之间保持双向的数据复制。
MySQL 数据复制支持从一个版本复制到下一个更高的版本,但并不明确支持相反过程(MySQL 复制的兼容性)。当我们在演示集群上测试将 8.0 主机升级为主节点时,我们发现所有的 5.7 副本的复制都中断了。下面是我们需要解决几个问题:
- 在 MySQL 中,utf8mb4 是默认的字符集,并使用更现代的 utf8mb4_0900_ai_ci 排序规则作为默认值,之前的 MySQL 5.7 版本支持 utf8mb4_unicode_520_ci 排序规则,但不支持最新版本的 Unicode utf8mb4_0900_ai_ci。
- MySQL 8.0 引入了角色管理的特性,但这个特效在 MySQL 5.7 中并不存在。当将 8.0 实例提升为集群中的主节点时,我们遇到了问题,我们的配置管理会将某些权限集扩展为包含角色语句并执行它们,这会导致 5.7 副本的下游复制中断,我们是通过在升级窗口期间临时调整受影响用户的定义权限从而解决了这个问题。
为了解决字符排序规则的不兼容性,我们必须将默认字符编码设置为 utf8,排序规则设置为 utf8_unicode_ci。
对于 GitHub.com 的整体架构,我们的 Rails 配置确保字符排序规则一致,并且使得标准化客户端配置到数据库变得更容易。因此,我们对于我们最关键的应用程序能够保持双向复制具有很强的信心。
挑战
在我们的测试、准备和升级过程中,我们遇到了一些技术挑战。
关于 Vitess
我们使用 Vitess 对关系型数据进行水平分片。总体而言,升级我们的 Vitess 集群与升级 MySQL 集群并无太大不同。我们已经在 CI 中运行 Vitess,因此我们能够验证查询的兼容性。在我们分片集群的升级策略中,我们一次升级一个分片。VTgate,即 Vitess 代理层,会广播 MySQL 的版本信息,而一些客户端行为取决于这个版本信息。例如,一个应用程序使用了一个禁用了 5.7 服务器查询缓存的 Java 客户端,因为在 8.0 中移除了查询缓存,对于它们来说,这会生成阻塞错误。因此,一旦给定 keyspace 的单个 MySQL 主机完成升级,我们必须确保我们也更新了 VTgate 的设置以广播 8.0 的信息。
复制延迟
我们使用读取副本来扩展我们的读取可用性,GitHub.com 需要低的复制延迟,以提供最新的数据。
在我们的测试早期,我们遇到了一个 MySQL 的复制错误,它在 8.0.28 版本中被修复:
Replication: If a replica server with the system variable replica_preserve_commit_order = 1 set was used under intensive load for a long period, the instance could run out of commit order sequence tickets. Incorrect behavior after the maximum value was exceeded caused the applier to hang and the applier worker threads to wait indefinitely on the commit order queue. The commit order sequence ticket generator now wraps around correctly. Thanks to Zhai Weixiang for the contribution. (Bug #32891221, Bug #103636)
我们恰好符合触发此错误的所有条件:
- 我们使用 replica_preserve_commit_order,因为我们使用基于 GTID 的复制。
- 我们的许多集群在很长一段时间内承受着高强度的负载,尤其是我们最关键的集群。大部分集群都有很高的写入压力。
由于此错误已在上游修复,我们只需要确保我们部署的 MySQL 版本高于 8.0.28 版本即可。
我们还观察到,在 MySQL 8.0 中,导致复制延迟的大量写入问题变得更加严重。这使得我们更加重视避免大量的写入突发情况。在 GitHub,我们使用 freno 来根据复制延迟来控制写入工作负载。
查询在 CI 中通过但是在生产环境失败
我们知道在生产环境中不可避免地会出现问题,因此我们采用了逐步升级副本的策略。我们遇到了一些在 CI 中通过的查询,在生产环境中遇到真实工作负载时却失败的情况。其中一个显著的问题是,在具有大型 WHERE IN 子句的查询中,MySQL 会崩溃。我们有一些包含数万个值的大型 WHERE IN 查询。在这些情况下,我们需要在继续升级过程之前重写这些查询。查询抽样有助于跟踪和检测这些问题。在 GitHub,我们使用 Solarwinds DPM(VividCortex),即一个 SaaS 数据库性能监控工具,用于查询可观察性。
经验教训和收获
经过测试、性能调优和解决已识别的问题,我们的整个升级过程持续了一年多,其中涉及到 GitHub 的多个团队的工程师。最终我们将我们的整个集群升级到了 MySQL 8.0,包括用户 GitHub.com 的演练集群、生产集群以及支持我们内部工具的实例。此次升级凸显了我们可观察性平台、测试计划和回滚能力的重要性。测试和逐步推出策略使我们能够及早发现问题,并减少主要升级中遇到新故障模式的可能性。
尽管有逐步推出的策略,我们仍然需要在每一步都具备回滚的能力,并且我们需要能够观察以识别何时需要回滚的信号。使回滚变得最具挑战的方面是保持从新的 8.0 主服务器到 5.7 副本的反向复制。我们发现 Trilogy 客户端库的一致性为我们提供了更可预测的连接行为,并让我们有信心,主 Rails 单体的连接不会破坏反向复制。
然而,对于我们的一些 MySQL 集群,连接来自不同框架/语言的多个不同客户端,我们发现反向复制在几个小时内就会中断,缩短了回滚机会的时间窗口。幸运的是,这些情况很少见,我们没有出现在我们需要回滚之前复制中断的情况。但对我们来说,这是一个教训,即具有已知且被充分理解的客户端连接配置是有益的。它强调了制定准则和框架以确保在这些配置中保持一致性的价值。
我们之前对数据进行分区的努力得到了回报——它使我们能够针对不同的数据领域进行更有针对性的升级。这一点非常重要,因为一个失败的查询会阻塞整个集群的升级,而不同的工作负载分区使我们能够逐步进行升级,并减少在过程中遇到的未知风险的影响范围。这种权衡的结果是我们的 MySQL 集群规模也随之增长。
GitHub 上次升级 MySQL 版本时,我们有五个数据库集群,现在我们有 50 个以上的集群。为了成功升级,我们不得不投资于监测、工具和管理整个群集的流程。
小结
此次 MySQL 升级只是我们必须执行的例行维护中的一种类型——对于我们在集群上运行的任何软件,都必须具备升级路径。作为升级项目的一部分,我们开发了新的流程和操作能力,成功完成了 MySQL 版本的升级。然而,在升级过程中仍然有太多需要手动干预的步骤,我们希望减少完成未来 MySQL 升级所需的工作量和时间。
我们预计随着 GitHub.com 的增长,我们的集群将继续扩大,并且我们有将数据进一步分区的目标,这将随着时间增加我们的 MySQL 集群数量。在操作任务和自愈能力方面进行自动化建设可以帮助我们未来扩展 MySQL 运营能力。我们相信,投资可靠的集群管理和自动化将使我们能够扩展 GitHub,并跟上所需的维护工作,提供更可预测和弹性的系统。
此次升级的经验为我们的 MySQL 自动化奠定了基础,并为将来的升级铺平了道路,使其更加高效完成,但仍然保持同样的关注和安全水平。
(全文完)