Skip to content

Commit 1ed5052

Browse files
mszeszko-metameta-codesync[bot]
authored andcommitted
Prepopulate block cache during compaction (facebook#14445)
Summary: When RocksDB operates with tiered or remote storage (e.g., Warm Storage, HDFS, S3), reading recently compacted data incurs high-latency remote reads because compaction output files are not present in the block cache. The existing `prepopulate_block_cache = kFlushOnly` avoids this for flush output but leaves compaction output cold until first access. Add a new `PrepopulateBlockCache::kFlushAndCompaction` enum value that warms all block types (data, index, filter, compression dict) into the block cache during both flush and compaction. Flush-warmed blocks use `LOW` priority (unchanged from kFlushOnly behavior), while compaction-warmed blocks use `BOTTOM` priority — compaction data is less temporally local than freshly flushed data, so it should be the first to be evicted when the cache is full. This gives the remote-read avoidance benefit without risking cache thrashing. The enum uses `kFlushAndCompaction` rather than separate `kCompactionOnly` + `kFlushAndCompaction` values because there is no practical use case for warming compaction output without also warming flush output. Flush output is by definition the hottest data (just written by the user), so if a workload benefits from warming the colder compaction output, it would always benefit from warming flush output too. The implementation reuses the existing `InsertBlockInCacheHelper` / `WarmInCache` infrastructure in `BlockBasedTableBuilder`. The only internal change is adding a `warm_cache_priority` field to `Rep` alongside the existing `warm_cache` bool, and plumbing it through to the `WarmInCache` call instead of the previously hardcoded `Cache::Priority::LOW`. ### Key changes - New `PrepopulateBlockCache::kFlushAndCompaction` enum value in table.h - `Rep::warm_cache_priority` field in BlockBasedTableBuilder for per-reason priority control - Serialization support ("kFlushAndCompaction" in string map) - db_bench support (--prepopulate_block_cache=2) - Crash test coverage (random choice includes new value) **NOTE:** Unlike flush output (which is inherently hot — just written by the user), it is hard to distinguish hot from cold blocks in compaction output. Warming all compaction output therefore risks polluting the block cache and evicting genuinely hot entries. The kFlushAndCompaction mode is recommended only for use cases where most or all of the database is expected to reside in cache (e.g., the working set fits in cache). For workloads where only a fraction of the data is hot, kFlushOnly remains the safer choice. Pull Request resolved: facebook#14445 Test Plan: - New `WarmCacheWithDataBlocksDuringCompaction` test: verifies data blocks from compaction output are present in the block cache and served without misses - Extended `DynamicOptions` test: verifies dynamic switching through kDisable -> kFlushAndCompaction -> kFlushOnly -> kDisable via SetOptions - Existing `WarmCacheWithDataBlocksDuringFlush` and parameterized `WarmCacheWithBlocksDuringFlush` tests continue to pass (kFlushOnly behavior unchanged) - db_block_cache_test: 81/81 passed - options_test: 74/74 passed - table_test: 6910/6910 passed Reviewed By: xingbowang Differential Revision: D95997952 Pulled By: mszeszko-meta fbshipit-source-id: 4ad568264992532053947df298e63e343821ddb5
1 parent db9449a commit 1ed5052

7 files changed

Lines changed: 181 additions & 24 deletions

File tree

db/db_block_cache_test.cc

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,90 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringFlush) {
466466
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
467467
}
468468

