整体架构
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依然会写入到新的文件中。