李淳竹(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 时部分导出后从断点继续导出。
\
联 系:channel #sig-migrate in the tidbcommunity slack workspace, you can join this channel through this invitation link。