469+
// Cache wrapper that tracks the priority of each Insert call.
470+
namespace {
471+
class PriorityTrackingCache : public CacheWrapper {
472+
public:
473+
explicit PriorityTrackingCache(std::shared_ptr<Cache> target)
474+
: CacheWrapper(std::move(target)) {}
475+
476+
const char* Name() const override { return "PriorityTrackingCache"; }
477+
478+
Status Insert(const Slice& key, ObjectPtr value,
479+
const CacheItemHelper* helper, size_t charge,
480+
Handle** handle = nullptr, Priority priority = Priority::LOW,
481+
const Slice& compressed_value = Slice(),
482+
CompressionType type = kNoCompression) override {
483+
insert_priorities_.push_back(priority);
484+
return CacheWrapper::Insert(key, value, helper, charge, handle, priority,
485+
compressed_value, type);
486+
}
487+
488+
void ResetPriorities() { insert_priorities_.clear(); }
489+
490+
bool HasPriority(Priority p) const {
491+
return std::find(insert_priorities_.begin(), insert_priorities_.end(), p) !=
492+
insert_priorities_.end();
493+
}
494+
495+
private:
496+
std::vector<Priority> insert_priorities_;
497+
};
498+
} // namespace
499+
500+
TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
501+
Options options = CurrentOptions();
502+
options.create_if_missing = true;
503+
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
504+
options.disable_auto_compactions = true;
505+
506+
auto tracking_cache =
507+
std::make_shared<PriorityTrackingCache>(NewLRUCache(1 << 25, 0, false));
508+
509+
BlockBasedTableOptions table_options;
510+
table_options.block_cache = tracking_cache;
511+
table_options.cache_index_and_filter_blocks = false;
512+
table_options.prepopulate_block_cache =
513+
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction;
514+
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
515+
DestroyAndReopen(options);
516+
517+
std::string value(kValueSize, 'a');
518+
519+
// Flush warming: inserts should use LOW priority.
520+
tracking_cache->ResetPriorities();
521+
ASSERT_OK(Put("key", value));
522+
ASSERT_OK(Flush());
523+
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::LOW));
524+
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
525+
526+
// Write overlapping key to force a real merge compaction (not trivial move).
527+
ASSERT_OK(Put("key", value + "2"));
528+
ASSERT_OK(Flush());
529+
530+
auto data_add_before =
531+
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD);
532+
533+
// Compaction warming: data block inserts should use BOTTOM priority.
534+
// Internal cache bookkeeping (e.g., cache entry stats) may insert at HIGH,
535+
// so we check for BOTTOM presence and LOW absence.
536+
tracking_cache->ResetPriorities();
537+
CompactRangeOptions cro;
538+
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
539+
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
540+
EXPECT_GT(options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD),
541+
data_add_before);
542+
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
543+
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
544+
545+
// Compaction output is in cache — reads should have zero misses.
546+
auto data_miss_before =
547+
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
548+
ASSERT_EQ(value + "2", Get("key"));
549+
EXPECT_EQ(data_miss_before,
550+
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
551+
}
552+
469553
// This test cache data, index and filter blocks during flush.
470554
class DBBlockCacheTest1 : public DBTestBase,
471555
public ::testing::WithParamInterface<uint32_t> {
@@ -620,6 +704,36 @@ TEST_F(DBBlockCacheTest, DynamicOptions) {
620704
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
621705
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
622706

707+
// Switch to kFlushAndCompaction
708+
++i;
709+
ASSERT_OK(dbfull()->SetOptions(
710+
{{"block_based_table_factory",
711+
"{prepopulate_block_cache=kFlushAndCompaction;}"}}));
712+
713+
ASSERT_OK(Put(std::to_string(i), value));
714+
ASSERT_OK(Flush());
715+
// Flush warming still works
716+
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
717+
718+
ASSERT_EQ(value, Get(std::to_string(i)));
719+
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
720+
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
721+
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
722+
723+
// Switch back to kDisable
724+
++i;
725+
ASSERT_OK(dbfull()->SetOptions(
726+
{{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}}));
727+
728+
ASSERT_OK(Put(std::to_string(i), value));
729+
ASSERT_OK(Flush());
730+
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
731+
732+
ASSERT_EQ(value, Get(std::to_string(i)));
733+
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
734+
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
735+
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
736+
623737
++i;
624738
// NOT YET SUPPORTED
625739
// FIXME: find a way to make this fail again (until well supported)

include/rocksdb/table.h

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,17 +739,34 @@ struct BlockBasedTableOptions {
739739

740740
// If enabled, prepopulate warm/hot blocks (data, uncompressed dict, index and
741741
// filter blocks) which are already in memory into block cache at the time of
742-
// flush. On a flush, the block that is in memory (in memtables) get flushed
743-
// to the device. If using Direct IO, additional IO is incurred to read this
744-
// data back into memory again, which is avoided by enabling this option. This
742+
// flush or compaction.
743+
//
744+
// On a flush, the data block that is in memory (in memtables) gets flushed to
745+
// the device. If using Direct IO, additional IO is incurred to read this data
746+
// back into memory again, which is avoided by enabling this option. This
745747
// further helps if the workload exhibits high temporal locality, where most
746748
// of the reads go to recently written data. This also helps in case of
747749
// Distributed FileSystem.
750+
//
751+
// On a compaction, output SST files are written to disk but not placed in the
752+
// block cache by default. With tiered or remote storage (e.g., HDFS, S3),
753+
// reading recently compacted data back incurs high latency.
754+
// Enabling compaction warming avoids these cold reads. However, unlike flush
755+
// output, it is hard to distinguish hot from cold blocks in compaction
756+
// output, so warming all of it risks polluting the cache. To mitigate this,
757+
// compaction-warmed blocks are inserted at BOTTOM priority (vs LOW for flush)
758+
// so they are evicted first under cache pressure. Even so,
759+
// kFlushAndCompaction is recommended only when most or all of the database is
760+
// expected to reside in cache. For workloads where only a fraction of the
761+
// data is hot, kFlushOnly is the safer choice.
748762
enum class PrepopulateBlockCache : char {
749763
// Disable prepopulate block cache.
750764
kDisable,
751765
// Prepopulate blocks during flush only.
752766
kFlushOnly,
767+
// Prepopulate blocks during flush and compaction. Flush-warmed blocks are
768+
// inserted at LOW priority, compaction-warmed blocks at BOTTOM priority.
769+
kFlushAndCompaction,
753770
};
754771

755772
PrepopulateBlockCache prepopulate_block_cache =

table/block_based/block_based_table_builder.cc

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,35 @@ struct BlockBasedTableBuilder::ParallelCompressionRep {
779779
#endif // BBTB_PC_WATCHDOG
780780
};
781781

782+
struct WarmCacheConfig {
783+
const bool enabled;
784+
const Cache::Priority priority;
785+
786+
static WarmCacheConfig Compute(
787+
BlockBasedTableOptions::PrepopulateBlockCache mode,
788+
TableFileCreationReason reason) {
789+
bool enabled = false;
790+
Cache::Priority priority = Cache::Priority::LOW;
791+
switch (mode) {
792+
case BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly:
793+
enabled = (reason == TableFileCreationReason::kFlush);
794+
break;
795+
case BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction:
796+
enabled = (reason == TableFileCreationReason::kFlush ||
797+
reason == TableFileCreationReason::kCompaction);
798+
if (reason == TableFileCreationReason::kCompaction) {
799+
priority = Cache::Priority::BOTTOM;
800+
}
801+
break;
802+
case BlockBasedTableOptions::PrepopulateBlockCache::kDisable:
803+
break;
804+
default:
805+
assert(false);
806+
}
807+
return {enabled, priority};
808+
}
809+
};
810+
782811
struct BlockBasedTableBuilder::Rep {
783812
const ImmutableOptions ioptions;
784813
// BEGIN from MutableCFOptions
@@ -819,7 +848,6 @@ struct BlockBasedTableBuilder::Rep {
819848
PartitionedIndexBuilder* p_index_builder_ = nullptr;
820849

821850
std::string last_ikey; // Internal key or empty (unset)
822-
bool warm_cache = false;
823851
bool uses_explicit_compression_manager = false;
824852

825853
uint64_t sample_for_compression;
@@ -921,6 +949,7 @@ struct BlockBasedTableBuilder::Rep {
921949

922950
std::unique_ptr<ParallelCompressionRep> pc_rep;
923951
RelaxedAtomic<uint64_t> worker_cpu_micros{0};
952+
const WarmCacheConfig warm_cache_config;
924953
BlockCreateContext create_context;
925954

926955
// The size of the "tail" part of a SST file. "Tail" refers to
@@ -1066,6 +1095,8 @@ struct BlockBasedTableBuilder::Rep {
10661095
flush_block_policy(
10671096
table_options.flush_block_policy_factory->NewFlushBlockPolicy(
10681097
table_options, data_block)),
1098+
warm_cache_config(WarmCacheConfig::Compute(
1099+
table_options.prepopulate_block_cache, reason)),
10691100
create_context(&table_options, &ioptions, ioptions.stats,
10701101
/*decompressor=*/nullptr,
10711102
tbo.moptions.block_protection_bytes_per_key,
@@ -1200,19 +1231,6 @@ struct BlockBasedTableBuilder::Rep {
12001231
// the table properties with placeholder info
12011232
}
12021233

1203-
switch (table_options.prepopulate_block_cache) {
1204-
case BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly:
1205-
warm_cache = (reason == TableFileCreationReason::kFlush);
1206-
break;
1207-
case BlockBasedTableOptions::PrepopulateBlockCache::kDisable:
1208-
warm_cache = false;
1209-
break;
1210-
default:
1211-
// missing case
1212-
assert(false);
1213-
warm_cache = false;
1214-
}
1215-
12161234
const auto compress_dict_build_buffer_charged =
12171235
table_options.cache_usage_options.options_overrides
12181236
.at(CacheEntryRole::kCompressionDictionaryBuildingBuffer)
@@ -2141,7 +2159,7 @@ IOStatus BlockBasedTableBuilder::WriteMaybeCompressedBlockImpl(
21412159
}
21422160
}
21432161

2144-
if (r->warm_cache) {
2162+
if (r->warm_cache_config.enabled) {
21452163
io_s = status_to_io_status(
21462164
InsertBlockInCacheHelper(*uncompressed_block_data, handle, block_type));
21472165
if (UNLIKELY(!io_s.ok())) {
@@ -2271,8 +2289,8 @@ Status BlockBasedTableBuilder::InsertBlockInCacheHelper(
22712289
// (de)compression dictionary, which will clone and save a dict-based
22722290
// decompressor from the corresponding non-dict decompressor.
22732291
s = WarmInCache(block_cache, key.AsSlice(), block_contents,
2274-
&rep_->create_context, helper, Cache::Priority::LOW,
2275-
&charge);
2292+
&rep_->create_context, helper,
2293+
rep_->warm_cache_config.priority, &charge);
22762294
if (LIKELY(s.ok())) {
22772295
BlockBasedTable::UpdateCacheInsertionMetrics(
22782296
block_type, nullptr /*get_context*/, charge, s.IsOkOverwritten(),

table/block_based/block_based_table_factory.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ static std::unordered_map<std::string,
231231
block_base_table_prepopulate_block_cache_string_map = {
232232
{"kDisable", BlockBasedTableOptions::PrepopulateBlockCache::kDisable},
233233
{"kFlushOnly",
234-
BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly}};
234+
BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly},
235+
{"kFlushAndCompaction",
236+
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction}};
235237

236238
static struct BlockBasedTableTypeInfo {
237239
std::unordered_map<std::string, OptionTypeInfo> info;

tools/db_bench_tool.cc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -745,8 +745,9 @@ DEFINE_bool(separate_key_value_in_data_block,
745745
"If true, data blocks store keys and values separately.");
746746

747747
DEFINE_int64(prepopulate_block_cache, 0,
748-
"Pre-populate hot/warm blocks in block cache. 0 to disable and 1 "
749-
"to insert during flush");
748+
"Pre-populate hot/warm blocks in block cache. 0 to disable, 1 "
749+
"to insert during flush, and 2 to insert during flush and "
750+
"compaction");
750751

751752
DEFINE_uint32(uncache_aggressiveness,
752753
ROCKSDB_NAMESPACE::ColumnFamilyOptions().uncache_aggressiveness,
@@ -4696,6 +4697,10 @@ class Benchmark {
46964697
prepopulate_block_cache =
46974698
BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly;
46984699
break;
4700+
case 2:
4701+
prepopulate_block_cache = BlockBasedTableOptions::
4702+
PrepopulateBlockCache::kFlushAndCompaction;
4703+
break;
46994704
default:
47004705
fprintf(stderr, "Unknown prepopulate block cache mode\n");
47014706
}

tools/db_crashtest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def apply_random_seed_per_iteration():
327327
"user_timestamp_size": 0,
328328
"secondary_cache_fault_one_in": lambda: random.choice([0, 0, 32]),
329329
"compressed_secondary_cache_size": lambda: random.choice([8388608, 16777216]),
330-
"prepopulate_block_cache": lambda: random.choice([0, 1]),
330+
"prepopulate_block_cache": lambda: random.choice([0, 1, 2]),
331331
"memtable_prefix_bloom_size_ratio": lambda: random.choice([0.001, 0.01, 0.1, 0.5]),
332332
"memtable_whole_key_filtering": lambda: random.randint(0, 1),
333333
"detect_filter_construct_corruption": lambda: random.choice([0, 1]),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction` to prepopulate the block cache during both flush and compaction. Compaction-warmed blocks are inserted at `BOTTOM` priority (vs `LOW` for flush) so they are evicted first under cache pressure. Recommended only for use cases where most or all of the database is expected to reside in cache (e.g., tiered or remote storage where the working set fits in cache).

0 commit comments

Comments
 (0)