在这篇文章中,我们将会讲述我们在Rockset使如何使用RocksDB和对RocksDB进行调优从而达到更好的性能的。我们认为读者对基于LSM tree构建的存储引擎,例如RocksDB 如何工作的已经很熟悉了。
在Rockset,我们想要我们的用户能够以次秒级的写延迟速度将数据导入到Rockset里面,并能够在10毫秒的速度进行查询,因此,我们需要一个既可以支持快速在线写入又可以支持快速读取的存储引擎。RocksDB就是这样一种高性能的存储引擎。RocksDB被很多大公司使用,例如Facebook、Linkedin、Uber等。还有些项目例如MongoRocks、Rocksandra、MyRocks等等都是使用RocksDB作为存储引擎,并且已经成功的减少了空间放大和写延迟的问题。RocksDB的KV模型很适合用来实现混合索引。混合索引是输入一个文档,在RocksDB存三份数据,行式数据、列式数据、和搜索索引。因此我们决定使用RocksDB作为我们的存储引擎。我们团队在RocksDB上有很丰富的经验,我们的CTO-Dhruba Borthakur在facebook开发了RocksDB。对于每一个数据的文档,产生一系列的KV对,然后把他们写入到RocksDB数据库里面。
让我快速描述一下RocksDB存储节点在整个系统架构中的位置。
当用户创建一个collection,系统内部会创建N个分片,每个分片会进行K次复制(通常k=2),以实现高读取可用性,每一个分片副本被分配给叶结点。每一个叶节点被分配了许多集合的许多分片副本。在生产环境中,每个节点分配的大概100个分片副本。叶节点为分配给他们的每个分片副本创建一个RocksDB实例。对于每个分片副本,叶结点不断从DistributedLogStore提取更新数据,并将更新数据应用在RocksDB实例中,当收到查询请求的时候,给叶节点分配了查询计划片段。更多细节请参考Aggregator Leaf Tailer 或者 Rockset White Paper.
为了实现在每个叶子节点在不断更新的同时保持高效的查询,我们花费了大量时间对RocksDB进行调优,下面,我们将会描述如何对RocksDB进行调用。
RocksDB-Cloud
RocksDB 是一个嵌入式的KV存储。一个RocksDB实例的数据不会复制到另外一台机器上。当机器宕机,RocksDB不能恢复。为了实现持久性,我们开发了RocksDB-cloud这个项目,RocksDB-cloud把所有RocksDB实例的数据和元数据传到S3上。所有叶节点的SST文件都复制到S3里面,当一个叶子节点宕机的时候,这台节点的s3数据将会被分配给所有分片复制节点中的一个节点。对于每一个新的分片复制,叶子节点将会读取相对应的失败叶子节点的s3 Bucket里的RocksDB文件。
Disable WAL
RocksDB将会把所有的更新写入一个write ahead Log 里和活跃的memtable里面。这个write ahead log 是当进程重启的时候,用来恢复memtables 里面的数据。在Rockset,所有的对于collections的更新都会首先被写入到DistributedLogStore。DistributedLogStore 本身的作用和write ahead log 的作用是一样的,而且,我们也不需要保证查询之间的数据一致性。因此先丢失memtables的数据,然后在重新启动的时候,从DistributedLogStore 里面重新获取也是可以。所以,禁用RocksDB的write ahead log,意味着所有RocksDB写操作都会在内存中进行。
Writer rate Limit
正如上面所讲的,叶节点主要复制更新和数据查询,相比于查询,我们可以容忍更高的写延迟,我们尽可能多的用一小部分可用计算容量来处理写请求,用大部分计算用量来处理读请求。我们限制叶结点上的RocksDB实例每秒写入的数据量。我们也限制写线程的数量。这有助于最小化RocksDB写入对查询延迟的影响。此外,通过这种方式限制写操作,我们永远不会以LSM树不平衡或触发RocksDB内置的不可预测的back-pressure或stall机制而结束。注意,这两个特性在RocksDB中都不可用,但是我们在RocksDB上实现了它们。RocksDB支持速率限制,它可以限制存储设备的写入速率,但是我们需要一种可以限制从应用程序写入RocksDB实例的机制。
Sorted Write Batch
如果单个update 可以通过WriteBatch实现批处理更处理,更进一步,如果WriteBatch 中连续的keys 是按顺序进行,那么RocksDB可以实现更高的写入吞吐量。我们可以充分利用这两个优势,我们首先将持续传入的更新批量处理为100KB大小的微型批次,并对其进行排序,然后将其写入RocksDB中。
Dynamic Level Target Sizes
在具有分层压缩策略的LSM树中,直到超过当前level的目标大小的时候,才会使用下一个level的文件压缩策略。每个level的目标大小是根据level 1 的目标大小 和 level 系数(通常为10)确定的。当last level达到RocksDB的博客所说的目标大小的时候,将会导致一个比预期的更高的空间放大系数。为了缓解这个现象,RocksDB可以根据上一个level的目标大小动态的设置每一个level的目标大小。无论RocksDB中存储的数据量为多少,我们都将通过使用此特性来实现RocksDB预期的1.111的空间放大系数。这个特性可以通过设置dvancedColumnFamilyOptions::level_compaction_dynamic_level_bytes
为true
开启。
Shared Block Cache
如上所诉,叶结点分配了许多集合的分片副本,每个分片副本都有一个RocksDB实例,我们没有为每个RocksDB实例分配单独的块缓存,而是为一个叶结点上的所有RocksDB实例分配了一个全局的块缓存。通过将所有分片副本中未使用的块踢出叶子内存,有助于提升内存的利用率。我们为块缓存分配叶子容器大约25%的可用内存。即使有多余的可用内存,我们也不增加块缓存大小。这是因为我们希望操作系统的页缓存可以使用这部分内存。页缓存缓存所有的压缩块,而块缓存缓存未压缩块。因此页缓存可以更密集的缓存那些不经常用的文件块。正如Optimizing Space Amplification in RocksDB 这篇论文所讲的那样,FaceBook 部署了三个RocksDB实例,页缓存使文件系统的读取减少了52%。页缓存由计算机上的所有容器共享,因此页缓存为计算机上运行的所有页容器提供服务。
No Compression For L0 & L1
根据RocksDB的设计原则,与其他level相比,LSM 树中的L0 和 L1级别包含的数据非常少。因此,在L0和L1级别上压缩数据没有什么效果。但是只要不在这些级别上进行压缩,就可以节省一些CPU。从L0到L1的每次压缩都需要访问所有L1文件。同样,range scan 不使用布隆过滤器,而是查找L0中所有文件。如果L0和L1的数据在读取的时候进行解压缩,在写入的时候进行压缩,那么这两个频繁的CPU密集型操作将使用CPU。这就是RocksDB小组不推荐压缩L0和L1中的数据,而推荐使用LZ4压缩其他级别数据的原因。
Bloom Filters ON key Prefixes
正如在converged indexing 中描述的那样,我们将以三种不同的方式和三钟不同的key ranges 将每个文档的每一列存到RocksDB中。对于查询,我们对每个key ranges的读取方法不同,具体来说,我们不会使用一个具体的key来在这些key ranges里面查找key。我们通常使用较小的共享key 前缀来查找key。因此,通过设置BlockBasedTableOptions::whole_key_filtering
为false,整个keys就不会用来填充,进而导致为每个sst文件创建布隆过滤器。我们也可以设置ColumnFamilyOptions::prefix_extractor
,这样只会为有用的key的前缀,创建布隆过滤器。
Iterator Freepool
当处理查询的时候,从RocksDB读取数据,我们需要创建一个或者多个 rocksdb::Iterators。对于查询来说,需要执行range scan 或者 检索多个字段,因此需要创建多个iterators。但是创建这些迭代器是非常昂贵的和浪费资源的。我们可以使用这些迭代器的空闲池,并尝试在一次查询中重复使用用迭代器。 但是我们不能在多次查询中重复使用迭代器。因为每个迭代器都引用特定的RocksDB快照。对于一次查询来说,我们使用相同的RocksDB快照。
最后,这里有一些我们设置的RocksDB具体配置文件。
Options.max_background_flushes: 2
Options.max_background_compactions: 8
Options.avoid_flush_during_shutdown: 1
Options.compaction_readahead_size: 16384
ColumnFamilyOptions.comparator: leveldb.BytewiseComparator
ColumnFamilyOptions.table_factory: BlockBasedTable
BlockBasedTableOptions.checksum: kxxHash
BlockBasedTableOptions.block_size: 16384
BlockBasedTableOptions.filter_policy: rocksdb.BuiltinBloomFilter
BlockBasedTableOptions.whole_key_filtering: 0
BlockBasedTableOptions.format_version: 4
LRUCacheOptionsOptions.capacity : 8589934592
ColumnFamilyOptions.write_buffer_size: 134217728
ColumnFamilyOptions.compression[0]: NoCompression
ColumnFamilyOptions.compression[1]: NoCompression
ColumnFamilyOptions.compression[2]: LZ4
ColumnFamilyOptions.prefix_extractor: CustomPrefixExtractor
ColumnFamilyOptions.compression_opts.max_dict_bytes: 32768</pre>
网友评论