RocksDB 是一个广泛应用于生产环境的、非常复杂的存储引擎。直接阅读源码并非最佳入门方式。
你也可以用 RocksDB 实现一个类似 Redis/Cassandra 的服务,学习如何用 KV 拼接出上层数据结构。这样能对 RocksDB 形成直观理解,遇到问题时知道从哪里下手排查。
本文内容:
调优的前提是观测。没有数据就改参数,和赌博没什么区别。
Statistics
options.statistics = rocksdb::CreateDBStatistics();
// 随时获取人类可读的统计信息
std::string stats = options.statistics->ToString();
这会给你全局的操作计数、延迟分布等信息。开启有少量性能开销,但排查问题时非常有用。
Compaction and DB Stats
std::string value;
db->GetProperty("rocksdb.stats", &value);
db->GetProperty("rocksdb.cfstats", &value); // Column Family 级别
这是最重要的调试入口。你能看到:
Perf Context and IO Stats Context
用于追踪单个请求在各阶段的耗时:
rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex);
rocksdb::get_perf_context()->Reset();
// ... 执行操作 ...
std::string perf = rocksdb::get_perf_context()->ToString();
能看到 mutex 等待、block cache 查找、磁盘 IO 等各环节的时间。
RocksDB 的性能瓶颈往往体现在系统层面:
| 指标 | 症状 | 排查方向 |
|---|---|---|
| Disk Write Bandwidth | Write Stall,读延迟抖动 | 检查 compaction 是否跟得上,是否需要限速 |
| Disk Read IOPS | 读延迟高,P99 抖动 | 检查 block cache 命中率,bloom filter 是否生效 |
| CPU | 整体吞吐上不去 | 检查压缩算法、bloom filter、compaction 线程数 |
| Space | 磁盘快满了 | 检查压缩策略、level_compaction_dynamic_level_bytes |
用 iostat、dstat、perf top 等工具配合观察。
| 资源 | 说明 |
|---|---|
| RocksDB Tuning Guide | 官方调优指南,必读 |
| Setup Options and Basic Tuning | 基础配置建议 |
| Write Stalls | 写停顿的原因和解决方案 |
| Compaction | Compaction 机制详解 |
| Block Cache | 缓存配置 |
| Statistics | 统计信息详解 |
| Perf Context | 请求级追踪 |
RocksDB 是个 LSM-Tree 存储引擎。所有调优本质上都是在 读放大、写放大、空间放大 三者之间做权衡(RUM Conjecture)。你不可能同时优化这三个,最多优化其中两个。
数据先写入内存中的 MemTable,写满后变成 Immutable MemTable,然后 flush 到磁盘成为 L0 的 SST 文件。
MemTable 类型选择
| 类型 | 特点 | 适用场景 |
|---|---|---|
| SkipList(默认) | 有序,支持范围查询,并发写入友好 | 通用场景 |
| HashSkipList | 前缀哈希 + SkipList | 前缀扫描场景,需配合 prefix_extractor |
| HashLinkList | 前缀哈希 + 链表 | 每个前缀下 key 数量少的场景 |
| Vector | 无序,只支持追加 | 批量导入,写入后立即 flush |
大多数情况用默认的 SkipList 就够了。
关键参数
write_buffer_size:单个 MemTable 大小。太小导致频繁 flush(写放大↑),太大导致恢复慢max_write_buffer_number:允许多少个 MemTable 同时存在(包括 active 和 immutable)min_write_buffer_number_to_merge:flush 前先合并几个 MemTable,可减少写放大Block Cache 缓存从 SST 文件读上来的数据块、索引块、filter 块。
Cache 类型
| 类型 | 特点 |
|---|---|
| LRUCache(默认) | 标准 LRU,分 shard 减少锁竞争 |
| ClockCache | Clock 算法,锁竞争更小,但命中率略低 |
| HyperClockCache | 更激进的并发优化 |
关键配置
// 创建 Block Cache
auto cache = NewLRUCache(
capacity, // 大小,建议系统内存的 1/3 ~ 1/2
num_shard_bits, // 分片数 = 2^num_shard_bits,默认 6
strict_capacity_limit, // 是否严格限制容量
high_pri_pool_ratio // 高优先级池比例,放 index/filter
);
BlockBasedTableOptions table_options;
table_options.block_cache = cache;
table_options.cache_index_and_filter_blocks = true; // 重要!
table_options.pin_l0_filter_and_index_blocks_in_cache = true;
必须开启 cache_index_and_filter_blocks=true。否则 index 和 filter 会在堆外分配,内存不可控,容易 OOM。
Partitioned Index/Filter
对于大数据量场景(几百 GB 以上),index 和 filter 本身就很大。可以开启分区:
table_options.index_type = BlockBasedTableOptions::kTwoLevelIndexSearch;
table_options.partition_filters = true;
table_options.metadata_block_size = 4096;
这样只缓存用到的部分,减少内存占用。
LSM-Tree 的核心是把随机写变成顺序写。代价是后台需要不断做 compaction,合并整理数据。
三种 Compaction 策略
| 策略 | 写放大 | 读放大 | 空间放大 | 适用场景 |
|---|---|---|---|---|
| Level(默认) | 高(10-30x) | 低 | 低(~10%) | 读多写少,空间敏感 |
| Universal | 低(可控) | 高 | 高(可达 2x) | 写多读少,SSD 寿命敏感 |
| FIFO | 极低 | 高 | 无 | TTL 缓存场景 |
Level Compaction 的层级结构
L0: [SST] [SST] [SST] [SST] ← 可能有重叠
L1: [--SST--] [--SST--] ← 无重叠,有序
L2: [----SST----] [----SST----] [----SST----]
...
Lmax: 最大一层,存放最多数据
当 RocksDB 写入速度超过 compaction 处理速度时,会触发写限流或停写。
Stall 触发条件
| 条件 | 默认值 | 行为 |
|---|---|---|
L0 文件数 ≥ level0_slowdown_writes_trigger | 20 | 限速写入 |
L0 文件数 ≥ level0_stop_writes_trigger | 36 | 停止写入 |
| Pending compaction bytes 过多 | - | 限速/停写 |
| MemTable 数量过多 | - | 停止写入 |
诊断方法
db->GetProperty("rocksdb.is-write-stopped", &value);
db->GetProperty("rocksdb.actual-delayed-write-rate", &value);
或者看 LOG 文件里的 Stalling 关键字。
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
write_buffer_size | 64MB | 64-256MB | 单个 MemTable 大小 |
max_write_buffer_number | 2 | 3-6 | MemTable 最大数量 |
min_write_buffer_number_to_merge | 1 | 1-2 | flush 前合并几个 MemTable |
block_cache (capacity) | 8MB | 系统内存 1/3~1/2 | 读缓存大小 |
cache_index_and_filter_blocks | false | true | 必开,否则 OOM |
pin_l0_filter_and_index_blocks_in_cache | false | true | L0 的 filter 常驻缓存 |
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
compaction_style | Level | - | Level/Universal/FIFO |
max_background_jobs | 2 | CPU 核数的一半 | 后台线程数 |
max_subcompactions | 1 | 2-4 | 单个 compaction 的并行度 |
level0_file_num_compaction_trigger | 4 | 4-8 | L0 触发 compaction 的文件数 |
level0_slowdown_writes_trigger | 20 | 20-40 | L0 触发限速的文件数 |
level0_stop_writes_trigger | 36 | 36-64 | L0 触发停写的文件数 |
target_file_size_base | 64MB | 64-256MB | L1 单个 SST 文件大小 |
max_bytes_for_level_base | 256MB | 256MB-1GB | L1 层总大小 |
max_bytes_for_level_multiplier | 10 | 10 | 层间大小比例 |
level_compaction_dynamic_level_bytes | false | true | 动态调整层级大小,省空间 |
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
compression | Snappy | - | 全局压缩算法 |
compression_per_level | - | 见下文 | 分层压缩 |
bottommost_compression | - | ZSTD | 最底层压缩 |
block_size | 4KB | 4-16KB | 数据块大小,影响压缩率和点查 |
推荐的分层压缩配置:
options.compression_per_level = {
kNoCompression, // L0
kNoCompression, // L1
kLZ4Compression, // L2
kLZ4Compression, // L3
kZSTD, // L4
kZSTD, // L5
kZSTD // L6
};
上层热数据不压缩省 CPU,底层冷数据强压缩省空间。
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
filter_policy | nullptr | NewBloomFilterPolicy(10) | 必开,10 bits per key |
whole_key_filtering | true | true | 对完整 key 建 filter |
optimize_filters_for_hits | false | true (读重) | 最底层不建 filter,省空间 |
Bloom Filter 能极大减少点查时的磁盘 IO。10 bits per key 的误判率约 1%。
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
bytes_per_sync | 0 | 1MB | 写入多少字节后 sync,平滑 IO |
wal_bytes_per_sync | 0 | 1MB | WAL sync 频率 |
enable_pipelined_write | false | true | 提升并发写性能 |
allow_concurrent_memtable_write | true | true | 并发写 MemTable |
max_total_wal_size | 0 | - | WAL 总大小限制,触发 flush |
delete_obsolete_files_period_micros | 6h | - | 清理过期文件的周期 |
##典型工作负载配置
这类场景通常是 Redis 协议兼容,读写混合,需要平衡延迟和吞吐。
// 内存
options.write_buffer_size = 128 * 1024 * 1024; // 128MB
options.max_write_buffer_number = 4;
options.min_write_buffer_number_to_merge = 2;
// Compaction
options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 4;
options.level0_slowdown_writes_trigger = 20;
options.level0_stop_writes_trigger = 36;
options.max_background_jobs = 8;
options.level_compaction_dynamic_level_bytes = true;
// 压缩
options.compression_per_level = {kNoCompression, kNoCompression,
kLZ4Compression, kLZ4Compression, kZSTD, kZSTD, kZSTD};
// Cache(假设 32GB 内存的机器)
table_options.block_cache = NewLRUCache(10 * 1024 * 1024 * 1024LL); // 10GB
table_options.cache_index_and_filter_blocks = true;
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
Kvrocks 特有优化:
SQL 场景特点:大量点查、范围扫描、事务支持。
MyRocks 典型配置
rocksdb_max_open_files=-1
rocksdb_max_background_jobs=8
rocksdb_max_subcompactions=4
rocksdb_block_size=16384
rocksdb_block_cache_size=8G
# 写缓冲
rocksdb_write_buffer_size=256M
rocksdb_max_write_buffer_number=4
# Compaction
rocksdb_default_cf_options=
level0_file_num_compaction_trigger=4;
level0_slowdown_writes_trigger=20;
level0_stop_writes_trigger=36;
target_file_size_base=128M;
max_bytes_for_level_base=512M;
level_compaction_dynamic_level_bytes=true;
compression_per_level=kNoCompression:kNoCompression:kLZ4:kLZ4:kZSTD:kZSTD
# Bloom Filter 对点查很重要
rocksdb_whole_key_filtering=ON
TiKV 典型配置
TiKV 使用两个 RocksDB 实例:raftdb(存 Raft 日志)和 kvdb(存数据)。
[rocksdb]
max-background-jobs = 8
max-sub-compactions = 3
[rocksdb.defaultcf]
block-size = "64KB"
block-cache-size = "8GB"
write-buffer-size = "128MB"
max-write-buffer-number = 5
compression-per-level = ["no", "no", "lz4", "lz4", "lz4", "zstd", "zstd"]
level0-file-num-compaction-trigger = 4
level0-slowdown-writes-trigger = 20
level0-stop-writes-trigger = 36
Flink 使用 RocksDB 作为状态后端,特点是写多读少、checkpoint 时需要快速遍历。
Flink RocksDB 配置
RocksDBStateBackend backend = new RocksDBStateBackend("hdfs://...");
// 通过 RocksDBOptionsFactory 配置
public class CustomOptionsFactory implements ConfigurableRocksDBOptionsFactory {
@Override
public DBOptions createDBOptions(DBOptions currentOptions,
Collection<AutoCloseable> handlesToClose) {
return currentOptions
.setMaxBackgroundJobs(4)
.setMaxOpenFiles(-1);
}
@Override
public ColumnFamilyOptions createColumnOptions(
ColumnFamilyOptions currentOptions,
Collection<AutoCloseable> handlesToClose) {
return currentOptions
.setCompactionStyle(CompactionStyle.LEVEL)
.setLevelCompactionDynamicLevelBytes(true)
.setWriteBufferSize(128 * 1024 * 1024)
.setMaxWriteBufferNumber(4)
.setMinWriteBufferNumberToMerge(2)
.setTargetFileSizeBase(64 * 1024 * 1024)
.setCompressionType(CompressionType.LZ4_COMPRESSION);
}
}
Flink 特殊考虑:
Flink 配置文件方式
state.backend.rocksdb.memory.managed: true
state.backend.rocksdb.memory.fixed-per-slot: 256mb
state.backend.rocksdb.block.cache-size: 64mb
state.backend.rocksdb.writebuffer.size: 64mb
state.backend.rocksdb.writebuffer.count: 4
state.backend.rocksdb.compaction.level.target-file-size-base: 64mb
遇到性能问题,按这个顺序检查:
rocksdb.stats 里的 Stalls 统计max_background_jobs 是否太小level0_slowdown_writes_trigger 和 level0_stop_writes_triggerlevel_compaction_dynamic_level_bytes 是否开启cache_index_and_filter_blocks 是否为 truemax_open_files,-1 表示不限制RocksDB 调优核心就两点:
默认配置对大多数场景已经够用。如果要调,一次只改一个参数,用 db_bench 验证效果。
最后,这些项目的调优实践值得参考:
他们踩过的坑,你不用再踩一遍。