leveldb

leveldb是一个写性能十分优秀的存储引擎,是典型的LSM-tree的实现。LSM的核心思想是为了换取最大的写性能而放弃掉部分读性能。那么,为什么leveldb写性能高?简单来说它就是尽量减少随机写的次数。leveldb首先将数据更新到内存中。当内存中的数据量达到一定阈值,将这部分数据再真正刷新到磁盘文件中。一般来说,顺序写60MB/s,随机写45MB/s.

整体架构


leveldb主要由以下几个重要的部件构成:
1.memtable
2.immutable memtable
3.sstable
4.manifest
5.current
6.log

memtable

刚才提到leveldb的一次写入操作并不是直接将数据写入到磁盘文件,而是采用先将数据写入内存的方式。所以,memtable就是一个内存中进行数据组织与维护的结构。在memtable中,数据按用户定义的方法排序之后按序存储。等到其存储内容到达阈值时(4MB)时,便将其转换成一个不可修改的memtable,与此同时创建一个新的memtable来供用户进行读写操作。memtable底层采用跳表,它的大多数操作都是O(logn)。

immutable memtable

当memtable的容量达到阈值时,便会转换成一个不可修改的memtable即immutable memtable。它同memtable的结构定义一样。两者的区别只是immutable memtable是只读的。immutable memtable被创建时,leveldb的后台压缩进程便会利用其中的内容创建一个sstable,然后持久化到磁盘中。

sstable

leveldb虽然采用了先写内存的方式来提高写效率。但是内存中的数据不可能是无线增长,并且日志中记录的写入操作过多会造成异常发生,而且恢复时间过长。因此内存中的数据达到一定容量就得将数据持久化到磁盘中。除了某些元数据文件,leveldb的数据主要都是通过sstable来存储的。
虽然在内存中,所有的数据都是按序排列的,但是当多个memtable数据持久化到磁盘后,其对应的sstable之间是存在交集的,这样造成在读操作时得对所有的sstable文件进行遍历,严重影响了读效率。所以,leveldb会定期整合这些文件,也叫做compaction。随着compaction的进行,sstable文件在逻辑上被分成若干层。通过内存数据直接dump出来的是level 0 层文件,后期整合出来的level i层文件。sstable本身是不可修改的。

manifest

在leveldb中有个版本的概念。一个版本记录了每一层所有文件的元数据。元数据包括如下几点:

  • 文件大小
  • 最大key值
  • 自小key值
    版本信息十分关键,除了在查找数据时利用两个key值来加快查找,还在其中为了一些compaction的统计值来控制compaction的进行。
    可以看到文件的元数据主要包含最小和最小key
// tFile holds basic information about a table.type tFile struct {fd storage.FileDescseekLeft int32size int64imin, imax internalKey}

版本则维护了每一层所有文件的元数据信息。入下代码所示:

type version struct {s *session // session - versionlevels []tFiles // file meta// Level that should be compacted next and its compaction score.// Score < 1 means compaction is not strictly needed. These fields// are initialized by computeCompaction()cLevel int // next levelcScore float64 // current scorecSeek unsafe.Pointerclosingboolrefintreleased bool}

当每次compaction完成时,leveldb都会创建一个新的version。compaction完成简单来说就是sstable的新增或者减少。而version创建的规则是:
versionNew = versionOld + versionEdit
这里的versionEdit指的是在旧版本基础上变化的内容。一般指sstable的增加或者删除。
manifest文件就是用来记录这些versionEdit信息的。一个versionEdit数据会编码成一条记录写入到manifest文件中。如下图所示

一共有两条versionEdit记录,每条记录包括

  1. 新增哪些sstable文件
  2. 日志文件编号
  3. 删除哪些sstable文件
  4. 当前compaction的下标
  5. 操作seqNumber等信息
    通过这些信息,leveldb变可以启动时创建一个空的version,不断apply这些记录。最终可以得到一个上次运行结束时的版本信息。

current

主要是记录当前manifest 的文件名。为什么需要这个?因为leveldb每次启动时,都会创建一个新的manifest文件,因此会出现很多个manifest文件。current则用来指出那个才是我们需要关心的文件。

log

leveldb写操作不是直接写入磁盘,而是先写入内存。加入写入到内存的数据还未来得及持久化,leveldb发生异常或者服务器宕机等会造成写入的数据丢失。因此,leveldb在写入内存之前会首先将所有的写操作写入日志文件中。每次写操作都是一次顺序写入,这样写效率高,整体写入性能好。此外,leveldb写操作的原子性也可以通过log来实现。
异常情况主要有以下几种:
1.写log完成,写内存未完成
2.写log期间进程异常
3.write操作完成后(写日志、写内存都完成)异常
4.immutable memtable持久化过程异常
5.其它压缩异常
第2种情况发生,数据库重启读取log时,发现异常日志数据则丢弃该条日志数据,即视作这次用户写入失败
第1、3、4情况发生时,都可以通过读取redo日志文件中记录的写入操作来完成数据库的恢复。