RocksDB 批量导入数据的最快速度可以达到多少?我们面临这个挑战,因为我们想让我们的客户可以尽快把大量数据导入Rockset,试用Rockset。尽管批量导入数据到LSM tree也是一个比较重要的主题,但是并没有很多文章讨论。在这篇文章里,我将讲诉一些优化技巧,可以提高RocksDB批量导入数据的速度,大概提高20倍左右。虽然我们也需要解决一些分布式的挑战,但是在这片文章中,我将集中讨论RocksDB单台机器的优化。熟悉RocksDB的人对RocksDB的LSM tree结构肯定也很熟悉。
Rockset的写流程包括以下几步:
- 首先我们通过分布式日志存储系统来检索文档,一篇文档代表一个已经被编译为二进制的JSON结构。
- 对每一个文档来说,我们需要向RocksDB插入大量K-V数据,接下来,我们需要将多列文档转换为多列K-V数据。重要的是,我们同时需要从RocksDB数据库读取数据,确定这个文档是否已经在RocksDB中里面,如果文档已经存在,需要更新secondary index entries。
- 最后,我们提交这些KV数据到RocksDB数据库。
延迟和吞吐量的取舍
Rockset被设计成一个实时系统,只要用户写入一个文档到Rockset,就必须为它创建索引。我们没有太多时间去构建大量文档数据。这是一个遗憾,因为增加批处理文档的大小可以最大限度的减少每批操作的开销。但是批量导入的时候,没有必要优化单个文档写入的时间,在批量导入数据的时候,可以通过增加每批写操作处理数据的大小,来提高写入数据的吞吐量,通常写操作的数据一般为百MB。
并行写
常规操作,我们使用单线程来执行写流程。这已经足够了,因为RocksDB通过压缩把写进程放入到背景线程里面去,为了应对查询的负载,需要更多的CPU。在最初的批量导入的时候,查询负载并不是特别重要,所有的CPU都忙于写操作,因此,我们并行化了写操作。一旦我们构建了一系列的批处理的文档,我们立刻把这些批处理的文档分发到工作线程组,每一个工作线程各自独立将数据插入到RocksDB数据库里。需要考虑的一个重要设计是去最大程度地减少对共享数据结构地独占访问。否则,一些写线程将会处于等待状态。
避免Memtable
RocksDB提供了一个特性:你可以自己构建SST 文件,然后把他们添加到RocksDB里面,而不需要经过memtable。这个api是IngestExternalFile()
。这个特性对于批量导入来说十分有利,因为写线程并不需要同步写数据到memtable。写线程只需要对KV数据排序,然后将数据写入SST文件里就可以了。将SST文件加入到RocksDB里面是一项比较廉价的操作,因为它只需要更新元数据就可以了。
在现在实现的版本里,每一个写线程构建一个SST文件,但是如果存在大量小的文件,那么压缩操作将会比更少数量的大文件慢很多。我们正在研究一个方法,从而可以让每一次批量写操作的所有线程只产生一个大的SST文件。
关闭压缩带来的挑战
RocksDB批量导入数据最常见的建议是关闭压缩,最后执行一次大的压缩。这个方法也是RocksDB官方文章里所提到的方法。毕竟,RocksDB执行压缩的唯一原因是通过牺牲写开销来优化读取。但是,这个建议有两个非常重要的局限性。
在Rockset我们每次写入文档前必须执行一次读取,我们需要执行一次主键查询来检测是否有新的文档还存在数据库里。通过关闭压缩,我们很快出现上千个SST文件。主键查询变成了比较大的缺点,为了避免这种情况,我们在所有的主键上构建了布隆过滤器。因为我们批量导入数据的时候,通常不会有重复数据,所以布隆过滤器能够使我们避免昂贵的主键查询。细心的读者会发现RocksDB本身也有布隆过滤器,但是它是针对每个文件构建的,检查上千个布隆过滤器仍然代价高昂。
第二个问题是最后的压缩操作默认是单线程的,这有一个选项能够使RocksDB变成多线程压缩,这个选项使max_subcompactions
。但是增加子压缩的数目并不会起到太大的作用。如果所有文件的级别都为level 0,压缩算法无法为每个子压缩找到良好的边界,而是决定使用单个线程。我们通过首先执行priming压缩来固定子压缩的数目。我们首先压缩少量的问题通过CompactFiles()
。而且RocksDB也有些文件处于非0的level上,这个可以决定好的子压缩的边界,从而可以使用多线程压缩。
我们处于Level 0 的文件并没有进行压缩。因为我们不想降低写线程的速度。而且把它们都压缩了,也没有太多好处。最后的压缩会压缩输出文件
结论
通过上述优化,我们可以只用18个核将200GB的未压缩的数据(通过LZ4压缩后,大小为80GB)在52分钟内(70MB/s)导入到RocksDB里。开始的批量导入花费35分钟,最后的压缩花费17分钟。如果不做任何优化,将会花费18个小时。如果只使增加批处理数据的大小和并行写,对RocksDB不做其他操作,将会花费5个小时。需要注意的是,这些数据只是在单机上测得。Rockset可以多机器并行写,这可以达到更高的写吞吐量。
imageRocksDB批量导入数据可以抽象为并行排序,但是数据不能全加载到内存里,另一个约束是排序的时候,我们需要读去一部分数据。这仍然有很多有趣的关于并行排序的工作。我们希望能够调研一些其他的技术,并把这些技术应用到我们的设置中。我们也希望其他RocksDB的用户可以分享他们批量导入数据的技术。
注:本文为翻译。
原文:https://rockset.com/blog/optimizing-bulk-load-in-rocksdb/
网友评论