LSM-Tree 存储引擎深度解析:MemTable、WAL、Compaction 与读写放大

LSM-Tree(Log-Structured Merge-Tree)是与 B+ 树并驾齐驱的存储引擎范式,被 RocksDB、LevelDB、Cassandra、HBase、TiKV 等大量写密集系统采用。它的核心哲学是"把随机写变成顺序写":所有写入先追加到内存中的 MemTable,再批量刷成 …

LSM-Tree(Log-Structured Merge-Tree)是与 B+ 树并驾齐驱的存储引擎范式,被 RocksDB、LevelDB、Cassandra、HBase、TiKV 等大量写密集系统采用。它的核心哲学是"把随机写变成顺序写":所有写入先追加到内存中的 MemTable,再批量刷成不可变的 SSTable,最后通过后台 Compaction 分层合并。对机械盘或 SSD 而言,顺序写远比随机写快,这让 LSM-Tree 在写入吞吐上轻松碾压 B+ 树——代价是读路径要检查多层文件,存在读放大。本指南从零构建 LSM-Tree 的心智模型,深入 MemTable、WAL、SSTable、Compaction 策略、Bloom Filter 与读写放大权衡,并给出 RocksDB 的生产调优实践。

一、为什么需要 LSM-Tree

1.1 B+ 树的写痛点

B+ 树写入路径:
  写入 → 定位叶子页 → 就地修改(随机写)→ 页分裂/合并

痛点:
  · 随机写性能差(磁盘寻道 / SSD 写放大)
  · 页分裂导致写放大与碎片
  · 高并发写下索引更新竞争激烈

LSM-Tree 的思路:
  不立刻落盘 → 先写内存(顺序)→ 批量刷盘(顺序)→ 后台合并
  → 随机写变顺序写,吞吐量提升 1-2 个数量级

ℹ️ 核心洞察:LSM-Tree 把"写"的成本从"访问磁盘任意位置"(随机)变成"追加到日志末尾"(顺序),用一个内存缓冲区把随机写先"攒起来",再成批落盘。

1.2 适用场景与代价

场景LSM-Tree 优势B+ 树优势
写密集(日志、时序、IoT)✅ 写入吞吐高❌ 随机写慢
读密集(点查)❌ 读放大✅ 稳定 O(logN)
空间利用率合并后有空洞紧凑
实现复杂度Compaction 复杂相对简单

结论:写多读少的系统选 LSM;读多写少、对点查延迟敏感的系统选 B+ 树。


二、LSM-Tree 的核心结构

2.1 分层结构

内存层:         MemTable(可写)→ 满后转 Immutable
                 ↓ flush
磁盘层(L0):    SSTable 文件(无序,多个)
                 ↓ compaction
磁盘层(L1+):   SSTable 文件(有序,按 key 范围划分)
                 ↓ compaction(逐层下沉)
               Lmax(最底层)

每个 SSTable:Sparse Index + Bloom Filter + 数据块
WAL(Write-Ahead Log):落盘前先写日志,崩溃恢复用

2.2 一次写入的生命周期

Put(key, value):
  1. 追加到 WAL(顺序写,持久化保障)
  2. 写入 MemTable(有序结构,内存)
  3. MemTable 达到阈值 → 冻结为 Immutable
  4. 后台线程将 Immutable flush 为 SSTable(L0)
  5. 后续 compaction 将 L0 合并到 L1、L2...

删除(Tombstone):
  Delete(key) → 写入一个删除标记(tombstone)
  → 真正物理删除要等 compaction 时把被标记的旧版本清掉
# lsm_put.py — 写入路径伪代码
def lsm_put(wal, memtable, key, value):
    wal.append(key, value)            # 1. 先写 WAL(顺序追加)
    memtable.put(key, value)          # 2. 写内存表
    if memtable.size > threshold:     # 3. 满则冻结
        immutable = memtable.freeze()
        schedule_flush(immutable)     # 4. 异步刷盘

def lsm_get(memtable, immutables, sstables, bloom_filters, key):
    if key in memtable: return memtable.get(key)          # 1. 内存最新
    for imm in reversed(immutables):                      # 2. 冻结表(新→旧)
        if key in imm: return imm.get(key)
    for sst in level_order(sstables):                     # 3. 磁盘层(L0→Lmax)
        if bloom_filters[sst].might_contain(key):         #    布隆过滤
            if val := sst.get(key): return val
    return None                                           # 4. 未找到

三、MemTable 与 WAL

3.1 MemTable 的实现选择

结构特点使用
SkipList有序、并发友好RocksDB 默认
平衡树有序、实现复杂较少
哈希表无序、快但 range 慢LevelDB 变体
// RocksDB MemTable 相关选项
Options options;
options.write_buffer_size = 64 * 1024 * 1024;  // 单 MemTable 64MB
options.max_write_buffer_number = 4;            // 最多 4 个(含冻结)
options.min_write_buffer_number_to_merge = 2;   // flush 前至少 2 个

