LevelDB 底层原理学习笔记

一、LevelDB简介

LevelDB不同于关系型数据库,他内部的数据全部以KV形式存储,不支持SQL,只支持API调用。

LevelDB是个noSQL的数据库引擎,由google开发并开源,由C++编写。Facebook在此基础上推出了RocksDB,后来包括TiDB等多种分布式noSQL数据库底层都是基于LevelDB。

二、LevelDB架构

在这里插入图片描述

2.1、内存中的MemTable和ImmuTable MemTable

首先上层是MemTable, Immutable MemTable。MemTable本质上就是个存放在内存中的SkipList数据结构。

Immutable MemTable本质上也是MemTable,ImmuTable是不可修改的意思:当MemTable中的内容超过阈值时,需要将其中的内容写到一个SSTable文件,ImmuTable MemTable就是这时候用的。

当一个MemTable在开始执行持久化之前,会先转化成ImmTable MemTable,可以认为是加上了不可修改的限制。另外,会再新建一个新的MemTable,用于维持服务。之后再将ImmuTable MemTable写入SSTable文件。

理解MemTable和ImmuTable MemTable,打个比方:MemTable就好像是收银台里的收银柜,当收银柜中的钱快满的时候,需要把里面的钱存到银行,但此时顾客还在不断的付钱,不能停掉他。所以那出一个新的收银柜来收钱,把原来的收银柜锁上后,把收银柜一起送到银行。MemTable就好比是MemTable, 加锁后变成了ImmuTable MemTable。

2.2、.log文件

.log文件类似于数据库中的binlog,用来在系统发生故障时恢复数据的。

2.3、SSTable文件、mainfest文件

在原始的LSMT中SSTable是顺序存储的,所以在查询数据时才依次查询,当发现第一个SSTable中没有要查询的内容时,就往下查询下一个文件。

而在LevelDB中,SSTable是按照层级存储的,第一层是Level 0,第二层是Level 1,依次类推,层级逐渐增高。这也是LevelDB得名的原因。

SSTable文件本质上是一个的key-value的序列表,并且其中的key是有序的。既然key是有序的,那就有最大值和最小值。我们把最大值和最小值记录下来,可以在查询的时候快速判断,这样我们可以知道要查询的key可能在哪个SSTable文件当中,从而加快查询效率。
在这里插入图片描述
这个记录SSTable文件中最小key和最大key的文件就是mainfest, 除了最小最大key之外,还会记录SSTable属于哪个Level,文件名称等信息,可以看下图:
在这里插入图片描述

2.4、current文件

current是一个指针。因为实际运行的mainfest文件不止一个,伴随着压缩等操作,会产生新的mainfest文件,我们需要一个指针记录当前最新的mainfest文件是哪个,方便查找。而且mainfest文件中的数据量并不小,所以我们不能全部存放在内存中,通过放在文件里,用指针来引用是一个最佳选择。

三、LevelDB的增删改查

3.1、LevelDB的写

LevelDB的写、删、改操作和裸的LSMT基本一样,分一下几个步骤:

Step1、将变更的数据先写入.log文件中,持久化的目的是为了防止宕机导致数据丢失。

Step2、当写入.log文件成功后,写入MemTable。由于LevelDB的SSTable是采用Skiplist实现的,所以写入速度快,是log(n)的时间复杂度。如果MemTable容量达到阈值,会把MemTable转成ImmuTable MemTable, 之后创建一个新的MemTable接收后续的请求,当dump指令下达后,会将ImmuTable MemTable写入SSTable文件进行存储。

上述流程和LSMT大同小异,只有细微的区别。严格来说LevelDB不支持修改操作,可以转化成插入一条新数据,或先删除再插入,这两者本质上是一致的,会再后续数据压缩过程中进行合并。

3.2、LevelDB的读

LevelDB的读和LSMT稍有区别:
在这里插入图片描述
当执行查询指令时,首先会在MemTable和ImmuTable MemTable即内存中(Skiplist)进行查找,如果没找到,那么会去磁盘查找。

和裸的LSMT按顺序逐个查询SSTable不同,LevelDB会先查SSTable文件的索引文件mainfest,根据mainfest文件中记录的SSTable文件的key的范围来找到对应的SSTable文件。

对于同一个key来说,可能同时出现在不同level的SSTable文件中,但由于LevelDB在写入SSTable的时候,遵循越晚写入数据越新的原则,也就是说,level序号越小,数据越新,所以如果找到了多个值,会优先返回上层的结果。

整个LevelDB的读写是在原本的LSMT基础上加入了优化。在一些场景中,我们的内存资源比较充足,并且对查找有一定的要求,那我们可以将mainfest缓存在内存中,这样可以减少读取mainfest文件的时间。

四、LevelDB的压缩策略

LevelDB的压缩策略,也是LevelDB的精髓。

Google有一遍BigTable:A distributed Storage System for Structured Data的论文,可以认为是LevelDB的基础。在BigTable论文当中,提到了三种压缩策略:

第一种压缩策略叫做minor Compaction:就是简单地把MemTable中的数据导入SSTable文件

第二种压缩策略叫做Major Compaction:会合并不同层级的SSTable文件,也就是说Major Compaction会减少Level的数量

最后一种压缩策略叫做full Compaction:会将所有的SSTable文件合并。

