Skip to main content

lsm-tree

📅 2026-04-10 ✏️ 2026-04-10 CS
No related notes

1 · lsm-tree#

1.1 · S - 场景#

需要大量写入且写吞吐量优先的场景:

  • 数据库引擎(RocksDB、LevelDB、SQLite4)
  • 时间序列数据库(InfluxDB、Prometheus)
  • 日志系统(Kafka、Cassandra)
  • 分布式KV存储(HBase、DynamoDB)

1.2 · C - 核心权衡#

维度传统 B-treeLSM-tree
写入模式随机写(寻道)顺序追加(快)
读取直接查询需要多层查询
空间利用较低(重复版本)
CPU利用高(compaction)

1.3 · Q - 核心问题与解决#

1.3.1 · Q1. 写快读慢的问题#

问题:直接追加 key-value,同一个 key 有多个版本,读取时需要全文件扫描。

解决方案

  1. 内存 memtable:小规模数据排序存储,快速查询 O(log n)
  2. 多层 SSTable:按 key 范围分层,快速定位
  3. 布隆过滤器:快速判断 key 是否存在,避免不必要的查询

1.3.2 · Q2. 多版本管理问题#

问题:旧值不删除,磁盘持续增长,读取需要找”最新版本”。

解决方案:后台 compaction

  • 合并多个 SSTable
  • 删除旧版本、过期版本(tombstone)
  • 重新排序、去重

1.3.3 · Q3. 宕机数据丢失#

问题:内存 memtable 未刷盘的数据在宕机时丢失。

解决方案:Write-Ahead Log (WAL)

  • 写入前先顺序写 WAL
  • 宕机恢复时重放 WAL 恢复 memtable

1.4 · A - LSM-tree 架构#

1.4.1 · 核心思想

延迟处理:写的时候尽量顺着写(顺序I/O),读的时候再靠索引和合并补回来。

1.4.2 · 数据流

┌─ 客户端 Write ─┐
│               │
├──> WAL (顺序写) ──────────┐
│   (磁盘预写日志)          │
│                          ↓
└──> Memtable (内存有序表)  │
    (SkipList 或 B-tree)   │
         │                  │
         ├─ 大小 > 阈值 ────┘
         │   (如 2MB)

    SSTable 0 (新)
    SSTable 1
    SSTable 2
    ...
    SSTable N (旧)

后台合并(Compaction):
  多层 SSTable ──> 合并排序 ──> 去重 ──> 新 SSTable

1.4.3 · 读取流程(Get 操作)#

查询 key
  ├─> [O(log n)] Memtable(有序跳表)
  │        └─> 找到 → 返回
  │        └─> 未找到 ↓
  ├─> [O(k log n)] SSTable 层级查询(k = 层数)
  │     ├─ SSTable 0 [最新]
  │     │   ├─ 布隆过滤器快速判断
  │     │   └─ 二分查找
  │     ├─ SSTable 1
  │     ├─ SSTable 2
  │     └─ ...
  └─> 返回最新版本或 null

1.5 · 关键组件详解

1.5.1 · 1. Memtable(内存表)#

作用:缓存最近写入的数据,实现高速写入

实现

  • 跳表(skiplist):原理简单、查询快、易实现
  • 红黑树:更好的平衡性
  • B+ 树:范围查询友好

特点

  • key 必须有序
  • 达到大小阈值(如 2MB)后转换为 SSTable
  • 新写入优先查询这里
  • 宕机时需要 WAL 恢复

1.5.2 · 2. SSTable(Sorted String Table)#

特点

  • 不可变的有序文件
  • 一旦写入就不再修改
  • 按 key 范围分级:L0、L1、L2…

内部结构

┌──────────────────────────┐
│  Data Block 1            │  实际 key-value 对
│  Data Block 2            │
├──────────────────────────┤
│  Index Block             │  key 范围索引
├──────────────────────────┤
│  Bloom Filter            │  概率数据结构,快速判断 key 是否存在
├──────────────────────────┤
│  Footer/Metadata         │  版本号、校验和、指针
└──────────────────────────┘

查询过程

  1. 布隆过滤器判断 key 可能存在 → 继续
  2. Index Block 二分查找定位 Data Block 范围
  3. 在对应 Data Block 中二分查询

1.5.3 · 3. Compaction(合并)#

目的

  • 删除旧版本和过期数据(tombstone)
  • 重新组织数据,减少读放大
  • 回收磁盘空间

两种策略对比

Leveled Compaction(LevelDB)

L0: [SST1] [SST2] [SST3]  (多个文件,可能有重叠)
    ↓ (size > threshold)
L1: [SST1] [SST2] [SST3]  (固定容量,key 不重叠)

L2: [SST1] [SST2]

L3: [SST1]
  • 特点:每层内 key 不重叠,读放大小
  • 代价:写放大大(频繁重组)

Tiered Compaction(RocksDB)

Level 0: [SST1] [SST2] [SST3] (不排序)
  ↓ (积累 > threshold)
Level 1: [SST1] [SST2] ... [SSTn] (多个文件可重叠)

Level 2: [SST1] [SST2] ...
  • 特点:同层内文件可重叠,写放大小
  • 代价:读放大大(需要查多个文件)

