Uber 分布式数据库迁移

Dec 29, 2022 21:30 · 2440 words · 5 minute read Distributed Database

原文:https://www.uber.com/blog/mysql-to-myrocks-migration-in-uber-distributed-datastores/


介绍

Uber 使用 MySQL 作为 SchemalessDocstore 的底层数据库引擎。MySQL 默认使用最流行的 InnoDB 引擎,这是一种 B+ 树结构的数据存储。[MyRocks] 是另一种集成了 RocksDB 的 MySQL 存储引擎。RocksDB 基于日志结构的合并树(或 LSM 树)并且针对快速存储优化过,将出色的空间和写入效率与可接受的读取性能相结合。

Uber 存储平台团队自从 2019 年开始就已经所有 Schemaless 实例和部分 Docstore 实例迁移至 MyRocks。

动机

Schemaless/Docstore 为很多关键业务和平台服务,例如打车、外卖、零售、地图、支付等等。两者都存储几十 PB 的数据,每秒处理几千万个请求。随着 Uber 业务的增长,这些数字只会增大而且磁盘空间成为了瓶颈:有些节点的数据量达到了几 TB,这使得一些日常的运维任务变得更困难,比如数据重新同步和备份。

磁盘成为瓶颈,导致 CPU 和内存资源利用不完全,提高了运行 Schemaless 的成本。对我们来说解决这个问题并降低成本是至关重要的。

挑战

一种解决方案是拆分节点以分布磁盘空间,但是不仅耗时,而且在某些实例上还有分片限制,鉴于我们的规模和增速,不是一个长期可持续的方案。我们尝试着用 InnoDB 表压缩技术来缓解磁盘压力,这只节省了大约 5% 的磁盘空间,因为数据在被存储到数据库之前已经由查询引擎层压缩过,额外的压缩效果不大。

在寻找解决方案的同时,我们用 MyRocks 做了一个基准测试,在适当的延迟下节省了 30% 以上的磁盘空间。一个主要原因是 RocksDB 不使用固定的页面大小,因此可以实现高效的压缩,浪费最少。而且 InnaDB 使用 B+ 树索引可能会碎片化,导致占用额外的磁盘空间。

基本上 MyRocks 就是带有 RocksDB 存储引擎的 MySQL。它有相同的客户端协议,无需改动我们的查询引擎层和客户端,因此我们得出结论,MyRocks 将是支持 Uber 快速业务增长进而导致数据库容量增大的最佳选择。

如此大规模的数据和节点迁移同时保证严格的 SLA 和一致性、可用性还有透明度是很难的。这些挑战包括:

  • 详尽的自动迁移策略,不影响下游服务或活用,包括回滚计划。
  • 确保迁移后的读取性能是可以接受的。
  • 在迁移之前大规模从 MySQL 5.7 升级到 MySQL 8.0 以适应 MyRocks 的版本要求。
  • 快速解决 MyRocks 中的 bug。

升级和迁移

Schemaless/Docstore 实例的拓扑结构中数据是分片的,映射至可配置数量的物理分区。每个分区在单个区域内有多个数据副本,并通过 Raft 共识协议保证一致性。分区也会跨地域区域复制。

为确保迁移对用户透明且无影响,我们设计了一种增量迁移策略,从单个分区到多个分区,从单个区域到所有区域。在每个节点迁移期间,我们验证了数据的完整性并监控延迟和错误率以确保没有性能下降,所有这些都是自动完成的。

升级 MySQL 8.0

Uber 利用 XtraBackup 来做数据同步和备份。Xtrabackup 从 MySQL 8.0 版本开始支持 MyRocks,所以作为 MyRocks 迁移进程的第一步,我们要把集群升级到 MySQL 8.0。另一个升级动机则是因为 Docstore 计划使用 MySQL 8.0 提供的 JSON 特性。

我们只通过替换二进制就地进行了 MySQL 升级。总体步骤如下:

  1. 升级单个区域中单个分区的一个 follower
  2. 升级同一个区域中相同分区的所有 follower
  3. 升级同一个区域中相同分区的 leader
  4. 在相同区域中重复上述步骤
  5. 在另一个区域中重复上述步骤

