leveldb存储引擎分析

整体架构

leveldb存储引擎主要包括Memtable、Log文件、Immutable Memtable、SST文件、Manifest文件、Current文件几个模块,存储引擎的静态结构如下:

模块说明

  • Memtable:内存中数据结构,内部实现为跳表,每次写入新的数据,都会写入到Memtable。
  • Log文件:leveldb采用WAL实现机制,每次写入Memtable前会先写Log文件,Log通过append的方式顺序写入。机器宕机时,内存数据丢失,启动时可以从log文件恢复,保证数据不丢失。
  • Immutable Memtable:当Memtable插入的数据占用内存到了一个界限后,Memtable会变为Immutable,Immutable Mumtable不再接受用户写入,同时会有新的Memtable生成。LevelDb后台调度会将Immutable Memtable的数据导出到磁盘,形成一个新的SSTable文件。
  • SST文件:SST文件为磁盘数据存储文件,分为Level 0到Level N多层,每一层包含多个SST文件,单个SST文件容量随层次增加成倍增长,文件内数据有序。其中Level0的SST文件由Immutable直接Dump产生,其他Level的SST文件由其上一层的文件和本层文件归并产生;SST文件在归并过程中顺序写生成,生成后仅可能在之后的归并中被删除,而不会有任何的修改操作。
  • Manifest文件:记录SSTable各个文件的管理信息,比如属于哪个Level,文件名称叫啥,最小key和最大key各自是多少。
//manifest文件可能会有多个,格式如代码所示:
char buf[100];
snprintf(buf, sizeof(buf), "/MANIFEST-%06llu", static_cast<unsigned long long>(number)); 

说明
manifest文件一般会在VersionSet::LogAndApply函数调用时生成或修改,manifest文件数据存储也是通过log::Writer类来实现的,调用AddRecord实现数据存储,数据存储的详细格式可参考log文件

  • Current文件:在LevleDb的运行过程中,随着Compaction的进行,SSTable文件会发生变化,会有新的文件产生,老的文件被废弃,Manifest也会跟着反映这种变化,会新生成Manifest文件来记载这种变化,而Current则用来记载当前的manifest文件名,实际的current文件名为/CURRENT。

log文件

log文件布局:

数据详细结构1:

数据详细结构2:

SSTable文件

逻辑结构:

所有block的结构是一致的,如下:

BlockData结构如下:

IndexBlock
DataBlock中存储的是实际的kv数据,IndexBlock的存储结构跟DataBlock一致,只是IndexBlock的key为DataBlock的最大一个key(lastkey)、value为DataBlock在文件中的offset和DataBlock的大小。

Compaction

LevelDB中会有后台线程来执行Compaction的操作,将上层文件与下层文件归并生成新的下层文件,Version中记录的各层的文件信息来帮助决定进行Compaction的时机。

  • 容量触发Compaction:每个Version在其生成的时候会初始化两个值compaction_level_、compaction_score_,compaction_level_记录了当前compaction_score_对应的level,compaction_score_对应level层文件需要进行Compaction的紧迫程度,score大于1被认为是需要马上执行的。

    for (int level = 0; level < config::kNumLevels-1; level++) {
       double score;
       if (level == 0) {
         score = v->files_[level].size() /
             static_cast<double>(config::kL0_CompactionTrigger);
       } else {
         // Compute the ratio of current size to size limit.
         const uint64_t level_bytes = TotalFileSize(v->files_[level]);
         score = static_cast<double>(level_bytes) / MaxBytesForLevel(level);
       }
    
       if (score > best_score) {
        best_level = level;
         best_score = score;
       }
     }
     v->compaction_level_ = best_level;
     v->compaction_score_ = best_score;
     ... ...
  • Seek触发Compaction:Version中会记录file_to_compact_和file_to_compact_level_,这两个值会在Get操作每次尝试从文件中查找时更新。LevelDB认为每次查找同样会消耗IO,这个消耗在达到一定数量可以抵消一次Compaction操作消耗的IO,所以对Seek较多的文件应该主动触发一次Compaction。但在引入布隆过滤器后,这种查找消耗的IO就会变得微不足道了,因此由Seek触发的Compaction其实也就变得没有必要了。

    目前针对一次查询,LevelDB可能需要在每个level上进行一次磁盘随机访问,通过使用bloom filter可以大大减少所需要的随机访问操作次数。比如,假设调用者正在查找一个值为”Foo”的key,LevelDB会从每个level下选择那些range包含了该key的SSTable文件,之后会在这些SSTable文件上进行随机读。如果每个SSTable都有一个对应的bloom filter,那么查找时就可以很容易地通过检查bloom filter跳过那些不包含该key的SSTable文件。
    在TableBuilder::Add()函数调用的同时会通过调用AddKey函数更新filter_block中bloom filter,在TableBuilder::Flush()函数调用时,会调用StartBlock函数更新filter_block中boolm filter,在TableBuilder::Finish()函数调用时,会首先写入filter block数据,然后写入metaindex block,然后再写入是index block,最后是footer。

    bloom filter的核心思想是使用多个hash函数,对同一个key,使用每个hash计算得到对应bit,然后将bitmap中该bit均置为1,因此,当判断某个key是否在bitmap时,使用多个hash函数计算得到对应bit,判断所有hash函数极端得到的bit的值是否均为1,如果均为1,则表示该key存在,否则表示该key不存在,这样的设计使得结果有一定误报,即判断key存在,但实际并不存在。

