参考文章:

https://zhuanlan.zhihu.com/p/51358869系列文章

一、存储引擎

存储引擎是数据库的一个重要功能模块,它负责将逻辑上的数据转化为磁盘上的物理文件。

二、存储引擎的对比

1、Hash存储引擎

代表数据库:redis、memcache等

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

这里列举缺点:

(1)Hash 索引仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询。

(2)Hash 索引无法被用来避免数据的排序操作。

(3)Hash 索引不能利用部分索引键查询。

(4)Hash 索引在任何时候都不能避免表扫描。

Hash碰撞,就是链式扫描:

由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。

  1. Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。

2、B树存储引擎

B树存储引擎是B树的持久化实现,不仅支持单条记录的增、删、读、改操作,还支持顺序扫描(B+树的叶子节点之间的指针),对应的存储系统就是关系数据库(Mysql等)。

     相比哈希存储引擎,B树存储引擎不仅支持随机读取,还支持范围扫描。关系数据库中通过索引访问数据,在Mysql InnoDB中,有一个称为聚集索引的特殊索引,行的数据存于其中,组织成B+树(B树的一种)数据结构。

3、LSM树存储引擎

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

4、LSM树和B树对比

1、LSM具有批量特性,存储延迟。当写读比例很大的时候(写比读多),LSM树相比于B树有更好的性能。因为随着insert操作,为了维护B树结构,节点分裂。读磁盘的随机读写概率会变大,性能会逐渐减弱。多次单页随机写,变成一次多页随机写,复用了磁盘寻道时间,极大提升效率。

2、B树的写入过程:对B树的写入过程是一次原位写入的过程,主要分为两个部分,首先是查找到对应的块的位置,然后将新数据写入到刚才查找到的数据块中,然后再查找到块所对应的磁盘物理位置,将数据写入去。当然,在内存比较充足的时候,因为B树的一部分可以被缓存在内存中,所以查找块的过程有一定概率可以在内存内完成,不过为了表述清晰,假定内存很小,只够存一个B树块大小的数据。需要两次随机寻道(一次查找,一次原位写),才能够完成一次数据的写入,代价是很高的。

3、目前市面上主流分布式数据库的存储引擎都是使用LSM树结构研发的。我们决定以LSM树入手进行尝试。

三、LSM-Tree研究报告

1、系统结构:

levelDB存储引擎

上图是存储引擎的架构图,黄线上面是内存组件,黄线下面是磁盘组件。内存组件包括memtable、immutable memtable、log,磁盘组件包括:CURRENT文件、MANIFEST文件、.log文件、LOG文件、LOCK锁文件。

操作接口Put是写数据的接口,数据先写入log文件,然后写入memtable,如果memtable大小超过了设定的阈值,则memtable转成immutable memtable,immutable memtable只能读不能写。Immutable memtable会compact到磁盘上形成sstable文件。

操作接口Get是读数据的接口,读数据的顺序是:1.memtable 2.immutable memtable 3.sst file。

sstable文件是用户数据在磁盘上持久化存储的文件,逻辑上按层存储,内部按key排序,除了L0层外,其他层sstabel文件的key之间没有重叠。存储引擎支持多版本,MANIFEST文件记录了版本变化的信息,随着sstable文件增多,MANIFEST文件也是会增多的,所以CURRENT文件指向当前的MANIFEST文件。除了immutable memtable会compact成sstable文件,磁盘上相邻层之间也会发生compaction操作。

Memtable:

存储引擎数据在内存中的存储方式,写操作会先写入memtable,memtable有最大限制(write_buffer_size)。memtable的实现有skiplist、hash-skiplist、hash-linklist,启用何用数据结构可在配置中选择。当memtable的size达到阈值,会变成只读的memtable(immutable memtable)。后台compaction线程负责把immutable memtable dump成sstable文件。(存储引擎也可以增加column family,有了列簇的概念,可把一些相关的key存储在一起。)

 

2、关LSM-Tree写流:

levelDB存储引擎

插入的数据是追加写的方式产生新的数据文件(SST),后台的compaction线程会合并有重叠key的SST文件。N行的随机写会产生很少的磁盘IO。LSM Tree不会像B+Tree那样频繁的更新数据,磁盘上的SST文件只读不能修改,对写多读少的场景很友好。

存储引擎写的逻辑如下:

