简介
从 Google 发表了 BigTable 的论文之后,基于 LSM-Tree 的存储系统越来越常见,尤其是对于写多读少的时序数据库,通过 LSM-Tree 可以将离散的随机写转换成批量的顺序写请求 (WAL + Compaction),以此来提高写性能。
读写放大
当然,天下没有免费的午餐,同时也带来了一些问题:
- 读放大 (Read Amplification),查询需要读取磁盘的次数。读取数据需要从新到旧一层一层查找,直到找到所需数据,会导致这一过程可能需要不止一次 IO 操作,尤其是范围查询。
- 空间放大 (Space Amplification),实际使用磁盘大小与存储数据大小比例。所有写入都是添加 (Append-Only) 的,而不是替换 (In-place Update),这意味着相同数据可能存在多个版本,过期数据不会马上清理;在 Compaction 过程中,也需要临时存储空间。
为此,由于 LSM-Tree 的机制,需要定期通过后台的 Compaction 线程来减少读放大 (减少 SST 文件数量)和空间放大 (清理过期数据),不过也因此带来了写放大 (Write Amplification) 问题。
- 写放大,写入磁盘的数据量与写数据库的数据量比例,一般是以写入速率比较。其中涉及到了一直写入的 WAL 以及周期性的 Compaction 操作,所以,实际观察写入速率可能会出现周期性的波动。
磁盘主要分成了两类:A) Hard Disk Drive, HDD 传统的机械磁盘,包含了磁头、马达、磁盘等主要元器件,读写速度慢、容易损坏等;B) Solid State Drive, SSD 固态硬盘,包含了控制和存储单元,没有了机械结构也更可高,速度也更高。当然,也有两者的结合 Hybrid Hard Drive, HHD 混合硬盘,其实就是在性能和成本之间的平衡,随着 SSD 发展,慢慢会被取代。
写放大对于 SSD 来说,尤其严重,因为 SSD 不支持覆盖写,必须先擦除再写入,而擦除的次数是有限的。在 PebblesDB: Building Key-Value Stores using Fragmented Log-Structured Merge Trees 中有关于写入放大的评估。
原始的论文可以查看 The Log-Structured Merge-Tree (LSM-Tree) 中的详细介绍。
压缩
Compaction 是 LSM 树中很重要的步骤,决定了树的形状,期间会合并数据,例如将已经删除的数据删除、合并多次操作的数据等,在 RocksDB 中,同时还会根据 Key 做排序。
Level0 在内存中,其中的 Key 可能会重叠,而 Level1~LevelN 是保存在磁盘上的,已经排好序,而且不会重叠。
MiniLSM
MiniLSM 不错的基于 Rust 实现 LSM 教程,其中的一个实现可以参考 Solution 实现,相同作者实现。如下介绍一些常见的依赖库:
- Moka 类似 Java Caffeine 的缓存库,支持并发、多种淘汰策略。
Memtable
通过 crossbeam
中的 skiplist
实现,其作为有序的 KV 内存存储,支持并发读写,其提供了类似 BTreeMap
的接口,区别是,即使是 put
操作也不需要 mut
对象,这样就意味着即使是修改,也只需要读锁就行。
另外,Memtable 的 delete
操作是通过将 Value
设置为空实现,而没有提供独立的删除接口。
冻结
超过某个指定的限制之后需要对其进行冻结,因为涉及到并发,可能存在两个请求进入临界区,此时就需要获取锁之后再检查一次是否超过限制。
因为冻结之后会有多个版本,那么读取时就需要获取到最新的数据。
Iterator
实现时尽量避免动态分派,也就是不使用 Box<dyn StorageIterator>
类型,更倾向于使用泛型进行静态分派,
MergeIterator
对多个底层数据进行合并,并将每个键的最新版本返回,例如有如下
参考
- A Comparison of Fractal Trees to Log-Structured Merge (LSM) Trees 有关于写放大的定义,不过感觉还是以写入速率好些,也就是 RocksDB Tuning Guide 中的定义。