如何在Shopify进行最新的MySQL升级

292 阅读18分钟

在Shopify升级MySQL

在2021年9月初,我们退役了最后一台运行Percona Server 5.7.21的Shopify数据库虚拟机(VM),标志着我们完全切换到了5.7.32。在这篇文章中,我将分享数据库平台团队如何在Shopify进行最新的MySQL升级。我将谈论我们在回滚测试中遇到的一些障碍,我们建立的内部工具,以帮助升级和扩展我们的舰队,以及我们对未来升级的指导方针,我们希望这将对社区的其他成员有用。

为什么要升级,为什么是现在?

我们对升级特别感兴趣,因为复制的改进可以在多层复制的层次中通过事务写集来保持复制的并行性。然而,从一般意义上讲,升级我们的MySQL版本在我们的脑海中已经有一段时间了,而且随着时间的推移,随着我们的成长,这些原因变得更加重要:

  • 随着时间的推移,我们已经将更多的负载转移到我们的复制中,如果没有复制的改进,高负载可能会导致复制滞后,以及商家和买家体验不佳。
  • 由于我们的全球足迹不断增加,为了保持效率,我们的复制拓扑结构可以达到四个 "跳 "的深度,这增加了我们复制性能的重要性。
  • 如果没有复制的改进,在黑色星期五/疯狂星期一(BFCM)和闪电销售等高负荷时期,复制滞后的可能性更大,这反过来又加剧了商家在写手故障时的数据可用性风险。
  • 保持与所有软件的相关性,以获得安全和稳定补丁是行业的最佳做法。
  • 我们预计最终会升级到MySQL 8.0。构建这个小升级所需的升级工具有助于我们为此做好准备。

最后一点,作为这次升级的一部分,我们肯定想实现的一件事是--用我的同事阿克塞的话说--"让MySQL升级在Shopify成为未来的任务清单,而不是一个完整的项目。"理想的情况是,在项目结束时,我们有关于如何执行升级的文档,数据库平台团队的任何人都可以遵循,完成升级需要几周的时间,而不是几个月的时间。

Shopify的数据库基础设施

核心数据库

Shopify的核心数据库基础设施是按店铺进行水平分片的,分布在数百个分片上,每个分片由一个写手和五个或更多的副本组成。这些分片是在谷歌计算引擎虚拟机(VM)上运行,并运行MySQL的Percona Server分叉。我们的备份系统利用了谷歌云的持久性磁盘快照。当我们运行Percona Server的上游版本时,我们维护一个内部分叉和构建管道,使我们能够在必要时打补丁。

梅森

在没有自动化的情况下,由于我们的虚拟机群规模庞大,仅仅是日常的操作就需要付出非同小可的劳动。虚拟机可能会因为很多原因而瘫痪,包括GCP实时迁移失败、区域断电,或者只是普通的虚拟机故障。Mason是为了应对虚拟机故障而开发的,它通过启动一个虚拟机来替代它--这项任务更适合于机器人而不是人类,尤其是在半夜。

Mason是为我们基于虚拟机的数据库开发的自我修复服务,是在2019年底的Shopify Hack Days项目中诞生的。

愈合并不是全部需要的

Shopify的查询工作量在不同的分片之间会有很大的不同,这就需要维护巨大的不同配置。我们的最小配置是六个实例:三个实例在谷歌云的us-east1区域,三个实例在us-central1。然而,每个分片的配置可以在其他方面有所不同:

  • 可能会有额外的复制,以适应更高的读取工作负载或在全球其他地方提供复制。
  • 复制区的虚拟机可能有不同数量的核心或内存,以适应不同的工作负载。

考虑到所有这些,你可能可以想象,围绕维护这些差异而建立的自动化是多么的理想--没有它,随叫随到的任务中涉及的很大一部分手工劳作将仅仅是供应虚拟机,这不是一套令人羡慕的责任。

使用Mason来升级MySQL

在我们的规模中,升级是非常费力的,因为目前我们的虚拟机群的数量达到了数千。我们决定在Mason上建立额外的功能,以实现MySQL升级的自动化,并将其称为声明式数据库拓扑结构项目。Mason以前只是作为一个被动的工具,只维护一个硬编码的默认配置,我们设想它的下一个迭代是一个主动的工具--允许我们定义每个分片的拓扑结构,并做配置工作,使其当前状态与期望状态相一致。这样做将使我们能够自动配置升级后的虚拟机,从而消除升级大型机群所涉及的大部分工作,并为BFCM或其他高流量事件自动进行扩展配置。

项目计划

