LevelDB 完全指南 / 第 8 章 · Compaction 机制
第 8 章 · Compaction 机制
8.1 什么是 Compaction
Compaction 是 LSM-Tree 的核心后台操作,负责将内存数据刷盘、合并多个 SSTable 文件、清理过期数据。它是 LevelDB 写放大(Write Amplification)的主要来源,也是读性能的关键保障。
没有 Compaction 的后果:
MemTable → SSTable L0 → 不断累积
L0 文件越来越多 → 读取需要检查所有 L0 文件
Tombstone 不清理 → 空间不断膨胀
旧版本数据堆积 → 磁盘空间耗尽
8.2 两种 Compaction 类型
Minor Compaction(MemTable → SSTable)
┌──────────────────┐
│ Immutable │
│ MemTable │──────→ SSTable L0
│ (只读,等待刷盘) │
└──────────────────┘
| 属性 | 说明 |
|---|
| 触发条件 | MemTable 超过 write_buffer_size |
| 操作 | 将 Immutable MemTable 序列化为 SSTable L0 文件 |
| 影响 | 产生一个 L0 文件 |
| 耗时 | 通常很快(毫秒级) |
Major Compaction(SSTable 合并)
Level 0: [SST-1] [SST-2] [SST-3] ← 文件可能重叠
│
│ Major Compaction
▼
Level 1: [SST-4] [SST-5] [SST-6] ← 文件不重叠
│
│ Major Compaction (当 Level 1 超过限制)
▼
Level 2: [SST-7] [SST-8] [SST-9] [SST-10]
| 属性 | 说明 |
|---|
| 触发条件 | Level N 的文件数/大小超过阈值 |
| 操作 | 合并相邻 Level 的 SSTable,清理 Tombstone |
| 影响 | 减少文件数、回收空间、提升读性能 |
| 耗时 | 可能较长(秒到分钟级) |
8.3 Compaction 触发条件
Minor Compaction 触发
条件 1: Active MemTable 大小 >= write_buffer_size (默认 4MB)
条件 2: Immutable MemTable 数量 >= max_write_buffer_number (默认 2)
→ 如果超过,写入会被阻塞直到 Immutable 刷盘完成
Major Compaction 触发
| 条件 | 说明 | 阈值参数 |
|---|
| Level 0 文件数 | L0 文件数过多影响读性能 | kL0_CompactionTrigger = 4 |
| Level 0 慢写 | 写入速度降低的警告 | kL0_SlowdownWritesTrigger = 8 |
| Level 0 暂停 | 写入被完全阻塞 | kL0_StopWritesTrigger = 12 |
| Level N 大小 | Level N 超过限制时触发 N→N+1 合并 | max_bytes_for_level_base × multiplier^n |
| Seek 触发 | 某个 SSTable 被频繁 Seek | kMaxMemCompactLevel |
Level 大小限制
| Level | 最大大小 | 计算方式 |
|---|
| 0 | ~10 MB | 特殊(文件数触发) |
| 1 | 10 MB | max_bytes_for_level_base |
| 2 | 100 MB | 10 MB × 10 |
| 3 | 1 GB | 100 MB × 10 |
| 4 | 10 GB | 1 GB × 10 |
| 5 | 100 GB | 10 GB × 10 |
| 6 | 1 TB | 100 GB × 10 |
8.4 Compaction 流程详解
Level 0 → Level 1 Compaction
输入:
Level 0: [SST-a: key 1-100] [SST-b: key 50-150] [SST-c: key 120-200]
Level 1: [SST-d: key 1-80] [SST-e: key 81-200]
步骤:
1. 选择 SST-a(L0 最老的文件)
2. 确定 SST-a 的 key 范围 [1, 100]
3. 找到 L1 中与 [1, 100] 重叠的文件 → SST-d
4. 合并 SST-a + SST-d + SST-e 的重叠部分
5. 生成新的 L1 文件,删除旧文件
输出:
Level 0: [SST-b] [SST-c]
Level 1: [SST-f: key 1-100] [SST-e: key 101-200]
Level N → Level N+1 Compaction(N ≥ 1)
输入:
Level 2: [SST-x: key 100-200]
Level 3: [SST-y: key 50-150] [SST-z: key 150-300]
步骤:
1. 选择 Level 2 的 SST-x
2. 找到 Level 3 中 key 范围重叠的文件 → SST-y, SST-z
3. 合并所有文件
4. 生成新的 Level 3 文件
输出:
Level 2: (SST-x 被删除)
Level 3: [SST-new1: key 50-200] [SST-new2: key 201-300]
合并过程中的清理
合并时的记录处理:
Key="name", Seq=10, Type=Put, Value="张三" ← 保留(最新 Put)
Key="name", Seq=5, Type=Put, Value="李四" ← 丢弃(旧版本)
Key="name", Seq=3, Type=Put, Value="王五" ← 丢弃(旧版本)
Key="age", Seq=8, Type=Delete ← 如果是底层 Level → 丢弃 Tombstone
Key="age", Seq=6, Type=Put, Value="30" ← 被 Delete 掩盖 → 丢弃
Key="email", Seq=12, Type=Put, Value="[email protected]" ← 保留
8.5 Compaction 参数调优
关键 Options
| 参数 | 默认值 | 说明 | 调优建议 |
|---|
write_buffer_size | 4MB | MemTable 大小 | 增大可减少 Minor Compaction |
max_write_buffer_number | 2 | 最大 MemTable 数量 | 增大可缓冲更多写入 |
max_bytes_for_level_base | 10MB | Level 1 大小上限 | 增大可减少 L0→L1 Compaction |
max_bytes_for_level_multiplier | 10 | 相邻 Level 大小比 | 减小可降低写放大 |
level0_file_num_compaction_trigger | 4 | L0 触发 Compaction 的文件数 | 减小可更积极 Compaction |
level0_slowdown_writes_trigger | 8 | L0 写入降速阈值 | |
level0_stop_writes_trigger | 12 | L0 写入停止阈值 | |
max_background_compactions | 1 | 后台 Compaction 线程数 | 增大可加速 Compaction |
target_file_size_base | 2MB | Level 1 SSTable 大小 | 增大可减少文件数 |
写密集型场景
leveldb::Options options;
options.write_buffer_size = 64 * 1024 * 1024; // 64MB
options.max_write_buffer_number = 4;
options.max_bytes_for_level_base = 256 * 1024 * 1024; // 256MB
options.target_file_size_base = 16 * 1024 * 1024; // 16MB
options.level0_file_num_compaction_trigger = 8;
options.level0_slowdown_writes_trigger = 16;
options.level0_stop_writes_trigger = 24;
读密集型场景
leveldb::Options options;
options.write_buffer_size = 16 * 1024 * 1024; // 16MB
options.max_bytes_for_level_base = 64 * 1024 * 1024; // 64MB
options.level0_file_num_compaction_trigger = 2; // 积极 Compaction
options.max_bytes_for_level_multiplier = 8; // 减小层级递增
8.6 写放大分析
什么是写放大
写放大 = 实际磁盘写入量 / 用户写入量
示例:
用户写入 1MB 数据
实际磁盘写入:
- WAL: 1MB
- L0 Flush: 1MB
- L0→L1 Compaction: 2MB(读 L0+L1,写新 L1)
- L1→L2 Compaction: 10MB
总计: 14MB
写放大 = 14 / 1 = 14x
写放大的来源
| 来源 | 贡献 | 说明 |
|---|
| WAL | 1x | 必要开销 |
| MemTable Flush | 1x | Minor Compaction |
| Level Compaction | 10-30x | 主要来源 |
| 总计 | 12-32x | 典型范围 |
降低写放大的方法
方法 1: 增大 max_bytes_for_level_multiplier
→ 减少 Level 层数,降低 Compaction 频率
→ 但会增加读放大
方法 2: 增大 write_buffer_size
→ 减少 Minor Compaction 频率
→ 但增加内存占用和崩溃恢复时间
方法 3: 使用 Tiered Compaction(RocksDB 特性)
→ LevelDB 不支持,需要迁移到 RocksDB
8.7 Compaction 对读写的影响
写入影响
写入停顿场景:
L0 文件数 = 12 (达到 kL0_StopWritesTrigger)
→ 写入完全阻塞,等待 Compaction 完成
解决方案:
1. 增大 write_buffer_size
2. 增大 kL0_StopWritesTrigger
3. 使用 SSD 加速 Compaction
读取影响
Compaction 期间的读取:
- 后台线程执行磁盘 I/O
- 可能影响前台读取的延迟
- Block Cache 被 Compaction 的读取污染
解决方案:
1. Compaction 读取使用 fill_cache=false
2. 使用 Rate Limiter 限制 Compaction I/O(RocksDB 特性)
8.8 手动触发 Compaction
// 手动压缩某个 Key 范围
leveldb::CompactRangeOptions compact_opts;
compact_opts.change_level = false;
compact_opts.target_level = -1;
// 压缩整个数据库
db->CompactRange(compact_opts, nullptr, nullptr);
// 压缩特定范围
leveldb::Slice start = "user:0001";
leveldb::Slice end = "user:9999";
db->CompactRange(compact_opts, &start, &end);
💡 提示:手动 CompactRange 适用于以下场景:
- 批量导入数据后,压缩以提升读性能
- 大量删除后,回收磁盘空间
- 数据迁移前,确保数据整洁
8.9 业务场景
场景一:数据清理
// 删除 30 天前的日志
void CleanupOldLogs(leveldb::DB* db) {
uint64_t cutoff = time(nullptr) - 30 * 86400;
char cutoff_key[32];
snprintf(cutoff_key, sizeof(cutoff_key), "log:%020lu", cutoff);
leveldb::WriteBatch batch;
auto* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst();
it->Valid() && it->key().compare(cutoff_key) < 0;
it->Next()) {
batch.Delete(it->key());
}
delete it;
db->Write(leveldb::WriteOptions(), &batch);
// 手动压缩以回收空间
db->CompactRange(leveldb::CompactRangeOptions(), nullptr, nullptr);
}
场景二:批量导入优化
// 批量导入数据时,临时禁用自动 Compaction
void BulkImport(leveldb::DB* db, const std::vector<KV>& data) {
// 暂停 Compaction
db->SuspendCompactions();
for (const auto& kv : data) {
db->Put(leveldb::WriteOptions(), kv.key, kv.value);
}
// 恢复 Compaction
db->ResumeCompactions();
// 手动压缩以优化读性能
db->CompactRange(leveldb::CompactRangeOptions(), nullptr, nullptr);
}
8.10 监控 Compaction
通过日志观察
LevelDB LOG 文件内容示例:
Compacting 4@0 + 6@1 files
Compacted 4@0 + 6@1 files => 156MB
Compacting 2@1 + 8@2 files
...
通过 Statistics 监控
leveldb::Options options;
options.statistics = leveldb::CreateDBStatistics();
// ... 使用数据库 ...
// 查看 Compaction 统计
auto stats = options.statistics;
std::cout << "Compaction count: "
<< stats->getTickerCount(leveldb::COMPACTION_KEY_DROP) << std::endl;
std::cout << "Compaction bytes written: "
<< stats->getTickerCount(leveldb::COMPACT_BYTES_WRITTEN) << std::endl;
8.11 本章小结
| 类型 | 触发条件 | 操作 | 影响 |
|---|
| Minor | MemTable 满 | MemTable → SSTable L0 | 快速 |
| Major | Level 文件数/大小超限 | 合并相邻 Level 的 SSTable | 耗时较长 |
| 调优方向 | 方法 |
|---|
| 减少写放大 | 增大 max_bytes_for_level_multiplier |
| 减少读放大 | 积极 Compaction(降低触发阈值) |
| 减少空间放大 | 及时清理 Tombstone |
| 避免写停顿 | 增大 write_buffer_size 和 L0 触发阈值 |
扩展阅读
- Compaction 实现:
db/compaction.cc, db/compaction_picker.cc - Leveled vs Tiered Compaction:RocksDB Wiki - Compaction
- 写放大分析论文:“An Efficient Design and Implementation of LSM-Tree Based Key-Value Store on Open-Channel SSD” (EuroSys 2014)
← 第 7 章 · 快照与 MVCC | 第 9 章 · Block Cache →