3.2 WAL:崩溃恢复的保障

WAL 的作用:
  · 数据先落 WAL 再进内存 → 进程崩溃不丢已确认的写
  · 恢复时重放 WAL 重建 MemTable

WAL 的代价:
  · 每次写多一次磁盘 fsync(可调:每次/批/关闭)
  · 可用 group commit 合并 fsync 提升吞吐

RocksDB 选项:
  WriteOptions.sync = true/false   # 是否强制刷盘
  WAL 与 MemTable flush 后 WAL 可清理

3.3 WAL 配置实践

WriteOptions write_options;
write_options.sync = true;        // 强持久化(金融场景)
// 或
write_options.sync = false;       // 高吞吐,靠 OS 缓存 + 周期刷

四、SSTable 文件格式

4.1 SSTable 内部布局

SSTable 文件结构:
  ┌──────────────────────────┐
  │  Data Block 0 (有序 keys) │
  │  Data Block 1             │
  │  ...                      │
  │  Sparse Index(块级索引) │
  │  Bloom Filter(块级/整体)│
  │  Metadata                 │
  │  Footer(索引位置)        │
  └──────────────────────────┘

查询一个 key:
  1. Bloom Filter 快速判断"是否存在"(不存在的直接返回)
  2. Sparse Index 二分定位可能包含 key 的 Data Block
  3. 读取该 Data Block,块内二分/线性扫描定位

ℹ️ Bloom Filter 是 LSM 读性能的灵魂:它可以以极小内存代价(1% 误判率 ≈ 每 key 约 10bit)过滤掉绝大多数"不存在"的 key 查询,显著降低读放大。

4.2 分层与有序性

L0:文件无序(来自不同 flush),查询需扫描多个文件
L1+:文件内部有序 + 文件间按 key 范围互不重叠(可用稀疏索引定位单个文件)

→ L0 越少越好,compaction 优先把 L0 压到 L1
→ 查询优先从内存 → L0 → 低层逐层搜索

五、Compaction:LSM 的心脏

5.1 为什么需要 Compaction

数据永远在膨胀:
  · 旧版本 key 需要清理(多版本 + tombstone)
  · 小 SSTable 太多,查询要扫的文件越来越多
  · 无界增长导致读放大失控

Compaction 做三件事:
  1. 合并小文件成大文件(减少文件数)
  2. 清理旧版本与 tombstone(回收空间)
  3. 保持层级有序(优化读路径)

5.2 三种 Compaction 策略

策略思路优缺点
Size-Tiered等大小文件合并成大文件写放大低、读放大高
Leveled每层固定大小、逐层下沉读放大低、写放大较高
FIFO按时间淘汰(不合并)适合时序/日志
// RocksDB 选择 Leveled(默认)
options.compaction_style = CompactionStyle::kCompactionStyleLevel;

// Size-Tiered(写密集时序场景可选)
options.compaction_style = CompactionStyle::kCompactionStyleUniversal;

5.3 Compaction 对性能的影响

写放大(Write Amplification):
  实际写入磁盘的数据量 / 用户写入数据量
  Leveled 通常 10-30x;Size-Tiered 5-15x
  → 高写放大磨损 SSD、限制写吞吐

读放大(Read Amplification):
  一次查询实际读取的数据量 / 目标数据量
  → 越高点查越慢

权衡:压缩越激进(Leveled),读放大越低但写放大越高
  需按 workload 调 L0 大小、层数、触发阈值

六、Bloom Filter 与点查优化

6.1 配置 Bloom Filter

// RocksDB 每个 SSTable 启用 Bloom Filter
options.filter_policy = rocksdb::NewBloomFilterPolicy(10);  // 10bit/key
// 不同级别的 filter 可差异化:
// L0/L1 关闭(小层查询成本低),高层开启(省内存)

// 也支持整表前缀 Bloom(针对前缀查询)

6.2 Bloom Filter 效果

命中分析:
  存在 key:Bloom 几乎必过 → 进入索引定位
  不存在 key:Bloom 大概率拒绝(1% 误判才多读一层)

收益:
  · 点查不存在 key 的成本 ≈ O(1)
  · 点查存在 key 的成本大幅下降(跳过无关文件)
  · 代价:每个 SSTable 增加 10-20% 内存/存储

七、读写放大权衡与调优

7.1 关键旋钮

// RocksDB 生产调优常用参数
options.write_buffer_size = 128 << 20;      // MemTable 128MB
options.max_write_buffer_number = 4;        // 内存缓冲深度
options.min_write_buffer_number_to_merge = 2;
options.max_background_compactions = 4;     // 并行 compaction
options.max_background_flushes = 2;
options.level0_file_num_compaction_trigger = 8;   // L0 触发 compaction
options.level0_slowdown_writes_trigger = 20;      // 写降速阈值
options.level0_stop_writes_trigger = 36;          // 写暂停阈值(防雪崩)
options.soft_pending_compaction_bytes_limit = 64GB;
options.hard_pending_compaction_bytes_limit = 128GB;

