无中断升级 1200 多台 MySQL 服务器,GitHub 是如何做到的

15 年前,GitHub 还只是一个使用单个 MySQL 数据库的 Ruby on Rails 应用。自那时起,为了满足平台的扩展性和可靠性需求,GitHub 的 MySQL 架构发生了变化,包括 构建高可用性实现测试自动化数据分区 等。如今,MySQL 仍然是 GitHub 基础设施的核心组成部分,也是我们在关系型数据库方面的主要选择。

本文将分享我们将 1200 多台 MySQL 主机升级到 8.0 的故事。在不影响 SLO 的情况下升级整个主机群可不是一件小事——规划、测试和升级本身就花费了一年多时间,并且需要 GitHub 多个内部团队的协作。

升级的动机

为什么升级到 MySQL 8.0?随着 MySQL 5.7 的生命周期即将接近尾声,我们将我们的主机群升级到下一个主要版本——MySQL 8.0。我们希望使用包含最新安全补丁、错误修复和性能增强的 MySQL 版本。MySQL 8.0 还带来了一些新特性,我们想要使用它们并从中受益,如即时 DDL、不可见索引和压缩的二进制日志等。

GitHub 的 MySQL 基础设施

在深入讨论如何进行升级之前,我们先简要了解一下我们的 MySQL 基础设施:

  • 我们的主机群包含 1200 多台主机,由 Azure 虚拟机和我们数据中心的裸机组成。

  • 我们存储超过 300TB 的数据,分布在 50 多个数据库集群中,提供每秒 550 万次的查询。

  • 每个集群都采用了高可用性配置,具有主节点加副本节点的集群设置。

  • 我们的数据采用了分区机制。我们利用水平和垂直分片来扩展 MySQL 集群。我们有存储特定产品数据域的 MySQL 集群。我们也有为规模超过单一 MySQL 集群可承受的大型数据域而创建的水平分片 Vitess 集群。

  • 我们有一个庞大的工具生态系统,包括 Percona Toolkit、gh-ostorchestratorfreno,以及用于操作主机群的内部自动化工具。

所有这些组成了一个多样化且复杂的部署,我们需要在保持 SLO 水平的同时进行升级。

准备工作

作为 GitHub 的主要数据存储,我们对可用性有着很高的要求。鉴于主机群的规模和 MySQL 基础设施的重要性,我们对升级过程有如下要求。

  • 我们必须能够在保持 SLO 和 SLA 水平的情况下升级每个 MySQL 数据库。

  • 在测试和验证阶段,我们无法考虑到所有的故障模式。因此,为了保持 SLO 水平,在发生故障时我们需要能够在不中断服务的情况下回滚到先前的 MySQL 5.7 版本。

  • 我们的 MySQL 主机群拥有多样化的工作负载。为了降低风险,我们需要原子升级每个数据库集群,并为其他重大变更安排好时间。这意味着升级过程会是一个漫长的过程。因此,我们从一开始就知道我们需要能够维持运行一个混合 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 Codespaces 中启用了选择 MySQL 8.0 预构建容器进行调试的选项,并为额外的预生产测试提供了 MySQL 8.0 开发集群。

沟通和透明度

我们使用 GitHub Projects 创建了一个滚动日历,用于内部沟通和跟踪升级计划。为协调升级,我们为应用程序团队和数据库团队创建了用于跟踪检查清单的问题模板。


用于跟踪 MySQL 8.0 升级时间表的项目看板

升级计划

为了达到我们的可用性标准,我们采用了渐进式的升级策略,允许在整个过程中设定检查点和回滚。

第 1 步:滚动升级副本

我们从升级单个副本开始,并在它们处于离线状态时进行监控,确保基本功能稳定。然后,我们开启生产流量,并继续监控查询延迟、系统指标和应用程序指标。我们逐渐将 8.0 版副本上线,直到升级完整个数据中心,然后迭代其他数据中心。为了能够回滚,我们保留了足够的 5.7 版在线副本,但对它们禁用了生产流量,让 8.0 版本的服务器提供所有读取流量。


