初识LSM

274 阅读9分钟

一.LSM树

LSM应用于各大框架中,如HBase、Kafka、Flink等等

Flink中的RocskDb采用了LSM的思想
// 配置分级存储
env.setStateBackend(newRocksDBStateBackend("file:///storage"));

// 智能图书管理:
// 畅销书 → 车上书架(内存)
// 常备书 → 中途书库(SSD)
// 罕见书 → 总馆书库(HDD)

但是各大框架中的LSM是有区别的

  • HBase:强一致性;但维护复杂、Compation影响行性能
  • Kafka:写入快、高吞吐、扩展强;但不适合小消息、磁盘占用大
  • Flink的RocksDB:支持大状态、精准一次、故障恢复快;但状态回溯成本高、磁盘IO有瓶颈

1.核心思想

核心思想:利用顺序写入的方式来提高写操作的性能,并通过牺牲部分读取性能来换取更高的写入吞吐量

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/683b60c1f91b4b5795e7f047727382bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753661&x-orig-sign=9J7Vxp%2B6xCOpYRkMdn87otEWYUI%3D

转变顺序:MemTable-->Immutable MemTable-->SSTable(落盘的)

0) WAL

预写日志,就像图书馆的入库登记簿,确保即使发生意外,也知道有哪些书应该在哪。

1) MemTable

内存表:就像新书暂存区

MemTable是在内存中的数据结构(跳表),用于保存最近更新的数据,会按照Key有序地组织这些数据,LSM树对于具体如何组织有序地组织数据并没有明确的数据结构定义,例如Hbase、Rocksdb默认是跳表来保证内存中key的有序。

因为数据暂时保存在内存中,内存并不是可靠存储,如果断电会丢失数据,因此通常会通过WAL(Write-ahead logging,预写式日志)的方式来保证数据的可靠性。

跳表就像多层索引系统:
   顶层:快速导航 → 哲学区
   中层:分类指引 → 中国哲学  
   底层:详细定位 → 《论语》架位3层2号

2) Immutable MemTable

当 MemTable达到一定大小后,会转化成Immutable MemTable。 Immutable MemTable是将转MemTable变为SSTable的一种中间状态。 写操作由新的MemTable处理,在转存过程中不阻塞数据更新操作。

3) SSTable(Sorted String Table)

就像打包好的箱装书,按ISBN编号有序存放。

有序键值对集合(有序字符串表),是LSM树组在磁盘中的数据结构。为了加快SSTable的读取,可以通过建立key的索引以及布隆过滤器来加快key的查找

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/4e7de807438b4939b0205052f372b6fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753662&x-orig-sign=39JCJ7mV8vqftZMEprMZZtutA1c%3D

这里需要关注一个重点,LSM树(Log-Structured-Merge-Tree)正如它的名字一样,LSM树会将所有的数据插入、修改、删除等操作记录(注意是操作记录)保存在内存之中,当此类操作达到一定的数据量后,再批量地顺序写入到磁盘当中。这与B+树不同,B+树数据的更新会直接在原数据所在处修改对应的值,但是LSM数的数据更新是日志式的,当一条数据更新是直接append一条更新记录完成的。 这样设计的目的就是为了顺序写,不断地将Immutable MemTable进行flush到持久化存储即可,而不用去修改之前的SSTable中的key,保证了顺序写

因此当MemTable达到一定大小flush到持久化存储变成SSTable后,在不同的SSTable中,可能存在相同Key的记录, 当然最新的那条记录才是准确的。 这样设计的虽然大大提高了写性能,但同时也会带来一些问题:

1)冗余存储,对于某个key,实际上除了最新的那条记录外,其他的记录都是冗余无用的,但是仍然占用了存储空间。因此需要进行Compact操作(合并多个SSTable)来清除冗余的记录。 2)读取时需要从最新的倒着查询,直到找到某个key的记录。最坏情况需要查询完所有的SSTable,这里可以通过前面提到的索引/布隆过滤器来优化查找速度。

3) Compaction

压缩:就像图书整理日,合并书籍、去重、优化布局。

2.LSM树的Compact策略

Compact操作是十分关键的操作,否则SSTable数量会不断膨胀。在Compact策略上 ,主要介绍两种基本策略:size-tiered和leveled。

先介绍三个比较重要的概念,事实上不同的策略就是围绕这三个概念之间做出权衡和取舍。

  • 读放大 读取数据时实际读取的数据量大于真正的数据量。例如在LSM树中需要先在MemTable查看当前key是否存在,不存在继续从SSTable中寻找。
  • 写放大: 写入数据时实际写入的数据量大于真正的数据量。例如在LSM树中写入时可能触发Compact操作,导致实际写入的数据量远大于该key的数据量。
  • 空间放大: 数据实际占用的磁盘空间比数据的真正大小占用更多。上面提到的冗余存储,对于一个key来说,只有最新的那条记录是有效的,而之前的记录都是可以被清理回收的。

1) size-tiered 策略--固定层条数

