为 NoSQL打下半壁江山的 LSM-Tree 是如何工作的?
大家好,我是猿java。
在 数据库,你必须掌握这8种数据结构!这篇文章中,我们提到了 LSM-Tree 是现代数据库中必不可少的一个数据结构,那么,LSM-Tree 是什么?它是如何工作的?又有什么用途?今天我们就来一一解答。
一、什么是 LSM-Tree?
LSM-Tree:Log-Structured Merge Tree,翻译成中文是:日志结构合并树。其思想源于论文《The Log-Structured Merge Tree》,论文链接:www.cs.umb.edu/~poneil/lsm… LSM-Tree 的思路是将索引树结构拆成一大一小两棵树,小的索引树 C0 tree 存储在内存, 大的索引树 C1 tree 存储在磁盘,它们共同维护一个有序的 key空间。如下图:
但是,随着业务的快速发展,LSM-Tree 也在发生着变化,现代 LSM-tree 包含了三个部分:memtable、immutable memtable、SSTable,前两个位于内存,最后一个位于磁盘中。如图下:
不过,早期的 LSM-Tree 和现代的 LSM-Tree 的核心思想是一样:充分利用了磁盘批量顺序写的速度要远比随机写性能高出很多,再加上内存 + 磁盘多层合并,大大提升了读写性能。
从 LSM-Tree 的发展,我们可以看出,LSM-Tree 俨然已经从最开始的搜索树发展成了一种多层级的读写流程,不再是一棵树了。
二、LSM-Tree 工作原理
LSM-tree 核心是将写入操作与合并操作分离,通过将数据写入日志文件和内存缓存,然后定期进行合并操作来提高写入和查询的性能。下面就来看一下 LSM-Tree 的工作原理:
- 写入操作:
-
- 写入日志文件(Write-Ahead Log, WAL):当有新的 key-value需要写入时,首先将其追加到顺序写的日志文件中。这个操作称为预写日志(Write-Ahead Logging),它可以确保数据的持久性和一致性。
- 写入内存缓存(Memtable):同时将新的 key-value写入内存中的数据结构,通常是跳表或红黑树等有序数据结构。内存缓存可以快速响应读取操作,并且具有较高的写入性能。
- 合并操作:
-
- 内存与磁盘的合并(Memtable to SSTable):当内存缓存达到一定大小或者其他触发条件满足时,将内存缓存中的 key-value写入到磁盘上的 SSTable 文件中,通常以一种有序的方式进行存储。
- SSTable的合并:定期或根据其他策略执行 SSTable文件的合并操作。合并操作将多个 SSTable文件进行合并,消除重复的 key和删除的 key,并生成新的 SSTable文件。合并操作可以基于时间、文件大小或其他条件进行触发和控制。
- 查询操作:
-
- 内存缓存查询:首先在内存缓存中查找 key-value,由于内存缓存是有序的,可以快速定位。
- 磁盘上的SSTable查询:如果在内存缓存中未找到,则依次在磁盘上的 SSTable文件中进行查找,通常采用二分查找等算法进行快速定位和检索。
为了更好地理解 LSM-Tree 原理,这里以 LevelDB 的 LSM-Tree 为例来进行讲解:
如上 LSM-Tree 的示意图,LSM-Tree 是一个多层结构,自上而下存储的数据越来越多。最上层位于内存中,存储的是最近写入的 key-value 数据。接下来的 Level-0~Level-N 位于磁盘中,每一层都按照 key的字典顺序进行排序。
写入阶段:
假设有一个 put name=java 的操作,该操作首先会写入磁盘的 WAL(Write Ahead Log,预写 Log)日志中,用于故障恢复。
然后,将 name=java写入到内存的 memtable 中并且回复用户写入成功,此过程并不会判断 name这个 key 是否存在。
到此,put name=java 这个 write过程就完成了。
说明:WAL 主要是用于故障恢复,如果出现系统宕机,导致内存中来不及写入磁盘,可以从 WAL 中进行数据恢复,确保了数据写入的可靠性。比如:MySQL 的Binlog 等,就利用了 WAL预写log功能。
合并操作:
因为内存是有限的,所以当数据总量超出 memtable 的阈值后,memtable 就会被转换为 immutable memtable(只读的 memtable),然后再创建一个空的 memtable 继续写入数据。使用 immutable memtable,避免了将 memtable 中的内容拷贝到磁盘时阻塞了写操作。
另外,随着内存 memtable 拷贝到磁盘 SSTable 中的数据越来越多,SSTable的读写性能也会受影响,因此需要按照一定的策略,将多个 SSTable 文件(segment)进行合并,消除重复键、删除键以及更新过期键,并生成新的 SSTable文件。
查询操作:
比如查询name=java,首先查询 memtable 区域,如果不存在,再查询 immutable memtable 区域,同理,依次从 L0 层的 SSTable 文件开始,逐层遍历,如果能查询到则返回 value,否则返回空。因为 L0 ~ LN key是有序的,所以可以使用二分查找法进行快速定位。
**LSM-Tree 的写入功能是不是很像下面的多层喷泉模型,当上一层满了,就会溢出 **(合并) 到下一层,同理,查询功能也是如此,上层查不到就到下一层查找,直到找到 value 或者返回空。
三、LSM-Tree 的 三大组件
****在 LSM-Tree 的原理分析中,我们提到了memtable、immutable memtable、SSTable ,因此,我们来重点聊聊这 3个组件。
memtable
memtable 是内存中的数据结构,存储近期更新的记录值,因此可以采用一些有序集合来存储,比如跳表、红黑树都是非常不错的选择,整体来说,memtable 相对比较简单,采用的数据结构也比较常见。
immutable memtable
immutable memtable,可以理解成不可变表。它是因 memtable 存储容量达到阈值后转变而来,所以采用的数据结构和 memtable 一样。引入 immutable memtable 的原因是可以有效避免 memtable 的读写冲突竞争,从而避免阻塞,提高系统性能。
SSTable
SSTable 是 LSM-Tree 的核心,主要是用于数据在磁盘上的持久化存储,存储一些按照 key 有序排列的键值对组成的 segment。另外,为了加快查询速度,通常需要给 SSTable 创建索引,索引存储在 SSTable的尾部,用于帮助快速查找特定的 segment,结构示意图如下:
数据存 储: 因为内存中的数据是有序的,所以将内存中的数据拷贝到磁盘的最快的方式就是顺序写,因此可以把 immutable memtable中一批key-value 数据直接拷贝到 SSTable,而这一批 key-velue数据,在 SSTable中称为一个segment,segment 会一段一段往后叠加。检索数据时,按照segment序号从后往前查找,如下图所示:
随着数据的增多,segment也会越来越多,显然在一个 SSTable 中存放太多的segment 是不合理的,同时存在不同的 segment 中可能存在相同的key,该如何处理呢?
压缩数据: 当 segment越来越多,势必造成读写性能下降,因此需要把多个 segment压缩成一个 segment,并且合并相同的 key,如下图,可以将多个 segment合并成一个 segment,这样segment就不会无限膨胀,另外,对应相同的key,总是用最新的 value去覆盖最老的 value。****
了解完数据存储、数据压缩,数据检索后,如何删除数据呢?
删除数据: 在 LevelDB 的 LSM-Tree中,删除数据并不会立马做物理删除,而且给数据打一个特殊的删除标,在 LSM-Tree里面叫做 tombstone(墓碑),查询数据时,如果最先查到打了 tombstone 标的数据,就代表数据已经删除了(做数据的物理删除是在 segment 做合并压缩时)。如下示意图:
四、LSM-Tree 优缺点
优点: 具有较高的写入性能和压缩存储,适用于写多读少或写入速度较快的场景。通过将写入操作集中在内存和顺序写的日志文件中,可以获得较高的写入吞吐量。查询性能也较好,通过内存缓存和有序的SSTable文件,可以快速定位和检索键值对。
缺点: 读放大。如读取操作可能需要扫描多个 SSTable文件,导致查询性能不如B-tree 等结构稳定。此外,合并操作可能会引起较长的停顿时间,对于实时性要求较高的系统可能会有影响。为了解决这些问题,LSM树引入了一些优化技术,如布隆过滤器和层级结构。
布隆过滤器(Bloom Filter):LSM-Tree 中的每个 SSTable文件可以关联一个布隆过滤器。布隆过滤器是一种高效的概率型数据结构,用于快速判断某个键是否存在于SSTable文件中,从而避免不必要的磁盘访问。布隆过滤器可以在非常低的误判率下,快速准确地判断某个键是否可能存在于SSTable文件中。
层级结构:LSM-Tree 通常包含多个层级,如 Level-0、Level-1、Level-2等。Level-0是内存缓存和日志文件,Level-1及以上是磁盘上的SSTable文件。较小的层级(如Level-0)通常用于高频的写入操作,较大的层级(如Level-1及以上)用于存储稳定的数据。通过层级结构,可以减少查询时需要扫描的SSTable文件数量,提高读取性能。
压缩和合并策略:LSM树可以采用不同的压缩算法和合并策略来优化存储和性能。例如,可以使用压缩算法对SSTable文件进行压缩,减少磁盘占用空间。合并策略可以根据不同的触发条件和优先级,灵活地控制合并操作的频率和规模,以平衡写入性能和查询性能。
五、LSM-Tree 有什么用途?
- 分布数据库:LSM-Tree 被广泛用于分布式数据库系统,如Hbase、Apache Cassandra、LevelDB/RocksDB 等等。这些 NoSQL 都是使用 LSM-Tree来处理大规模的分布式数据存储和查询,以提供高吞吐量和低延迟的数据访问。
- 日志存储:由于LSM-Tree对写入操作具有高效的支持,它在日志存储场景中表现出色。例如,用于处理日志数据的系统,如ELK(Elasticsearch, Logstash, Kibana)堆栈,可以使用LSM-Tree来快速地写入和检索大量的日志事件。
- 高速缓存:LSM-Tree可用于实现高速缓存系统,以提供快速的数据访问和响应时间。它适用于需要处理大量数据并且对读取操作具有较高要求的场景,例如内存数据库或者缓存层。
- 分布式文件系统:一些分布式文件系统,如Hadoop的HDFS和Google的GFS,使用LSM-Tree来管理文件系统的元数据,例如文件和目录信息。这样可以提供高效的元数据访问和更新操作。
- 时间序列数据:由于LSM-Tree对写入操作的高效支持和时间序列数据的特性相符,它在时间序列数据存储和分析中被广泛使用。这包括物联网(IoT)应用、日志分析、金融数据等领域。
六、总结
LSM-Tree:Log-Structured Merge Tree,翻译成中文是:日志结构合并树;
早期 LSM-Tree 的思路是将索引树结构拆成一大一小两棵树,小的索引树 C0 tree 存储在内存, 大的索引树 C1 tree 存储在磁盘,它们共同维护一个有序的 key空间;
现代 LSM-tree 包含了三个部分:memtable、immutable memtable、SSTable,前两个位于内存,最后一个位于磁盘中;
memtable、immutable memtable 可以使用一些常用的有序集合,比如:跳表,红黑树,SSTable 是 LSM-Tree 的核心部分,内部存储有序的 segment 数据;
LSM-Tree 有着广泛的使用,其具有代表意义的就是使用在 NoSQL LevelDB/RocksDB 和 Apache Cassandra、Hbase中;
LSM-Tree 是为 NoSQL 存储系统而生,不同的 NoSQL 产品对LSM-Tree的实现略有差异,但其核心一样,可以借助多层喷泉的模型去理解 LSM-Tree;
交流学习
最后,把我的座右铭送给你:投资自己才是最大的财富
。 如果你觉得本文章对你有帮助,点赞,收藏不迷路,关注公众号:猿java,持续为你输出更多的硬核文章和面试经。
转载自:https://juejin.cn/post/7373964489680683071