1.将key-value封装成WriteBatch;

2.循环检查当前DB的状态,确定策略。

3.如果当前L0层的文件数目达到了阈值,则会延迟1s写入,该延迟只发生一次。

4.如果当前memtable的size未达到阈值 (默认4MB),则允许写入。

5.如果memtable的size已经达到阈值,但immutable memtable仍然存在,则等待compaction将其dump完成。

6.如果L0文件数目达到了阈值,则等待compaction memtable完成。

7.上述条件都不满足,则memtable已经写满,并且immutable memtable不存在,则将当前memetable 置成immutable memtable,产生新的memtable和log file,主动触发compaction,允许该次写。

8.设置WriteBatch的SequenceNumber。

9.先将WriteBatch中的数据写入。

10.然后将WriteBatch的数据写入memetable,即遍历WriteBatch并解析,Delete操作只写入删除的key,标记为删除,表示key以及被删除,后续compaction会删除此key-value。

11.更新SequenceNumber。

 

3、关LSM-Tree读流:

levelDB存储引擎

存储引擎读操作基于user_key可以找到当前最大SequenceNumber的数据,也支持获取指定快照的数据。读操作逻辑如下:

1.如果指定了snapshot,则将snapshot的Sequence Number作为最大的Sequence Number,否则,将当前最大的Sequence Number作为最大的Sequence Number。

2.在memtable中查找。

3.如果在memtable中未找到,并且存在immutable memtable,就在immutable memtable中查找。

4.如果(3)仍未找到,在sstable中查找,从L0开始,每个level上依次查找,一旦找到,即返回。

5.首先找出level上可能包含key的sstable,FileMetaData结构体内包含每个sstable的key范围。

6.L0的查找只能顺序遍历每个file,因为L0层的sstable文件之间可能存在重叠的key。在L0层可能找到多个sstable。

7.非L0层的查找,对file基于FileMetaData做二分查找即可定位到level中可能包含key的sstable。非L0上sstable之间key不会重叠,所以最多找到一个sstable。

8.如果该level上没有找到可能的sstable,跳过,否则,对要进行查找的sstable进行查找。

9.查找成功检查有效性,依据ValueType判断是否是有效的数据:

10.kTypeValue: 返回对应的value数据。

11.kTypeDeletion: 返回data not exist。

4、Manifest

manifest文件记录了SSTable 文件在不同的level 的分布,单个SSTable文件的最大key与最小key,以及存储引擎其他的一些元数据。Manifest 存储结构如下图所示:

levelDB存储引擎

 

 

 

5、Current

Current文件记录了当前Manifest的文件名,存储引擎启动时首先要找到当前的Manifest,随着系统的Compaction的进行,SSTable 文件会发生变化,Manifest将更改,current文件可以帮助用户找到最新的Manifest文件。

6、数据结构:

levelDB存储引擎

为了数据操作的效率以及方便性,将String类型的key和value封装成了一个Slice,返回指针类型,避免了繁重的拷贝任务,如下图所示:

 

 

本存储引擎使用到的key主要有三种: user_ key、InternalKey、 LookupKey。 用户传入的user_ key 在存储引擎内部转变成了Slice 结构,而InternalKey是在user_ _key的基础上封装了全局变量SequenceNumber和value标识符ValueType,前者是为了标识存储引擎每一次更新操作之后的版本,后者是为了标识该key是新写入的还是需要删除的,在存储引擎中,删除也是一种特殊形式的 写入。LookupKey 是为了在内部查找MemTable/SSTable,其形式如下图所示:

levelDB存储引擎

查询操作时,[start,end] 是对MemT able进行操作的,[kstart,end]是 对SSTable .适用的。

 

相比于String类型来说,返回的slice 的开销小,同时由于slice不返回真实数据,只返回一个指针,没有了数据拷贝的操作,所以节省了空间与时间的开销。其次由于存储引擎中设计的key/value键值对中包含‘/0’ 字符,所以规定不能返.回String类型,因此由slice 结构代替。

7、存储结构(skipList):

MemTable是本存储引擎的核心数据结构之一,所有的key-value数据都存储在MemTable、Immutable MemTable 和SSTable中,写入操作会首先将数据写入到log文件中以备系统崩溃时恢复,然后在数据写入MemTable成功后返回给用户一个应答,当数据写入到预设值时,进行短暂的内存“持久化”操作,后台调度程序会进行Compaction操作,将数据进行实际的持久化,刷新到磁盘,保存在LSM-Tree中。

