MySQL 是一种由 Oracle 开发的开源数据库,为 Facebook 的一些最重要的工作负载提供支持。我们积极开发 MySQL 中的新功能,以支持我们不断发展的需求。这些特性改变了 MySQL 的许多不同领域,包括客户端连接器、存储引擎、优化器和复制。 MySQL 的每个新主要版本都需要花费大量的时间和精力来迁移我们的工作负载。挑战包括: 我们上次升级到 MySQL 5.6 的主要版本花了一年多的时间才推出。当 5.7 版发布时,我们仍在开发 5.6 版上的 LSM-Tree 存储引擎 MyRocks。由于在构建新存储引擎的同时升级到 5.7 会显着减缓 MyRocks 的进度,因此我们选择继续使用 5.6,直到 MyRocks 完成。 MySQL 8.0 是在我们完成将 MyRocks 部署到我们的用户数据库 (UDB) 服务层时宣布的。该版本包括引人注目的功能,如基于写集的并行复制和提供原子 DDL 支持的事务数据字典。对我们来说,迁移到 8.0 还会带来我们错过的 5.7 功能,包括文档存储。 5.6 版即将结束,我们希望在 MySQL 社区中保持活跃,尤其是我们在 MyRocks 存储引擎上的工作。 8.0 中的增强功能,如即时 DDL,可以加速 MyRocks 架构更改,但我们需要在 8.0 代码库上使用它。考虑到代码更新的好处,我们决定迁移到 8.0。我们正在分享我们如何处理 8.0 迁移项目 - 以及我们在此过程中发现的一些惊喜。当我们最初确定项目的范围时,很明显迁移到 8.0 比迁移到 5.6 或 MyRocks 更加困难。当时,我们定制的 5.6 分支有超过 1,700 个代码补丁可以移植到 8.0。在我们移植这些更改时,新的 Facebook MySQL 功能和修复被添加到 5.6 代码库中,从而将目标移得更远。我们有许多 MySQL 服务器在生产中运行,为大量不同的应用程序提供服务。我们还拥有用于管理 MySQL 实例的广泛软件基础设施。这些应用程序执行诸如收集统计数据和管理服务器备份之类的操作。从 5.6 升级到 8.0 完全跳过了 5.7。在 5.6 中处于活动状态的 API 将在 5.7 中被弃用,并可能在 8.0 中被删除,这要求我们使用现已删除的 API 更新任何应用程序。许多 Facebook 功能与 8.0 中的类似功能不向前兼容,需要弃用和向前迁移。
我们首先设置了 8.0 分支,用于在我们的开发环境中进行构建和测试。然后,我们开始了从 5.6 分支移植补丁的漫长旅程。我们开始时有 1,700 多个补丁,但我们能够将它们分为几个主要类别。我们的大多数自定义代码都有很好的注释和描述,因此我们可以轻松确定应用程序是否仍然需要它,或者是否可以删除它。由特殊关键字或唯一变量名称启用的功能也使确定相关性变得容易,因为我们可以搜索我们的应用程序代码库以找到它们的用例。一些补丁非常模糊,需要侦探工作——挖掘旧的设计文档、帖子和/或代码审查评论——以了解它们的历史。删除:不再使用的功能或在 8.0 中具有等效功能的功能不需要移植。构建/客户端:移植了支持我们的构建环境和修改过的 MySQL 工具(如 mysqlbinlog)或添加的功能(如异步客户端 API)的非服务器功能。非 MyRocks 服务器:移植了 mysqld 服务器中与我们的 MyRocks 存储引擎无关的功能。我们使用电子表格跟踪每个补丁的状态和相关历史信息,并在删除补丁时记录我们的推理。更新相同功能的多个补丁被组合在一起进行移植。移植并提交到 8.0 分支的补丁使用 5.6 提交信息进行了注释。由于我们需要筛选大量补丁,这些注释帮助我们解决了这些问题,因此不可避免地会出现移植状态的差异。每个客户端和服务器类别自然而然地成为软件发布的里程碑。移植所有与客户端相关的更改后,我们能够将客户端工具和连接器代码更新到 8.0。一旦移植了所有非 MyRocks 服务器功能,我们就能够为 InnoDB 服务器部署 8.0 mysqld。完成 MyRocks 服务器功能使我们能够更新 MyRocks 安装。一些最复杂的功能需要对 8.0 进行重大更改,并且一些领域存在重大兼容性问题。例如,上游 8.0 binlog 事件格式与我们的一些自定义 5.6 修改不兼容。 Facebook 5.6 功能使用的错误代码与上游 8.0 分配给新功能的错误代码相冲突。我们最终需要修补我们的 5.6 服务器以与 8.0 向前兼容。
完成所有这些功能的移植花了几年时间。到最后,我们已经评估了 2,300 多个补丁并将其中的 1,500 个移植到 8.0。我们将多个 mysqld 实例组合成一个 MySQL 副本集。副本集中的每个实例都包含相同的数据,但在地理上分布到不同的数据中心,以提供数据可用性和故障转移支持。每个副本集有一个主实例。其余实例都是辅助实例。主节点处理所有写入流量并将数据异步复制到所有辅助节点。我们从包含 5.6 个主/5.6 个辅助的副本集开始,最终目标是包含 8.0 个主/8.0 个辅助的副本集。我们遵循了一个类似于 UDB MyRocks 迁移计划的计划。对于每个副本集,使用 mysqldump 通过逻辑副本创建和添加 8.0 次要副本。这些辅助节点不提供任何应用程序读取流量。每个副本集都可以独立地过渡上述每个步骤,并根据需要停留在一个步骤上。我们将副本集分成更小的组,并在每次转换中进行引导。如果我们发现问题,我们可以回滚到上一步。在某些情况下,副本集能够在其他步骤开始之前到达最后一步。为了自动化大量副本集的转换,我们需要构建新的软件基础设施。我们可以将副本集分组在一起,并通过简单地更改配置文件中的一行来将它们移动到每个阶段。任何遇到问题的副本集都可以单独回滚。作为 8.0 迁移工作的一部分,我们决定标准化使用基于行的复制 (RBR)。一些 8.0 功能需要 RBR,它简化了我们的 MyRocks 移植工作。虽然我们的大多数 MySQL 副本集已经在使用 RBR,但那些仍在运行基于语句的复制 (SBR) 的副本无法轻松转换。这些副本集通常有没有任何高基数键的表。完全切换到 RBR 是一个目标,但添加主键所需的长尾工作的优先级通常低于其他项目。
因此,我们将 RBR 作为 8.0 的要求。在评估并为每个表添加主键后,我们切换了今年最后一个 SBR 副本集。使用 RBR 还为我们提供了一种替代解决方案,用于解决我们在将一些副本集移动到 8.0 主版本时遇到的应用程序问题,稍后将对此进行讨论。大多数 8.0 迁移过程涉及使用我们的自动化基础设施和应用程序查询测试和验证 mysqld 服务器。随着我们的 MySQL 机群的增长,我们用来管理服务器的自动化基础设施也在增长。为了确保我们所有的 MySQL 自动化都与 8.0 版本兼容,我们投资构建了一个测试环境,该环境利用测试副本集和虚拟机来验证行为。我们编写了集成测试,对每个自动化部分进行金丝雀测试,以便在 5.6 版本和 8.0 版本上运行并验证它们的正确性。在进行此练习时,我们发现了几个错误和行为差异。由于每个 MySQL 基础设施都针对我们的 8.0 服务器进行了验证,我们发现并修复了(或解决了)许多有趣的问题:从错误日志、mysqldump 输出或服务器显示命令解析文本输出的软件很容易损坏。服务器输出的细微变化通常会揭示工具解析逻辑中的错误。 8.0 的默认 utf8mb4 排序规则设置导致我们的 5.6 和 8.0 实例之间的排序规则不匹配。 8.0 表可能会使用新的 utf8mb4_0900 排序规则,即使对于 5.6 的 show create table 生成的 create 语句,因为使用 utf8mb4_general_ci 的 5.6 模式没有明确指定排序规则。这些表差异通常会导致复制和模式验证工具出现问题。某些复制失败的错误代码发生了变化,我们必须修复我们的自动化以正确处理它们。
8.0 版本的数据字典废弃了表 .frm 文件,但我们的一些自动化使用它们来检测表架构修改。我们希望应用程序的转换尽可能透明,但一些应用程序查询会出现性能下降或在 8.0 上会失败。对于 MyRocks 迁移,我们构建了一个 MySQL 影子测试框架,用于捕获生产流量并将其重播到测试实例。对于每个应用程序工作负载,我们在 8.0 上构建测试实例并向它们重放影子流量查询。我们捕获并记录了从 8.0 服务器返回的错误,并发现了一些有趣的问题。不幸的是,并非所有这些问题都在测试过程中被发现。例如,在迁移过程中应用程序发现了事务死锁。在我们研究不同的解决方案时,我们能够暂时将这些应用程序回滚到 5.6。 8.0 中引入了新的保留关键字,其中一些与应用程序查询中使用的流行表列名称和别名相冲突,例如组和排名。这些查询没有通过反引号对名称进行转义,从而导致解析错误。使用自动转义查询中列名的软件库的应用程序没有遇到这些问题,但并非所有应用程序都使用它们。解决这个问题很简单,但追踪应用程序所有者和生成这些查询的代码库需要时间。一些应用程序在 InnoDB 上的重复键查询上遇到了涉及 insert … 的可重复读取事务死锁。 5.6 有一个错误,在 8.0 中得到纠正,但修复增加了事务死锁的可能性。在分析了我们的查询之后,我们能够通过降低隔离级别来解决它们。由于我们已切换到基于行的复制,因此我们可以使用此选项。我们的自定义 5.6 文档存储和 JSON 函数与 8.0 不兼容。使用文档存储的应用程序需要将文档类型转换为文本以进行迁移。对于 JSON 函数,我们向 8.0 服务器添加了 5.6 兼容版本,以便应用程序可以在以后迁移到 8.0 API。我们对 8.0 服务器的查询和性能测试发现了一些需要立即解决的问题。
我们在 ACL 缓存周围发现了新的互斥量争用热点。当同时打开大量连接时,它们都可以阻止检查 ACL。当存在许多 binlog 文件且高 binlog 写入速率频繁轮换文件时,binlog 索引访问也会出现类似的争用。几个涉及临时表的查询被破坏。查询将返回意外错误或运行时间过长而超时。内存使用与 5.6 相比有所增加,特别是对于我们的 MyRocks 实例,因为必须加载 8.0 中的 InnoDB。默认的 performance_schema 设置启用了所有仪器并消耗了大量内存。我们通过仅启用少量仪器并更改代码以禁用无法手动关闭的表来限制内存使用。但是,并非所有增加的内存都由 performance_schema 分配。我们需要检查和修改各种 InnoDB 内部数据结构,以进一步减少内存占用。这一努力将 8.0 的内存使用率降低到可接受的水平。到目前为止,8.0 迁移已经花费了几年时间。我们已将许多 InnoDB 副本集转换为完全在 8.0 上运行。其余的大部分都处于迁移路径的不同阶段。现在我们的大部分自定义功能都已移植到 8.0,更新到 Oracle 的次要版本相对容易,我们计划与最新版本保持同步。首先,我们无法就地升级服务器,需要使用逻辑转储和还原来构建新服务器。但是,对于非常大的 mysqld 实例,这在实时生产服务器上可能需要很多天,而且这个脆弱的过程可能会在完成之前被中断。对于这些大型实例,我们不得不修改我们的备份和恢复系统来处理重建。其次,检测 API 更改要困难得多,因为 5.7 可以向我们的应用程序客户端提供弃用警告以修复潜在问题。相反,我们需要运行额外的影子测试来发现故障,然后才能迁移生产工作负载。使用自动转义架构对象名称的 mysql 客户端软件有助于减少兼容性问题的数量。
在一个副本集中支持两个主要版本是很困难的。一旦副本集将其主实例提升为 8.0 实例,最好尽快禁用并删除 5.6 实例。应用程序用户往往会发现仅 8.0 支持的新功能,例如 utf8mb4_0900 排序规则,使用这些功能可能会中断 8.0 和 5.6 实例之间的复制流。尽管在我们的迁移过程中遇到了所有障碍,但我们已经看到了运行 8.0 的好处。一些应用程序选择提前转换到 8.0,以利用文档存储和改进的日期时间支持等功能。我们一直在考虑如何在 MyRocks 上支持 Instant DDL 等存储引擎功能。总的来说,新版本极大地扩展了我们可以用 MySQL @ Facebook 做的事情。