在LevelDB当中,实现了前面两种压缩策略,minor Compaction和major Compaction。

4.1、minor Compaction

minor Compaction就是将ImmuTable MemTable内存中的Skiplist当中的数据写入到磁盘生成SSTable文件:

在这里插入图片描述
Skiplist结构中的key是有序的,SSTable也是有序的,所以我们只需要依次遍历写入即可。

根据越晚生成的SSTable的level序号越小,层级越高的原则,我们最新生成的SSTable是level 0。之后我们要记录这个新生成的SSTable中的索引,完成写入操作。需要注意的是,在这个过程中,不会对数据进行删除操作。这样设计的原因也很简单,因为我们并不知道要删除的数据究竟在哪个level下的SSTable里,找到对应的SSTable并删除会带来大量的耗时,所以我们依旧原封不动地记录下来,等待后续合并时再处理这些删除操作。

另外,再文件的末尾部门会将所有key值的信息以索引形式存储。由于我们读取文件的时候,优先会读取到这些索引信息,这样就可以根据读到的索引信息快速锁定SSTable当中的数据而不用读取整个文件了。

4.2、major Compaction

major Compaction是LevelDB分层机制的核心,不然的话插入SSTable也只会都是Level 0, 层次结构就无从谈起了。

详细介绍前,先弄清一个背景知识:对于LevelDB中除level 0外其他level的SSTable文件而言,都是通过major Compaction生成的,我们可以保证同一层的SSTable中没有重叠的元素;但Level 0不同,level 0当中的SSTable中通过minor Compaction生成的,所以可能会有重叠的。

LevelDB中触发major Compaction的情况有哪些?
1、Size Compaction:相当于平衡操作,当系统发现某一层的SSTable梳理超过阈值的时候会触发
2、manual Compaction:人工手动触发,通过接口认为地去调用
3、seek Compaction:LevelDB会记录每一层Level中每一个SSTable文件的miss rate,当发现某一个文件当中的数据总是miss,而在下一层的文件中查找到了,这时候levelDB就会认为这个文件不配待在这一层,将他和下一层的数据进行合并,以减少IO消耗。

对于以上三种触发Compaction的情况中,最长出现的还是Size Compaction, 就是当LevelDB发现某一层的SSTable数据大小超过阈值时,会执行Compaction操作。

在major Compaction当中,假设LevelDB选择的level i 的文件进行合并,这时候需要分情况讨论:

  • 当i=0, 及要合并的是level 0的数据,而level 0中的不同文件的数据可能会重叠,这时候需要将所有key值有重叠的文件都纳入到待合并的集合中来。在挑选待合并集合的时候,levelDB会记录上一次触发压缩时的最大key值,这一次会选择大于这个key值得文件开始执行压缩。也就是说,levelDB 设计了一种轮训机制,保证level当中的每一个文件都有被合并的机会。
  • 当我们level i 的文件选择结束后,接下来就要从 level i+1 当中选择文件进行合并Lee,选择的标准也很简单,我们会将所有和待合并集合中key值有重叠的文件全部挑选处理进行合并。

合并的过程本质上是一个多路归并的过程,如下图:
在这里插入图片描述
由于所有文件当中的key值都是有序的,我们都从他们的头部开始,对于每一个key我们都会进行判断,是应该保留还是该丢弃。判断的逻辑是,对于某一个Key而言,如果这个key在更低级别的level中出现过,那说明他有更新的value存储,我们需要进行抛弃。

当Compaction完成之后,所有参与归并的文件都已经没有用处了,可以进行删除。

本质上,这个归并过程和裸的LSMT原理是一样的,只是增加了层级结构而已。

由于mainfest是SSTable文件的索引,所以无论哪种Compaction发生,都会改变整个Level的结构,所以我们需要再每次Compaction只会,都重新生成一个新的mainfest文件,然后将此次Compaction带来的文件变动记录进去,最后,将Current指向新生成的mainfest。

五、总结

对比LSMT,只是增删改查及Compaction增加了一些细节,但底层的框架其实还是LSMT这一套,因为核心原理是一样的。所以和纯LSMT一样,LevelDB当中的SSTable同样可以使用布隆过滤器来进行优化,除此之外,还有cache的灵活使用,进一步提升了查询效率。

另外,严格来说,LevelDB只是数据库引擎,并不是真正的数据库系统。基于LevelDB可以开发出完善的数据库系统,但他本身只提供底层最核心的增删改查服务的基础。除了基础功能职位,一个成熟的数据库系统还需要开发大路的细节以及做大量的优化。目前为止,基于LevelDB开发的数据库引擎很多,但完整的数据库系统非常少,毕竟这需要长久时间的开发和积累。

如果我们简单把分布式系统分成分布式计算系统和分布式存储系统的话,会发现分布式存储系统的精华占了大半。而分布式存储系统有可以简单任务是底层的数据结构加上上次解决分布式带来的一致性等问题的共识协议。而分布式系统当中最常用的底层数据结构无非就那么几种,所以说对于这些数据结构的了解和学习是深入理解分布式系统的基础。而一个系统架构师,解决业务场景当中的分布式问题是常态,而解决问题的能力的核心,其实就在于对这些底层基础知识的理解和运用。

猜你喜欢

转载自blog.csdn.net/shijinghan1126/article/details/109299244