diff options
author | Peter Dillinger <peterd@meta.com> | 2024-08-16 15:34:31 -0700 |
---|---|---|
committer | Facebook GitHub Bot <facebook-github-bot@users.noreply.github.com> | 2024-08-16 15:34:31 -0700 |
commit | 4d3518951a2aa0dc3d3c56bcdedcfec147436602 (patch) | |
tree | f01b544e41736b943a269b2c9035c68d29bfbecf /include | |
parent | 75a1230ce85fb720e13252888599f6787b5ef449 (diff) |
Option to decouple index and filter partitions (#12939)
Summary:
Partitioned metadata blocks were introduced back in 2017 to deal more gracefully with large DBs where RAM is relatively scarce and some data might be much colder than other data. The feature allows metadata blocks to compete for memory in the block cache against data blocks while alleviating tail latencies and thrash conditions that can arise with large metadata blocks (sometimes megabytes each) that can arise with large SST files. In general, the cost to partitioned metadata is more CPU in accesses (especially for filters where more binary search is needed before hashing can be used) and a bit more memory fragmentation and related overheads.
However the feature has always had a subtle limitation with a subtle effect on performance: index partitions and filter partitions must be cut at the same time, regardless of which wins the space race (hahaha) to metadata_block_size. Commonly filters will be a few times larger than indexes, so index partitions will be under-sized compared to filter (and data) blocks. While this does affect fragmentation and related overheads a bit, I suspect the bigger impact on performance is in the block cache. The coupling of the partition cuts would be defensible if the binary search done to find the filter block was used (on filter hit) to short-circuit binary search to an index partition, but that optimization has not been developed.
Consider two metadata blocks, an under-sized one and a normal-sized one, covering proportional sections of the key space with the same density of read queries. The under-sized one will be more prone to eviction from block cache because it is used less often. This is unfair because of its despite its proportionally smaller cost of keeping in block cache, and most of the cost of a miss to re-load it (random IO) is not proportional to the size (similar latency etc. up to ~32KB).
## This change
Adds a new table option decouple_partitioned_filters allows filter blocks and index blocks to be cut independently. To make this work, the partitioned filter block builder needs to know about the previous key, to generate an appropriate separator for the partition index. In most cases, BlockBasedTableBuilder already has easy access to the previous key to provide to the filter block builder.
This change includes refactoring to pass that previous key to the filter builder when available, with the filter building caching the previous key itself when unavailable, such as during compression dictionary training and some unit tests. Access to the previous key eliminates the need to track the previous prefix, which results in a small SST construction CPU win in prefix filtering cases, regardless of coupling, and possibly a small regression for some non-prefix cases, regardless of coupling, but still overall improvement especially with https://github.com/facebook/rocksdb/issues/12931.
Suggested follow-up:
* Update confusing use of "last key" to refer to "previous key"
* Expand unit test coverage with parallel compression and dictionary training
* Consider an option or enhancement to alleviate under-sized metadata blocks "at the end" of an SST file due to no coordination or awareness of when files are cut.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/12939
Test Plan:
unit tests updated. Also did some unit test runs with "hard wired" usage of parallel compression and dictionary training code paths to ensure they were working. Also ran blackbox_crash_test for a while with the new feature.
## SST write performance (CPU)
Using the same testing setup as in https://github.com/facebook/rocksdb/issues/12931 but with -decouple_partitioned_filters=1 in the "after" configuration, which benchmarking shows makes almost no difference in terms of SST write CPU. "After" vs. "before" this PR
```
-partition_index_and_filters=0 -prefix_size=0 -whole_key_filtering=1
923691 vs. 924851 (-0.13%)
-partition_index_and_filters=0 -prefix_size=8 -whole_key_filtering=0
921398 vs. 922973 (-0.17%)
-partition_index_and_filters=0 -prefix_size=8 -whole_key_filtering=1
902259 vs. 908756 (-0.71%)
-partition_index_and_filters=1 -prefix_size=8 -whole_key_filtering=0
917932 vs. 916901 (+0.60%)
-partition_index_and_filters=1 -prefix_size=8 -whole_key_filtering=0
912755 vs. 907298 (+0.60%)
-partition_index_and_filters=1 -prefix_size=8 -whole_key_filtering=1
899754 vs. 892433 (+0.82%)
```
I think this is a pretty good trade, especially in attracting more movement toward partitioned configurations.
## Read performance
Let's see how decoupling affects read performance across various degrees of memory constraint. To simplify LSM structure, we're using FIFO compaction. Since decoupling will overall increase metadata block size, we control for this somewhat with an extra "before" configuration with larger metadata block size setting (8k instead of 4k). Basic setup:
```
(for CS in 0300 1200; do TEST_TMPDIR=/dev/shm/rocksdb1 ./db_bench -benchmarks=fillrandom,flush,readrandom,block_cache_entry_stats -num=5000000 -duration=30 -disable_wal=1 -write_buffer_size=30000000 -bloom_bits=10 -compaction_style=2 -fifo_compaction_max_table_files_size_mb=10000 -fifo_compaction_allow_compaction=0 -partition_index_and_filters=1 -statistics=1 -cache_size=${CS}000000 -metadata_block_size=4096 -decouple_partitioned_filters=1 2>&1 | tee results-$CS; done)
```
And read ops/s results:
```CSV
Cache size MB,After/decoupled/4k,Before/4k,Before/8k
3,15593,15158,12826
6,16295,16693,14134
10,20427,20813,18459
20,27035,26836,27384
30,33250,31810,33846
60,35518,32585,35329
100,36612,31805,35292
300,35780,31492,35481
1000,34145,31551,35411
1100,35219,31380,34302
1200,35060,31037,34322
```
If you graph this with log scale on the X axis (internal link: https://pxl.cl/5qKRc), you see that the decoupled/4k configuration is essentially the best of both the before/4k and before/8k configurations: handles really tight memory closer to the old 4k configuration and handles generous memory closer to the old 8k configuration.
Reviewed By: jowlyzhang
Differential Revision: D61376772
Pulled By: pdillinger
fbshipit-source-id: fc2af2aee44290e2d9620f79651a30640799e01f
Diffstat (limited to 'include')
-rw-r--r-- | include/rocksdb/table.h | 36 |
1 files changed, 27 insertions, 9 deletions
diff --git a/include/rocksdb/table.h b/include/rocksdb/table.h index a801a3349..c7fe503ff 100644 --- a/include/rocksdb/table.h +++ b/include/rocksdb/table.h @@ -291,15 +291,11 @@ struct BlockBasedTableOptions { // Same as block_restart_interval but used for the index block. int index_block_restart_interval = 1; - // Block size for partitioned metadata. Currently applied to indexes when - // kTwoLevelIndexSearch is used and to filters when partition_filters is used. - // Note: Since in the current implementation the filters and index partitions - // are aligned, an index/filter block is created when either index or filter - // block size reaches the specified limit. - // Note: this limit is currently applied to only index blocks; a filter - // partition is cut right after an index block is cut - // TODO(myabandeh): remove the note above when filter partitions are cut - // separately + // Target block size for partitioned metadata. Currently applied to indexes + // when kTwoLevelIndexSearch is used and to filters when partition_filters is + // used. When decouple_partitioned_filters=false (original behavior), there is + // much more deviation from this target size. See the comment on + // decouple_partitioned_filters. uint64_t metadata_block_size = 4096; // `cache_usage_options` allows users to specify the default @@ -398,6 +394,23 @@ struct BlockBasedTableOptions { // block cache even when cache_index_and_filter_blocks=false. bool partition_filters = false; + // When both partitioned indexes and partitioned filters are enabled, + // this enables independent partitioning boundaries between the two. Most + // notably, this enables these metadata blocks to hit their target size much + // more accurately, as there is often a disparity between index sizes and + // filter sizes. This should reduce fragmentation and metadata overheads in + // the block cache, as well as treat blocks more fairly for cache eviction + // purposes. + // + // There are no SST format compatibility issues with this option. (All + // versions of RocksDB able to read partitioned filters are able to read + // decoupled partitioned filters.) + // + // decouple_partitioned_filters = false is the original behavior, because of + // limitations in the initial implementation, and the new behavior + // decouple_partitioned_filters = true is expected to become the new default. + bool decouple_partitioned_filters = false; + // Option to generate Bloom/Ribbon filters that minimize memory // internal fragmentation. // @@ -679,6 +692,11 @@ struct BlockBasedTablePropertyNames { static const std::string kWholeKeyFiltering; // value is "1" for true and "0" for false. static const std::string kPrefixFiltering; + // Set to "1" when partitioned filters are decoupled from partitioned indexes. + // This metadata is recorded in case a read-time optimization for coupled + // filter+index partitioning is ever developed; that optimization/assumption + // would be disabled when this is set. + static const std::string kDecoupledPartitionedFilters; }; // Create default block based table factory. |