基于LSM树的存储机制简述

下午听了关于MyRocks-PASV的研究讲座,很有意思所以学习了一下LSM树的一些简单的底层原理。现在整理一下


我们都知道目前Key:Value型的数据库普遍较之关系型数据库有着更好的表现,为什么会有这样的一个差异呢?关键就在于存储形式和读写机制的不同。
Key:Value型数据库可以通过LSM Tree(Log-Structured-Merge-Tree)来进行存储,而以MySQL为代表的关系型数据库则以B+树的形式来组织聚簇索引文件和二级索引文件来进行存储。这里的重点并不是这些实现形式,这里关系型数据库的存储问题关键在于,它是直接保存数据值在文件中的,也就是说当我们对这些值进行更新时,必须要在索引中一个一个把这些值找出来然后进行磁盘的随机存取,这才是关键,二者的实现机制由于是否顺序写产生了很大的差别。

LSM树的具体机制

LSM树其实并不是一个真正意义上的树形结构,其核心特点是利用顺序写来提高写性能,顺序写很好理解。举个很多人都背过的例子就是MySQL的InnoDB引擎在写入内容时会首先通过WAL机制,先将改动写进磁盘中的redo log这个物理日志中,物理日志记录的是对某表的某数据页某偏移量处做了某更新,然后再找机会把redo log中的内容刷进磁盘中真正存储数据的地方。这么做的好处就是因为写入物理日志只需要接着之前写到的位置往下写就行,不需要随机存取磁盘内容,可以进行的相对快速。

但问题就是,MySQL终究还是要把redo log中的操作写入磁盘中真正存储数据的地方,而这个动作就限制了关系型数据库的性能发挥。而LSM树就是直接将WAL机制作为存储数据的主要机制来实现数据库的存取。

LSM树示意图

如图所示,LSM树有以下三个重要组成部分

  • MemTable
  • Immutable MemTable
  • SS Table

MemTable

MemTable是在内存中的数据结构,用于保存最近更新的数据操作,这里会按照Key有序地组织这些数据。至于具体如何组织这些数据以使其按照Key有序,LSM树并没有做出相应的规定,可以自行实现。

要注意的是因为数据暂时保存在内存中,那么如果断电了就会存在丢失数据的风险,因此这里还是会跟InnoDB一样通过WAL机制来写入磁盘,以保证数据不会因为崩溃或断电而丢失。

Immutable MemTable

当MemTable存储的数据达到阈值后,就会将该Memtable就地转化成Immutable MemTable。Immutable MemTable是将转MemTable变为SSTable的一种中间状态。你可以理解其为一个暂存文件,该文件不再继续写入,而是等待刷入磁盘的SSTable中。之后的写入工作则由一个新创建的MemTable处理。

SSTable(Sorted String Table)

其本质是有序键值对的集合文件,是LSM树在磁盘中的数据结构,我们可以建立key的索引来加快查找。

重点来了,LSM树会将所有的数据插入、修改、删除等操作记录(注意是操作记录)保存在内存之中,当此类操作达到一定的数据量后,再批量地顺序写入到磁盘当中。注意,这与B+树不同,B+树数据的更新会直接在原数据所在处修改对应的值,但是LSM数的数据更新是日志式的,就跟上面说的WAL机制的redo log一样,一条数据更新是直接将更新记录写进log的最后来完成的。而这样设计的目的就是为了顺序写,我们只需要不断地将Immutable MemTable刷入磁盘进行持久化存储即可,不用去修改之前的SSTable中的key,也就保证了顺序写,提升了效率。

不过不断地向后写也就意味着,在不同的SSTable中,可能存在相同Key的记录,显然最新的那条记录才是真实的。那么这样设计的虽然大大提高了写性能,但同时也会带来一些问题:

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

Compact策略

讲到SSTable,我们可以看到这里从MemTable到SSTable一路下来全是顺序写的,问题是磁盘容量并不是无限的,何况SSTable中实际上的真实数据只有最新的那一条,所以如何对SSTable进行Compact来释放空间是很重要的。

Compact释放空间的策略讲到底就是围绕着三大性能的权衡策略。即在以下三个问题中,我们必须至少接受其中之一:

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

size-tiered 策略

简单来说就是把SSTable分层,限制各层的SSTable数量,并且要求每一层的SSTable的大小都要相近,注意不是相同。当上层SSTable过多时,就将多出来的SSTable合并,作为一个更大的SSTable存入下一层。

可以看出,当层数达到一定数量时,最底层的单个SSTable的大小会变得非常大。而且这种策略会导致空间放大严重。因为即使对于同一层的SSTable,每个key的记录是可能存在多份的,只有当该层的SSTable执行compact操作才会消除这些key的冗余记录,那么积累到整个结构中,冗余的记录占比可想而知。

leveled 策略

leveled策略也是采用分层的思想,与size-tiered策略不同的地方在于,leveled策略会将每一层切分成多个大小相近的SSTable,并且保证这些SSTable是这一层是全局有序的,而这就意味着一个key在每一层至多只有1条记录,不存在冗余记录。

Leveled策略是通过合并策略来保证每一层的SSTable全局有序的。具体实现方法为当某一层总大小超过限制,那么选择该层中与下一层存在交集的SSTable文件进行合并并放入下一层,重复这个操作直到该层大小恢复到限制以内为止。并且,多个不相干的合并是可以并发进行的,这大大提高了效率。

这种策略极大缓解了空间放大的问题,但是代价是写放大问题更加突出。在最坏的情况下,如果该层某个SSTable的key的范围跨度非常大,覆盖了下一层所有key的范围,那么进行Compact时将涉及下一层的全部数据。

不过这里只是简单的聊了一下这些机制,具体使用LSM树进行存储的那些应用中,基本都对它们进行了进一步的优化。这里不再细说

posted on 2022-11-25 00:18  寒光潋滟晴方好  阅读(283)  评论(0编辑  收藏  举报