Change default compression from Snappy to LZ4 (#14818)

Summary:
The historical default block compression `kSnappyCompression` dates to when Snappy was the obvious fast/cheap choice. On modern server CPUs LZ4 matches or beats Snappy on compression ratio while decompressing substantially cheaper, so it is a better default. This changes the default `ColumnFamilyOptions::compression` to `kLZ4Compression`, with a runtime fallback of LZ4 -> Snappy -> none depending on what is compiled into the binary (new `GetDefaultCompressionType()` in util/compression.h). Only column families that do not explicitly set `compression` are affected (including compaction output when
`CompactionOptions::compression == kDisableCompressionOption`), and only newly written SST files; existing data is read as before. Doc comments, the Java bindings, and the sorted_run_builder example are updated to describe the new default.

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

Test Plan:
Adjusted two unit tests that implicitly depended on the old Snappy default to pin `compression = kSnappyCompression`: db_iterator_test's ReadAhead (readahead byte thresholds assume Snappy-sized files) and compaction_service_test's CustomFileChecksum (LSM shape after auto-compaction determined whether a manual CompactRange had work to do).

This change is primarily validated by performance testing. Added a `compressreject` db_bench benchmark (output buffer sized just below the predicted compressed size) to measure the cost of attempting then declining compression, alongside `compress`/`uncompress`. Both db_bench and sst_dump compress/decompress a modest block at a time, as in a real workload.

sst_dump on SST files from production workloads (4 files, 16KB blocks, single thread) on recent server-class AMD, Intel, and ARM CPUs, LZ4 vs Snappy:
  - Compression ratio: comparable; LZ4 is slightly smaller on the more compressible files (up to ~8%) and within ~2% on the rest.
  - Compression (write) CPU: a wash, within ~2% either direction.
  - Decompression (read) CPU: the clear win -- Snappy costs ~1.2x-1.5x as much as LZ4, i.e. LZ4 saves ~25-30% read CPU, consistently across AMD, Intel, and ARM.

db_bench synthetic workload (100-byte values), at 1 / 12 / 160 threads, LZ4 vs Snappy:
  - Compression throughput: LZ4 ~10-30% higher.
  - Decompression throughput: LZ4 much higher, and the advantage grows with core count -- from ~+12% at 12 threads to ~+45-50% at 160 threads, i.e. better multi-core scaling.
  - Rejection (insufficient ratio) path: comparable; LZ4 ~15-18% faster on compressible-but-rejected blocks and Snappy within ~10% on barely-compressible blocks. No meaningful regression, confirming incompressible data is still efficiently detected and stored raw.

Reviewed By: xingbowang

Differential Revision: D107536490

Pulled By: pdillinger

fbshipit-source-id: f8abaee630d782674778338148e36ed0c84e3661
This commit is contained in:
Peter Dillinger
2026-06-04 10:48:27 -07:00
committed by meta-codesync[bot]
parent 5177a8e6c2
commit 8053b9414f
11 changed files with 109 additions and 18 deletions
+4
View File
@@ -1316,6 +1316,10 @@ TEST_F(CompactionServiceTest, TruncatedOutput) {
TEST_F(CompactionServiceTest, CustomFileChecksum) { TEST_F(CompactionServiceTest, CustomFileChecksum) {
Options options = CurrentOptions(); Options options = CurrentOptions();
// Pin compression so the auto-compacted LSM shape (and thus whether the
// manual CompactRange below schedules a remote compaction) doesn't depend on
// the default compression type. kNoCompression is always available.
options.compression = kNoCompression;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory(); options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
ReopenWithCompactionService(&options); ReopenWithCompactionService(&options);
GenerateTestData(); GenerateTestData();
+4
View File
@@ -2277,6 +2277,10 @@ TEST_P(DBIteratorTest, ReadAhead) {
options.env = env_; options.env = env_;
options.disable_auto_compactions = true; options.disable_auto_compactions = true;
options.write_buffer_size = 4 << 20; options.write_buffer_size = 4 << 20;
// Pin compression so the readahead byte thresholds below don't depend on the
// default compression type. kNoCompression keeps the SST files large enough
// to exercise readahead and is always available.
options.compression = kNoCompression;
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics(); options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
BlockBasedTableOptions table_options; BlockBasedTableOptions table_options;
table_options.block_size = 1024; table_options.block_size = 1024;
+11 -7
View File
@@ -191,18 +191,22 @@ struct ColumnFamilyOptions : public AdvancedColumnFamilyOptions {
// Compress blocks using the specified compression algorithm. // Compress blocks using the specified compression algorithm.
// //
// Default: kSnappyCompression, if it's supported. If snappy is not linked // Default: kLZ4Compression if support is compiled in, else kSnappyCompression
// with the library, the default is kNoCompression. // if support is compiled in, else kNoCompression
// //
// Typical speeds of kSnappyCompression on an Intel(R) Core(TM)2 2.4GHz: // Typical single-core speed of kLZ4Compression on an AMD EPYC-Genoa
// ~200-500MB/s compression // ~800 - 1200 MB/s compression
// ~400-800MB/s decompression // ~8000 - 16000 MB/s decompression
// and with a 160 threads to saturate the cores:
// ~60 - 90 GB/s compression
// ~900 - 1000 GB/s decompression
// using db_bench compress/uncompress benchmarks.
// //
// Note that these speeds are significantly faster than most // Note that these speeds are significantly faster than most
// persistent storage speeds, and therefore it is typically never // persistent storage speeds, and therefore it is typically never
// worth switching to kNoCompression. Even if the input data is // worth switching to kNoCompression. Even if the input data is
// incompressible, the kSnappyCompression implementation will // incompressible, the compression implementation will
// efficiently detect that and will switch to uncompressed mode. // efficiently detect that and fall back on no compression.
// //
// If you do not set `compression_opts.level`, or set it to // If you do not set `compression_opts.level`, or set it to
// `CompressionOptions::kDefaultCompressionLevel`, we will attempt to pick the // `CompressionOptions::kDefaultCompressionLevel`, we will attempt to pick the
@@ -31,8 +31,9 @@ struct SortedRunBuilderOptions {
uint64_t target_file_size_bytes = 64 * 1024 * 1024; uint64_t target_file_size_bytes = 64 * 1024 * 1024;
// Compression type for output SST files. // Compression type for output SST files.
// Default: kNoCompression (always available). Set to kSnappyCompression, // Default: kNoCompression (always available). Set to kLZ4Compression,
// kZSTD, etc. if the desired library is linked with your binary. // kSnappyCompression, kZSTD, etc. if the desired library is linked with
// your binary.
CompressionType compression = kNoCompression; CompressionType compression = kNoCompression;
// Max number of background compaction threads. // Max number of background compaction threads.
@@ -33,7 +33,7 @@ public class CompactionOptions extends RocksObject {
/** /**
* Set the compaction output compression type. * Set the compaction output compression type.
* *
* Default: snappy * Default: lz4 if available, otherwise snappy if available, otherwise none
* *
* If set to {@link CompressionType#DISABLE_COMPRESSION_OPTION}, * If set to {@link CompressionType#DISABLE_COMPRESSION_OPTION},
* RocksDB will choose compression type according to the * RocksDB will choose compression type according to the
@@ -136,7 +136,8 @@ public interface MutableColumnFamilyOptionsInterface<
* Compress blocks using the specified compression algorithm. This * Compress blocks using the specified compression algorithm. This
* parameter can be changed dynamically. * parameter can be changed dynamically.
* <p> * <p>
* Default: SNAPPY_COMPRESSION, which gives lightweight but fast compression. * Default: LZ4_COMPRESSION if available, otherwise SNAPPY_COMPRESSION if
* available, otherwise NO_COMPRESSION.
* *
* @param compressionType Compression Type. * @param compressionType Compression Type.
* @return the reference to the current option. * @return the reference to the current option.
@@ -148,7 +149,8 @@ public interface MutableColumnFamilyOptionsInterface<
* Compress blocks using the specified compression algorithm. This * Compress blocks using the specified compression algorithm. This
* parameter can be changed dynamically. * parameter can be changed dynamically.
* <p> * <p>
* Default: SNAPPY_COMPRESSION, which gives lightweight but fast compression. * Default: LZ4_COMPRESSION if available, otherwise SNAPPY_COMPRESSION if
* available, otherwise NO_COMPRESSION.
* *
* @return Compression type. * @return Compression type.
*/ */
+1 -1
View File
@@ -253,7 +253,7 @@ struct MutableCFOptions {
max_sequential_skip_in_iterations(0), max_sequential_skip_in_iterations(0),
paranoid_file_checks(false), paranoid_file_checks(false),
report_bg_io_stats(false), report_bg_io_stats(false),
compression(Snappy_Supported() ? kSnappyCompression : kNoCompression), compression(GetDefaultCompressionType()),
bottommost_compression(kDisableCompressionOption), bottommost_compression(kDisableCompressionOption),
last_level_temperature(Temperature::kUnknown), last_level_temperature(Temperature::kUnknown),
default_write_temperature(Temperature::kUnknown), default_write_temperature(Temperature::kUnknown),
+1 -1
View File
@@ -129,7 +129,7 @@ AdvancedColumnFamilyOptions::AdvancedColumnFamilyOptions(const Options& options)
} }
ColumnFamilyOptions::ColumnFamilyOptions() ColumnFamilyOptions::ColumnFamilyOptions()
: compression(Snappy_Supported() ? kSnappyCompression : kNoCompression), : compression(GetDefaultCompressionType()),
table_factory( table_factory(
std::shared_ptr<TableFactory>(new BlockBasedTableFactory())) {} std::shared_ptr<TableFactory>(new BlockBasedTableFactory())) {}
+73 -4
View File
@@ -219,6 +219,10 @@ DEFINE_string(
"\txxhash -- repeated xxHash of <block size> data\n" "\txxhash -- repeated xxHash of <block size> data\n"
"\txxhash64 -- repeated xxHash64 of <block size> data\n" "\txxhash64 -- repeated xxHash64 of <block size> data\n"
"\txxh3 -- repeated XXH3 of <block size> data\n" "\txxh3 -- repeated XXH3 of <block size> data\n"
"\tcompressreject -- repeated compression of <block size> data forced to "
"be rejected (output buffer ~10% of input smaller than the predicted "
"compressed size), to measure the cost of attempting then declining "
"compression\n"
"\tacquireload -- load N*1000 times\n" "\tacquireload -- load N*1000 times\n"
"\tfillseekseq -- write N values in sequential key, then read " "\tfillseekseq -- write N values in sequential key, then read "
"them by seeking to each key\n" "them by seeking to each key\n"
@@ -3994,6 +3998,8 @@ class Benchmark {
method = &Benchmark::AcquireLoad; method = &Benchmark::AcquireLoad;
} else if (name == "compress") { } else if (name == "compress") {
method = &Benchmark::Compress; method = &Benchmark::Compress;
} else if (name == "compressreject") {
method = &Benchmark::CompressReject;
} else if (name == "uncompress") { } else if (name == "uncompress") {
method = &Benchmark::Uncompress; method = &Benchmark::Uncompress;
} else if (name == "randomtransaction") { } else if (name == "randomtransaction") {
@@ -4506,8 +4512,8 @@ class Benchmark {
auto working_area = compressor->ObtainWorkingArea(); auto working_area = compressor->ObtainWorkingArea();
GrowableBuffer compressed; GrowableBuffer compressed;
// Compress 1G // Compress 3G
while (bytes < int64_t(1) << 30) { while (bytes < int64_t(3) << 30) {
compressed.ResetForSize(input.size()); compressed.ResetForSize(input.size());
CompressionType actual_type = kNoCompression; CompressionType actual_type = kNoCompression;
s = compressor->CompressBlock(input, compressed.data(), s = compressor->CompressBlock(input, compressed.data(),
@@ -4536,6 +4542,68 @@ class Benchmark {
} }
} }
// Like Compress, but forces every block's compression to be rejected by
// giving the compressor an output buffer slightly smaller than the data is
// expected to compress to. This exercises (and measures the speed of) the
// path that attempts compression but falls back to storing data uncompressed,
// as happens for incompressible data.
void CompressReject(ThreadState* thread) {
RandomGenerator gen;
Slice input = gen.Generate(FLAGS_block_size);
int64_t bytes = 0;
Status s;
auto compressor = GetCompressor();
if (!compressor) {
thread->stats.AddMessage("(compression type not supported)");
return;
}
auto working_area = compressor->ObtainWorkingArea();
// RandomGenerator produces data that compresses to about
// FLAGS_compression_ratio of its original size (see CompressibleString),
// which should be an accurate/optimistic predictor of the compressed size.
// Make the output buffer 10% of the uncompressed size smaller than that
// predicted compressed size, so the real output won't fit and compression
// is rejected on every call. Clamp to at least 1 byte, which should always
// reject.
double reject_fraction = std::max(0.0, FLAGS_compression_ratio - 0.10);
size_t reject_size = std::max<size_t>(
1, static_cast<size_t>(reject_fraction * input.size()));
GrowableBuffer compressed;
// Compress 3G
while (bytes < int64_t(3) << 30) {
compressed.ResetForSize(reject_size);
CompressionType actual_type = kNoCompression;
s = compressor->CompressBlock(input, compressed.data(),
&compressed.MutableSize(), &actual_type,
&working_area);
if (UNLIKELY(!s.ok())) {
break;
}
if (UNLIKELY(actual_type != kNoCompression)) {
s = Status::Aborted(
"Compression unexpectedly not rejected; try a smaller "
"compression_ratio");
break;
}
bytes += input.size();
thread->stats.FinishedOps(nullptr, nullptr, 1, kCompress);
}
if (!s.ok()) {
thread->stats.AddMessage("(compression failure: " + s.ToString() + ")");
} else {
char buf[340];
snprintf(buf, sizeof(buf), "(rejected; output buffer %.1f%% of input)",
(reject_size * 100.0) / input.size());
thread->stats.AddMessage(buf);
thread->stats.AddBytes(bytes);
}
}
void Uncompress(ThreadState* thread) { void Uncompress(ThreadState* thread) {
RandomGenerator gen; RandomGenerator gen;
Slice input = gen.Generate(FLAGS_block_size); Slice input = gen.Generate(FLAGS_block_size);
@@ -4568,8 +4636,9 @@ class Benchmark {
actual_type); actual_type);
auto decomp_working_area = decompressor->ObtainWorkingArea(actual_type); auto decomp_working_area = decompressor->ObtainWorkingArea(actual_type);
int64_t bytes = 0; uint64_t bytes = 0;
while (bytes < 1024 * 1048576) { // Decompress 20GB
while (bytes < uint64_t{20} * 1024U * 1048576U) {
Decompressor::Args args; Decompressor::Args args;
args.compression_type = actual_type; args.compression_type = actual_type;
args.compressed_data = compressed.AsSlice(); args.compressed_data = compressed.AsSlice();
@@ -0,0 +1 @@
The default `compression` for column families is now `kLZ4Compression` rather than `kSnappyCompression`. This affects only column families that do not explicitly set `compression`, and only newly-written SST files. The change is fully compatible: existing data remains readable with no migration needed, as RocksDB selects the decompressor per block. LZ4 offers slightly better compression ratios and decompression CPU efficiency vs. Snappy and similar compression CPU, tested across various server CPUs. When support is not compiled in, the fallback path for default compression is LZ4 -> Snappy -> NoCompression.
+6
View File
@@ -421,6 +421,12 @@ inline bool LZ4_Supported() {
#endif #endif
} }
inline CompressionType GetDefaultCompressionType() {
return LZ4_Supported()
? kLZ4Compression
: (Snappy_Supported() ? kSnappyCompression : kNoCompression);
}
inline bool XPRESS_Supported() { inline bool XPRESS_Supported() {
#ifdef XPRESS #ifdef XPRESS
return true; return true;