LSM Tree

关于LSM Tree的介绍,这篇文章http://www.benstopford.com/2015/02/14/log-structured-merge-trees/讲得很是具体。html

背景

因为磁盘的顺序IO比随机IO效率高得多,为了提升写的吞吐量,有如下几个方法:termed logging,journalling, a heap filesql

这种状况下,读操做会比写操做花费更多时间(须要反向扫描,直至找到key)。数据库

基于log/journal的方法只适用于简单场景,如数据被总体访问(大部分数据库的预写日志,WAL, write-ahead logging),或经过已知偏移量访问,如简单的消息系统kafka。缓存

对于复杂场景,如基于key或随机访问,有四种方式:数据结构

1. Searched sorted file:将数据存入文件,用key排序,若数据定义了长度,则使用binary search,不然使用page index + scanoop

2. Hash:将数据hash到桶,以后直接读取性能

3. B+:使用可导航的文件组织,如B+树,ISAM等优化

4. 外部文件:将数据存为log/heap,使用额外的hash或tree索引至数据设计

以上四种方式大大提高了读性能(大部分状况下O(lgn)),但也牺牲了写性能(因为增长了排序)。日志

此时,有如下问题:

1. 每次写操做有两次IO,一次是读页面,一次是写回该页面,而log法只须要一次IO

2. 若要更新hash或B+索引结构,则需更新文件系统的特定部分,而这种原地更新须要缓慢的随机IO

一个常见的解决方法是使用方法4,为journal构建index,并将index至于内存。因而Log Structured Merge Trees(LSM Tree)应运而生。

LSM Tree

LSM树的设计思想很是朴素:将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操做批量写入磁盘,不过读取的时候稍微麻烦,须要合并磁盘中历史数据和内存中最近修改操做,因此写入性能大大提高,读取时可能须要先看是否命中内存,不然须要访问较多的磁盘文件。极端状况下,基于LSM树实现的HBase的写性能比Mysql高了一个数量级,读性能低了一个数量级。

LSM树原理把一棵大树拆分红N棵小树,它首先写入内存中,随着小树愈来愈大,内存中的小树会flush到磁盘中,磁盘中的树按期能够作merge操做,合并成一棵大树,以优化读性能。

具体工做原理为:当一个更新请求到达时,将会被加入内存缓存区(通常状况下是一棵树,如红黑树,B树等,来保存key的有序性)。这个memtable会做为write-ahead-log复制于磁盘中,以便出现问题时的修复。当memtable区满时,将会flush到磁盘中做为一个新的文件。这个过程会随着写入的增多而不断重复。

因为旧文件不会被更新,重复的entry会被建立来取代以前的记录(或者移除的标记),这会带来一些冗余。系统会按期进行压缩操做,压缩的作法是选择多个文件,将他们进行合并,并移除重复的更新或者删除操做。这对消除冗余和提升读性能(因为读性能会随着文件数增长而递减)很是重要。此外,因为每一个文件都是有序的,因此合并文件的操做也很是高效。

当读操做到达时,系统会先检查memtable,若是没有找到对应的key,则在磁盘文件中以逆时间顺序进行查找,直至找到key。每一个文件都是有序的,不过读操做仍然会由于文件数目的增加而变慢。为了解决这个问题,有一些小trick。其中最多见的方法是在内存中保持一个page-index,以便使你更接近目标key。LevelDB,RocksDB和BigTable在每一个文件的保存了一个block-index。这会比直接进行二分查找更高效,由于它容许使用可变长度,更适用于压缩数据。

哪怕使用紧凑的读操做,文件访问次数依然不少。大部分实现经过使用Bloomfilter来进行改进。Bloom filters是一种判断文件中是否包含某个key的内存高效方法。

 

总的来讲,LSM Tree是在随机写IO和随机读IO之间进行trade off。若是可使用软件方法(如Bloom filters)或硬件方法(如大文件缓存)来优化读性能,那么这个trade off是一个明智的选择。

基本compaction

根据特定size限制进行compaction,譬如5个文件,每一个文件有10行,会被合并为一个有50行(或者比50行小一些)的文件。而5个50行的文件又会被合并为一个具备250行的文件,以此类推。

这种方法的问题是:会建立大量的文件,全部文件都须要被分别搜索以读取结果。

分层compaction

新的实现方法,如LevelDB,RocksDB和Cassandra等,经过level-based而不是size-based进行compaction以解决上述问题。这种level-based方法主要有如下两点不一样:

1. 每一层能够包含一系列文件,而且保证不会有重叠的key。这意味着key在全部可用文件中被切分。因此在特定层寻找一个key只须要读取一个文件。(要注意的是,第一层比较特殊,相同keys能够在多个文件中)

2. 多个文件会一次合并至上层的一个文件中。当一层填满时,会从该层取出一个文件,合并至上层以建立空间使更多的数据能够被加入。

这种改动意味着level-based解决方案随着时间推移进行压缩而且须要的空间更少。此外,它的读性能也更好。

HBase

HBase存储主要原理为:

1. 小树先写到内存中,为了防止内存数据丢失,写内存的同时须要暂时持久化到磁盘,对应了HBase的MemStore和HLog

2. MemStore上的树达到必定大小以后,须要flush到HRegion磁盘中(通常是Hadoop DataNode),这样MemStore就变成了DataNode上的磁盘文件StoreFile,HRegionServer按期对DataNode的数据作merge操做,完全删除无效空间,多棵小树在这个时机合并成大树,来加强读性能。

LevelDB

LevelDB一样也利用了LSM Tree,这篇文章http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html讲得很是具体。这里只简单介绍一下其整体结构以下:

LevelDb的Log文件和Memtable与Bigtable论文中介绍的是一致的,当应用写入一条Key:Value记录的时候,LevelDb会先往log文件里写入,成功后将记录插进Memtable中,这样基本就算完成了写入操做,由于一次写入操做只涉及一次磁盘顺序写和一次内存写入,因此这是为什么说LevelDb写入速度极快的主要缘由。LevelDb的Memtable采用了SkipList数据结构(Redis也使用了该数据结构提升插入效率)。

SSTable中的文件是Key有序的,就是说在文件中小key记录排在大Key记录以前,各个Level的SSTable都是如此,可是这里须要注意的一点是:Level 0的SSTable文件(后缀为.sst)和其它Level的文件相比有特殊性:这个层级内的.sst文件,两个文件可能存在key重叠。对于其它Level的SSTable文件来讲,则不会出现同一层级内.sst文件的key重叠现象。

SSTable中的某个文件属于特定层级,并且其存储的记录是key有序的,那么必然有文件中的最小key和最大key,这是很是重要的信息,LevelDb应该记下这些信息。Manifest就是干这个的,它记载了SSTable各个文件的管理信息,好比属于哪一个Level,文件名称叫啥,最小key和最大key各自是多少。下图是Manifest所存储内容的示意:

Current文件的内容只有一个信息,就是记载当前的manifest文件名。由于在LevleDb的运行过程当中,随着Compaction的进行,SSTable文件会发生变化,会有新的文件产生,老的文件被废弃,Manifest也会跟着反映这种变化,此时每每会新生成Manifest文件来记载这种变化,而Current则用来指出哪一个Manifest文件才是咱们关心的那个Manifest文件。

相关文章
相关标签/搜索