15 年前,GitHub 作为一个 Ruby on Rails 应用程序开始,只有一个 MySQL 数据库。从那时起,GitHub 已经发展了其 MySQL 架构,以满足平台的扩展和弹性需求,包括构建高可用性,实现测试自动化和分区数据。今天,MySQL 仍然是 GitHub 基础设施的核心部分,也是我们选择的关系数据库。
这是我们如何将 1200 多台 MySQL 主机升级到 8.0 的故事。在不影响我们的服务水平目标(SLO)的情况下升级车队并不是一个小的功能规划,测试和升级本身花了一年多的时间,并在 GitHub 内的多个团队之间进行协作。
升级动机
为什么要升级到 MySQL 8.0?随着 MySQL 5.7 的生命周期即将结束,我们将集群升级到下一个主要版本 MySQL 8.0。我们还希望 MySQL 的版本能够获得最新的安全补丁,错误修复和性能增强。我们还希望测试 8.0 中的新特性并从中受益,包括 Instant DDL、不可见索引和压缩 bin 日志等。
GitHub 的 MySQL 基础设施
在我们深入讨论如何进行升级之前,让我们先来看看我们的 MySQL 基础设施:
- 我们的集群由 1200+ 主机组成。它是 Azure 虚拟机和我们数据中心中的裸机主机的组合。
- 我们在 50 多个数据库集群中存储 300 多 TB 的数据,每秒处理 550 万次查询。
- 每个群集都配置为具有高可用性,即主群集加副本群集设置。
- 我们的数据是分区的。我们利用水平和垂直分片来扩展我们的 MySQL 集群。我们有 MySQL 集群来存储特定产品领域的数据。我们也有水平分片的 Vitess 集群,用于超过单主 MySQL 集群的大型域区域。
- 我们拥有一个庞大的工具生态系统,包括 Percona Toolkit、gh-ost、orchestrator、freno 和用于操作车队的内部自动化。
所有这些都归结为一个多样化和复杂的部署,需要在维护我们的 SLO 的同时进行升级。
准备旅程
作为 GitHub 的主要数据存储,我们对可用性有很高的要求。由于我们集群的规模和 MySQL 基础设施的关键性,我们对升级过程有一些要求:
- 我们必须能够升级每个 MySQL 数据库,同时遵守我们的服务水平目标(SLO)和服务水平协议(SLA)。
- 我们无法解释测试和确认阶段的所有失效模式。因此,为了保持在 SLO 内,我们需要能够回滚到 MySQL 5.7 的早期版本,而不会中断服务。
- 我们的 MySQL 车队有非常多样化的工作负载。为了降低风险,我们需要原子地升级每个数据库集群,并围绕其他主要更改进行调度。这意味着升级过程将是一个漫长的过程。因此,我们从一开始就知道,我们需要能够持续运行混合版本的环境。
升级的准备工作于 2022 年 7 月开始,即使在升级单个生产数据库之前,我们也有几个里程碑要达到。
准备基础架构以进行升级
我们需要为 MySQL 8.0 确定适当的默认值,并执行一些基准性能测试。由于我们需要操作两个版本的 MySQL,我们的工具和自动化需要能够处理混合版本,并了解 5.7 和 8.0 之间的新语法,不同语法或弃用语法。
确保应用程序兼容性
我们为所有使用 MySQL 的应用程序添加了 MySQL 8.0 到持续集成(CI)。我们在 CI 中并行运行 MySQL 5.7 和 8.0,以确保在长时间的升级过程中不会出现退化。我们在 CI 中检测到各种错误和不兼容性,帮助我们删除任何不支持的配置或功能,并转义任何新的保留关键字。
为了帮助应用程序开发人员过渡到 MySQL 8.0,我们还启用了一个选项,可以在 GitHub Codespaces 中选择 MySQL 8.0 预构建容器进行调试,并提供 MySQL 8.0 开发集群进行额外的预生产测试。
沟通和透明度
我们使用 GitHub Projects 创建了一个滚动日历,以便在内部沟通和跟踪我们的升级计划。我们为应用程序团队和数据库团队创建了跟踪检查表的问题模板,以协调升级。
升级计划
为了满足我们的可用性标准,我们有一个渐进的升级策略,在整个过程中允许检查点和回滚。
步骤 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 副本。
Replastrator 还配置为将 5.7 主机列为潜在故障转移候选主机的黑名单,以防止在发生计划外故障转移时意外回滚。
步骤 4:升级面向内部的实例类型
一旦我们确认集群不需要回滚并成功升级到 8.0,我们就删除了 5.7 服务器。验证包括至少一个完整的 24 小时交通周期,以确保在交通高峰期间没有问题。
回滚能力
保持升级策略安全的核心部分是保持回滚到 MySQL 5.7 之前版本的能力。对于读取副本,我们确保有足够的 5.7 副本保持在线以服务于生产流量负载,并且如果 8.0 副本的性能不佳,则通过禁用它们来启动回滚。对于主服务器,为了在不丢失数据或服务中断的情况下进行回滚,我们需要能够在 8.0 和 5.7 之间维护向后数据复制。
MySQL 支持从一个版本复制到下一个更高的版本,但不明确支持反向复制(MySQL 复制兼容性)。当我们测试将一个 8.0 主机升级到我们的临时集群上的主主机时,我们看到所有 5.7 复制副本上的复制都中断了。有几个问题我们需要克服:
- 在 MySQL 8.0 中,
utf8mb4
是默认字符集,并使用更现代的utf8mb4_0900_ai_ci
排序规则作为默认值。MySQL 5.7 的早期版本支持utf8mb4_unicode_520_ci
排序规则,但不支持 Unicodeutf8mb4_0900_ai_ci
的最新版本。 - MySQL 8.0 引入了管理权限的角色,但 MySQL 5.7 中不存在此功能。当一个 8.0 实例被提升为集群中的主实例时,我们遇到了问题。我们的配置管理正在扩展某些权限集,以包括角色语句并执行它们,这破坏了 5.7 副本中的下游复制。我们通过在升级窗口期间临时调整受影响用户的定义权限解决了此问题。
为了解决字符排序规则不兼容的问题,我们必须将默认字符编码设置为 utf8
,将排序规则设置为 utf8_unicode_ci
。
对于 GitHub.com monolith,我们的 Rails 配置确保了字符排序的一致性,并使得将客户端配置标准化到数据库变得更加容易。因此,我们非常有信心能够为我们最关键的应用程序保持向后复制。
挑战
在我们的测试、准备和升级过程中,我们遇到了一些技术挑战。
Vitess 处理
我们使用 Vitess 对关系型数据进行水平分片。在很大程度上,升级我们的 Vitess 集群与升级 MySQL 集群并无太大不同。我们已经在 CI 中运行 Vitess,因此能够验证查询兼容性。在我们针对分片集群的升级策略中,我们逐个升级每个分片。Vitess 代理层 VTgate 广告展示了 MySQL 的版本信息,某些客户端行为取决于这个版本信息。例如,某个应用程序使用了一个 Java 客户端,在 5.7 服务器上禁用了查询缓存——因为在 8.0 中移除了查询缓存,对他们来说这会产生阻塞错误。因此,一旦给定 keyspace 下的单个 MySQL 主机完成升级,我们必须确保更新 VTgate 的设置以展示 8.0 版本。
复制延迟
在我们测试的早期,我们在 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` 设置为1的副本服务器长时间在密集负载下使用,则该实例可能会用完提交顺序序列票证。超过最大值后的错误行为导致应用程序挂起,并且应用程序工作线程在提交顺序队列中无限期等待。提交顺序票证生成器现在可以正确地环绕。感谢翟伟祥的贡献。(Bug#32891221,错误#103636)
我们正好符合所有的标准击中这个错误。
- 我们使用
replica_preserve_commit_order
是因为我们使用基于 GTID 的复制。 - 我们的许多集群都长时间处于密集负载状态,当然还有所有最关键的集群。我们的大多数集群都是非常重写的。
由于这个错误已经在新版本修复,我们只需要确保我们部署的 MySQL 版本高于 8.0.28。
我们还观察到在 MySQL 8.0 中,导致复制延迟的大量写操作问题变得更加严重。这使得我们更加重视避免大量的写入突发情况。在 GitHub,我们使用 freno 根据复制延迟来限制写入工作负载。
测试 pass,生产 fail
我们知道在生产环境中第一次出现问题是不可避免的,因此我们采用了逐步升级副本的策略。我们遇到了通过 CI 的查询,但在遇到实际工作负载时会在生产中失败。最值得注意的是,我们遇到了一个问题,带有大型 WHERE IN
子句的查询会使 MySQL 崩溃。我们有一个包含数万个值的大型 WHERE IN
查询。在这些情况下,我们需要在继续升级过程之前重写查询。查询采样有助于跟踪和检测这些问题。在 GitHub 上,我们使用 SolarwindsTM(VividCortex),一个 SaaS 数据库性能监视器,用于查询可观察性。
学习和收获
在测试、性能调优和解决已发现的问题之间,整个升级过程花了一年多的时间,并涉及来自 GitHub 多个团队的工程师。我们将整个集群升级到 MySQL 8.0 GitHub 这次升级突出了我们的可观测性平台、测试计划和回滚功能的重要性。测试和逐步部署策略使我们能够及早发现问题,并降低主要升级遇到新故障模式的可能性。
虽然有一个渐进的推出策略,但我们仍然需要在每一步都回滚的能力,我们需要可观察性来识别信号,以指示何时需要回滚。启用回滚最具挑战性的方面是保持从新的 8.0 主副本到 5.7 副本的反向复制。我们了解到,Trilogy 客户端库的一致性给了我们更多的连接行为的可预测性,并让我们相信来自主 Rails 单体的连接不会中断向后复制。
然而,对于我们的一些 MySQL 集群,它们与来自不同框架/语言的多个不同客户端的连接,我们看到向后复制在几个小时内就会中断,这缩短了回滚的机会窗口。幸运的是,这些情况很少,我们没有在需要回滚之前复制中断的实例。但对我们来说,这是一个教训,即拥有已知和良好理解的客户端连接配置是有好处的。委员会强调了制定准则和框架以确保这种组合的一致性的重要性。
先前对数据进行分区的努力取得了成效 — 它使我们能够针对不同的数据领域进行更有针对性的升级。这是很重要的,因为一个失败的查询会阻塞整个集群的升级,而不同的工作负载分区允许我们分阶段升级,减少在过程中遇到的未知风险的影响范围。这里的权衡是,这也意味着我们的 MySQL 集群规模变大了。
上一次 GitHub 升级 MySQL 版本时,我们有五个数据库集群,而现在我们有 50 个以上的集群。为了成功升级,我们不得不在观测性、工具和管理集群的流程上进行投资。
结论
MySQL 升级只是我们必须执行的例行维护之一 — 对于我们运行的任何软件,拥有一个升级路径都至关重要。作为升级项目的一部分,我们开发了新的流程和操作能力,以成功完成 MySQL 版本的升级。然而,在升级过程中仍存在许多需要手动干预的步骤,我们希望减少完成未来 MySQL 升级所需的工作量和时间。
我们预计随着 GitHub.com 的增长,我们的集群规模也将继续增长,并且我们有将数据进一步分区的目标,这将随着时间推移增加我们的 MySQL 集群数量。为运营任务构建自动化和自愈能力可以帮助我们未来扩展 MySQL 运营。我们相信,投资可靠的集群管理和自动化将使我们能够扩展 GitHub,并跟上所需的维护,从而提供更可预测和更具弹性的系统。
这个项目的经验为我们的 MySQL 自动化奠定了基础,为未来更高效地进行升级铺平了道路,但依然保持了同样的关怀和安全性水平。