LevelDB基础原理(2) LSM Tree
1. 介绍
1.1 描述
LSM Tree(Log Structured merge Tree) 意思是日志结构合并树。目前广泛应用于一些流行的KV存储引擎中(LevelDBl、HBase、Bigtable等)
- LSM树并不是像红黑树,B树那样树严格的树状结构,而是一种存储结构
- 其中日志结构指的是用日志形式的追加写
1.2 背景(为什么需要LSM Tree)
磁盘的顺序读写性能要比随机读写性能高两个数量,而日志结构下的写入就是顺序写。因此LSM Tree相比于传统的B+树存储引擎有更高的写入性能,通过把磁盘随机写转换为顺序写,可以大幅提高写入性能。但是需要牺牲部分读的性能,以及会面临写放大的问题。
2. 结构
2.1 LSM Tree结构
LSM Tree由两个或者更多的分层数据结构组成,最简单的是两层LSM Tree,C0层与C1层,C0层常驻内存,C1层在磁盘上。C0层保存最近更新的数据,一般使用红黑树、跳表、B树等数据结构实现,当插入操作使C0层到达一定的阈值的时候,他会与C1层合并到磁盘上。
LSM Tree的高性能来源于其每个组件都根据其底层存储介质进行调优,并且使用类似归并排序的算法,以滚动批次的方式有效的跨介质合并数据。
多层LSM Tree的结构类似,在两层的基础上扩展出C2、C3 ~ Cn层,其中C1~Cn层都在磁盘上,每一层都是在一个在key上有序的结构。
2.1.1 写入数据流程
当发生写入数据的请求时,请求首先会被写入WAL(Write Ahead Log)日志,然后写入C0,如果C0大小达到阈值,则会与C1层合并写入新的new C1,由于C0和C1都是有序的数据组织,可以通过类似归并排序的算法合并(Compaction), 合并的new C1会顺序写入磁盘,同样的,当C1达到阈值的时候,会和下层的C2合并,以此类推。
- 首先写入WAL日志的原因在于内存并不是可靠存储,如果发生断电可能会丢数据,先预写日志可以保证数据的可靠性
2.2.2 读数据流程
根据LSM Tree的结构可以发现,LSM Tree每层对相同的数据独立去重,意味着层之间会同时存在多份相同数据,其中最新的数据在C0层,最旧的数据在Cn层,因此读的时候可以从C0层依次向下读取,直到读取到对应的数据。
- 这也是为什么LSM Tree对读不友好的原因,对于不存在的数据查询会在每层进行多次查询,也就是存在读放大的问题
2.2 LevelDB 中的 LSM结构
LevelDB中的LSM及结构如下所示。 LSM Tree由两块组成:内存表、磁盘上的SSTable文件
2.2.1 内存表
内存表默认由4MB左右的内存块(可以调整), 存放有序的Key-Value。
- level db并未明确的定义如何有序的组织数据,例如HBase使用的跳表
内存表分为MemTable和ImmutableTable(不可变表),当有数据写入时,先将数据写入到Memtable,达到指定大小后,将其变为ImmuableTable,并将其以异步的方式合并到Level-0。
2.2.2 SSTable文件
2.2.2.1 文件结构
SSTable文件存储了落到磁盘上的数据,每层按照key range进行分区,将数据存放在多个SSTable文件中。level-i层最多拥有个SSTable,并且通过合并操作来约束第i层的数据至多只会与第i+1层的数据有N个文件的交集(所谓交集就是key range有重叠部分)。
每层的SSTable文件相互之间key range都不重叠,特殊的,level0层的SSTable文件相互之间存在重叠,因为level 0数据由immuableTable直接写入,不同的immuableTable之家无法保证key不重叠
2.2.2.2 合并(Compaction)
对于level i层而言,当每一层的大小超过 MB的时候,就会触发合并操作。
合并操作的流程是
- 选择level i层的一个SSTable文件,与level i+1层的所有有交集的SSTable文件进行合并并在i + 1层生成新的SSTable文件
- 当新生成的SSTable文件大小达到2MB时,会触发在i + 1层生成一个新的SSTable文件
- 当新生成的SSTable与下一层(也就是i + 2层)在重叠的key range的SSTable个数超过了N个时,也会在第i + 1层一个新的SSTable文件,这样可以保证在合并中不至于读取太多的SSTable文件(这会影响合并性能)
- 合并完成之后丢掉旧的SSTable文件(包括level i和level i+1层参与合并的文件)。
2.2.3 LevelDB中LSM Tree的问题
2.2.3.1 写放大(Write Amplification)
写放大指的是实际写的数据量大于用户需要的数据量,是由于合并操作会对同一条数据在磁盘上进行多次的重新写入导致的。这是LevelDB的硬伤,RocksDB通过将参数N变为动态来缓解写放大的问题,写放大会降低Flash磁盘(SSD)的寿命。
2.2.3.2 读放大(Read Amplification)
读放大指的是实际的读取量大于用户需要的数据量。LevelDB查询数据时,会按照数据的鲜度读排序(也就是依次从MemTable->ImmuableTable -> level0 -> level1)的顺序查询,特别的当查询key不存在的时候,每次都会查询到最后一层,一般结合布隆过滤器可以降低读放大的问题。
2.2.3.3 空间放大 (Space Amplification)
由于写入的数据都是顺序写而非原地更新的,所以过期的数据不会马上被清理掉,合并操作可以减少空间放大的问题,但是也会因此引入写放大的问题。
2.2.4 LSM Tree的合并策略
LSM Tree中的合并操作很重要,可以有效限制SSTable的数量,在合并操作上有两种基本策略:size-tiered和leveled,不同的策略都是在围绕2.2.3中列出的几个问题进行权衡与取舍,之前介绍的合并流程中选择的就是leveled策略
2.2.4.1 size-tiered 策略
size-tiered策略保证每层SSTable大小相近的同时限制每一层SSTable的数量。例如每层限制SSTable为N,当每层SSTable数量到达N的时候,就会触发合并操作合并这些SSTable,并将合并后的结果写入下一层。
这么做可以在数据量很大的情况下限制SSTable的总数以及发生合并的次数,但是当层数到达一定数量的时候,会导致最底层的SSTable单个大小变得非常大,并且由于新SSTable是以追加而不是合并的方式进入到新的一层的,这会导致即使是在同一层,每个key的记录也会存在多份,会有比较严重的空间放大。
2.2.4.2 leveled策略
leveled策略就是之前描述流程中使用的策略,每一层会切分成多个大小相近的SSTable,同时这些SSTable在这一层是全局有序的,也就是对于一个key每层至多只有一条记录,不会有冗余记录。相比于size tiered来说,这么做能够缓解空间放大的问题,但是写放大的问题会更突出,当level i层的某个SSTable设计的key返回特别大的时候,需要对下一层的很多SSTable进行合并,产出很多新的SSTable,相比于size-tiered直接追加,显然会多出很多的写成本。