LSM 算法

原文:
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf

Google的BigTable架构在分布式结构化存储方面大名鼎鼎,其中的MergeDump模型在读写之间找到了一个较好的平衡点,很好的解决了web scale数据的读写问题。

MergeDump的理论基础是LSM-Tree (Log-Structured Merge-Tree)

思想

LSM的思想,在于对数据的修改增量保持在内存中,达到指定的限制后将这些修改操作批量写入到磁盘中,相比较于写入操作的高性能,读取需要合并内存中最近修改的操作和磁盘中历史的数据,即需要先看是否在内存中,若没有命中,还要访问磁盘文件。

原理:把一颗大树拆分成N棵小树,数据先写入内存中,随着小树越来越大,内存的小树会flush到磁盘中。磁盘中的树定期做合并操作,合并成一棵大树,以优化读性能。

对应于使用LSM的Leveldb来说,对于一个写操作,先写入到memtable中,当memtable达到一定的限制后,这部分转成immutable memtable(不可写),当immutable memtable达到一定限制,将flush到磁盘中,即sstable.,sstable再进行compaction操作。

LSM思想非常朴素,就是将对数据的更改hold在内存中,达到指定的threadhold后将该批更改批量写入到磁盘,在批量写入的过程中跟已经存在的数据做rolling merge。

举个例子

拿update举个例子:
比如有1000万行数据,现在希望update table.a set addr='new addr' where pk = '833',如果使用B-Tree类似的结构操作,就需要:

  1. 找到该条记录所在的page
  2. load page到内存(如果恰好该page已经在内存中,则省略该步)
  3. 如果该page之前被修改过,则先flush page to disk
  4. 修改数据

上面的动作平均来说有两次disk I/O,如果采用LSM-Tree类似结构,则:

  1. 将需要修改的数据直接写入内存

可见这里是没有disk I/O的。但是,这样的话读的时候就费劲了,需要merge disk上的数据和memory中的修改数据,这显然降低了读的性能。

确实如此,所以作者其中有个假设,就是写入远大于读取的时候,LSM是个很好的选择。我觉得更准确的描述应该是”优化了写,没有显著降低读“,因为大部分时候我们都是要求读最新的数据,而最新的数据很可能还在内存里面,即使不在内存里面,只要不是那些更新特别频繁的数据,其I/O次数也是有限的。

所以LSM-Tree比较适合的应用场景是:insert数据量大,读数据量和update数据量不高且读一般针对最新数据。

浅析

内存的高效性是lsm的结构基础,我们在访问数据的时候速度肯定是内存大于磁盘的,这样的话为什么不全用内存呢?原因大家自己考虑下就行了,所以权衡下来还是需要用硬盘的,那么为了实现数据的快速插入和查询,存储应该怎么设计呢?
我们知道,要使一个表对查询的响应比较快,那么最主要的手段就是索引,但是索引多了就会影响数据插入的速度,这也是一种平衡,下面我们将分析lsm,看看它是设计了个完美的解决方案吗?

lsm tree解决了什么问题? 它解决了随机读写创景下,减少数据频繁的插入、修改、删除操作所需要的磁盘I/O次数

学过数据结构的同学都知道Btree树是很多索引的优先选择结构,b tree树访问的时间复杂度接近Logm(N/2),可以计算下,在成百上千的索引节点下,即使索引十几亿的数据,那么树的深度也不会很深的,应该是10以内吧,再加上对于lru算法的支持,可以很明显的减少io,那为什么hbase不用这个结构呢,答案就是本文开头的几句话。

因为hbase中数据插入是比较随机的或者说是无序的,在查询数据的时候回到索引上,也就是对于某个叶子节点的访问是很随机的,这个场景很重要,那么我们根据这个具体场景分析一下b+树,因为查询是随机的,那么也就是说我们上次调入内存的数据可能很久以后都不会被访问,所以lru算法失去了它的价值,主要的系统开销变成了访问B+树的io了,内存的命中率很低,对于插入数据来说道理是一样的。

再看看lsm tree是怎么做的:

lsm构造许多小的结构,每个结构在内存里排序一下构成内部有序,查询的时候对每个小结构就可以采用二分法高效的查找定位,我们都知道有序的东西查找起来速度肯定比无序的快,如果只是这么设计肯定不能达到快速插入和查询的目的,lsm还引入了Bloom filter和小树到大树的排序

Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,通过多hash函数判断一个元素是否属于这个集合。 Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

  • Bloom filter在lsm中的作用就是判断要查询的数据在哪个内存部件中,或者要插入的数据应该插入到哪个内存部件中。

  • 小树到大树的排序是为了节约内存,同时也是为了恢复,因为我们知道hbase的delete和update其实都是insert,这都是由lsm的特点决定的,新的数据会被写到磁盘新的位置上去,这样就保证了旧记录不会被覆盖,在系统crash后的恢复过程会很有用,只要按日志顺序恢复就ok了。

什么是lsm tree:
LSM-Tree通过使用一个基于内存的组件C0和一至多个基于磁盘的(C1, C2, …, CK)组件算法,对索引变更进行延迟及批量处理,并通过归并的方式高效的将更新迁移到磁盘。(有点耐个)

原理

Write & Read