// 压缩算法
options.compression = rocksdb::kSnappyCompression;      // 快速
options.bottommost_compression = rocksdb::kZSTD;        // 底层高压缩

7.2 调优思路

写吞吐不足:
  · 增大 write_buffer_size(攒更多再刷)
  · 增大 max_write_buffer_number(容忍内存延迟)
  · 增 max_background_compactions(并行压缩)
  · 检查写放大(Universal 是否更合适)

点查变慢(读放大):
  · 开 Bloom Filter
  · 加快 compaction(缩小 L0,减少小文件扫描)
  · 减少层级(压缩更激进)

空间使用高:
  · 用 zstd/lz4 压缩底层
  · 检查 tombstone 清理节奏(是否 compaction 太慢)

写停顿(stall):
  · pending compaction 达到 hard 阈值会暂停写
  · 提前扩容 / 增加压缩线程 / 用更轻的压缩算法

7.3 监控关键指标

RocksDB 关键指标(Prometheus):
  rocksdb_compaction_num                  # 压缩次数
  rocksdb_compaction_bytes_written        # 写放大字节
  rocksdb_slow_write_count                # 写停顿
  rocksdb_memtable_total_size             # 内存占用
  rocksdb_compaction_pending              # 待压缩量
  rocksdb_block_cache_hit_ratio           # 缓存命中率

八、LSM-Tree 变体与生态

8.1 生态一览

系统引擎特点
RocksDBLSMFacebook 出品,键值基石,支持事务/列族
LevelDBLSM谷歌原型,简单可靠
CassandraLSM列族 + 分区,写优化
HBaseLSM大数据生态,Region 服务
TiKVRocksDB分布式 KV,多副本
MySQL MyRocksRocksDB把 InnoDB 换成 LSM

8.2 与 B+ 树的混合趋势

演进方向:
  · MySQL 8.x:可选 RocksDB 引擎(MyRocks),写密集业务受益
  · RocksDB 引入"持久化 MemTable / 分块索引"改善读
  · 很多系统 LSM + 内存加速层(缓存热点)缓解读放大
  → 选型不是"二选一",而是按 workload 混用

九、生产落地清单

9.1 从理论到工程

落地 Checklist:
  □ 明确 workload:写多读少 / 读多写少
  □ 选择 compaction 策略(Leveled 通用 / Universal 写密集)
  □ MemTable 与 WAL 参数匹配写入速率
  □ Bloom Filter 按层差异化开启
  □ 压缩算法:热层 Snappy/LZ4,冷层 ZSTD
  □ 设置写停顿阈值(防止 compaction 追不上写入)
  □ 监控 read/write amplification 与 stall
  □ 定期做 compaction 健康检查与空间回收

9.2 常见陷阱

陷阱表现对策
写停顿周期性写阻塞提高压缩并行度/阈值
读放大失控点查慢开 Bloom + 加快合并
tombstone 累积空间不回收触发 compaction / 排查删除风暴
内存飙升MemTable 积压调 write_buffer 与触发阈值
缓存抖动热点 miss扩大 block cache / 前缀布隆

9.3 选型决策速查

写密集 + 点查少(日志/时序/Feed):LSM(RocksDB/Universal)
写密集 + 点查多:LSM + 大缓存层 + Bloom
读密集 + 低延迟:B+ 树(InnoDB/PostgreSQL)
需要事务/外键:B+ 树为主,写热点表可用 MyRocks
大数据分布式:TiKV/Cassandra(LSM 底座)

总结:LSM-Tree 核心决策表

环节关键决策
写入MemTable + WAL(顺序写,攒批刷盘)
读取内存→L0→高层 + Bloom Filter 过滤
合并Compaction:Leveled(读优)vs Universal(写优)
放大读/写放大权衡,按 workload 校准
持久化WAL 先行,group commit 平衡吞吐
调优内存缓冲、压缩并行、Bloom、压缩算法

LSM-Tree 用"以读换写、以空间换时间“换来了碾压式的写入吞吐——把随机写转为顺序写,把索引维护从热路径搬到后台 Compaction。它不是银弹:点查与空间放大是它的阿喀琉斯之踵,但配合 Bloom Filter、合理分层与压缩算法,这些代价可以被精确控制。理解 LSM-Tree 不只是理解一个数据结构,而是理解现代写密集系统的核心 trade-off——当你面对"为什么 RocksDB 写入这么快却偶尔变慢"“为什么 Cassandra 空间比想象中大"这类问题时,答案都藏在本指南的读写放大与 Compaction 权衡里。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 数据库安全加固与审计实战:权限最小化、加密、脱敏与合规
  2. 数据库容量规划与资源治理:从评估、监控到扩展路径
  3. 数据库字符集、排序规则与乱码实战:utf8mb4、Collation 选择与排查