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 →