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