• [LevelDB] 0.1 LSMTree介绍


    一、存储引擎介绍

    讲LSM树之前,需要提下三种基本的存储引擎,这样才能清楚LSM树的由来

    1.1 哈希存储引擎  

      是哈希表的持久化实现,支持增、删、改以及随机读取操作,但不支持顺序扫描,对应的存储系统为key-value存储系统。对于key-value的插入以及查询,哈希表的复杂度都是O(1),明显比树的操作O(n)快,如果不需要有序的遍历数据,哈希表就是your Mr.Right

    代表数据库:redis、memcache等

    通常也常见于其他存储引擎的查找速度优化上。 Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。虽然 Hash 索引效率高,但是 Hash 索引本身由于其特殊性也带来了很多限制和弊端。

    这里列举缺点:

    (1)Hash 索引仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询。
    (2)Hash 索引无法被用来避免数据的排序操作。
    (3)Hash 索引不能利用部分索引键查询。
    (4)Hash 索引在任何时候都不能避免表扫描。

    1.2 B树存储引擎

      B树(关于B树的由来,数据结构以及应用场景可以看之前一篇博文)的持久化实现,不仅支持单条记录的增、删、读、改操作,还支持顺序扫描(B+树的叶子节点之间的指针),对应的存储系统就是关系数据库(Mysql等)。

    代表数据库:MongoDB、mysql(基本上关系型数据库)等

    1.3 LSM树(Log-Structured Merge Tree)存储引擎

      B树存储引擎一样,同样支持增、删、读、改、顺序扫描操作。而且通过批量存储技术规避磁盘随机写入问题。当然凡事有利有弊,LSM树和B+树相比,LSM树牺牲了部分读性能,用来大幅提高写性能。

    LSM被设计来提供比传统的B+树或者ISAM更好的写操作吞吐量,通过消去随机的本地更新操作来达到这个目标。

    那么为什么这是一个好的方法呢?这个问题的本质还是磁盘随机操作慢,顺序读写快的老问题。这二种操作存在巨大的差距,无论是磁盘还是SSD。

    thoughput of two op

    上图很好的说明了这一点,他们展现了一些反直觉的事实,顺序读写磁盘(不管是SATA还是SSD)快于随机读写主存,而且快至少三个数量级。这说明我们要避免随机读写,最好设计成顺序读写。

    The Base LSM Algorithm

    从概念上说,最基本的LSM是很简单的 。将之前使用一个大的查找结构(造成随机读写,影响写性能),变换为将写操作顺序的保存到一些相似的有序文件(也就是sstable)中。所以每个文件包 含短时间内的一些改动。因为文件是有序的,所以之后查找也会很快。文件是不可修改的,他们永远不会被更新,新的更新操作只会写到新的文件中。读操作检查很 有的文件。通过周期性的合并这些文件来减少文件个数。

    basic lsm
    让我们更具体的看看,当一些更新操作到达时,他们会被写到内存缓存(也就是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在内存中。一些其它的系统则实现了更复杂的索引方法。

    即使有每个文件的索引,随着文件个数增多,读操作仍然很慢。通过周期的合并文件,来保持文件的个数,因些读操作的性能在可接收的范围内。即便有了合 并操作,读操作仍然会访问大量的文件,大部分的实现通过布隆过滤器来避免大量的读文件操作,布隆过滤器是一种高效的方法来判断一个sstable中是否包 含一个特定的key。(如果bloom说一个key不存在,就一定不存在,而当bloom说一个文件存在是,可能是不存在的,只是通过概率来保证)

    所有的写操作都被分批处理,只写到顺序块上。另外,合并操作的周期操作会对IO有影响,读操作有可能会访问大量的文件(散乱的读)。这简化了算法工 作的方法,我们交换了读和写的随机IO。这种折衷很有意义,我们可以通过软件实现的技巧像布隆过滤器或者硬件(大文件cache)来优化读性能。
    read

    Basic Compaction

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

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

    上述的方案有一个问题,就是大量的文件被创建,在最坏的情况下,所有的文件都要搜索。

    Levelled Compaction

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

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

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

    这些改变表明按层合并的策略减小了合并操作的影响,同时减少了空间需求。除此之外,它也有更好的读性能。但是对于大多数场景,总体的IO次数变的更多,一些更简单的写场景不适用。

    总结

    所以, LSM 是日志和传统的单文件索引(B+ tree,Hash Index)的中立,他提供一个机制来管理更小的独立的索引文件(sstable)。

    通过管理一组索引文件而不是单一的索引文件,LSM 将B+树等结构昂贵的随机IO变的更快,而代价就是读操作要处理大量的索引文件(sstable)而不是一个,另外还是一些IO被合并操作消耗。

    通过以上的分析,应该知道LSM树的由来了,LSM树的设计思想非常朴素:将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操作批量写入磁盘,不过读取的时候稍微麻烦,需要合并磁盘中历史数据和内存中最近修改操作,所以写入性能大大提升,读取时可能需要先看是否命中内存,否则需要访问较多的磁盘文件。极端的说,基于LSM树实现的HBase的写性能比Mysql高了一个数量级,读性能低了一个数量级。

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

    LSM的结构

    LSM的基本思想是将修改的数据保存在内存,达到一定数量后在将修改的数据批量写入磁盘,在写入的过程中与之前已经存在的数据做合并。同B树存储模型一样,LSM存储模型也支持增、删、读、改以及顺序扫描操作。LSM模型利用批量写入解决了随机写入的问题,虽然牺牲了部分读的性能,但是大大提高了写的性能。

    MemTable

    LSM本身由MemTable,Immutable MemTable,SSTable等多个部分组成,其中MemTable在内存,用于记录最近修改的数据,一般用跳跃表来组织。当MemTable达到一定大小后,将其冻结起来变成Immutable MemTable,然后开辟一个新的MemTable用来记录新的记录。而Immutable MemTable则等待转存到磁盘。

    Immutable MemTable

    所谓Immutable MemTable,即是只能读不能写的内存表。内存部分已经有了MemTable,为什么还要使用Immutable MemTable?个人认为其原因是为了不阻塞写操作。因为转存的过程中必然要保证内存表的记录不变,否则如果新插入的记录夹在两条已经转存到磁盘的记录中间,处理上会很麻烦,转存期间势必要锁住全表,这样一来就会阻塞写操作。所以不如将原有的MemTable变成只读Immutable MemTable,在开辟一个新的MemTable用于写入,即简单,又不影响写操作。

    SSTable

    SSTable是本意是指有序的键值对集合( a set of sorted key-value pairs )。是一个简单有用的集合,正如它的名字一样,它存储的就是一系列的键值对。当文件较大的时候,还可以为其建立一个键-值的位置的索引,指明每个键在SSTable文件中的偏移距离。这样可以加速在SSTable中的查询。(当然这一点是可选的,同时让我想去了Bitcask模型中hint文件,通过记录 键-值的位置 ,来加速索引构建)


    使用MemTable和SSTable这两个组件,可以构建一个最简单的LSM存储模型。这个模型与Bitcask模型相比,不存在启动时间长的问题,但是这个模型的读性能非常的差,因为一但在MemTable找不到相应的键,则需要在根据SSTable文件生成的时间,从最近到较早在SSTable中寻找,如果都不存在的话,则会遍历完所有的SSTable文件。
    如果SSTable文件个数很多或者没有建立SSTable的文件内索引的话,读性能则会大大下降。

    除了在对SSTable内部建立索引外,还可以使用Bloom Fileter,提高Key不在SSTable的判定速度。同样,定期合并旧的SSTable文件,在减少存储的空间的同时,也能提高读取的速度。下面这幅图很好的描述了在LSM的大部分结构和操作


    LevelDB如何优化读性能

    Leveldb是一个轻量级的,快速的以存储为目的的key-value存储引擎。其使用的正是LSM存储模型。我们可以看看LevelDB是如何来优化读性能的。在LevelDB中,存在一种元信息文件MANIFEST,用于记录leveldb的元信息,比如DB使用的Comparator名,以及各SSTable文件的管理信息:如Level层数、文件名、最小key和最大key等等。相比而言,元信息文件而SSTable文件的数目成正比,一般来说不会太多,是可以载入内存的,因此Level可以通过查询元信息,从而判断哪些文件中存在我们需要的Key对应的记录,减少SSTable文件读取次数。此外,LevelDB的合并操作Compaction是分层次进行的,每一层都有多个SSTable文件,每次合并后除了Level0和内存的MemTable,Immutable MemTable中会有重复的键值外,LevelN(N>=1)的各层内部的SSTable文件不会再有重复的键值。同时,如果在Level N 层读到了数据,那么就不需要再往后读Level N+1,Level N+2等层的数据了.因为Level N层的数据总是比Level N+1等层的数据更“新鲜”。

    实现一个简单的LSM存储模型

    根据上面讲述的原理,实现了一个简单的LSM模型(https://github.com/ym65536/Distributed_System/blob/master/Storage/LSM_Tree.py)。这个模型也内存表为一个跳跃表,SSTable就是简单的有序键值对集合,没有SSTable内部使用索引,没有使用Bloom过滤器。其实能就是将我之前的Bitcask模型进行了简单的改造:

    • 将原来的哈希表换成了跳跃表;
    • 原来读取记录完全依赖哈希表,现在如果在跳跃表中没有的话,就去读取文件SSTable文件中的数据,根据文件编号从大到小进行,编号越大,表示数据越新;
    • 去掉了加载数据的功能(LSM不需要);

    简单起见,没有完成对范围扫描的支持,不过内存表和SSTable都是有序的,因此这个也不是很难。

  • 相关阅读:
    schema的详解
    递归删除文件
    如何写一个schema文件
    如何写一个dtd文件
    WebService随笔记录
    文件分割
    三级数据显示
    数据库锁表查询及解除方法
    list分页
    JXLS模板导出多个sheet文件
  • 原文地址:https://www.cnblogs.com/ym65536/p/7752138.html
Copyright © 2020-2023  润新知