阅读 147

LSM-Tree简介

前言

LSM-Tree全称是Log Structured Merge Tree(日志结构合并树),最早听过这个存储引擎概念还是在《Bigtable: A Distributed Storage System for Structured Data》这篇paper中,后续在接触很多如Hbase,LevelDB,RocksDB这些数据库组件的时候发现他们都是在使用的LSM-Tree结构来组织的。我们可能最多接触的还是Mysql的B+Tree这样的数据结构,但是在大数据领域内的存储引擎基本上都是基于LSM-Tree来实现的。

我们一定好奇,为什么不能和MySQL一样用B+Tree来实现?B-Tree或者B+Tree其实就是一个多叉树的结构,每个叶子节点上存储了一个page的数据。B-tree这类存储结构多用于OLTP型的数据库,因为这类数据库主要以事务,或是行级别的读取和存储为主的。这种类型的数据库更多的操作是小批量或单行级别的更新或读取,并且可能还有事务方面的需求。B+Tree毋庸置疑,其结构赋予了很好的读性能,但是在大数据场景下,经常性的会有大量的数据的写入和更新。这样大量的随机数据的插入操作,就会让B+Tree不断的对页进行分裂,当有大量分裂时,会导致大量的磁盘随机寻道,从而降低性能。

上述B+Tree的问题,就是它在大量的更新插入操作的时候,无法解决随机IO暴增的问题。我们知道磁盘的顺序写的速度是远远大于随机写的速度的,所以便设想能不能有一种方式将所有的随机写都能转为顺序写?即使牺牲部分读性能。LSM-Tree就是这样,在B+树这些存储引擎中,如果有新插入的或者更新的数据,我们是需要去寻找到对应位置再将数据写入的,而LSM不是这样,他会将所有数据的操作都顺序写入日志和内存中,然后通过后面不断的合并来将数据更新。这样子,针对写入操作就会很快

LSM-Tree

如上所说:LSM树的核心是**放弃部分读性能,换取写入能力的最大化**。而让写能力最大化的方式就是 将所有的随机IO全部转为顺序IO(包括那些更新的操作)。而对于读性能的缺失,通过一些其他的机制来补救,比如:布隆过滤器,树合并。而且要注意一点,LSM和B+Tree不一样,它不算是一种索引结构,而是一种文件存储方式。LSM-Tree里面Tree的实现是没有固定标准的。

核心概念

LSM的基本思想:假定内存足够大,因此不需要每次有数据更新就必须将数据写入到磁盘中,因此可以先将最新的数据驻留在内存中,等到积累到最后多之后,再使用归并排序的方式将内存内的数据合并追加到磁盘队尾。

image-20210829184506981.png 如图,LSM-Tree的存储分为了2个部分:内存和磁盘。其中C0树存在于内存缓存,C1树存在于硬盘上。我们将新的数据的插入更新都在C0树进行操作,并直接在内存中对数据进行排序。当C0树达到我们设置的一定阈值的时候,就需要将C0树落盘到C1树上去。磁盘中一开始只有一个C1-Tree,经过多次的合并会逐步递增C2-Tree...Cn-Tree,树的大小也会越来越大。

上面介绍的比较粗糙,但是这就是LSM的核心思想,下面对其细化说明下。LSM-Tree在实现的时候,有下面重要的部分:

  • WAL(write ahead log)
    • 写前日志是所有数据库基本都有的一个功能,对数据的所有的更新操作都会不断的append到该日志文件,以备服务器宕机情况下数据的恢复。
  • MemTable
    • 这个就是C0-Tree在内存中对应的数据结构,这个树是什么结构,其实LSM并没有强约束,我们可以是红黑树,也可以是跳表结构。
  • Immutable MemTable
    • 当C0-Tree达到内存设置的阈值的时候,我们需要将数据进行落盘,但是在写的时候,总不能阻塞外面的写请求。所以我们会先将MemTable转变为一个不可变的Table,用来数据的持久化。同时新创建一个MemTable来处理新的读写请求
  • SSTable(Sorted String Table)
    • 这个是LSM树组在磁盘中的数据结构,是一个有序键值对集合。

image-20210829192038172.png

LSM tree的写入流程

  1. 先将写入操作append到wal日志里面。
  2. 将该数据存储到内存的memtable中,该表的实现可以是红黑树或者跳跃表。
  3. 当memtable达到内存阈值的时候,会将该部分数据形成快照,并新生产一个memtable用来处理后续的请求。
  4. 将immutable memtable持久化到磁盘的sstable中,该步骤也叫Minor Compaction,数据会先会合并到L0层,然后每层的sstable达到阈值的时候也会不断的向下合并(Major Compaction),这个阶段清除掉被标记删除掉的数据以及多版本数据的合并,避免浪费空间,因为SSTable都是有序的,我们一般采用merge sort进行合并。

LSM tree 的读取流程

  1. 先查询memtable以及immutable memtable,找到就返回内存中的(毕竟内存中是最新的)。
  2. 如果没有找到,则向下到sstable level 0 层开始查询,如果还是找不到,就继续往下面更高层次的sstable中查找,知道找到或者找不到进行返回。

LSM-Tree通过一些方式来优化其缺陷的读性能,因为我们不能像B+Tree那样找一次就能确定,很有可能要不断的下推查找。

  • Bloom filter:就是个带随即概率的bitmap,可以直接确定元素不存在。
  • compact:小树合并为大树,太多的小树,就会对应查太多的文件,所以定期将小树合并大树上可以有效减少查询次数。

LSM-Tree缺点

LSM-Tree存在的一个问题就是读写放大的问题:

读写放大 = 磁盘上实际读写的数据量 / 用户需要的数据量。

比如用户本来要写1KB数据,最后往磁盘写了10KB的数据,那么写放大就是 10,读也类似。

写放大

以RocksDB的Level Style Compaction 机制为例,这种合并机制每次拿上一层的所有文件和下一层合并,下一层大小是上一层的 r 倍。这样单次合并的写放大就是 r 倍,这里是 r 倍还是 r+1 倍跟具体实现有关,我们举个例子:

假如现在有三层:文件的大小分别为9k,90k,900k,r=10。此时需要写入1k,这时候就会不断合并,1+9=10,10+90=100,100+900=1000。总共写了 10+100+1000。按理来说写放大应该为 1110/1,但是各种论文里不是这么说的,论文里说的是等号右边的比上加号左边的和,也就是10/1 + 100/10 + 1000/100 = 30 = r * level。这只是最坏情况。

即因为达到阈值合并的原因,可能在某些条件下发生多米诺骨牌的效应,明明写很少的数据,实际底层却写了很多。

读放大

因为LSM-Tree的数据存在多级文件上,所以查询一个 1KB 的数据。比如sstable的L0层有8个文件(L0的key是不能保证唯一的)。然后实际数据可能在L6,那么最坏情况下需要读 L0 层的 8 个文件,再读 L1 到 L6 的每一个文件,一共 14 个文件。而每一个文件内部需要读 16KB 的索引,4KB的布隆过滤器,4KB的数据块(看不懂不重要,只要知道从一个SSTable里查一个key,需要读这么多东西就可以了)。一共 24*14/1=336倍。

总结

  • LSM-Tree通过将所有的随机IO转换为顺序IO,放弃部分读性能,换取写入能力的最大化。
  • LSM-Tree将数据的操作会先放在内存中直接执行,然后后面不断的进行compact操作来落盘,以及盘中数据的不断整合。所以内存越大,其实对LSM-Tree的性能越友好。
  • LSM-Tree还有很多知识点,包括优化,实现等等,后面有机会详解。
文章分类
后端
文章标签