每个数据中心的渐进式发布都采用的副本升级策略

第 2 步:更新复制拓扑

在所有的只读流量通过 8.0 版副本之后,我们对复制拓扑进行了调整:

  • 将一个 8.0 候选主节点配置为直接复制当前的 5.7 主机。

  • 在这个 8.0 版副本的下游创建两个复制链:一组只包含 5.7 版副本(不提供流量,为回滚做准备)。一组只包含 8.0 版副本(提供流量)。

  • 处于这种状态的拓扑时间很短暂(最多几小时),然后我们进入下一步。


为了帮助升级,拓扑结构包含了两个复制链

第 3 步:将 MySQL 8.0 主机提升为主数据库

我们不直接在主数据库主机上进行升级,相反,我们通过 Orchestrator 执行优雅的故障转移,将 MySQL 8.0 版副本提升为主数据库。此时,复制拓扑由一个 8.0 版主机和两个复制链组成:一个用于回滚的离线 5.7 版副本集和一个提供服务的 8.0 版副本集。

Orchestrator 还被配置为将 5.7 版主机列为潜在的故障转移候选项,防止在发生意外故障转移时出现非预期的回滚。


主节点故障转移和完成 MySQL 8.0 升级的其他步骤

第 4 步:升级内部实例

我们还有用于备份或处理非生产工作负载的辅助服务器,为了一致性,我们也对它们进行了升级。

第 5 步:清理

确认集群无需回滚并成功升级到 8.0 后,我们移除了 5.7 版服务器。验证过程包含至少一个完整的 24 小时流量周期,确保在高峰流量期间不出问题。

回滚能力

保证升级策略安全的一个核心部分是能够回滚到之前的 MySQL 5.7 版本。对于只读副本,我们确保保留了足够的 5.7 版副本在线来提供生产流量负载,如果 8.0 版副本表现不佳,则通过禁用 8.0 版副本来启动回滚。对于主数据库,为了在不丢失数据或中断服务的情况下回滚,我们需要能够在 8.0 版本和 5.7 版本之间保持逆向数据复制。

MySQL 支持从一个版本复制到下一个更高版本,但没有明确支持反向复制(MySQL 复制兼容性)。在测试将 8.0 主机提升为主数据库时,我们发现所有 5.7 版副本的复制都发生了中断。我们需要解决几个问题。

  1. 在 MySQL 8.0 中,utf8mb4 是默认的字符集,并使用 utf8mb4_0900_ai_ci 作为默认排序规则。之前的 MySQL 5.7 支持 utf8mb4_unicode_520_ci 排序规则,但不支持最新版本的 Unicode utf8mb4_0900_ai_ci

  2. MySQL 8.0 引入了用于管理权限的角色 ,但这个特性在 MySQL 5.7 中并不存在。当一个 8.0 版实例被提升为集群主数据库时,我们遇到了一些问题。我们的配置管理正在扩展某些权限集,包含并执行包含角色的语句,这导致 5.7 版副本的下游复制中断。我们通过在升级窗口期间临时调整受影响用户的定义权限解决了这个问题。

为解决字符排序不兼容问题,我们不得不将默认字符编码设置为 utf8,并将排序规则设置为 utf8_unicode_ci

对于整个 GitHub,我们的 Rails 配置确保了字符排序的一致性,并让客户端配置的数据库标准化变得更容易。因此,我们非常有信心能够维护关键应用的逆向复制。

挑战

在测试、准备和升级过程中,我们遇到了一些技术挑战。

Vitess

我们使用 Vitess 进行关系数据的水平分片。在大多数情况下,升级 Vitess 集群与升级 MySQL 集群没有太大区别。我们已经在 CI 中运行了 Vitess,因此能够验证查询兼容性。在我们的分片集群升级策略中,我们一次升级一个分片。Vitess 代理层 VTgate 会发布 MySQL 的版本信息,一些客户端行为会依赖这些版本信息。例如,一个使用 Java 客户端的应用程序对 5.7 版本的服务器禁用了查询缓存 —— 由于 8.0 移除了查询缓存,这会导致生成阻塞错误。所以,一旦给定键空间的 MySQL 主机升级完成,我们必须确保也更新了 VTgate 的设置,让它们发布 8.0 的版本信息。