LSM 算法_第1张图片
  • 当一些更新操作到达时,他们会被写到内存缓存(也就是memtable)中,memtable使用树结构来保持key的有序
  • 在大部 分的实现中,memtable会通过写WAL的方式备份到磁盘,用来恢复数据,防止数据丢失。
  • 当memtable数据达到一定规模时会被刷新到磁盘上的一 个新文件,重要的是系统只做了顺序磁盘读写,因为没有文件被编辑,新的内容或者修改只用简单的生成新的文件。
  • 所以越多的数据存储到系统中,就会有越多的不可修改的,顺序的sstable文件被创建,它们代表了小的,按时间顺序的修改。
  • 因为比较旧的文件不会被更新,重复的纪录只会通过创建新的纪录来覆盖,这也就产生了一些冗余的数据。所以系统会周期的执行合并操作(compaction)。
  • 合并操作选择一些文件,并把他们合并到一起,移除重复的更新或者删除纪录,同时也会删除上述的冗余。更重要的是,通过减少文件个数的增长,保证读操作的性 能。因为sstable文件都是有序结构的,所以合并操作也是非常高效的。
  • 当一个读操作请求时,系统首先检查内存数据(memtable),如果没有找到这个key,就会逆序的一个一个检查sstable文件,直到key 被找到。
  • 因为每个sstable都是有序的,所以查找比较高效(O(logN)),但是读操作会变的越来越慢随着sstable的个数增加,因为每一个 sstable都要被检查。(O(K log N), K为sstable个数, N 为sstable平均大小)。所以,读操作比其它本地更新的结构慢。
  • 幸运的是,有一些技巧可以提高性能。最基本的的方法就是页缓存(也就是leveldb的 TableCache,将sstable按照LRU缓存在内存中)在内存中,减少二分查找的消耗。
  • LevelDB 和 BigTable 是将** block-index 保存在文件尾部**,这样查找就只要一次IO操作,如果block-index在内存中。一些其它的系统则实现了更复杂的索引方法。
  • 即使有每个文件的索引,随着文件个数增多,读操作仍然很慢。通过周期的合并文件,来保持文件的个数,因些读操作的性能在可接收的范围内
  • 即便有了合 并操作,读操作仍然会访问大量的文件,大部分的实现通过Bloom filter来避免大量的读文件操作,布隆过滤器是一种高效的方法来判断一个sstable中是否包含一个特定的key。(如果bloom说一个key不存在,就一定不存在,而当bloom说一个文件存在是,可能是不存在的,只是通过概率来保证)所有的写操作都被分批处理,只写到顺序块上。
  • 另外,合并操作的周期操作会对IO有影响,读操作有可能会访问大量的文件(散乱的读)。这简化了算法工作的方法,我们交换了读和写的随机IO。这种折衷很有意义,我们可以通过软件实现的技巧像布隆过滤器或者硬件(大文件cache)来优化读性能


    LSM 算法_第2张图片

Basic Compaction

为了保持LSM的读操作相对较快,维护并减少sstable文件的个数是很重要的,所以让我们更深入的看一下合并操作。这个过程有一点儿像一般垃圾回收算法。

  • 当一定数量的sstable文件被创建,例如有5个sstable,每一个有10行,他们被合并为一个50行的文件(或者更少的行数)。
  • 这个过程一 直持续着,当更多的有10行的sstable文件被创建,当产生5个文件时,它们就被合并到50行的文件。
  • 最终会有5个50行的文件,这时会将这5个50 行的文件合并成一个250行的文件。这个过程不停的创建更大的文件。像下图:


    LSM 算法_第3张图片

Levelled Compaction


LSM 算法_第4张图片
  • Level 0可以认为是MemTable的文件映射内存, 因此每个Level 0的SSTable之间的key range可能会有重叠。其他Level的SSTable key range不存在重叠。
  • Level 0的写入是简单的创建-->顺序写流程,因此理论上,写磁盘的速度可以接近磁盘的理论速度。

更新的实现,像 LevelDB 和 Cassandra解决这个问题的方法是:

  • 实现了一个分层的,而不是根据文件大小来执行合并操作。
  • 这个方法可以减少在最坏情况下需要检索的文件个数,同时也减少了一次合并操作的影响。

按层合并的策略相对于上述的按文件大小合并的策略有二个关键的不同:

  • 每一层可以维护指定的文件个数,同时保证不让key重叠。也就是说把key分区到不同的文件。因此在一层查找一个key,只用查找一个文件。
  • 第一层是特殊情况,不满足上述条件,key可以分布在多个文件中。
  • 每次,文件只会被合并到上一层的一个文件。当一层的文件数满足特定个数时,一个文件会被选出并合并到上一层。
  • 这明显不同与另一种合并方式:一些相近大小的文件被合并为一个大文件。


    LSM 算法_第5张图片

    SSTable合并类似于简单的归并排序:根据key值确定要merge的文件,然后进行合并。因此,合并一个文件到更高层,可能会需要写多个文件。存在一定程度的写放大。
    这些改变表明按层合并的策略减小了合并操作的影响,同时减少了空间需求。除此之外,它也有更好的读性能。但是对于大多数场景,总体的IO次数变的更多,一些更简单的写场景不适用。

小结

  • LSM 是日志和传统的单文件索引(B+ tree,Hash Index)的中立,他提供一个机制来管理更小的独立的索引文件(sstable)。
  • 通过管理一组索引文件而不是单一的索引文件,LSM 将B+树等结构昂贵的随机IO变的更快,而代价就是读操作要处理大量的索引文件(sstable)而不是一个,另外还是一些IO被合并操作消耗

Ref:
https://www.zhihu.com/question/19887265
http://f.dataguru.cn/thread-24939-1-1.html

你可能感兴趣的:(LSM 算法)