1.5.4 · 4. Write-Ahead Log (WAL)#

原理:先写日志,再写内存

Client Put(key, value)

WAL.append(key, value)    ← 顺序写磁盘(必须成功)

Memtable.put(key, value)  ← 写内存

返回成功

恢复过程

宕机重启

读取 WAL 文件

重放日志,恢复 memtable

继续正常操作

1.6 · 性能特征

1.6.1 · 时间复杂度

操作复杂度说明
PutO(log n)memtable 的有序插入
GetO(k·log n)k = SSTable 层数,每层二分查找
DeleteO(log n)写入墓碑标记(tombstone)
Range ScanO(log n + m)m = 结果行数

1.6.2 · 放大因子

读放大(Read Amplification)

  • 定义:读一条数据时实际 I/O 操作数
  • LSM 的读放大:最坏需要查 k 层 SSTable(k ≈ 10)
  • 优化方式:缓存、布隆过滤器、分级索引

写放大(Write Amplification)

  • 定义:写 1 字节数据实际写入磁盘的字节数
  • LSM 的写放大:数据在 compaction 中被重写多次
    • L0 → L1:重写 1 次
    • L1 → L2:重写 1 次
    • 总写放大 ≈ (key_size + value_size) × (L+1)
    • L = 层数
  • LSM-tree 接受高写放大是因为顺序写仍比随机写快

空间放大(Space Amplification)

  • 定义:实际占用磁盘空间 / 有用数据大小
  • 原因:保留多版本、过期数据未及时清理
  • LSM 空间放大 ≈ 1.1 ~ 1.5(取决于 compaction 策略)

1.6.3 · 权衡示意

顺序写 vs 随机写:
  ├─ SSD 随机写: ~100MB/s
  └─ SSD 顺序写: ~500MB/s (快 5 倍)

LSM-tree 的权衡:
  ├─ 写优化:顺序 I/O,高吞吐
  ├─ 读代价:多层查询,高读放大
  └─ 空间代价:多版本保留,高空间放大

1.7 · 实现概览(伪代码)

class LSMTree:
    def __init__(self):
        self.memtable = SkipList()       # 内存有序表
        self.immutable = None            # 待刷盘的 memtable
        self.sstables = []               # SSTable 列表(按层级)
        self.wal = WriteAheadLog()       # 预写日志
        
    def put(self, key, value):
        # 1. 写 WAL(保证宕机安全)
        self.wal.append(key, value)
        # 2. 写 memtable
        self.memtable.put(key, value)
        # 3. 检查是否需要刷盘
        if self.memtable.size() > MEMTABLE_MAX_SIZE:
            self._flush_memtable()
            
    def get(self, key):
        # 1. 查询 memtable(最新)
        val = self.memtable.get(key)
        if val is not None:
            return val
        # 2. 查询待刷盘 memtable
        if self.immutable is not None:
            val = self.immutable.get(key)
            if val is not None:
                return val
        # 3. 查询 SSTable(从新到旧)
        for sstable in self.sstables:
            # 先检查布隆过滤器(快速排除)
            if not sstable.bloom_filter.might_contain(key):
                continue
            val = sstable.get(key)
            if val is not None:
                return val
        return None
        
    def delete(self, key):
        # LSM 的删除实际是写入墓碑标记
        self.put(key, TOMBSTONE)
        
    def _flush_memtable(self):
        # memtable 转换为 SSTable
        self.immutable = self.memtable
        self.memtable = SkipList()
        
        # 后台线程执行
        self._background_flush()
        
    def _background_flush(self):
        # 将 memtable 刷盘成 SSTable
        sstable = SSTable.from_memtable(
            self.immutable,
            build_bloom_filter=True
        )
        self.sstables.append(sstable)
        self.immutable = None
        self._trigger_compaction()
        
    def _trigger_compaction(self):
        # 检查是否需要 compaction
        if self._should_compact():
            self._compact()
            
    def _compact(self):
        # 选择要合并的 SSTable
        candidates = self._pick_compaction_files()
        # 合并、排序、去重
        merged = self._merge_sstables(candidates)
        # 删除旧文件,添加新文件
        for old_sstable in candidates:
            self.sstables.remove(old_sstable)
        self.sstables.append(merged)

1.8 · 对比其他数据结构

结构空间最佳场景
B-treeO(log n)随机O(log n)高利用通用数据库
LSM-treeO(1)顺序O(k log n)低利用写密集
Hash表O(1)O(1)中等内存缓存
日志结构O(1)追加O(n)扫描最低只写日志

1.9 · 常见优化

优化方法收益
减少读放大布隆过滤器、缓存、分层索引减少不必要的磁盘 I/O
减少写放大选择 Tiered Compaction减少重写数据量
加快 Compaction并行压缩、增量压缩减少后台 CPU
减少空间放大及时 compaction、压缩算法降低磁盘占用

1.10 · 工业实现

  • RocksDB:Facebook 开源,广泛用于生产
    • Tiered Compaction 为默认
    • 支持列式存储、预留空间等高级特性
  • LevelDB:Google 开源,参考实现
    • Leveled Compaction
    • 代码简洁,易于学习
  • WiredTiger:MongoDB 存储引擎
  • Cassandra:分布式数据库使用 LSM