强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 被频繁 SeekkMaxMemCompactLevel

Level 大小限制

Level最大大小计算方式
0~10 MB特殊(文件数触发)
110 MBmax_bytes_for_level_base
2100 MB10 MB × 10
31 GB100 MB × 10
410 GB1 GB × 10
5100 GB10 GB × 10
61 TB100 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_size4MBMemTable 大小增大可减少 Minor Compaction
max_write_buffer_number2最大 MemTable 数量增大可缓冲更多写入
max_bytes_for_level_base10MBLevel 1 大小上限增大可减少 L0→L1 Compaction
max_bytes_for_level_multiplier10相邻 Level 大小比减小可降低写放大
level0_file_num_compaction_trigger4L0 触发 Compaction 的文件数减小可更积极 Compaction
level0_slowdown_writes_trigger8L0 写入降速阈值
level0_stop_writes_trigger12L0 写入停止阈值
max_background_compactions1后台 Compaction 线程数增大可加速 Compaction
target_file_size_base2MBLevel 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

写放大的来源

来源贡献说明
WAL1x必要开销
MemTable Flush1xMinor Compaction
Level Compaction10-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 本章小结

类型触发条件操作影响
MinorMemTable 满MemTable → SSTable L0快速
MajorLevel 文件数/大小超限合并相邻 Level 的 SSTable耗时较长
调优方向方法
减少写放大增大 max_bytes_for_level_multiplier
减少读放大积极 Compaction(降低触发阈值)
减少空间放大及时清理 Tombstone
避免写停顿增大 write_buffer_size 和 L0 触发阈值

扩展阅读

  1. Compaction 实现db/compaction.cc, db/compaction_picker.cc
  2. Leveled vs Tiered Compaction:RocksDB Wiki - Compaction
  3. 写放大分析论文“An Efficient Design and Implementation of LSM-Tree Based Key-Value Store on Open-Channel SSD” (EuroSys 2014)

第 7 章 · 快照与 MVCC | 第 9 章 · Block Cache