本存储引擎中MemTable真正的操作是通过跳跃表SkipList实现的,包括数据的插入和读取等操作,SkipList 在1990年由William Pugh 发明的一种随 机化的算法结构,是平衡树的一种替代的数据结构,由于没有了B-Tree 节点的频繁调整,所以写入性能较高,具有一定的优势。SkipList采用“space-for-time”的概念,其结构示意图如下图所示:

levelDB存储引擎

SkipList 主要由4部分组成:

(1)表头header: 负责维护跳跃表的节点指针:

(2) 节点Node: 保存元素值;

(3)层level:保存指向其他元素的指针:

(4) 表尾:全部由NULL组成,表示跳跃表的末尾。

SkipList单个节点成为Node,存放一条key-value数据,当用户需要进行写入操作时,首先随机化的确定该元素占据的层数K,通过最上层的header指针,从高到低查找合适的插入位置,在第1至K层的链表中都插入该数据

8、Log:

Log文件的存储是为了以防系统崩溃而发生数据丢失情况,当系统发生故障时,Log文件会根据内容进行数据恢复,如果没有Log文件的存在,那么写入内存中的记录会因没有及时更新到磁盘而发生丢失现象,Redis内存存储引擎就存在这个问题。

当写入一条key-value数据时,存储引擎首先完成Log文件的写入,再将数据写入到MemTable中,一次写入完成。.

先将写入的数据序列化,分为一个Record存储,每一个 Log文件包含多个Record数据,多个Record数据组成一-个 Block, 所以,Log 文件的组织形式如下图3所示:

levelDB存储引擎

 

总体而言,Log文件由固定大小的Block构成,以Block为一个基本的读取单位,每个Block包含多个Record数据记录,针对于一条Record数据记录,主要由32位的校验和checksum,16位的存储数据的实际长度length,8位的标记类型Type和数据内容data 组成。其中,标记类型Type分为Full、First、 Middle 和Last,通过Type可以看出该Record是否完整的在当前的Block中,如果Type不是Full,则可指明其前后的Block中是否有当前Record的前驱后继,具体如下图所示:

levelDB存储引擎

SSTable文件是后台调度程序对数据执行Compaction操作而形成的一种层级结构,每一层都包含多个SSTable文件,其内部文件有序存储,归并产生,不可更改。

同Log文件相似,SSTable 也是将单个Block作为一个 独立的写入和解析的单位,key 有序的分块存储,SSTable 中的Block 主要有三类: data block、metaindexblock、index block,构成了SSTable 的数据存储部分和数据索引管理部分。整体结构如下图所示。

levelDB存储引擎

data block属于数据存储区,key 是有序存储,每个data block 分为了三部分:数据存储区data,标识位Type以及数据校验码CRC,其中Type是为了辨别data是否采用了压缩算法,与Log文件中的Type不同,结构图如下图所示。

levelDB存储引擎

数据存储区data是由多条cntry数据组成,由于在SSTable中是按key有序存储,所以存储引擎巧妙的利用了前缀压缩,有序数组相邻的key可能有相同的前缀的特点来减少存储数据量,每个entry 只记录自己的key与前一个entry 的key不同的部分,entry 的结构如下图所示:

levelDB存储引擎

例如要顺序存储的keyl=“ Apple”",key2= “ Applelen”两条数据,在key2中,共享内容为“Apple", 长度为5, 非共享长度为3, 非共享内容为“1en”, 虽然这种方式减少了数据存储,但是引入了新的风险,如果开头的entry数据损坏,其后涉及共享的entry都将无法恢复,为了降低这个风险,存储引擎引入了重启点,在每个Block中都设置- -个重启点,记录在entry后面的Tailer中,结构图如下图所示:

levelDB存储引擎

Index Block 则记录data block 位置信息的block,其中每一条 entry指向一个data block,其中的key为- -个data block最后一条 数据的key,而value是指向该data block 的handle。其存在主要是为了提高SSTable内的key-value的查找效率,可以直接定位到key所在的datablock.

Meta block 也可以称为Filter block, 主要用于判断- 一个key是否在某个datablock中,存储引擎主要使用的BloomFilter,写入datablock的数据会同步更新到meta block的过滤器中,读取数据时会通过meta block判断该key是否存在在对应的data block中。