复制延迟

我们使用读取副本来扩展读取可用性。为了确保能够提供最新的数据,GitHub 需要低延迟的复制。

在早期的测试中,我们遇到了一个 MySQL 复制 bug,这个 bug 在 8.0.28 版本中得到了修复

复制:如果一个设置了系统变量 replica_preserve_commit_order = 1 的副本在长时间高负载下运行,该实例可能会耗尽提交顺序序列票证。在超出最大值后的不正确行为导致应用程序挂起,并使应用程序工作线程在提交顺序队列中无限期等待。提交顺序序列票证生成器现在可以正确地回绕。感谢 Zhai Weixiang 的贡献。(Bug #32891221, Bug #103636)

我们碰巧符合击中这个 bug 的所有条件。

  • 我们配置了 replica_preserve_commit_order,因为我们使用基于 GTID 的复制。

  • 我们的许多集群长时间承受着大量的强写入负载,尤其是最关键的集群。我们的大多数集群的写入负载都很高。

由于这个 bug 已在上游进行了修补,我们只需要确保部署的 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 的临时集群、生产集群和为内部工具提供支持的实例。此次升级凸显了我们的可观察性平台、测试计划和回滚能力的重要性。测试和渐进式推出策略使我们能够尽早识别出问题,减少了在主升级过程中遇到新故障的可能性。

尽管有渐进式推出策略,我们仍然需要具备步骤级别的回滚能力,我们也需要可观察性来识别回滚信号。启用回滚最具挑战性的地方在于保持从 8.0 主节点到 5.7 副本的向后复制。Trilogy 客户端库 的一致性为我们提供了更多连接行为的可预测性,并让我们有信心来自主 Rails 单体的连接不会破坏向后复制。

然而,对于一些 MySQL 集群,它们的连接来自不同框架/语言的客户端,向后复制会在几小时内出现中断,这缩短了回滚的机会窗口。幸运的是,这种情况并不多,我们没有遇到在需要回滚之前就发生复制中断的情况。但对我们来说,这是一个教训,即深入了解客户端连接配置情况是有好处的,它强调了制定准则和框架来确保配置一致性的重要性。

之前 对数据进行分区 的努力得到了回报 —— 它使我们能够更有针对性地升级不同的数据域。这一点很重要,因为一个失败的查询会阻碍整个集群的升级,而对不同的工作负载进行分区使得我们能够进行渐进式升级,并减小在过程中遇到的未知风险的影响范围。

GitHub 上次升级 MySQL 版本时,我们有 5 个数据库集群,现在有 50 多个。为了成功升级,我们必须在管理集群的可观察性、工具和流程上投入。

结论

MySQL 升级只是我们必须执行的例行维护工作之一——对于我们来说,在我们的机群上运行的任何一个应用都拥有一个清晰的升级路径是至关重要的。作为升级项目的一部分,我们开发了新的流程和运维能力,成功完成了 MySQL 版本升级。然而,升级过程仍然有太多需要手动干预的地方,在未来,我们希望能够减少完成 MySQL 升级所需的工作量和时间。

我们预计,随着 GitHub 规模的增长,我们的服务器群将继续增长,我们也计划进一步对我们的数据进行分区,这也将导致我们 MySQL 集群数量的增加。为了能够在未来提升 MySQL 运维的伸缩性,将运维任务和自我修复功能自动化是至关重要的。我们相信,在可靠的服务器管理和自动化上投入将使我们能够扩展 GitHub 并跟上所需的维护,提供更可预测和更具弹性的系统。

从这个项目获得的经验为我们的 MySQL 自动化奠定了基础,并为未来的升级铺平了道路,使升级可以更高效地完成,并具备同样的安全性。

原文链接:https://github.blog/2023-12-07-upgrading-github-com-to-mysql-8-0/

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注