rocksdb-doc-cn

随着DB/内存比例变大,过滤器/索引块的内存空间变得越来越重要。尽管cache_index_and_filter_blocks允许在块缓存中只存储这些内容的一个子集,但是他们本身巨大的尺寸会通过以下方式降低性能:

这里我们讲解释问题的细节,以及如何给索引和过滤器分片,以减少消耗。

索引/过滤器块有多大?

RocksDB默认对每个SST文件有一个索引/过滤器块。根据配置的不同,索引/过滤器块的大小也会有所不同,但是,对于一个256MB的SST文件,通常他的索引/过滤器块的大小就是0.5/5MB,比常见的4~32KB的数据块要大很多。如果索引/过滤器块能完美的加载进内存,那是完全可以的,这样他们在SST的生命周期中只要被读取一次就可以,而不是需要跟数据块竞争块缓存空间,导致他们需要被重复从磁盘读取。

巨大的索引/过滤器块会有什么大问题吗?

如果索引/过滤器块存储在块缓存,他们会跟数据块(以及他们自己)竞争这部分稀有资源。一个5MB的过滤器占用的空间可以被1000多个数据块(4KB大小)使用。这会导致数据块出现更多的缓存未命中。巨大的索引/过滤器同时还会把对方踢出块缓存,导致他们自己的缓存命中率下降。这会导致只有一小部分的索引/过滤器块会在他的缓存生命周期内被实际使用。

在一个索引/过滤器的缓存未命中发生后,他需要从磁盘中重新加载,他巨大的尺寸不能帮助减轻IO开销。就算是一个简单的点查询也需要从LSM的每一层读取好几个数据块,这也可能导致载入大量的索引/过滤器块。如果这种情况经常发生,磁盘甚至可能会花费比 服务数据块 更多的时间来服务索引/过滤器。

什么是分片索引/过滤器?

使用分片,一个SST文件的索引/过滤器会被分片成多个更小的块,然后加入一个新的上层索引给他们。当读取一个索引/过滤器的时候,只有最上层的索引被加载到内存。分片索引/过滤器之后会使用顶层索引来按需加载该查询需要使用的索引/过滤器到块缓存。顶层索引有更小的内存空间,可以被存储在堆上,或者是块缓存,具体配置依赖于cache_index_and_filter_blocks。

优点:

缺点:

成功案例

HDD, 100TB DB

在这个例子里,我们有一个86G的DB存放在HDD上,用这个DB,使用直接IO,来模拟一个100TB数据的节点的小内存(跳过OS的文件缓存),使用块大小60MB。分片把性能从 5 op/s 提升了大概11倍吞吐到55ops/s。

./db_bench --benchmarks="readwhilewriting[X3],stats" --use_direct_reads=1 -compaction_readahead_size 1048576 --use_existing_db --num=2000000000 --duration 600 --cache_size=62914560 -cache_index_and_filter_blocks=false -statistics -histogram -bloom_bits=10 -target_file_size_base=268435456 -block_size=32768 -threads 32 -partition_filters -partition_indexes -index_per_partition 100 -pin_l0_filter_and_index_blocks_in_cache -benchmark_write_rate_limit 204800 -max_bytes_for_level_base 134217728 -cache_high_pri_pool_ratio 0.9

SSD,Linkbench

在这个例子里我们有一个SSD上的300GB的数据库,同样通过直接IO(跳过OS文件缓存)扮演本地另一个DB的小内存,块缓存大小为6G和2G。不使用分片的时候,把块缓存大小从6G调到2G,linkbench的吞吐从38k tps掉到 23kb。使用分片,吞吐下降为从38k掉到30k。

如何使用?

当前限制

底层实现

这里展示开发者关心的实现细节。

BlockBasedTable格式

可以在这里了解BlockBasedTable格式。使用分片的时候,差异会是索引块[index block],会被存储成:

[index block - partition 1]
[index block - partition 2]
...
[index block - partition N]
[index block - top-level index]