原理:每层达到多少条数,就进行一次合并,并将合并结果传给下一层

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ff304f00e04e443e9b8840e15075fc73~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753663&x-orig-sign=9hoAfxOSa8fkFeWeIHNDQnVfUY8%3D

size-tiered策略保证每层SSTable的大小相近,同时限制每一层SSTable的数量。如上图,每层限制SSTable为N,当每层SSTable达到N后,则触发Compact操作合并这些SSTable,并将合并后的结果写入到下一层成为一个更大的sstable。

由此可以看出,当层数达到一定数量时,最底层的单个SSTable的大小会变得非常大 并且size-tiered策略会导致空间放大比较严重 即使对于同一层的SSTable来说,每个key的记录是可能存在多份的,只有当该层的SSTable执行compact操作才会消除这些key的冗余记录。

2) leveled策略--固定层大小

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/970012b741974cdabfa6d7435245b5ad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753661&x-orig-sign=CtDOJm2JRLbJ7Aet1JCSDqeQfLY%3D

每一层的总大小固定,从上到下逐渐变大

leveled策略也是采用分层的思想,每一层限制总文件的大小。

但是跟size-tiered策略不同的是,leveled会将每一层切分成多个大小相近的SSTable。

这些SSTable是这一层是全局有序的,意味着一个key在每一层至多只有1条记录,不存在冗余记录。 之所以可以保证全局有序,是因为合并策略和size-tiered不同,接下来会详细提到。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e0ad09465fe2492dbc1c8dc3d31ae3df~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753661&x-orig-sign=cj7earuELgBDwxWLozfSe8oP1eE%3D

每一层的SSTable是全局有序的

假设存在以下这样的场景:

  1. L1的总大小超过L1本身大小限制:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/81d8338d5966445888bc9f6f4890e25b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753662&x-orig-sign=b3%2BBbeDXneI2uNX2DI4OFhwOppU%3D

此时L1超过了最大阈值限制

  1. 此时会从L1中选择至少一个文件,然后把它跟L2**有交集的部分(非常关键) **进行合并。生成的文件会放在L2:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/473e68eba218483290295d0335f3bb75~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753661&x-orig-sign=v%2FoUkeP%2B4ojXMDQFvJzinmLxGTM%3D

如上图所示,此时L1第二个SSTable的key的范围覆盖了L2中前三个SSTable,那么就需要将L1中第二个SSTable与L2中前三个SSTable执行Compact操作。

  1. 如果L2合并后的结果仍旧超出L5的阈值大小,需要重复之前的操作 —— 选至少一个文件,然后找到其与下层有关联的部分,最后把它合并到下一层:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7ef52509534340fe988c566e0c574c9f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753661&x-orig-sign=qQs1EIOM1bHqSFCaOnpcPA0WrFU%3D

需要注意的是,多个不相干的合并是可以并发进行的

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/6cc19efe359b4c189835edf268acf5b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753661&x-orig-sign=h0YO6%2Fj1XtaCiUuLXxbGVewkA%2FU%3D

leveled策略相较于size-tiered策略来说,每层内key是不会重复的,即使是最坏的情况,除开最底层外,其余层都是重复key,按照相邻层大小比例为10来算,冗余占比也很小。因此空间放大问题得到缓解。但是写放大问题会更加突出。

举一个最坏场景,如果LevelN层某个SSTable的key的范围跨度非常大,覆盖了LevelN+1层所有key的范围,那么进行Compact时将涉及LevelN+1层的全部数据。

3) 补充

<1> 布隆过滤器

可以将布隆过滤器部署在LSM磁盘Component之上,当做一个独立的过滤器使用。

  • 对于点查询,先通过布隆过滤器判断是否存在,由于布隆过滤器的判非特性,当不满足布隆过滤器,也就没有必要读取该Component了。
  • 对于当Component是B+树的情况,也可以将布隆过滤器部署在叶子上来达到降低IO的目的。
<2> 分区思想

另一种常见的方法是采用分区技术,即每一个Component又按照范围划分成多个不同范围的小的Component(每个称之为SSTable) 。这样做有很多好处:首先,可以将大的Component的合并分割成多个小的合并并且分区还可以优化顺序递增主键和非均匀更新的性能。目前主流的数据库比如LevelDB和RocksDB都是采用的这种分区技术, 称之为分区合并策略(Partitioned Leveling Merge Policy)。

由于level 0是存储在内存,所以没必要分区。 当将一个SSTable从level L合并到SSTable从level L+1 时,只需要把与level L有交集的level L+1取出来,与level L合并生成新的level L+1上的SSTable即可,其他的不需要处理。

下图显示了当需要将level 1的0-30合并到level 2时,只需要处理level 2的0-15和16-32两个SSTable即可。合并后,旧的SSTable直接交给回收系统进行垃圾回收。对于一次合并过程究竟选择哪一个SSTable,不同数据库系统采用不同的方案,LevelDB采用round-robin方式。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/445e8d54789444a3bba900c7d43cf19f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZXhwZWN0N2c=:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzUxNDQ3MTM4NzIzNTczNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1750753662&x-orig-sign=yIQi35%2FIQBUdNO2%2BPxAO56ZuHP8%3D