summaryrefslogtreecommitdiff
path: root/include
diff options
context:
space:
mode:
authorPeter Dillinger <peterd@meta.com>2024-08-16 15:34:31 -0700
committerFacebook GitHub Bot <facebook-github-bot@users.noreply.github.com>2024-08-16 15:34:31 -0700
commit4d3518951a2aa0dc3d3c56bcdedcfec147436602 (patch)
treef01b544e41736b943a269b2c9035c68d29bfbecf /include
parent75a1230ce85fb720e13252888599f6787b5ef449 (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.h36
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.