Uber 在 2018 年用了几个月将 MySQL 从 5.6 升级到 5.7,但是从 5.7 升级到 8.0 用了一年。我们花了很多时间解决在 MySQL 8.0 中遇到的 bug 和性能下降,包括:

  • MySQL 8.0 相较于 5.7 内存占用更多
  • 由于 MySQL 优化器变更而导致不同的执行计划,例如 bug 104083
  • 如果表太多,MySQL 升级需要很长时间才能填充数据字典表
  • 线程阻塞在等待 THD::release_resources 上 PS-7525
  • InnoDB 索引数损坏

MyRocks 迁移

我们同时在已经升级到 MySQL 8.0 的节点上将 MySQL 迁移至 MyRocks。与 MySQL 就地升级不同,MyRocks 迁移策略有点像 MySQL 逻辑升级:我们向一个区域添加额外的节点,使用 Xtrabackup 并在它们上面启动迁移进程。

节点迁移包括每张表上的引擎转换,还有一系列验证来确保 MyRocks 节点提供与 InnoDB 节点相同的数据一致性和查询性能。

  1. 首先我们需要构建一个节点。我们持续使用 Xtrabackup,它在 Uber 内部已经验证过比其他方式(比如 mysqldump)快速和靠谱。
  2. 为了确保迁移过程中没有数据损坏或丢表,我们收集了每张表的校验和,还有表号。
  3. 通过 RocksDB 批量加载模式,我们使用多线程更改所有用户表至 RocksDB 引擎。
  4. 在改完所有表后,我们就像步骤 2 那样收集相同的信息,但是在 MyRocks 上。
  5. 是时候对比步骤 2 收集到的 InnoDB 和步骤 4 收集到的 MyRocks 信息了。我们看到有时将数据表变更至 RocksDB 引擎,如果 MySQL 被 OOM,MySQL 重启后有些表不见了。
  6. 分析数据表重新生成 MyRocks 的统计数据。不能保证每个执行计划在转换后仍旧是一样的,所以分析数据表可以避免这种执行计划的改变,这可能会导致 SQL 性能下降。
  7. 为了对比性能,我们在 MyRocks 节点上回放了实时的生产查询以比较结果。我们遇到了一个问题,同样的查询 InnoDB 和 RocksDB 引擎返回了不同的结果(PS-7722)。我们也重放了查询来比较执行计划和执行时间,确保性能没有下降。

如果分区中的第一个节点成功转换至 MyRocks 并且通过了全部验证,我们将继续处理同一分区中的剩余节点。

  1. 添加第二个 MyRocks 节点作为 follower
  2. 添加第三个 MyRocks follower 节点(此时分区中有 3 个 MyRocks 2 个 MySQL 节点)
  3. 将 MyRocks 提升为 leader 接管服务的写流量
  4. 一个接一个移除 MySQL 节点

我们并行地在一个区域内的所有分区重复上述步骤,在完全转换为 MyRocks 后,我们暂停了该过程并比较两个区域的性能(例如延迟、错误率、CPU/IO 使用率等等)。如果需要我们仍可以回滚并将流量切换到 MySQL 的区域。

如果 MyRocks 区域万事大吉,我们就开始对下一个区域迁移。这时我们不会再增加一个额外的 MyRocks 节点了,因为一切都已经在之前的区域中经过了验证——就用 MyRocks 替换 MySQL 以节省需要的额外容量。最后我们在一个 Schemaless/Docstore 实例上完成了 MyRocks 迁移。

总结

通过迁移至 MyRocks 我们看到磁盘空间节省了 30% 以上。然而我们从迁移中得到的结论是,并非所有使用案例都可以迁移到 MyRocks。我们观察到一些实例的 CPU 和磁盘 IO 使用率更高了,这是节省磁盘空间带来的副作用。

整个升级和迁移的耗时比最初预计的要长。其中一个原因是 MyRocks 是一个非常新的数据库,没有成熟的社区可以让我们获取支持,我们需要等待来自 Percona 的补丁,或者在遇到问题时自己研究和解决。