原文标题:Upgrading GitHub.com to MySQL 8.0
原文链接:https://github.blog/2023-12-07-upgrading-github-com-to-mysql-8-0/
原文作者:Jiaqi Liu, Daniel Rogart, Xin Wu
译者:Shawn Yan
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 虚拟机和裸机主机的组合。
- 我们存储超过 300 TB 的数据,并在 50 多个数据库集群中每秒处理 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 8.0 添加到了所有使用 MySQL 的应用程序的持续集成 (CI) 中。 我们在 CI 中并行运行 MySQL 5.7 和 8.0,以确保在长时间的升级过程中不会出现回归。 我们检测到 CI 中的各种错误和不兼容性,帮助我们删除任何不受支持的配置或功能,并转义任何新的保留关键字。
为了帮助应用程序开发人员过渡到 MySQL 8.0,我们还启用了一个选项,可以在 GitHub Codespaces 中选择 MySQL 8.0 预构建容器进行调试,并提供 MySQL 8.0 开发集群以进行额外的预生产测试。
沟通和透明度
我们使用 GitHub Projects 创建滚动日历来在内部沟通和跟踪我们的升级计划。 我们创建了问题模板来跟踪应用程序团队和数据库团队的清单以协调升级。
Project Board for tracking the MySQL 8.0 upgrade schedule
升级计划
为了满足我们的可用性标准,我们采取了逐步升级策略,允许在整个过程中进行检查点和回滚。
步骤 1:滚动副本升级
我们首先升级单个副本并在其仍处于离线状态时进行监控,以确保基本功能稳定。 然后,我们启用生产流量并继续监控查询延迟、系统指标和应用程序指标。 我们逐渐将8.0副本上线,直到升级整个数据中心,然后迭代其他数据中心。 我们在线保留了足够的 5.7 副本以便回滚,但我们禁用了生产流量以开始通过 8.0 服务器提供所有读取流量。
The replica upgrade strategy involved gradual rollouts in each data center (DC).
步骤 2:更新复制拓扑
一旦所有只读流量都通过 8.0 副本提供服务,我们就调整了复制拓扑,如下所示:
- 8.0 主候选配置为直接在当前 5.7 主候选下复制。
- 在该 8.0 副本的下游创建了两个复制链:
- 一组仅 5.7 个副本(不提供流量,但已准备好以防回滚)。
- 一组仅 8.0 副本(服务流量)。
- 拓扑仅在很短的时间内(最多几小时)处于这种状态,直到我们进入下一步。
To facilitate the upgrade, the topology was updated to have two replication chains.
步骤 3:将 MySQL 8.0 主机提升为主节点
我们选择不在主数据库主机上进行直接升级。 相反,我们将通过使用 Orchestrator 执行的优雅故障转移将 MySQL 8.0 副本提升为主副本。 此时,复制拓扑由一个 8.0 主数据库和两个附加到其上的复制链组成:一组用于回滚的脱机 5.7 副本和一组服务 8.0 副本。
Orchestrator 还配置为将 5.7 主机列入黑名单作为潜在的故障转移候选者,以防止在发生计划外故障转移时意外回滚。
Primary failover and additional steps to finalize MySQL 8.0 upgrade for a database
步骤 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,因此我们能够验证查询兼容性。 在我们的分片集群升级策略中,我们一次升级一个分片。 VTgate(Vitess 代理层)通告 MySQL 的版本,某些客户端行为取决于此版本信息。 例如,一个应用程序使用的 Java 客户端禁用了 5.7 服务器的查询缓存,因为查询缓存在 8.0 中被删除,因此它会为它们生成阻塞错误。 因此,一旦单个 MySQL 主机针对给定的键空间进行了升级,我们就必须确保我们还更新了 VTgate 设置以宣传 8.0。
复制延迟
我们使用只读副本来扩展读取可用性。 GitHub.com 需要低复制延迟才能提供最新数据。
在我们的早期测试中,我们遇到了 MySQL 中的复制错误,该错误已在 8.0.28 上修补:
复制:如果设置了系统变量replica_preserve_commit_order = 1 的副本服务器在密集负载下长时间使用,则实例可能会耗尽提交顺序票证。 超过最大值后的不正确行为会导致应用程序挂起,并且应用程序工作线程无限期地等待提交顺序队列。 提交顺序票证生成器现在可以正确回绕。 感谢翟伟祥的贡献。 (错误#32891221,错误#103636)
我们碰巧满足了解决此错误的所有标准。
- 我们使用replica_preserve_commit_order,因为我们使用基于GTID的复制。
- 我们的许多集群,当然还有所有最关键的集群,在很长一段时间内都承受着密集的负载。 我们的大多数集群的写入量都很大。
由于这个错误已经在上游修复,我们只需要确保部署高于 8.0.28 的 MySQL 版本即可。
我们还观察到,导致复制延迟的大量写入在 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 版本时,我们有 5 个数据库集群,现在我们有 50 多个集群。 为了成功升级,我们必须投资于可观察性、工具和管理车队的流程。
结论
MySQL 升级只是我们必须执行的一种日常维护 - 对于我们在机群上运行的任何软件来说,拥有一个升级路径至关重要。 作为升级项目的一部分,我们开发了新的流程和操作能力来成功完成MySQL版本升级。 然而,我们在升级过程中仍然有太多需要手动干预的步骤,我们希望减少完成未来 MySQL 升级所需的工作量和时间。
我们预计,随着 GitHub.com 的发展,我们的集群将继续增长,并且我们的目标是进一步分区数据,这将随着时间的推移增加 MySQL 集群的数量。 构建操作任务的自动化和自我修复功能可以帮助我们在未来扩展 MySQL 操作。 我们相信,投资于可靠的车队管理和自动化将使我们能够扩展 github 并跟上所需的维护,从而提供更具可预测性和弹性的系统。
该项目的经验教训为我们的 MySQL 自动化奠定了基础,并将为未来更高效地进行升级铺平道路,但仍保持相同水平的维护和安全。