mirror of
https://github.com/facebook/rocksdb.git
synced 2026-10-23 08:09:02 +03:00
Summary: Auto-skip compression (stop attempting compression on data that isn't compressing well, to save CPU) was previously an experimental CompressionManager wrapper (CreateAutoSkipCompressionManager) whose predictor lived in each per-thread working area. Besides being fragmented across parallel workers and reset on a tumbling window, it decided based on how OFTEN blocks failed to compress, ignoring how MUCH they compress -- so on mixed data it could give up substantial compression to save a little CPU (see performance results). This makes auto-skip a first-class, optional, ratio-aware feature of the block-based table builder, controlled by two new CompressionOptions fields: - auto_skip (default false): the on/off switch. There is no separate threshold knob; the decision reuses max_compressed_bytes_per_kb (the existing per-block "minimum compression worth keeping" bar) as the bar for whether attempting compression on the stream is worthwhile. - auto_skip_min_sample_every: sampling interval that bounds the local impact around a transition back to compressible data. 0 selects an internal default (currently 64 skipped data blocks between forced samples). The emit thread makes the skip/attempt decision once per data block (serial and parallel paths alike; data blocks only), so a skipped block is never dispatched to a compression worker. It is driven by a continuously updated, shared estimate q_hat of the aggregate stored/uncompressed ratio the builder would achieve by attempting -- each block counted at its achieved ratio, or 1.0 when stored raw: attempt while q_hat is at or below the bar, otherwise skip. Because the estimate is payoff-weighted, a stream whose blocks each save little -- or where only a few blocks compress well -- does not clear the bar and is skipped, while a stream that compresses substantially is attempted. The compression given up per KB is bounded by (1024 - max_compressed_bytes_per_kb). q_hat uses an asymmetric exponentially weighted moving average (EWMA), slightly faster toward better compression than toward worse; the fast side is deliberately gentle so a single marginally-compressible sample does not flip a whole file back to attempting. It is kept on its own cache line to avoid false sharing. Under parallel compression q_hat is updated with relaxed atomics, and the emit thread can decide from a value that lags the learning it drives by the in-flight depth -- tolerable concessions of accuracy and responsiveness for a coarse heuristic. The first data block of each file is always attempted (the sample countdown starts at 0), and while skipping the builder keeps sampling (one block after a randomized gap) so a return to compressible data is detected. q_hat is carried across the files a flush/compaction thread emits via thread_local state keyed by preferred compression type, so a sustained region does not re-learn on every file. The old CompressionManager-based AutoSkip is removed (CostAware is retained); db_stress, db_bench, and db_crashtest are migrated to the new option. db_bench is extended so it can generate the relevant regimes: --compressible_data_fraction and --compressible_data_run_length produce a mix of compressible/incompressible blocks -- i.i.d. per value (run length 1) or in alternating regions (large run length). The default (fraction 1.0) reproduces the historical single-pool behavior. Pull Request resolved: https://github.com/facebook/rocksdb/pull/15079 Test Plan: New/updated unit tests in `util/compression_test.cc` (`DBAutoSkipTest`): - skips sustained incompressible data; - never skips compressible data; - disabled by default never bypasses a block; - 4-thread parallel compression round-trips all data across alternating compressible/incompressible regions. **Performance** (`db_bench fillseq`, release, zstd level 3; 100k values of 16 KiB each, roughly one value per data block; WAL and auto-compaction off; median of 5 runs on a quiet host). Auto-skip trades one cost for another, so each cell reports two costs, both lower-is-better: CPU cost as process CPU time (user+sys) per GB of uncompressed input (s/GB; ~1.64 GB uncompressed here), and storage cost as the resulting DB size. Auto-skip does not appear to change parallelizability, so wall-clock s/GB tracks CPU s/GB (the two agree here); CPU s/GB is reported. Percentages are relative to the no-auto-skip baseline: negative CPU = cheaper, positive size = more storage (shown only where storage grows). | Workload | baseline (off) | (a) old wrapper | (b) draft | (c) this version | |---|---|---|---|---| | Incompressible | 2.33 s/GB, 1568 MB | 0.96 (-59%), 1568 MB | 0.82 (-65%), 1568 MB | 0.82 (-65%), 1568 MB | | Compressible | 3.55 s/GB, 710 MB | 3.54 (~0%), 710 MB | 3.56 (~0%), 710 MB | 3.55 (~0%), 710 MB | | Marginal-payoff mix | 3.80 s/GB, 1376 MB | 1.54 (-59%), 1523 MB (+11%) | 3.81 (~0%), 1376 MB | 1.43 (-62%), 1531 MB (+11%) | | High-payoff mix | 2.20 s/GB, 929 MB | 1.14 (-48%), 1417 MB (+53%) | 2.16 (~0%), 929 MB | 2.17 (~0%), 929 MB | A strategy wins by cutting CPU cost without paying much storage cost. On incompressible data all three cut CPU ~60% at no storage cost; on compressible data none skip (within run-to-run noise). The two mixes (50% incompressible, 50% compressible per block) separate the strategies: - Marginal-payoff mix: the compressible half barely clears the per-block bar, so the stream is not worth compressing -- skipping is right. (c) and (a) cut CPU ~60% for ~11% more storage. That extra storage is compression (c) intentionally declines because its aggregate benefit is below the configured worthwhile threshold (max_compressed_bytes_per_kb, ~12.5% per KB); it stays within the (1024 - max_compressed_bytes_per_kb) bound. The ratio-unaware draft (b) cannot see the low payoff, keeps compressing, and saves no CPU. - High-payoff mix: the compressible half compresses hard, so compressing is right. (c) and (b) keep full compression at baseline storage; the old wrapper (a) cuts CPU 48% but inflates storage +53% -- far beyond that bound -- giving up worthwhile compression. Only (c) makes the right call on both mixes: it never gives up worthwhile compression (unlike (a)) and does not waste CPU on low-payoff data (unlike (b)). Reviewed By: xingbowang Differential Revision: D115444567 Pulled By: pdillinger fbshipit-source-id: 05e122d25de95ddcd0edeb0dd07c9d25bead7293
201 lines
8.5 KiB
C++
201 lines
8.5 KiB
C++
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
// 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).
|
|
//
|
|
|
|
#include "util/auto_tune_compressor.h"
|
|
|
|
#include "options/options_helper.h"
|
|
#include "rocksdb/advanced_compression.h"
|
|
#include "test_util/sync_point.h"
|
|
#include "util/random.h"
|
|
#include "util/stop_watch.h"
|
|
namespace ROCKSDB_NAMESPACE {
|
|
const std::vector<std::vector<int>> CostAwareCompressor::kCompressionLevels{
|
|
{0}, // KSnappyCompression
|
|
{}, // kZlibCompression
|
|
{}, // kBZip2Compression
|
|
{-1, -4, -9}, // kLZ4Compression (negative => LZ4 fast acceleration)
|
|
{1, 4, 9}, // kLZ4HCCompression
|
|
{}, // kXpressCompression
|
|
{1, 15, 22} // kZSTD
|
|
};
|
|
|
|
CostAwareCompressor::CostAwareCompressor(const CompressionOptions& opts)
|
|
: opts_(opts) {
|
|
// Creates compressor supporting all the compression types and levels as per
|
|
// the compression levels set in vector CompressionLevels
|
|
auto builtInManager = GetBuiltinV2CompressionManager();
|
|
const auto& compressions = GetSupportedCompressions();
|
|
for (size_t i = 0; i < kCompressionLevels.size(); i++) {
|
|
CompressionType type = static_cast<CompressionType>(i + 1);
|
|
if (type == kNoCompression) {
|
|
continue;
|
|
}
|
|
if (kCompressionLevels[type - 1].size() == 0) {
|
|
allcompressors_.emplace_back();
|
|
continue;
|
|
} else {
|
|
// if the compression type is not supported, then skip and remove
|
|
// compression levels from the supported compression level list
|
|
if (std::find(compressions.begin(), compressions.end(), type) ==
|
|
compressions.end()) {
|
|
allcompressors_.emplace_back();
|
|
continue;
|
|
}
|
|
std::vector<std::unique_ptr<Compressor>> compressors_diff_levels;
|
|
for (size_t j = 0; j < kCompressionLevels[type - 1].size(); j++) {
|
|
auto level = kCompressionLevels[type - 1][j];
|
|
CompressionOptions new_opts = opts;
|
|
new_opts.level = level;
|
|
compressors_diff_levels.push_back(
|
|
builtInManager->GetCompressor(new_opts, type));
|
|
allcompressors_index_.emplace_back(i, j);
|
|
}
|
|
allcompressors_.push_back(std::move(compressors_diff_levels));
|
|
}
|
|
}
|
|
}
|
|
|
|
const char* CostAwareCompressor::Name() const { return "CostAwareCompressor"; }
|
|
|
|
std::unique_ptr<Compressor> CostAwareCompressor::Clone() const {
|
|
return std::make_unique<CostAwareCompressor>(opts_);
|
|
}
|
|
Compressor::DictConfig CostAwareCompressor::GetDictGuidance(
|
|
CacheEntryRole block_type) const {
|
|
auto idx = allcompressors_index_.back();
|
|
return allcompressors_[idx.first][idx.second]->GetDictGuidance(block_type);
|
|
}
|
|
|
|
Slice CostAwareCompressor::GetSerializedDict() const {
|
|
auto idx = allcompressors_index_.back();
|
|
return allcompressors_[idx.first][idx.second]->GetSerializedDict();
|
|
}
|
|
|
|
CompressionType CostAwareCompressor::GetPreferredCompressionType() const {
|
|
return kZSTD;
|
|
}
|
|
std::unique_ptr<Compressor> CostAwareCompressor::MaybeCloneSpecialized(
|
|
CacheEntryRole block_type, DictConfigArgs&& dict_config) const {
|
|
// TODO: full dictionary compression support. Currently this just falls
|
|
// back on a non-multi compressor when asked to use a dictionary.
|
|
auto idx = allcompressors_index_.back();
|
|
return allcompressors_[idx.first][idx.second]->MaybeCloneSpecialized(
|
|
block_type, std::move(dict_config));
|
|
}
|
|
Status CostAwareCompressor::CompressBlock(Slice uncompressed_data,
|
|
char* compressed_output,
|
|
size_t* compressed_output_size,
|
|
CompressionType* out_compression_type,
|
|
ManagedWorkingArea* wa) {
|
|
// Check if the managed working area is provided or owned by this object.
|
|
// If not, bypass compressor logic since the working area lacks a predictor
|
|
if (allcompressors_.size() == 0) {
|
|
return Status::NotSupported("No compression type supported");
|
|
}
|
|
if (wa == nullptr || wa->owner() != this) {
|
|
// highest compression level of Zstd
|
|
size_t choosen_compression_type = 6;
|
|
size_t compression_level_ptr = 2;
|
|
return allcompressors_[choosen_compression_type][compression_level_ptr]
|
|
->CompressBlock(uncompressed_data, compressed_output,
|
|
compressed_output_size, out_compression_type, wa);
|
|
}
|
|
auto local_wa = static_cast<CostAwareWorkingArea*>(wa->get());
|
|
std::pair<size_t, size_t> choosen_index(6, 2);
|
|
size_t choosen_compression_type = choosen_index.first;
|
|
size_t compresion_level_ptr = choosen_index.second;
|
|
return CompressBlockAndRecord(choosen_compression_type, compresion_level_ptr,
|
|
uncompressed_data, compressed_output,
|
|
compressed_output_size, out_compression_type,
|
|
local_wa);
|
|
}
|
|
|
|
Compressor::ManagedWorkingArea CostAwareCompressor::ObtainWorkingArea() {
|
|
auto wrap_wa = allcompressors_.back().back()->ObtainWorkingArea();
|
|
auto wa = new CostAwareWorkingArea(std::move(wrap_wa));
|
|
// Create cost predictors for each compression type and level
|
|
wa->cost_predictors_.reserve(allcompressors_.size());
|
|
for (size_t i = 0; i < allcompressors_.size(); i++) {
|
|
CompressionType type = static_cast<CompressionType>(i + 1);
|
|
if (allcompressors_[type - 1].size() == 0) {
|
|
wa->cost_predictors_.emplace_back();
|
|
continue;
|
|
} else {
|
|
std::vector<IOCPUCostPredictor*> predictors_diff_levels;
|
|
predictors_diff_levels.reserve(kCompressionLevels[type - 1].size());
|
|
for (size_t j = 0; j < kCompressionLevels[type - 1].size(); j++) {
|
|
predictors_diff_levels.emplace_back(new IOCPUCostPredictor(10));
|
|
}
|
|
wa->cost_predictors_.emplace_back(std::move(predictors_diff_levels));
|
|
}
|
|
}
|
|
return ManagedWorkingArea(wa, this);
|
|
}
|
|
void CostAwareCompressor::ReleaseWorkingArea(WorkingArea* wa) {
|
|
// remove all created cost predictors
|
|
for (auto& prdictors_diff_levels :
|
|
static_cast<CostAwareWorkingArea*>(wa)->cost_predictors_) {
|
|
for (auto& predictor : prdictors_diff_levels) {
|
|
delete predictor;
|
|
}
|
|
}
|
|
delete static_cast<CostAwareWorkingArea*>(wa);
|
|
}
|
|
|
|
Status CostAwareCompressor::CompressBlockAndRecord(
|
|
size_t choosen_compression_type, size_t compression_level_ptr,
|
|
Slice uncompressed_data, char* compressed_output,
|
|
size_t* compressed_output_size, CompressionType* out_compression_type,
|
|
CostAwareWorkingArea* wa) {
|
|
assert(choosen_compression_type < allcompressors_.size());
|
|
assert(compression_level_ptr <
|
|
allcompressors_[choosen_compression_type].size());
|
|
assert(choosen_compression_type < wa->cost_predictors_.size());
|
|
assert(compression_level_ptr <
|
|
wa->cost_predictors_[choosen_compression_type].size());
|
|
StopWatchNano<> timer(Env::Default()->GetSystemClock().get(), true);
|
|
Status status =
|
|
allcompressors_[choosen_compression_type][compression_level_ptr]
|
|
->CompressBlock(uncompressed_data, compressed_output,
|
|
compressed_output_size, out_compression_type,
|
|
&(wa->wrapped_));
|
|
std::pair<size_t, size_t> measured_data(timer.ElapsedMicros(),
|
|
*compressed_output_size);
|
|
auto predictor =
|
|
wa->cost_predictors_[choosen_compression_type][compression_level_ptr];
|
|
auto output_length = measured_data.second;
|
|
auto cpu_time = measured_data.first;
|
|
predictor->CPUPredictor.Record(cpu_time);
|
|
predictor->IOPredictor.Record(output_length);
|
|
TEST_SYNC_POINT_CALLBACK(
|
|
"CostAwareCompressor::CompressBlockAndRecord::GetPredictor",
|
|
wa->cost_predictors_[choosen_compression_type][compression_level_ptr]);
|
|
return status;
|
|
}
|
|
|
|
const char* CostAwareCompressorManager::Name() const {
|
|
// should have returned "CostAwareCompressorManager" but we currently have an
|
|
// error so for now returning name of the wrapped container
|
|
return wrapped_->Name();
|
|
}
|
|
|
|
std::unique_ptr<Compressor> CostAwareCompressorManager::GetCompressorForSST(
|
|
const FilterBuildingContext& context, const CompressionOptions& opts,
|
|
CompressionType preferred) {
|
|
assert(GetSupportedCompressions().size() > 1);
|
|
(void)context;
|
|
(void)preferred;
|
|
return std::make_unique<CostAwareCompressor>(opts);
|
|
}
|
|
|
|
std::shared_ptr<CompressionManagerWrapper> CreateCostAwareCompressionManager(
|
|
std::shared_ptr<CompressionManager> wrapped) {
|
|
return std::make_shared<CostAwareCompressorManager>(
|
|
wrapped == nullptr ? GetBuiltinV2CompressionManager() : wrapped);
|
|
}
|
|
|
|
} // namespace ROCKSDB_NAMESPACE
|