在BFCM准备工作开始之前,我们有大约8个月的时间来完成以下工作:

  • 挑选一个新版本的MySQL。
  • 对新版本进行基准测试,以发现任何倒退或错误
  • 进行回滚测试,并创建一个回滚计划,以便在必要时我们可以安全地降级。
  • 最后,执行实际的升级。

同时,我们还需要发展Mason,以:

  • 增加其稳定性
  • 从全局的硬编码配置转移到每个分片的动态配置
  • 当配置发生变化时,让它对扩容做出反应
  • 让它也关心Chef的配置
  • ...安全地完成所有这些工作。

我们必须做的第一件事是选择Percona服务器的版本。我们希望最大限度地提高我们从升级中获得的收益,同时将我们的风险降到最低。这使我们选择了Percona Server 5.7的最高次要版本,即项目开始时的5.7.32。通过这样做,我们从上次升级后的错误和安全修复中获益;用我们一位主管的话说,"那些从未发生过的事件 "是因为我们升级了。同时,我们避免了与重大版本升级相关的一些较大的风险。

一旦我们确定了一个版本,我们就对Chef进行修改,让它处理原地升级。基本上,我们用现有的配置代码创建了一个新的Chef角色,但为MySQL服务器版本变量指定了新的版本,并修改了代码,以便发生以下情况:

  1. 在安装了5.7.32的虚拟机上恢复5.7.21虚拟机的备份。
  2. 允许虚拟机和MySQL服务器进程正常启动。
  3. 检查数据目录中的mysql_upgrade_info 文件的内容。如果版本与所安装的MySQL服务器版本不同,请运行mysql_upgrade (通过一个包装脚本,这对于解释mysql_upgrade 脚本的意外行为是必要的,该脚本在不需要升级时以返回代码2退出,而不是典型的返回代码0)。
  4. 执行必要的复制配置,并继续进行其余的MySQL服务器启动工作。

在这项工作完成后,我们为配置升级版本所要做的就是指定用新的Chef角色构建新的虚拟机。

为升级做准备

从操作上来说,执行升级是很容易的部分。你可以用旧版本的备份启动一个实例,让mysql_upgrade ,让它加入现有的复制拓扑结构,可以选择从这个实例中获取新版本的备份,填充拓扑结构的其余部分,然后执行一个接管。然而,确保较新的版本以我们期望的方式执行,并且可以安全地回滚到旧版本,这才是棘手的部分。

在我们的基准测试中,我们没有发现任何性能上的异常。然而,当测试从5.7.32降级到5.7.21时,我们发现MySQL服务器不能正常启动。这就是我们在跟踪错误日志时看到的情况。

当我们允许在启动时计算瞬时统计信息到完成时,由于在我们的一些分片上有一个冗长的表分析过程,它花了一天多的时间--如果我们需要比这更紧急的回滚,这不是很好。

粗略地看了一下Percona服务器的源代码,发现innodb_index_statsinnodb_table_stats 中的table_name 列从 [VARCHAR(64)](https://github.com/percona/percona-server/blob/2a37e4ea859cd2c2de51388ecc0cbf6fb10d0ed5/scripts/mysql_system_tables.sql#L94,L104)在5.7.21中改为 [VARCHAR(199)](https://github.com/percona/percona-server/blob/56885206451/scripts/mysql_system_tables.sql#L101,L111)在5.7.32中。我们在内部Percona Server fork中修补了mysql_system_tables_fix.sql,使列的长度被设置回5.7.21的预期值,并重新测试了回滚。这一次,我们没有看到关于列长的错误,但是我们仍然看到分析表进程导致了全表重建,再次导致了不可接受的启动时间,我们清楚地看到,我们通过修复这些列长仅仅解决了问题的一个症状。

此时,在调查我们的选项时,我们想到,这个分析表过程可能发生的原因之一是,我们运行ALTER TABLE 命令作为MySQL服务器启动的一部分:我们运行一个启动脚本,将表上的AUTO_INCREMENT 值设置为最小值(这是由于auto_increment 计数器没有跨重启持续存在,这是一个长期存在的错误,已在MySQL 8.0中解决)。

调查该错误

一旦我们有了我们的假设,我们就开始测试它。这在一次小组调试会议上达到了高潮,我们小组的一些成员发现,以下步骤重现了导致全表重建的错误:

  1. 在5.7.32上:恢复了之前从5.7.21开始的备份。
  2. 在5.7.32版本中:在一个应该只是瞬时元数据变化的表上运行ALTER TABLE ,例如:ALTER TABLE t AUTO_INCREMENT=n 。该表被瞬间改变,正如预期的那样。
  3. 在5.7.32:进行了一次备份。
  4. 在5.7.21: 在上一步中从5.7.32中获取的备份被恢复了。
  5. 在5.7.21:MySQL服务器被启动,并且mysql_upgrade ,执行原地降级。
  6. 在5.7.21:执行类似于步骤1的ALTER TABLE 语句。一个完整的表重建被执行,出乎意料地和不必要地。

用GNU调试器(GDB)踏过上述步骤,我们发现了MySQL服务器源代码中的一个地方,它错误地断定索引已经以需要重建表的方式发生了变化(从Percona服务器5.7.21中的sql/sql_table.cc的has_index_def_changed函数)。

我们在GDB中检查时看到,旧版表的flags (上面的table_key->flags )与新版表的new_key->flags )不一致,尽管事实上只应用了元数据的变化。