const bool size_compaction = (current_->compaction_score_ >= 1);
const bool seek_compaction = (current_->file_to_compact_ != NULL);
if (size_compaction) 
{
    level = current_->compaction_level_;
    c = new Compaction(level);
    ...
    if (c->inputs_[0].empty()) 
    {
        c->inputs_[0].push_back(current_->files_[level][0]);
    }
} else if (seek_compaction) 
{
    level = current_->file_to_compact_level_;
    c = new Compaction(level);
    c->inputs_[0].push_back(current_->file_to_compact_);
  } else {
    return;
  }
  • 手动Compaction:,用户指定触发某个Key Range的Compaction时,会调用LevelDB提供的外部接口CompactRange,手动Compaction的优先级高于上述两种自动触发的优先级。
bool is_manual = (manual_compaction_ != NULL);
InternalKey manual_end;
if (is_manual) 
{
    ManualCompaction* m = manual_compaction_;
    c = versions_->CompactRange(m->level, m->begin, m->end);
    m->done = (c == NULL);
    if (c != NULL) 
    {
      manual_end = c->input(0, c->num_input_files(0) - 1)->largest;
    }
    ...
} else 
{
    c = versions_->PickCompaction();
}

compation处理

1) PickCompaction函数获得待compation的文件集合

PickCompaction函数中首先将level层的一个文件加入input_[0](如果是第0层文件,由于第0层文件key range有重合,因此,需要将第0层中所有与该文件key有重合的其他文件也加入input_[0]),然后获得所有与input_[0]文件中Key Range重合的level+1层文件加入input_[1]。(如果获得level层中所有与input_[0]及input_[1]中文件key range有重合的文件集合expanded0后,再从level+1层获取与expanded0中key range重合的文件集合expanded1,如果expanded1与input_[1]相同,说明level+1层文件已收敛,因此以expanded0作为level层的待压缩文件集合。

2) 待压缩文件的遍历

最新的数据存放在较低的level中,且最新的数据对应的seq一定比old的记录的seq要大,level 0中可能存在Key值相同的数据,但其后面的seq也不同,数据越新,其对应的seq越大,同时,level 0文件中的的记录是按照user_key递增,seq递减的方式存储的。
在遍历时,由于相同user_key对应的记录是聚集在一起的,且按照seq递减的方式存放的。因此,在Compaction时,只需要处理第一条出现的user_key相同的记录即可,后面的相同user_key的记录都可以丢弃。

如果发现第一条记录为kTypeDeletion,则需要判断后面未参与compact的其他层还有该key,如果有相同的key,则该第一条key依然会写入到新的文件中。

参考文档

LevelDB原理探究与代码分析

猜你喜欢

转载自blog.csdn.net/shangshengshi/article/details/78196126