然后SST的脚注指向顶级索引块(他自身就是一个索引分片块的索引)。每个独立的索引块分片遵从与kBinarySearch相同的格式。顶级索引格式同时遵从kBinarySearch,因此可以使用普通的数据块读取器来读取。

类似的结果被用于过滤器块分片。每个独立的过滤器块的格式遵从kFullFilter的格式。顶级索引格式遵从kBinarySearch的格式,类似索引块的顶级索引。

注意,对于带分片的SST文件,检查SST的工具,如sst_dump,会报告索引/过滤器上的顶级索引的大小,而不是索引和过滤器块的总大小。

构造器

分片索引和过滤器可以通过PartitionedIndexBuilder和PartitionedFilterBlockBuilder分别构造。

PartitionedIndexBuilder维护sub_index_builder_,一个指向ShortenedIndexBuilder的指针,用来构建当前的索引分片。当通过flush_policy_指定的时候,构造器会把指针跟索引块中最后一个key一起保存,然后创建一个新的活跃ShortenedIndexBuilder。当你对这个构造器调用::Finish,他会对最早的子索引构造器调用::Finish然后返回分片块的结果。下次调用PartitionedIndexBuilder::Finish同样会包含之前返回的SST文件分片的偏移,会被用作顶级索引的值。最后一次PartitionedIndexBuilder::Finish 的调用会完成顶级索引,这次会返回该顶级索引。把顶级索引存储到SST文件之后,他的偏移会被用作索引块的偏移。

PartitionedFilterBlockBuilder继承FullFilterBlockBuilder,它带有一个FilterBitsBuilder可以用于构建bloom过滤器。他也有一个指针指向PartitionedIndexBuilder,并且使用该指针调用ShouldCutFilterBlock,来决定什么时候应该吧一个过滤器块切片(在一个索引块被切片后)。为了切割一个过滤器块,他执行完FilterBitsBuilder并且把 返回的块 和 PartitionedIndexBuilder::GetPartitionKey()提供的分片key 存储在一起,然后重置FilterBitsBuilder,一边下一个分片使用。在最后一次 PartitionedFilterBlockBuilder::Finish 被调用的时候,其中一个分片会返回,并且前面分片的偏移会被用于构建顶级索引。调用::Finish会返回顶级索引块。

之所以基于PartitionedIndexBuilder实现PartitionedFilterBlockBuilder,是为了优化索引/过滤器分片在SST文件的插入。不过这个优化效果不是很明显,我们以后可能会放弃这个依赖。

读取器

分片索引通过PartitionIndexReader来读取,他会操作顶级索引块。当NewIterator被调用,一个TwoLevelIterator会作用于顶级索引块。这个简单的实现的原理是这样的,因为每个索引分片都是kBinarySearch格式,他们跟数据块是一样的,因此他们可以简单地插入一层迭代器来实现迭代。如果pin_l0_filter_and_index_blocks_in_cache被设置了,底层的迭代器会被固定到PartitionIndexReader,这样,只要PartitionIndexReader还存活,他们对应的索引分片都会被固定到块缓存。BlockEntryIteratorState使用一系列的固定的分片偏移来避免两次解除一个索引分片的固定。

PartitionedFilterBlockReader使用顶级索引找到过滤器分片的偏移。然后调用BlockBasedTable对象的GetFilter,通过FilterBlockReader对象读取块缓存上的过滤器分片(如果没有,则从磁盘读入到这里来) ,然后释放FilterBlockReader对象。为了让table_options.pin_l0filter_and_index_blocks_in_cache支持分片索引,PartitionedFilterBlockReader并不会释放这些块的缓存句柄(或者说,保证他们会被固定到块缓存)。他从另一个方向维护了filter_cache,一个固定的FilterBlockReader映射表,被用于在PartitionedFilterBlockReader析构的时候释放缓存项。

看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持! 觉得写的不错那就给点吧, 在线乞讨 微信转账