深入挖掘,我们发现过去曾试图修复这个错误。在5.7.23版本的说明中,有如下内容。

"对于使用ALTER TABLE ,用INPLACE 算法增加InnoDB表的VARCHAR 列的长度的尝试,如果该列有索引,则尝试失败。如果索引大小超过InnoDB限制的767字节的COMPACTREDUNDANT 行格式,CREATE TABLEALTER TABLE 没有报告错误(在严格的SQL模式下)或警告(在非严格的模式下)。(Bug #26848813)"

对这个bug的修复被合并了,然而我们看到有第二次尝试来修复这个行为。在5.7.27版本说明中,我们看到。

"对于包含VARCHAR列上的索引并在MySQL 5.7.23之前创建的InnoDB表,在升级到MySQL 5.7.23或更高版本后,一些本应在原地进行的简单ALTER TABLE 语句在表重建时被执行。(Bug #29375764,Bug #94383)"

对这个bug也合并了一个修复,但它没有完全解决一些ALTER TABLE 语句的问题,这些语句应该是简单的元数据变化,而导致了整个表的重建。

我的同事Akshay 针对这个问题提交了一个bug,但是包括的补丁最终没有被MySQL团队接受。为了安全地升级过这个bug,我们仍然需要MySQL在降级时表现得合理,我们最终在我们的内部分叉中修补了Percona服务器。我们在最后的回滚测试中成功测试了我们的补丁版本,解除了我们的升级障碍。

到底什么是 "打包的钥匙"?

MyISAM存储引擎的PACK_KEYS 功能允许对键进行压缩,从而使索引更小,并提高性能。InnoDB存储引擎不支持这个功能,因为它的索引布局和期望完全不同。在MyISAM中,当索引的VARCHAR 列被扩展到8个字节以上,从而从未打包的键转换为打包的键时,它(正确地)会触发索引重建。

然而,我们可以看到,在5.7.23中第一次尝试修复这个错误时,同样类型的变化在InnoDB中触发了同样的行为,尽管打包键不被支持。为了解决这个问题,从5.7.23开始,如果存储引擎不支持,HA_PACK_KEYHA_BINARY_PACK_KEY 标志就不会被设置。

然而,这意味着如果一个表是在5.7.23之前创建的,即使在不支持它的存储引擎上也会意外地设置这些标志。因此,在升级到5.7.23或更高版本后,在InnoDB表上执行的任何仅有元数据的ALTER TABLE 命令都会错误地得出结论,认为有必要进行完全索引重建。这给我们带来了第二个修复问题的尝试,即如果存储引擎不支持,则完全删除标志。不幸的是,第二个错误修复没有考虑到标志可能已经改变的情况,但在评估早期版本的索引是否需要重建时,应该忽略这种差异,这就是我们提出的补丁中所解决的问题。在我们的补丁中,在降级过程中,如果旧版本的表(来自5.7.32)没有指定标志,但新版本的表(在5.7.21中)有,那么我们就绕过索引重建。

同时,在梅森项目中...

当所有这些回滚测试工作都在进行时,团队的另一部分人正在努力工作,在Mason中提供新的功能,让它处理升级。这些是我们指导项目工作的一些要求:

  • 创建一个 "优先 "通道--自我修复应始终优先于与扩展相关的配置请求。
  • 我们需要对扩展配置队列进行节流,以限制同时进行的工作的数量。
  • 需要使用功能标志来限制释放扩展功能的分片数量,这样我们就可以控制哪些分片被配置,并谨慎地释放新功能。
  • 为了让我们能够测试这些功能,而不立即对生产系统进行修改,有必要为扩展供应提供一个干运行模式。

