专栏文章
专栏文章
存储算法系列
1. 存储算法 #01:LSM Tree 2. 存储算法 #02:Bloom Filter 3. 存储算法 #03:B+ Tree 4. 存储算法 #04:SSTable 5. 存储算法 #05:Copy-on-Write(COW) 6. 存储算法 #06:Roaring Bitmap 7. 存储算法 #07:WAL(Write-Ahead Log) 8. 存储算法 #08:Bitcask 9. 存储算法 #09:Redo 与 Undo Log 10. 存储算法 #10:Varint 与 ZigZag 编码 11. 存储算法 #11:Delta 编码 12. 存储算法 #12:LZ4 与 Snappy 与 Zstd

存储算法 #04:SSTable

发布于 2026-06-02 02:06 👁 5 次阅读
#算法#storage#lsm-tree#sstable#immutable

SSTable(Sorted String Table)是 LSM Tree 的磁盘存储格式:一个不可变的、按 key 有序排列的键值对文件。理解 SSTable 是理解 LSM Tree 读写路径、Compaction 机制的基础。LevelDB、RocksDB、Cassandra、HBase 均使用 SSTable。

相关文章LSM Tree · Bloom Filter · B+ Tree


目录

章节 说明
核心特性 不可变 + 有序:两个关键约束
文件内部结构 数据块 / 索引块 / 过滤块 / Footer
写入过程 从 MemTable Flush 到磁盘
读取过程 Footer → 索引 → Bloom Filter → 数据
Compaction 合并 多路归并排序
与 LSM Tree 的关系 SSTable 是 LSM Tree 的磁盘形态
异常场景 文件损坏、Compaction 中断

核心特性

不可变(Immutable):
  SSTable 写入磁盘后永远不修改
  更新 = 写一条新的 SSTable 记录(带更高的序列号)
  删除 = 写一条 tombstone(墓碑)标记

有序(Sorted):
  文件内所有 key 按字典序排列
  支持二分查找定位 key
  多个 SSTable 合并时可用归并排序,O(N) 时间复杂度

两者结合的优势:
  不可变 → 无需加锁,多线程并发读安全
  有序 → 支持高效点查和范围扫描
         支持 O(N) 多文件合并(Compaction)

文件内部结构

sstable structure

SSTable 文件布局(从文件头到文件尾):

┌─────────────────────────────────────┐
│          Data Block 0               │  ← 实际 key-value 数据,约 4KB
│          Data Block 1               │
│          Data Block 2               │
│          ...                        │
├─────────────────────────────────────┤
│          Meta Block                 │  ← 统计信息(key 数量、大小等)
├─────────────────────────────────────┤
│          Filter Block               │  ← Bloom Filter(可选)
├─────────────────────────────────────┤
│          Index Block                │  ← 每个 Data Block 的最大 key + 偏移量
├─────────────────────────────────────┤
│          Metaindex Block            │  ← Filter Block / Meta Block 的偏移量
├─────────────────────────────────────┤
│          Footer(固定 48 字节)      │  ← Metaindex 和 Index Block 的偏移量
└─────────────────────────────────────┘

Data Block(数据块)

每个 Data Block 约 4KB(一个磁盘页),存储若干有序 key-value 对

内部结构(支持前缀压缩,节省空间):
  key="apple",   value="red"
  key="application", value="software"  ← 前缀 "appl" 共享,只存差异 "ication"
  key="banana",  value="yellow"
  ...
  trailing: 重启点列表(每 16 个 key 一个完整 key,支持二分查找)
            checksum(CRC32,用于检测数据损坏)

Index Block(索引块)

每条记录:{最大 key,Data Block 偏移量,Data Block 大小}

查找 key=X 时:
  在 Index Block 二分查找,找到第一个 ≥ X 的条目
  → 得到对应 Data Block 的偏移量
  → 直接定位读取该 Data Block

避免了全文件扫描,读取 1 个 SSTable 最多 2 次磁盘 I/O
(1次读 Index Block,1次读 Data Block)

Filter Block(过滤块)

默认存储 Bloom Filter(每 2KB 数据块一个,或全文件一个)

