李淳竹(lichunzhu),TiDB 研发工程师
SIG 组:Migrate SIG Community,主要涵盖 TiDB 数据处理工具,包含 TiDB 数据备份/导入导出,TiDB 数据变更捕获,其他数据库数据迁移至 TiDB 等
前言
Dumpling 是由 Go 语言编写的用于对数据库进行数据导出的工具。目前支持 MySQL 协议的数据库,并且针对 TiDB 的特性进行了优化。Go Dumpling! 让导出数据更稳定文章对 Dumpling 进阶使用进行了介绍。本文接下来将会介绍 Dumpling 内部表内并发的优化逻辑,从而帮助大家更深刻地理解 Dumpling 工作原理。
为什么需要表内并发
Dumpling 内部的导出逻辑可以用生产消费者模型进行诠释。生产者线程会遍历待导出数据库表集合,再会将生成好的导出 SQL 发送给消费者线程,由消费者线程将 SQL 执行结果格式化后写入文件。不难看出,不同消费者间可以互不干扰地进行并发导出。
由上文较容易推导的是,待导出的数据表彼此并无联系,可以由不同消费者并发导出。但大部分业务场景中,表和表之间的数据量差异巨大,很容易会出现线程空转在一张大表的情况。因此需要将大表划分为更小的“导出单元”(后文将简称为 chunk )以便于消费者线程并行导出,从而提升导出速度。chunk 划分也应该保证尽可能均匀,不均匀的 chunk 划分与大表小表并发导出的问题类似,会使得导出时间加倍,并极大提升数据库服务器内存使用。
导出 MySQL 时的表内并发
那么如何将大表划分为更小且较为均匀的 chunk 呢?可以想到,相比于其他类型,整型数字可以较为均匀地划分为多个 limit 范围,是个最为理想的划分方式。同时,为了保证划分的整数范围能够命中索引,避免重复扫全表从而浪费计算资源,使用的划分范围应该为索引的第一列。由此可以得到针对 MySQL 的表内并发划分方式:
首先选取第一列为整数的索引列记为 field,按照主键、唯一索引、具有最大 Cardinality 的索引的顺序进行选取,从而保证该列整型数据尽量不同。选择好整数列后,Dumpling 通过 explain 语句粗略估算该表在限定条件下会导出的数据行数并记为 count。根据开头指定了划分行数大小的参数 rows,可以得到 Dumpling 需要将数据划分为 count/rows 个 chunk。随后通过 select min(field), max(field) 的方式得出在限定条件下的数据中的最大最小 field 记为 max_field 与 min_field。假设在这个范围内数据是呈现大体均匀分布的,则可以求出划分步长为 d=(max_field-min_field)*rows/count。各个表内并发 chunk 通过 where 条件约束,范围分别为 [min_field, min_field+d), [min_field+d, min_field+2d) …
从上述实现可以看出指定 rows 后划分 chunk 并不一定为 rows 行。同时,调大 rows 将直接增大各个 chunk 的步长范围即增大各个 chunk 的数据量。因此,如果发现 Dumpling 导出时对数据库内存消耗过大时,可以适当调小 rows 从而减小各个 chunk 的数据量。在实际导出场景中,rows 设置应较为适中:过大会消耗过多内存,且容易使并发效果不好;过小则容易导致 Dumpling 频繁向数据库请求少量数据,使导出速度下降。在目前的实践场景中,配置 --rows=200000 一般能够兼顾并发效果与导出速度。
导出 TiDB v3.0/v4.0 时的表内并发
从上文可以看出,当用户表不存在分布均匀的整数索引,或者 explain 语句获取数据行数的结果不准确时,表内并发效果将大打折扣。那么,TiDB 和 Dumpling 会怎么处理这一问题呢?在 TiDB 数据库如何计算一文中,提到了 TiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。该 RowID 表内唯一且可以通过 select _tidb_rowid 的方式直接从数据库中获取。因此,简单的思路是直接将 _tidb_rowid 当作上文中的整型主键,采用相同的方式进行 chunk 划分即可。
然而,在 TiDB 高并发写入场景最佳实践中提到,为了避免 TiDB 写入热点,TiDB 表时常会使用 AUTO_RANDOM 列或在建表时加入 SHARD_ROW_ID_BITS 参数。这些参数会使得 _tidb_rowid 列分布极其不均匀,从而导致 Dumpling 导出表内并发划分 chunk 时划分不准确形成大 chunk,影响导出速度甚至引发 OOM。
在 TiDB 数据库的存储中,可以得到 TiDB 的数据映射为 KV 键值对后,以 range region 的形式存储在 TiKV 上,每个 region 保存了 [StartKey,EndKey) 范围的数据且 TiKV 会尽量保持每个 Region 中保存的数据不超过一定的大小。这些特性非常有利于 Dumpling 划分均匀的 chunk 数据。因此,Dumpling 通过 TiDB 的 INFORMATION_SCHEMA 库下的 TIKV_REGION_STATUS 表获取导出目标表所有 Region 的 StartKey,解码出所需要的 row_id,再使用得到 rowid 作为 WHERE 条件划分出 chunk。
从上述实现中可以看出 Dumpling 的表内并发的划分尺度为 region 大小,rows 的具体值已经不对划分结果产生影响。但是 rows 值设置与否仍将决定 Dumpling 是否采取表内并发的方式导出 TiDB 数据库。
导出 TiDB v5.0 时的表内并发
TiDB v5.0.0 开始支持了聚簇索引来避免 TiDB 此前使用 rowid 时的回表操作,提升写入查询速度。开启聚簇索引的表将不再有 _tidb_rowid 列。同时,在 split region 等特定场景下,region 的 StartKey 也不一定为合法值。但上文按 region 划分的思路仍然是行之有效的方法,然而需要更好的获取 region 边界划分数据的方法。
为了解决这一问题,TiDB 在 v5.0.0 及以上版本支持了 SELECT fields FROM table TABLESAMPLE REGIONS() 语法。执行该 SQL 后,TiKV 会扫描出表涉及到的每个 region 并获取第一个合法 kv 对,再将得到的数据返回给 Dumpling。例如使用该 SQL SELECT 聚簇索引的各个列时,该 SQL 会返回该表每个 REGION 中第一行聚簇索引的各列值用于均匀划分 chunk。
Dumpling 后续开发计划
以下为 Dumpling 后续开发的一些计划与设想。目前 Dumpling 已经迁移到 tidb repo,欢迎大家在 Dumpling Repo 一起交流讨论,参与开发。
- 支持导出更多种类的源数据库(issue#11)
一般来说,只要需要支持的数据库有对应的 database driver 或 client,比如 Oracle 数据库的 golang driver godror,都可以轻微改造导出语句和调用的 Go 代码库后就实现该数据库的导出支持。这里也欢迎社区的小伙伴们参与,帮助 Dumpling 支持导出更多类型的数据库。
- 支持导出 Sequence(issue#61)
Dumpling 目前不支持导出 TiDB Sequence,支持该功能将使导出功能更完整。
Dumpling 需要支持 checksum[4] [5] 校验来保证导出数据的正确性。
支持 Dumpling 使用 snapshot 模式导出 TiDB 时部分导出后从断点继续导出。