日志结构合并树是一种巧妙的数据结构, 它是一种妥协的艺术: 它在关注写性能的同时也尽量照顾到了读性能, 适用于写多读少、写多读中等的场景, 甚至在一些数据库产品中直接将其作为核心存储引擎 (如 TiDB -> TiKV 基于 RocksDB);
因此很有必要学习一下 LSM-Tree 的原理;

B+ 树 (如数据库索引) 和 log append 型操作 (如数据库 WAL 日志) 是数据读写效率的两个极端; 磁盘读写分为随机读写和顺序读写,一般来说随机读写的效率要低于顺序读写。

  • B+ 树解决的是磁盘随机读慢的问题, 但随机写无法保证效率, 因为写入数据时涉及到树的重构: 读效率高而写效率差;
  • log append 型文件操作解决的是磁盘随机写慢的问题, 但读时需要遍历整个文件: 写效率高而读效率差;

为了在排序和 log append 型文件操作之间做个折中, 就引入了 Log-Structed Merge Tree 模型, LSM Tree 既有日志型的文件操作提升写效率, 又在每个 sstable 中排序保证了查询效率;

LSM-Tree 的数据结构

WAL

MemTable

SST (Sorted String Table)

LSM-Tree 的压缩

在 LSM-Tree 中,压缩算法主要有两种策略:TieringLeveling, 它们的区别主要体现在如何组织和管理不同层级的数据,以及如何执行合并操作;

Tiering

Tiering 策略允许每一层(Level)包含多个 SSTable,并且每一层的 SSTable 之间可以有重叠的键范围。压缩操作主要发生在同一层内,将多个小的 SSTable 合并成一个更大的 SSTable,然后将其推送到下一层。

特点:

  • 写放大较低:由于每一层可以包含多个 SSTable,压缩操作不需要频繁地将数据向下层移动,因此写放大(Write Amplification)较低。
  • 读放大较高:由于每一层的 SSTable 之间可能有重叠的键范围,查询时可能需要检查多个 SSTable,导致读放大(Read Amplification)较高。
  • 空间放大较高:每一层可能包含多个 SSTable,且这些 SSTable 之间可能有重复的数据,因此空间放大(Space Amplification)较高。

适用场景:

  • Tiering 适合写密集型工作负载,尤其是对写性能要求较高的场景。

Leveling

Leveling 策略要求每一层(Level)只能包含一个 SSTable,并且每一层的 SSTable 的键范围不重叠。压缩操作主要发生在相邻的两层之间,将上层的 SSTable 与下层的 SSTable 合并,生成一个新的 SSTable 并推送到下层。

特点:

  • 写放大较高:由于每一层只能包含一个 SSTable,压缩操作需要频繁地将数据向下层移动,导致写放大较高。
  • 读放大较低:由于每一层的 SSTable 之间没有重叠的键范围,查询时通常只需要检查一个 SSTable,因此读放大较低。
  • 空间放大较低:每一层只有一个 SSTable,且键范围不重叠,因此空间放大较低。

适用场景:

  • Leveling 适合读密集型工作负载,尤其是对读性能要求较高的场景。

策略对比

特性 Tiering Leveling
写放大 较低 较高
读放大 较高 较低
空间放大 较高 较低
适用场景 写密集型工作负载 读密集型工作负载
压缩策略 同一层内合并多个 SSTable 相邻层之间合并 SSTable
SSTable 重叠 允许同一层内 SSTable 键范围重叠 不允许同一层内 SSTable 键范围重叠

混合策略:
在实际应用中,有些系统会采用 混合策略,结合 Tiering 和 Leveling 的优点。例如,在较低的层级使用 Tiering 以减少写放大,而在较高的层级使用 Leveling 以降低读放大和空间放大。

通过选择合适的压缩策略,LSM-Tree 可以在不同的工作负载下实现性能的优化。

参考资料