所有这一切的基础是在发布新功能时的谨慎态度。由于我们的团队规模很大,我们不想冒着风险配置大量我们不需要的虚拟机或配置不正确的虚拟机,这将使我们在GCP资源使用或工程时间上花费更多的时间来停用资源。

在项目的最初阶段,稳定服务是很重要的,因为它在维护我们的MySQL拓扑结构方面起着关键作用。随着时间的推移,它已经变成了我们基础设施的一个重要组成部分,大大改善了我们的待命生活质量。早期需要完成的一些任务只是让它成为我们拥有的服务中的一流公民。我们稳定了它所部署的暂存环境,创建并改进了现有的监控,并开始使用它向Datadog发出指标,说明拓扑结构何时配置不足(在Mason未能完成其工作的情况下)。

另一个挑战是,Mason本身在我们的基础设施中与许多不同的组件对话:GCP API、Chef、Kubernetes API、ZooKeeper、Orchestrator以及数据库VM本身。预测故障情况往往是一个挑战--通常情况下,所经历的故障是全新的,在现有的测试中不会被发现。这仍然是一个持续的挑战,我们希望通过改进集成测试来解决这个问题。

后来,随着我们在项目中加入新的人员,并开始引入更多的功能,很明显,应用程序在目前的状态下是相当脆弱的;由于现有的复杂性,增加新的功能变得越来越困难,特别是当它们被同时进行时。这使我们认识到分解有可能成为硬块的工作流的重要性,并强调了一个精心设计的代码库可以减少这种情况发生的机会。

我们面临许多挑战,但最终按时交付了项目。现在,项目已经完成,我们正在花时间改进代码库,使其更容易维护和方便开发者。

升级本身

在回滚测试的过程中,我们已经在为金丝雀测试保留的几个分片上运行了几个月的5.7.32。这些分片中的一些分片定期进行负载测试,所以我们有理由相信,这与我们自己的基准测试一起,使它为我们的生产工作负载做好准备。

接下来,我们创建了一个回滚计划,以防新版本由于不可预见的原因在生产中不稳定。早期的风险缓解建议之一是在每个分片上保留一个5.7.21虚拟机,并继续从它们那里进行备份。然而,这在操作上是很复杂的,而且还需要创建更多的工具和监控,以确保我们总是有5.7.21虚拟机在每个分片上运行(当分片的数量达到数百个时,就会很麻烦了)。最终,我们决定反对这个计划,特别是考虑到我们有信心在必要时可以回滚到我们的Percona服务器补丁版本。

我们的意图是尽我们所能,通过进行广泛的回滚测试来降低升级的风险,但最终我们宁愿尽可能地向前修复。也就是说,预计回滚的选择将只作为最后的手段。

在我们的工具和回滚计划就绪后,我们于8月25日开始使用Mason认真地配置新的5.7.32虚拟机。我们决定通过创建几个批次的分片来错开升级。这使得升级后的碎片可以 "烘烤",在发生意外的情况下不会危及整个舰队。我们也不想一下子提供所有的新虚拟机,因为资源流失的数量(PB级)和压力会给谷歌云带来影响。

9月7日,最后的碎片被完成,标志着升级项目的结束。

我们从这次升级中得到了什么?

这个升级项目强调了回滚测试的重要性。如果没有我们进行的广泛测试,我们永远不会知道有一个关键的错误阻止了潜在的回滚。尽管需要用旧版本重建机群来降级是件麻烦事,也是不可取的,但5.7.21的补丁给了我们继续升级的信心,因为我们知道如果有必要,我们可以选择安全降级。

另外,随着时间的推移,我们所依赖的工具--Mason变得更加重要。在过去,Mason被认为是一个较低级别的应用程序,当它的行为出乎意料时,简单地关闭它是一个创可贴式的解决方案。当遇到错误时,修复它往往不是优先事项。然而,随着时间的推移,我们已经认识到它在减轻劳动强度和保持健康的待命期望方面发挥了多大的作用,特别是随着我们车队规模的扩大。我们通过提高测试覆盖率和重构代码库的关键部分来减少复杂性和提高可读性,对它投入了更多的时间和资源。我们也有未来的计划,以改善本地开发环境和简化其部署管道。

最后,对升级的文档和易重复性的投资,对Shopify和我们的团队来说是一个很大的胜利。当我们刚开始计划这次升级时,寻找过去的升级方式有点像寻宝游戏,需要大量的机构知识。通过制定指南和文档,我们为未来的升级铺平了道路,使其更快、更安全、更有效地完成。我们现在可以把MySQL的升级看作是使用我们现有的工具来遵循一系列的指导方针,而不是每次都要进行紧张的、手动的背景收集过程,这样做是没有任何好处的。