Metaindex Block与index Block 类似,由一组handle组成,指向Meta block,用于快速索引到meta Block的位置。

9、Minor Compaction:

在存储引擎中,从内存中的Immutable MemTable到磁盘中SSTable的转换,通过单独的后台线程调度Compaction方式对数据整理压缩归并而成,主要有MinorCompaction和Major Compaction两种方式。

 

minor compaction主要做两件事情:1、构造sstable 2、新的sstable文件写入哪一层。流程如下图:

levelDB存储引擎

从策略上要尽量将新compact的文件推至高level,毕竟在level 0 需要控制文件过多,compaction IO和查找都比较耗费,另一方面也不能推至过高level,一定程度上控制查找的次数,而且若某些范围的key更新比较频繁,后续往高层compaction IO消耗也很大。所以折中方案是如下图:

levelDB存储引擎

 

10、Major compaction:

当某个level中的SSTable文件的大小达到设定值后,会触发MajorCompaction,存储引擎会将该层的SSTable中选择-一个文件,和level+l层的SSTable的文件进.行合并,对于特殊的level 0来说,由于两个SSTable文件可能在key范围上有重叠,所以在对level0进行合并时.要找出所有重叠文件和level 1的文件进行合并,可能会有多个文件参与此过程,而对于除level 0以外的文件合并,只涉及到一个文件的选中。

当前合并选定level L的文件A进行合并后,还需要选择Level L+l层中和A文件在key范围上有重叠的所有文件进行Compaction流程图如下所示:

levelDB存储引擎

在上述过程中,借鉴了多路归并的算法,对所有文件中的记录进行重新排序,判断是否有删除标记或者是否是旧的数据,如果符合条件,则丢弃数据,否则继续保存,将其写入level L+1层中,生成一一个新的SSTable文件,在此过程中,实现存储引擎的删除操作,同时完成了levelL与levelL+I层数据记录的合并压缩过程。

11、Mvcc

levelDB存储引擎

Version相关的数据结构有3个,Version VersionEdit and VersionSet。其中VersionEdit顾名思义,是编辑或修改Version,它记录的是两个Version之间的差异。

sequence number 是一个由VersionSet直接持有的全局的编号,每次写入(注意批量写入时sequence number是相同的),就会递增。根据我们之前对写入操作的分析,当插入一条key的时候,实际参与排序的key和sequence number以及type组成的 InternalKey。

当我们进行Get操作时,我们只需要找到目标key,同时其sequence number 小于等于sequence number:

普通的读取,sepcific sequence number =last sequence number

snapshot读取,sepcific sequence number =snapshot sequence number

snapshot 其实就是一个sequence number,获取snapshot,即获取当前的last sequence number。

sstable级别的MVCC就是利用Version实现的。

只有一个current version,持有最新的sstable集合。

VersionEdit 代表一次更新,新增了哪些sstable file,以及删除了哪些sstable file

Version保存了各个level下每个sstable的FileMetaData

Version的结构如下图所示:

levelDB存储引擎

next、prev分别指向前后版本,DB中所有的Version构成了一个环,此环中只有一个当前版本如上图所示。file_保存了每层sstable的元数据FileMetaData,Version类还记录了下一次参与Compaction的文件。

MANIFEST:

    MANIFEST是跟版本变更有关的磁盘文件,MANIFEST文件的内容就是VersionEdit序列化后的内容,可用来恢复。MANIFEST中record存储的方式跟log存储方式一样。

MANIFEST的内容如下图所示:

levelDB存储引擎一次版本的变更信息保存在VersionEdit中,VersionEdit中的信息经过Encode后形成Record,一个Record有可能很大,MANIFEST存储Record的方式与WAL日志中存储Record方式一样,也分为:KFullType、KFirstType、KMiddleType、KLastType。随着系统不断的运行,发生版本变化的次数会越来越多,MANIFEST文件数也会变多,需要一个类似指针的东西指向当前使用的MANIFEST,CURRENT文件就充当这个指针的作用,它存储了当前使用的MANIFEST的文件名。

相关文章:

  • 2022-12-23
猜你喜欢
  • 2021-08-27
  • 2021-04-30
  • 2021-11-27
  • 2021-11-13
  • 2022-02-09
  • 2022-01-05
相关资源
相似解决方案