Implement MixedCompressor that Round robins on compression algorithm (#13647)

Summary:
**Summary**
This pull request introduces a mixed compressor, RoundRobinManager and RoundRobinCompressor, which selects algorithms in a loop. This implementation replaces the current hacky approach to round-robin compression in BuiltInCompressorV2. Additionally, it configures RocksDB to optionally utilize this customized compressor in the db stress test.

**Testing**
Testing was performed by verifying the stdout output from both RoundRobinCompressor and BuiltInCompressorV2.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/13647

Reviewed By: pdillinger

Differential Revision: D75921997

Pulled By: shubhajeet

fbshipit-source-id: 8f42ac46f08ba982b2cd70241bd7dc13ff5a1225
This commit is contained in:
Sujit Maharjan
2025-06-04 10:18:44 -07:00
committed by Facebook GitHub Bot
parent 0119a8c78b
commit fccc881894
9 changed files with 281 additions and 32 deletions
+105
View File
@@ -30,6 +30,7 @@
#include "test_util/testutil.h"
#include "util/defer.h"
#include "util/random.h"
#include "util/simple_mixed_compressor.h"
#include "utilities/fault_injection_env.h"
namespace ROCKSDB_NAMESPACE {
@@ -1883,6 +1884,110 @@ TEST_F(DBTest2, CompressionOptions) {
}
}
TEST_F(DBTest2, RoundRobinManager) {
if (ZSTD_Supported()) {
auto mgr = std::make_shared<RoundRobinManager>(
GetDefaultBuiltinCompressionManager());
for (CompressionType type : {kZSTD}) {
std::vector<std::string> values;
for (bool use_wrapper : {true}) {
SCOPED_TRACE("Compression type: " + std::to_string(type) +
(use_wrapper ? " with " : " no ") + "wrapper");
Options options = CurrentOptions();
options.compression = type;
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
options.statistics->set_stats_level(StatsLevel::kExceptTimeForMutex);
BlockBasedTableOptions bbto;
bbto.enable_index_compression = false;
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
options.compression_manager = use_wrapper ? mgr : nullptr;
DestroyAndReopen(options);
Random rnd(301);
constexpr int kCount = 13;
// Highly compressible blocks, except 1 non-compressible. Half of the
// compressible are morked for bypass and 1 marked for rejection. Values
// are large enough to ensure just 1 k-v per block.
for (int i = 0; i < kCount; ++i) {
std::string value;
if (i == 6) {
// One non-compressible block
value = rnd.RandomBinaryString(20000);
} else {
test::CompressibleString(&rnd, 0.1, 20000, &value);
}
values.push_back(value);
ASSERT_OK(Put(Key(i), value));
ASSERT_EQ(Get(Key(i)), value);
}
ASSERT_OK(Flush());
// Ensure well-formed for reads
for (int i = 0; i < kCount; ++i) {
ASSERT_NE(Get(Key(i)), "NOT_FOUND");
ASSERT_EQ(Get(Key(i)), values[i]);
}
ASSERT_EQ(Get(Key(kCount)), "NOT_FOUND");
}
}
}
}
TEST_F(DBTest2, SimpleMixedCompressionManager) {
if (ZSTD_Supported()) {
auto mgr = std::make_shared<SimpleMixedCompressionManager>(
GetDefaultBuiltinCompressionManager());
// Currently mixedmanager only supports with preffered compression manager
// zstd
for (CompressionType type : {kZSTD}) {
std::vector<std::string> values;
for (bool use_wrapper : {true}) {
SCOPED_TRACE("Compression type: " + std::to_string(type) +
(use_wrapper ? " with " : " no ") + "wrapper");
Options options = CurrentOptions();
options.compression = type;
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
options.statistics->set_stats_level(StatsLevel::kExceptTimeForMutex);
BlockBasedTableOptions bbto;
bbto.enable_index_compression = false;
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
options.compression_manager = use_wrapper ? mgr : nullptr;
DestroyAndReopen(options);
Random rnd(301);
constexpr int kCount = 13;
// Highly compressible blocks, except 1 non-compressible. Half of the
// compressible are morked for bypass and 1 marked for rejection. Values
// are large enough to ensure just 1 k-v per block.
for (int i = 0; i < kCount; ++i) {
std::string value;
if (i == 6) {
// One non-compressible block
value = rnd.RandomBinaryString(20000);
} else {
test::CompressibleString(&rnd, 0.1, 20000, &value);
}
values.push_back(value);
ASSERT_OK(Put(Key(i), value));
ASSERT_EQ(Get(Key(i)), value);
}
ASSERT_OK(Flush());
// Ensure well-formed for reads
for (int i = 0; i < kCount; ++i) {
ASSERT_NE(Get(Key(i)), "NOT_FOUND");
ASSERT_EQ(Get(Key(i)), values[i]);
}
ASSERT_EQ(Get(Key(kCount)), "NOT_FOUND");
}
}
}
}
TEST_F(DBTest2, CompressionManagerWrapper) {
// Test that we can use a custom CompressionManager to wrap the built-in
// CompressionManager, thus adopting a custom *strategy* based on existing
+1
View File
@@ -398,6 +398,7 @@ DECLARE_bool(use_adaptive_mutex_lru);
DECLARE_uint32(compress_format_version);
DECLARE_uint64(manifest_preallocation_size);
DECLARE_bool(enable_checksum_handoff);
DECLARE_string(compression_manager);
DECLARE_uint64(max_total_wal_size);
DECLARE_double(high_pri_pool_ratio);
DECLARE_double(low_pri_pool_ratio);
+3 -1
View File
@@ -467,7 +467,9 @@ DEFINE_uint64(blob_file_size,
DEFINE_string(blob_compression_type, "none",
"[Integrated BlobDB] The compression algorithm to use for large "
"values stored in blob files.");
DEFINE_string(compression_manager, "mixed",
"Ability to change compression manager specified in "
"simple_mixed_manager.h (mixed -> roundRobin)");
DEFINE_bool(enable_blob_garbage_collection,
ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
.enable_blob_garbage_collection,
+23 -1
View File
@@ -34,6 +34,7 @@
#include "rocksdb/utilities/write_batch_with_index.h"
#include "test_util/testutil.h"
#include "util/cast_util.h"
#include "util/simple_mixed_compressor.h"
#include "utilities/backup/backup_engine_impl.h"
#include "utilities/fault_injection_fs.h"
#include "utilities/fault_injection_secondary_cache.h"
@@ -3411,7 +3412,28 @@ void StressTest::Open(SharedState* shared, bool reopen) {
InitializeOptionsFromFlags(cache_, filter_policy_, options_);
}
InitializeOptionsGeneral(cache_, filter_policy_, sqfc_factory_, options_);
if (!strcasecmp(FLAGS_compression_manager.c_str(), "mixed")) {
// Currently limited to ZSTD compression. Table property compression_name
// needs to set to zstd for now even when there can be more than one
// algorithm in the table under your compressor.
options_.compression = kZSTD;
options_.bottommost_compression = kZSTD;
if (!ZSTD_Supported()) {
fprintf(stderr,
"ZSTD compression not supported thus mixed compression cannot be "
"used\n");
exit(1);
}
auto mgr = std::make_shared<RoundRobinManager>(
GetDefaultBuiltinCompressionManager());
options_.compression_manager = mgr;
} else if (!strcasecmp(FLAGS_compression_manager.c_str(), "none")) {
// Nothing to do using default compression manager
} else {
fprintf(stderr, "Unknown compression manager: %s\n",
FLAGS_compression_manager.c_str());
exit(1);
}
if (FLAGS_prefix_size == 0 && FLAGS_rep_factory == kHashSkipList) {
fprintf(stderr,
"prefeix_size cannot be zero if memtablerep == prefix_hash\n");
+12 -3
View File
@@ -347,7 +347,9 @@ default_params = {
"memtable_op_scan_flush_trigger": lambda: random.choice([0, 10, 100, 1000]),
"memtable_avg_op_scan_flush_trigger": lambda: random.choice([0, 2, 20, 200]),
"ingest_wbwi_one_in": lambda: random.choice([0, 0, 100, 500]),
"compression_manager": lambda: random.choice(["mixed", "none"]),
}
_TEST_DIR_ENV_VAR = "TEST_TMPDIR"
# If TEST_TMPDIR_EXPECTED is not specified, default value will be TEST_TMPDIR
_TEST_EXPECTED_DIR_ENV_VAR = "TEST_TMPDIR_EXPECTED"
@@ -995,10 +997,17 @@ def finalize_and_sanitize(src_params):
# have to disable metadata write fault injection to other file
dest_params["exclude_wal_from_write_fault_injection"] = 1
dest_params["metadata_write_fault_one_in"] = 0
# Enabling block_align with compression is not supported
if dest_params.get("block_align") == 1:
dest_params["compression_type"] = "none"
# Disabling block align if mixed manager is neing used
if dest_params.get("compression_manager") == "mixed":
if dest_params.get("block_align") == 1:
dest_params["block_align"] = 0
dest_params["compression_type"] = "zstd"
dest_params["bottommost_compression_type"] = "none"
else:
# Enabling block_align with compression is not supported
if dest_params.get("block_align") == 1:
dest_params["compression_type"] = "none"
dest_params["bottommost_compression_type"] = "none"
# If periodic_compaction_seconds is not set, daily_offpeak_time_utc doesn't do anything
if dest_params.get("periodic_compaction_seconds") == 0:
dest_params["daily_offpeak_time_utc"] = ""
+7 -5
View File
@@ -44,6 +44,7 @@
#include "util/cast_util.h"
#include "util/coding.h"
#include "util/file_checksum_helper.h"
#include "util/simple_mixed_compressor.h"
#include "util/stderr_logger.h"
#include "util/string_util.h"
#include "util/write_batch_util.h"
@@ -867,11 +868,12 @@ bool LDBCommand::ParseCompressionTypeOption(
"No compressions are supported in this build for \"mixed\".");
return false;
}
// A temporary hack to generate an SST file with a mix of compression
// types, as this has been *de facto* supported for a long time on the
// read side with no code to generate them on the write side. We can test
// that functionality, e.g. in check_format_compatible.sh, with this hack
g_hack_mixed_compression.StoreRelaxed(1);
options_.compression = kZSTD;
options_.bottommost_compression = kZSTD;
auto mgr = std::make_shared<RoundRobinManager>(
GetDefaultBuiltinCompressionManager());
options_.compression_manager = mgr;
// Need to list zstd in the compression_name table property if it's
// potentially used by being in the mix (i.e., potentially at least one
// data block in the table is compressed by zstd). This ensures proper
-16
View File
@@ -254,7 +254,6 @@ class BuiltinCompressorV2 : public Compressor {
void ReleaseWorkingArea(WorkingArea* wa) override {
delete static_cast<CompressionContext*>(wa);
}
Status CompressBlock(Slice uncompressed_data, std::string* compressed_output,
CompressionType* out_compression_type,
ManagedWorkingArea* wa) override {
@@ -264,17 +263,6 @@ class BuiltinCompressorV2 : public Compressor {
ctx = static_cast<CompressionContext*>(wa->get());
}
CompressionType type = type_;
#ifndef NDEBUG
if (type != kNoCompression && g_hack_mixed_compression.LoadRelaxed() > 0U) {
// To assert that if zstd is in the mix, the compression_name table
// property (which comes from `type_`) needs to be set to kZSTD, for
// proper handling of context and dictionaries.
assert(!ZSTD_Supported() || type == kZSTD);
const auto& compressions = GetSupportedCompressions();
auto counter = g_hack_mixed_compression.FetchAddRelaxed(1);
type = compressions[counter % compressions.size()];
}
#endif // !NDEBUG
if (ctx == nullptr) {
tmp_ctx.emplace(type, opts_);
ctx = &*tmp_ctx;
@@ -821,7 +809,6 @@ Status BuiltinDecompressorV2OptimizeZstd::MaybeCloneForDict(
serialized_dict);
return Status::OK();
}
class BuiltinCompressionManagerV2 : public CompressionManager {
public:
BuiltinCompressionManagerV2() = default;
@@ -954,7 +941,4 @@ GetDefaultBuiltinCompressionManager() {
// END built-in implementation of customization interface
// ***********************************************************************
#ifndef NDEBUG
RelaxedAtomic<uint64_t> g_hack_mixed_compression{0};
#endif // !NDEBUG
} // namespace ROCKSDB_NAMESPACE
-6
View File
@@ -1988,10 +1988,4 @@ class ZSTDStreamingUncompress final : public StreamingUncompress {
#endif
};
#ifndef NDEBUG
// 0 == disable the hack
// > 0 => counter for rotating through compression types
extern RelaxedAtomic<uint64_t> g_hack_mixed_compression;
#endif
} // namespace ROCKSDB_NAMESPACE
+130
View File
@@ -0,0 +1,130 @@
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
//
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
#pragma once
#include <random>
#include "compression.h"
#include "options/options_helper.h"
#include "rocksdb/advanced_compression.h"
namespace ROCKSDB_NAMESPACE {
class MultiCompressorWrapper : public Compressor {
public:
explicit MultiCompressorWrapper(const CompressionOptions& opts,
CompressionType type,
CompressionDict&& dict = {}) {
assert(type != kNoCompression);
assert(type == kZSTD);
auto builtInManager = GetDefaultBuiltinCompressionManager();
const auto& compressions = GetSupportedCompressions();
for (auto type_ : compressions) {
if (type_ == kNoCompression) { // Avoid no compression
continue;
}
compressors_.push_back(builtInManager->GetCompressor(opts, type_));
}
(void)dict;
(void)type;
}
size_t GetMaxSampleSizeIfWantDict(CacheEntryRole block_type) const override {
return compressors_.back()->GetMaxSampleSizeIfWantDict(block_type);
}
Slice GetSerializedDict() const override {
return compressors_.back()->GetSerializedDict();
}
CompressionType GetPreferredCompressionType() const override { return kZSTD; }
ManagedWorkingArea ObtainWorkingArea() override {
return compressors_.back()->ObtainWorkingArea();
}
virtual std::unique_ptr<Compressor> MaybeCloneSpecialized(
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override {
return compressors_.back()->MaybeCloneSpecialized(block_type,
std::move(dict_samples));
}
protected:
std::vector<std::unique_ptr<Compressor>> compressors_;
private:
mutable std::mutex mutex_; // Protects access to current_index_
};
struct SimpleMixedCompressor : public MultiCompressorWrapper {
using MultiCompressorWrapper::MultiCompressorWrapper;
Status CompressBlock(Slice uncompressed_data, std::string* compressed_output,
CompressionType* out_compression_type,
ManagedWorkingArea* wa) override {
const auto& compressions = GetSupportedCompressions();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(
1, (int)compressions.size() - 2); // avoiding no compression and zstd
auto selected = dis(gen);
auto& compressor = compressors_[selected % compressors_.size()];
// fprintf(stdout, "[MultiCompressorWrapper] selected compressor
// typeint:%d\n",
// selected);
Status status = compressor->CompressBlock(
uncompressed_data, compressed_output, out_compression_type, wa);
return status;
}
};
class SimpleMixedCompressionManager : public CompressionManagerWrapper {
using CompressionManagerWrapper::CompressionManagerWrapper;
const char* Name() const override { return wrapped_->Name(); }
std::unique_ptr<Compressor> GetCompressorForSST(
const FilterBuildingContext& context, const CompressionOptions& opts,
CompressionType preferred) override {
assert(preferred == kZSTD);
(void)context;
return std::make_unique<SimpleMixedCompressor>(opts, preferred);
}
};
struct RoundRobinCompressor : public MultiCompressorWrapper {
using MultiCompressorWrapper::MultiCompressorWrapper;
Status CompressBlock(Slice uncompressed_data, std::string* compressed_output,
CompressionType* out_compression_type,
ManagedWorkingArea* wa) override {
const auto& compressions = GetSupportedCompressions();
auto counter = block_counter.FetchAddRelaxed(1);
auto sel_idx = counter % (compressions.size() - 1);
auto& compressor = compressors_[sel_idx];
// auto type = compressions[sel_idx];
// fprintf(stdout,
// "[CompressorWrapper] selected compression algo: %s typeint:%d\n",
// std::to_string(type).c_str(), type);
return compressor->CompressBlock(uncompressed_data, compressed_output,
out_compression_type, wa);
}
static RelaxedAtomic<uint64_t> block_counter;
};
RelaxedAtomic<uint64_t> RoundRobinCompressor::block_counter{0};
class RoundRobinManager : public CompressionManagerWrapper {
using CompressionManagerWrapper::CompressionManagerWrapper;
const char* Name() const override { return wrapped_->Name(); }
std::unique_ptr<Compressor> GetCompressorForSST(
const FilterBuildingContext& context, const CompressionOptions& opts,
CompressionType preferred) override {
assert(preferred == kZSTD);
(void)context;
// fprintf(stdout,
// "[CompressorWrapper] selected compression algo: %s typeint:%d\n",
// void)context;
return std::make_unique<RoundRobinCompressor>(opts, preferred);
}
};
} // namespace ROCKSDB_NAMESPACE