查找时先查 Bloom Filter:
  → 不存在:直接跳过整个 SSTable(节省磁盘 I/O)
  → 可能存在:再走 Index → Data Block 流程

作用:大幅减少"key 不存在"时的无效磁盘读
RocksDB 实测:Bloom Filter 可减少 90%+ 的不必要 SSTable 读取

写入过程

MemTable(内存红黑树/跳表,有序)→ Flush 为 SSTable:

1. 遍历 MemTable,按 key 顺序输出所有 key-value 对
2. 每积累 4KB 数据,写一个 Data Block
3. 每写完一个 Data Block,更新 Index Block(记录该块最大 key + 偏移)
4. 同时为每个 Data Block 计算 Bloom Filter
5. 所有 key-value 写完后,顺序写入:
   Data Blocks → Filter Block → Index Block → Footer
6. fsync 刷盘

整个写入过程是顺序写,I/O 效率极高

读取过程

查找 key=X 在 SSTable 中:

1. 读 Footer(文件最后 48 字节)
   → 获取 Metaindex Block 偏移量

2. 读 Metaindex Block
   → 获取 Filter Block 和 Index Block 的偏移量

3. 读 Filter Block,查 Bloom Filter
   → 不存在:直接返回(无需读数据,节省 I/O)
   → 可能存在:继续

4. 读 Index Block,二分查找目标 Data Block 的偏移量

5. 读目标 Data Block,二分查找 key=X
   → 找到则返回 value
   → 未找到则返回不存在

实际 I/O 次数:
  key 不存在(Bloom Filter 拦截):1~2 次(Footer + Filter)
  key 存在:3~4 次(Footer + Filter + Index + Data)

Compaction 合并

多个 SSTable 合并为一个(或多个)新 SSTable:

输入:SSTable A [apple:1, cherry:3, mango:2]
      SSTable B [banana:4, cherry:5, grape:1](cherry 有两个版本)

合并算法(多路归并排序):
  维护最小堆,每次取最小 key
  相同 key 取版本号(序列号)更大的值(即更新的写入)
  tombstone key 直接丢弃(删除操作的最终清理)

输出:[apple:1, banana:4, cherry:5, grape:1, mango:2]
      (cherry 旧版本 3 被丢弃,空间回收)

时间复杂度:O(N log k),N = 总 key 数,k = 参与合并的文件数

与 LSM Tree 的关系

LSM Tree 是架构,SSTable 是 LSM Tree 磁盘层的存储格式:

                  ┌─────────────┐
  写入    →        │  MemTable   │  (内存)
                  └──────┬──────┘
                   Flush │
                  ┌──────▼──────┐
  L0 层   →        │  SSTable 0  │  (磁盘,不可变)
                  │  SSTable 1  │
                  └──────┬──────┘
                Compact  │
                  ┌──────▼──────┐
  L1 层   →        │  SSTable A  │  (有序,不重叠)
                  │  SSTable B  │
                  └─────────────┘

每次 Flush/Compaction 都产生新的 SSTable 文件
旧文件在 Compaction 完成后删除

异常场景

文件损坏检测

每个 Data Block 末尾存 CRC32 checksum
读取时验证 checksum:
  不匹配 → 文件损坏,报错
  → 从副本(如 Raft 其他节点)重新获取数据

RocksDB 还支持对整个 SSTable 文件做定期校验(Verify Checksum)

Compaction 中途崩溃

SSTable 是不可变文件:
  Compaction 生成新 SSTable 文件
  全部写完 + fsync 后,原子性地更新 MANIFEST 文件(记录哪些文件有效)
  崩溃后:
    新文件写完但 MANIFEST 未更新 → 新文件当成垃圾清理,原文件仍有效 ✅
    MANIFEST 已更新 → 正常使用新文件,原文件标记删除 ✅

关键:MANIFEST 的原子更新保证了一致性

参考资料

  • LevelDB SSTable 格式:https://github.com/google/leveldb/blob/main/doc/table_format.md
  • RocksDB BlockBasedTable:https://github.com/facebook/rocksdb/wiki/Rocksdb-BlockBasedTable-Format
  • 《Designing Data-Intensive Applications》Ch.3 — SSTables and LSM-Trees
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论