一文弄懂LSM-Tree
LSM-Tree是什么?
LSM-Tree(Log Structured Merge Tree)是一种数据结构,它被设计用于处理大量写入操作的场景,常见于许多NoSQL数据库中,如BigTable、Cassandra、RocksDB和LevelDB等。
LSM-Tree的核心思想是将所有的更新操作(包括插入、删除和修改)都转换为追加写操作,从而充分利用磁盘顺序写性能远高于随机写性能的特性。
在LSM-Tree中,数据被分为内存组件和硬盘组件两部分。内存组件通常包含一个或多个MemTable,这些MemTable以某种有序的数据结构(如跳表或红黑树)存储数据。当MemTable达到一定大小后,它会被写入到磁盘上,成为不可变的MemTable,并且新的写入操作会进入一个新的MemTable。
硬盘组件由不同级别的SSTable(Sorted String Table)组成,这些SSTable是不可变的,并且按照层次结构存储。SSTable中的数据是有序的,当执行读取操作时,系统会先在MemTable中查找数据,如果不在MemTable中找到,则会按照从最新到最旧的顺序在SSTable中查找。
LSM-Tree的写入操作是高效的,因为它们只涉及追加操作,不需要像B+树那样需要找到数据在磁盘上的确切位置并进行更新。删除操作在LSM-Tree中通常不是通过物理删除数据来实现的,而是通过写入一个删除标记来表示该数据已被删除。读取操作可能会涉及多个层次的查找,因为数据可能存在于多个SSTable中。
为了减少读取操作的开销,LSM-Tree会定期进行合并(Compaction)操作,合并过程中会删除过时的数据,并将新的数据版本合并到更低层次的SSTable中。这个过程有助于减少读取操作时需要检查的SSTable数量,并且可以回收磁盘空间。
总的来说,LSM-Tree非常适合写入密集型的应用场景,因为它优化了写入操作的性能,但可能会牺牲一些读取性能。通过内存和磁盘上的分层存储,以及定期的数据合并,LSM-Tree能够提供高效的数据存储解决方案。
LSM-Tree原理
该图来源 https://www.cnblogs.com/zxporz/p/16021373.html
写操作:
图片来源:Stefan Richter Flink Forward 2018
插入操作只需要单纯的向memtable插入键值对。如果memtable已经存在对应索引键,那么变为更新操作。
可以看到LSM tree插入操作非常快,只需要在memtable中插入一条数据即可。复杂度无论是skiplist还是红黑树结构都是O(logN)。
1.WAL(预写日志):所有写操作首先记录到WAL中,确保数据的持久性和一致性。
2.Memtable:WAL操作完成后,数据(键值对)写入内存中的Memtable,通常使用跳表或红黑树等有序数据结构,以便快速访问和保持数据有序。
3.Immutable Memtable:当Memtable达到一定大小后,它会被冻结成Immutable Memtable,新写入的数据进入新的Memtable,保证写操作的连续性和性能。
4.Minor Compaction:Immutable Memtable会定期写入磁盘,形成SSTable的level 0层。SSTable由有序的键值对和索引组成,优化了磁盘的顺序写性能。
5.Major Compaction:随着时间的推移,level 0的SSTable数量增多,会触发Major Compaction。多个SSTable被归并合并,形成更大的SSTable并移动到更高的层级,释放空间并减少查询时需要遍历的SSTable数量。
修改流程:
更新数据在memtable中我们直接更新即可。如果不在内存,我们就直接插入新的键值对。
1.WAL:修改操作首先记录到WAL。
2.Memtable:在Memtable中找到对应的键并进行修改,如果键不存在则作为新键插入。
3.Immutable Memtable & Minor Compaction:随着数据的积累,Immutable Memtable会被创建并最终写入磁盘的SSTable。
4.Major Compaction:在后续的合并过程中,新的键值对会替换掉旧的键值对。
删除流程:
删除操作无论我们的数据在不在内存中都只是插入一个“墓碑标记”。
1.WAL:删除操作首先记录到WAL。
2.Memtable:在Memtable中找到对应的键并标记为删除(tombstone),如果键不存在则直接插入一个删除标记。
3.Immutable Memtable & Minor Compaction:删除标记随着Immutable Memtable写入磁盘的SSTable。
4.Major Compaction:在后续的合并过程中,被标记删除的键会被真正删除。
读操作:
1.查询Memtable:首先,在内存中的Memtable中查找所需的键。Memtable通常是一个有序的数据结构,如跳表或红黑树,这使得查找操作相对高效。
2.查询Immutable Memtable:如果Memtable中未找到键,接下来会在Immutable Memtable中查找。Immutable Memtable是Memtable满了之后冻结的版本,它也存储在内存中,但不允许修改。
3.查询L0层SSTable:如果前两步都没有找到键,接下来会在磁盘上的L0层SSTable中查找。L0层可能包含多个SSTable,因为每个Immutable Memtable写入磁盘时都会生成一个新的SSTable。由于这些SSTable可能包含重叠的键,所以需要遍历所有L0层的SSTable。
4.查询非L0层SSTable:如果数据不在L0层,接下来会按照层级从L1到LN查询。在非L0层,由于使用了Level Compaction策略,每个层级的SSTable都是全局有序的,并且一个键在每一层中最多只出现一次。因此,只需要遍历一个SSTable来确定键是否存在。
L0层可能有重叠的键
当Memtable达到一定大小后,它会被写入到磁盘上,变成L0层的一个SSTable。由于写操作是持续进行的,可能会有多个Memtable同时存在,当它们分别变成SSTable并写入到L0层时,就可能出现键的重叠。这是因为:
并发写入:多个Memtable可能同时被写入磁盘,每个Memtable转换成的SSTable可能包含相同的键。
未合并:L0层的SSTable是直接从Memtable转换来的,它们还没有经过合并(Compaction)过程,所以这些SSTable中的键可能还没有被整理过,即存在多个版本的同一个键。
非L0层全局有序
非L0层(L1, L2, …,LN)的SSTable是通过合并L0层或更低层的SSTable形成的。在合并(Compaction)过程中,这些层的SSTable会被组织成全局有序的:
归并排序:在Compaction过程中,系统会将多个SSTable归并排序,确保同一个键的所有版本都被合并,并且只保留最新的版本。
层次结构:每一层的SSTable都是全局有序的,意味着在整个层级中,每个键只会出现一次。这是因为在Compaction过程中,旧版本的键会被删除,只保留最新的键。
分层存储:每一层的SSTable数量和大小通常有限制,当达到这个限制时,会触发Compaction,将当前层的SSTable与下一层的SSTable合并,形成新的、更大的SSTable,并且移动到下一层。
举例说明
假设我们有一个键 A,其值在系统中被更新了三次,分别在不同的Memtable中:
Memtable1:A -> value1
Memtable2:A -> value2
Memtable3:A -> value3
当这些Memtable被写入磁盘时,它们会变成L0层的SSTable,可能如下所示:
SSTable_L0_1:A -> value1
SSTable_L0_2:A -> value2
SSTable_L0_3:A -> value3
这时,L0层的SSTable中键 A 是重叠的,因为它们包含 A 的不同版本。
随后,Compaction过程会被触发,将L0层的SSTable合并成一个新的SSTable,并且移动到L1层:
SSTable_L1_1:A -> value3
在L1层,键 A 是全局有序的,因为只保留了最新的版本 value3。
这样,查询操作在L0层需要检查所有SSTable以找到最新的版本,在非L0层则只需要检查一个SSTable即可。
合并策略
SSTable有两种合并策略:
Leveling Merge Policy:
每个level仅有1个组件,L0和L1合并,合并到L1中;
由于组件较少,查询性能较高,LevelDB和RocksDB使用该策略;
Tiering Merge Policy:
每个Level有N个组件,合并后生成Level+1的一个新组件;
由于可以降低合并的频率,写入性能较高;