GitHub分享了他们将自己1200 节点、300 TB数据存储的MySQL集群从5.7升级至8.0的故事
官方文献: https://github.blog/2023-12-07-upgrading-github-com-to-mysql-8-0/
全文译:
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 版本。此外,我们还希望测试 8.0 中的新功能并从中受益,包括即时 DDL、隐形索引和压缩的 bin 日志等。
GitHub 的 MySQL 基础架构
在深入探讨如何进行升级之前,让我们先从 10,000 英尺的高度来看看我们的 MySQL 基础架构:
- 我们的机群由 1200 多台主机组成。这是 Azure 虚拟机和数据中心裸机主机的组合。
- 我们在 50 多个数据库集群中存储 300 多 TB 的数据,每秒提供 550 万次查询。
- 每个集群都采用主集群加副本集群的高可用性配置。
- 我们的数据是分区的。我们利用水平和垂直分片来扩展 MySQL 集群。我们有存储特定产品领域数据的 MySQL 集群。我们还有水平分片的 Vitess 集群,用于存储超出单主 MySQL 集群的大型领域数据。
- 我们拥有一个庞大的工具生态系统,包括 Percona Toolkit、gh-ost、orchestrator、freno 以及用于运营机群的内部自动化工具。
所有这一切构成了一个多样而复杂的部署,需要在保持 SLO 的同时进行升级。
准备旅程
作为 GitHub 的主要数据存储,我们对可用性要求很高。由于我们团队的规模和 MySQL 基础设施的重要性,我们对升级过程有一些要求:
- 我们必须能够在遵守服务级别目标(SLO)和服务级别协议(SLA)的前提下升级每个 MySQL 数据库。
- 我们无法在测试和验证阶段考虑到所有故障模式。因此,为了保持在 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 项目创建了一个滚动日历,以便在内部沟通和跟踪升级计划。我们创建了问题模板,跟踪应用程序团队和数据库团队协调升级的清单。
升级计划
为了达到可用性标准,我们采取了渐进式升级策略,在整个过程中允许检查点和回滚。
步骤 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 主机列入潜在故障切换候选黑名单,以防止意外故障切换时出现意外回滚。
步骤 4:面向内部的实例类型升级
我们还有用于备份或非生产工作负载的辅助服务器。为了保持一致性,我们随后对这些服务器进行了升级。
第 5 步:清理
确认群集无需回滚并成功升级到 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 整理,但不支持最新版本的 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,因此能够验证查询的兼容性。在分片集群的升级策略中,我们一次升级一个分片。Vitess 代理层 VTgate 会公布 MySQL 的版本,某些客户端行为依赖于该版本信息。例如,一个应用程序使用的 Java 客户端禁用了 5.7 服务器的查询缓存--因为查询缓存在 8.0 中被移除,所以会产生阻塞错误。因此,一旦给定键空间的单台 MySQL 主机升级,我们就必须确保同时更新 VTgate 设置以宣传 8.0。
复制延迟
我们使用读取复制来扩展我们的读取可用性。 GitHub.com 要求低复制延迟,以便提供最新数据。
在测试的早期阶段,我们遇到了 MySQL 中的一个复制错误,该错误已在 8.0.28 中得到修补:
我们碰巧满足了击中这个错误的所有标准。
- 我们使用 replica_preserve_commit_order,因为我们使用基于 GTID 的复制。
- 我们的许多集群,当然还有所有最关键的集群,都长期处于高强度负载状态。我们的大多数集群写入量都很大。
由于该漏洞已在上游得到修补,我们只需确保部署的 MySQL 版本高于 8.0.28。
我们还观察到,导致复制延迟的大容量写入在 MySQL 8.0 中更加严重。因此,避免大量写入变得更加重要。在 GitHub,我们使用 freno 根据复制延迟来控制写入工作量。
查询会通过 CI,但在生产环境中会失败
我们知道在生产环境中难免会首次出现问题,因此我们采取了升级副本的渐进式推广策略。我们遇到过通过 CI 的查询,但在生产环境中遇到实际工作负载时却会失败。最值得注意的是,我们遇到了一个问题,即带有大型 WHERE IN 子句的查询会导致 MySQL 崩溃。我们的大型 WHERE IN 查询包含数以万计的值。在这种情况下,我们需要在继续升级之前重写查询。查询采样有助于跟踪和检测这些问题。在 GitHub,我们使用 SaaS 数据库性能监控器 Solarwinds DPM (VividCortex) 进行查询观察。
经验教训
在测试、性能调整和解决发现的问题之间,整个升级过程耗时一年多,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 自动化奠定了基础,并将为未来更高效地完成升级铺平道路,但仍需保持同样的谨慎和安全。