为了使 bulk load 导入模式能够达到理想性能,最大化利用系统资源,我们将一些在实践过程中积累的调优经验总结罗列如下,供参考。
按序导入
按序导入是指数据按照主键从小到大的顺序去导入,数据库可以更高效地处理有序的数据。相对于乱序导入,按序导入性能提高20%~300%。
这里的按序,分为两个层面的有序。
bulk load 事务内部有序:在一个 bulk load 事务内部,数据需要按照主键顺序排列。
bulk load 事务之间可排序:将一个事务写入数据的最小主键和最大主键作为这个事务的区间,如果事务的区间互相没有重叠,则认为事务之间是可排序的。换句话说,将两个事务写入的数据,按照主键从小到大的顺序进行排序,不存在相互交叉的情况,则这两个事务是可排序的。
当以上两个有序的条件同时满足时,认为数据导入是完全有序的,性能最佳。
如果第一条不满足,即事务内部的一条条记录还没有按照主键顺序排好序,那么在导入之前,必须将参数 tdsql_bulk_load_allow_unsorted 打开,否则会失败报错。当 tdsql_bulk_load_allow_unsorted 打开后,每个事务中的数据会先暂存在临时文件中,在提交阶段对其进行外部归并排序,最后再按序输出到外部 SST 文件中。如果事务内部数据有序,则不需要该步骤,直接写到外部 SST 文件。因此事务内部无序要比有序消耗更多的 CPU 和 I/O 资源。
如果第二条不满足,那么事务和事务之间生成的外部 SST 文件会有区间重叠,当插入到 LSM-tree 时,会因为相互重叠导致在下层没有合适的插入位置,导致更多的 SST 文件被最终插入到最上层(level-0),会触发向下层的 compaction 操作,在 bulk load 数据导入期间,消耗一部分 I/O 资源,影响整体性能。
并发导入
当磁盘性能较好时(NVMe 磁盘),bulk load 性能瓶颈点很可能在 CPU 而不在 I/O,可以尝试适当增加导入线程的并发数(50~100)来提升导入效率。
系统支持通过
tdstore_bulk_load_sorted_data_sst_compression、tdstore_bulk_load_unsorted_data_sst_compression、tdstore_bulk_load_temp_file_compression参数(需要超级管理员权限)调整 bulk load 导入过程中产生的外部 SST 文件和待排序临时数据文件所使用的压缩算法, 来平衡 CPU 和 I/O、磁盘空间。SET GLOBAL tdstore_bulk_load_sorted_data_sst_compression=zstd,snappy,lz4,auto,nocompression,...;
控制事务大小
与普通事务先把未提交数据暂存在内存的 write batch 不同,bulk load 事务会把未提交数据直接写到外部 SST 文件。因此大事务不会导致内存占用过多 OOM。
与之相反,bulk load 事务不宜过小,否则一方面会产生较小的 SST 文件,另一方面频繁的 bulk load 事务提交导致导入性能也不会好。
通常一个 bulk load 事务的数据量在200M以上,会达到比较好的性能。注意,外部 SST 文件默认是开启压缩算法的,以 ZSTD 为例,与原始数据相比,压缩率可能在10倍甚至更多,因此可以考虑让一个 bulk load 事务写入的原始数据量在2GB以上。
事务自动攒批提交
对于性能要求更高的大规模数据迁移导入,且迁移阶段无读请求的使用场景,也可以考虑开启 bulk load 事务自动攒批提交,
SET GLOBAL tdsql_bulk_load_allow_auto_organize_txn = ON; 由数据库自行控制 bulk load 事务数据到合适的规模后发起自动提交,从而维持较高的导入性能。但需注意,该选项开启后,当客户端返回事务提交成功请求时,对应的数据可能还在攒批,未达到自动提交的数据量阈值,因此可能不可见。建议导入完毕后进行行数校验。二级索引
对于表上存在二级索引的场景,表上的二级索引越多,bulk load 性能会变差。这是因为二级索引数据一定是无序的,需要进行额外的排序操作,从而增加了导入过程中的计算开销。针对存在二级索引的场景,优化建议适当增大 bulk load 事务的大小。
在21.x或之后的版本,更为推荐的方式是:用 bulk load 导数据时,考虑建表时先不建立二级索引。待导入完成后,再创建二级索引,创建二级索引时开启 fast online DDL 优化(需要手动开启),整体性能上会更好。
分区表
从计算层看,每个分区表会分配一个 unique t_index_id。实际处理逻辑跟处理一张普通表是类似的。需要考虑一个 bulk load 事务跨多个分区表的场景。
如果是 range 分区,且按序导入,那么一个 bulk load 事务中的数据通常会集中在一个 range 分区上,或者少量 range 相邻的分区上。此时一个 bulk load 事务通常不会涉及太多的分区表,跟普通表的场景类似,不需要做额外处理。
如果是 hash 分区,且按序导入,那么一个 bulk load 事务中的数据会被均匀地打散到各个 hash 分区上。此时一个 bulk load 事务会几乎涉及所有的分区表。此时需要把 bulk load 事务大小继续调大,从而保证一个 bulk load 事务中分配到每个 hash 分区上的数据量不要太少。假设一共 N 个 hash 分区,那么推荐 bulk load 事务大小比普通表场景扩大 N 倍。目前在分成16个 hash 分区的场景下,性能要比不进行 hash 分区(普通表)损失15%左右。
如果存在二级分区,需要考虑一个 bulk load 事务中的数据会涉及多少个二级分区,最终分配到每个二级分区上的数据量不要过少。
总而言之,如果一个 bulk load 事务涉及的分区数量越多,性能会逐渐退化。从存储层去看,其实就是一个 bulk load 事务涉及了过多的 RG 作为事务参与者。类似于普通事务也会有类似的问题,涉及的分区越多,RG 层面的参与者越多,性能越差。