leveldb源码剖析----compaction

2023-12-16 16:38

本文主要是介绍leveldb源码剖析----compaction,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

根据前面的分析,背景线程的主体工作在BackgroundCompaction函数中完成。这个函数主要完成以下两个工作:

  1. 如果imm_非空,则将imm_写入到磁盘中生成新的sstable文件
  2. 对level中的文件进行合并。合并的目的主要是避免某个level中sstable文件过多,并且可以通过合并的过程删除掉过期的key-value和被用户删除的key-value。

这篇文章主要是从BackgroundCompaction函数开始,分析level中文件合并的过程


void DBImpl::BackgroundCompaction() { //在背景线程中执行mutex_.AssertHeld();if (imm_ != NULL) {CompactMemTable(); //将imm_写到level 0return;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这部分主要是完成上面所说的第一个工作,将imm_写入level 0 中。因为这个函数需要处理很多整个数据库共享的数据结构,比如imm_,versions_等,因此必须保证在临界区中执行。

  Compaction* c;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;}Log(options_.info_log,"Manual compaction at level-%d from %s .. %s; will stop at %s\n",m->level,(m->begin ? m->begin->DebugString().c_str() : "(begin)"),(m->end ? m->end->DebugString().c_str() : "(end)"),(m->done ? "(end)" : manual_end.DebugString().c_str()));}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这部分是在设置manual_compaction_ 的时候被执行,它主要是用于测试数据库的compaction功能,用于debug,数据库实际运行时一般不会运行这段代码,这里且略过,不过从功能实现上和后面的分析也是一样的。

else {c = versions_->PickCompaction(); //找出最适合compaction的level}
  • 1
  • 2
  • 3

这个条件分支是一个重点,它负责从数据库中选出适合进行compaction的level。它将返回可以进行compaction的level中的文件元信息,这些元信息存储在Compaction类中

可以看一下compaction的数据成员:


这里写图片描述

这个compaction将会合并level和level+1中的部分文件。

如果我们深入到PickCompaction就会看到leveldb是怎么选择需要进行compaction的level的。

leveldb首先从当前的versions中选择一个最适合进行compaction的level,这里的最适合主要是通过版本控制里面的compaction_score_变量进行衡量,同时每个版本都会有一个变量跟踪当前版本中最适合进行compaction的level(current_->compaction_level_)。除此之外,leveldb为每个level中的文件维持一个 数组compact_pointer_,这个compact_pointer_[level]指向当前level中上次被compaction的最大key的值,因此下次对这个level进行compaction时,就要从key大于compact_pointer_[level]的文件开始,而且我们可以看到,通常是选择第一个largest key大于compact_pointer_[level]的文件作为当前level需要进行compaction的文件。

从当前的level选择出适合进行compaction的文件后,我们就可以通过版本控制中的元信息找到这个文件所包含的key的最大值和最小值,通过这个最大值和最小值,我们就可以找到level+1中有哪些文件和level中的这个文件的key可能有重合,然后将这些文件和level中的那个文件进行compaction,compaction后的文件放入到level+1中。需要注意的是,每次compaction可能生成多个文件,而不是一个compaction只生成一个文件

回到上面的compaction类,通过上面步骤得到的level和level+1中需要进行compaction的文件的元信息放在compaction的数据成员inputs_数组中。其中inputs_[0]包含的是需要进行合并的level中的那个文件,inputs_[1]包含的是level+1中需要和level中的那个文件进行合并的所有文件元信息。

前面说过,一般都是从当前level中选择一个文件,然后从level+1中找到所有和这个文件的key范围有重合的文件进行合并,最后将合并的文件都放在level+1中,因此这保证了level+1中所有的文件的key不会有重合。而这里之所以只在level中选择一个文件,我们是基于level中的文件的key不会有重合的假设,这对于大于0的所有level都是成立的,但是对level 0 不成立,因为level 0 中可能会有重合的key。因此我们需要对level0 进行特殊处理,如果当前需要合并的level为0,则我们首先从level 0 中选择一个文件,然后找出level 0 中所有和这个文件的key重合的文件放入inputs_[0]中

这些可以从PickCompaction的实现中找到答案。

class compaction中的其他成员,主要是负责合并后文件的生成。比如max_output_file_size_控制合并生成的sstable文件的大小。grandparents_数组和grandparent_index_也是用于控制生成sstable文件的大小,grandparents_数组中维护的是level+2中和从level中选出的进行合并的文件的key范围重合的左右文件元信息,因此通过grandparents_数组,我们可以控制新合并生成放在level+1中的每个sstable文件不要和level+2中的过多文件有key范围重合,因为如果level+1中的某个文件和level+2中的很多文件都有key范围重合的话,那下次将level+1中的这个文件和level+2进行合并时,会比较耗时,因为level+2中和这个文件key重合的文件太多了。这里主要是平摊的思想,不要让某个文件承担太多的合并压力。

class compaction中的seen_key_主要是保证每个合并而成的sstable中都有key-value数据。想象一个场景,如果合并过程中第一个打算加入的key是一个比level+2中很多文件的最大key都大的数值,则我们可能会误以为当前合并而成的文件已经足够大了,准备把它写盘,但是实际却是当前文件中还没有key-value,于是就会出现问题。

class compaction中的其他成员变量主要是版本控制信息,后面再介绍。

level_ptrs_也是一个数组,它里面存储的是整型变量,记录每次遍历各层文件时的下标信息,主要用于判断一个key是否在level+2以及更高层的的文件中。可以看IsBaseLevelForKey函数的实现。

好了分析完compaction的结构信息,我们继续看BackgroundCompaction中的代码:

现在已经拿到了需要compaction的文件信息,这些信息就存储在c中

  Status status;if (c == NULL) {// Nothing to do} 
  • 1
  • 2
  • 3
  • 4

如果c为空,说明没有文件需要进行compaction,那就无事可做了

else if (!is_manual && c->IsTrivialMove()) { // Move file to next levelassert(c->num_input_files(0) == 1);FileMetaData* f = c->input(0, 0);c->edit()->DeleteFile(c->level(), f->number);c->edit()->AddFile(c->level() + 1, f->number, f->file_size,f->smallest, f->largest);status = versions_->LogAndApply(c->edit(), &mutex_); if (!status.ok()) {RecordBackgroundError(status);}VersionSet::LevelSummaryStorage tmp;Log(options_.info_log, "Moved #%lld to level-%d %lld bytes %s: %s\n",static_cast<unsigned long long>(f->number),c->level() + 1,static_cast<unsigned long long>(f->file_size),status.ToString().c_str(),versions_->LevelSummary(&tmp));}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这个条件分支主要是处理level+1中没有文件需要和level中的那个文件进行合并的情况。这种情况,很简单,直接把level中的那个需要合并的文件移动到level+1中即可。

{ // 否则进行compactionCompactionState* compact = new CompactionState(c); // c中包含需要compaction的文件的元信息status = DoCompactionWork(compact); if (!status.ok()) {RecordBackgroundError(status);}CleanupCompaction(compact);c->ReleaseInputs();DeleteObsoleteFiles();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这个分支就是主要的工作了。此时说明level+1中有文件和level中的那个需要合并的文件key范围重合。因此需要将这个文件和level+1中的那些文件进行合并操作。主体工作是在DoCompactionWork函数中完成。

下面我们分析DoCompactionWork的源码,至于BackgroundCompaction函数,到这里已经基本把我们关心的工作完成了。后面我们就不会到BackgroundCompaction函数了。


DoCompactionWork函数的实现

leveldb的compaction操作主要是由DoCompactionWork函数完成:

Status DBImpl::DoCompactionWork(CompactionState* compact) { const uint64_t start_micros = env_->NowMicros();int64_t imm_micros = 0;  // Micros spent doing imm_ compactionsLog(options_.info_log,  "Compacting %d@%d + %d@%d files",compact->compaction->num_input_files(0),compact->compaction->level(),  compact->compaction->num_input_files(1),compact->compaction->level() + 1);assert(versions_->NumLevelFiles(compact->compaction->level()) > 0);assert(compact->builder == NULL);assert(compact->outfile == NULL);if (snapshots_.empty()) {compact->smallest_snapshot = versions_->LastSequence();} else {compact->smallest_snapshot = snapshots_.oldest()->number_;}// Release mutex while we're actually doing the compaction workmutex_.Unlock();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

最开始这部分不是我们关心的内容,先放着,后面再介绍

  Iterator* input = versions_->MakeInputIterator(compact->compaction);input->SeekToFirst();
  • 1
  • 2

这是获得一个可以遍历需要compaction的所有文件(level和level+1中所有需要进行compaction操作的文件)的迭代器,每个迭代器对应一个key-value,这样,我们通过这个迭代器就可以找到compaction结构中的inputs_数组里面的所有文件的key-value。leveldb里面提供了很多迭代器,它通过迭代器封装了文件内部访问的复杂细节。

  Status status;ParsedInternalKey ikey;std::string current_user_key;bool has_current_user_key = false;SequenceNumber last_sequence_for_key = kMaxSequenceNumber;
  • 1
  • 2
  • 3
  • 4
  • 5

这些是对后面需要用到的一些变量的初始化。

后面就是一个大循环,这个循环依次通过上面的迭代器遍历所有参与compaction的文件的所有key,循环的主体工作是判断当前迭代器对应的key是否应该加入到新合并生成的文件中

  for (; input->Valid() && !shutting_down_.Acquire_Load(); ) { //每个input对应的是一个 K/V// Prioritize immutable compaction workif (has_imm_.NoBarrier_Load() != NULL) {const uint64_t imm_start = env_->NowMicros();mutex_.Lock();if (imm_ != NULL) {CompactMemTable(); //这里就是将imm_写入磁盘中bg_cv_.SignalAll();  // Wakeup MakeRoomForWrite() if necessary}mutex_.Unlock();imm_micros += (env_->NowMicros() - imm_start);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

每次循环开始先判断当前的imm_是否为空,如果为空的话,先将它写入磁盘,这主要是为了防止imm_没有及时写盘造成用户线程不能写mem。可以参见前面的分析。

    Slice key = input->key(); if (compact->compaction->ShouldStopBefore(key) &&compact->builder != NULL) {status = FinishCompactionOutputFile(compact, input); //生成一个sstableif (!status.ok()) {break;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里先把迭代器对应的key提取出来,因为在此之前我们可能以及遍历过多个key-value了,也就是可能已经将多个key-value写入到新的sstable中了。这里通过ShouldStopBefore函数判断是否符合生成一个新的sstable的条件,如果符合的话就将这个sstable写盘,如果不符合的话,就继续往里面加key-value。

前面我们分析过,影响是否将当前的sstable写盘的主要有两个原因:

  1. 当前的sstable是否已经足够大了
  2. 当前的sstable是否和过多的level+2中的文件重合

这里处理的是第二个条件。这里需要注意的是,到目前位置这个迭代器对应的key-value都没有写入到当前的sstable中。

bool drop = false;
  • 1

这是一个标记位,主要是用于标记一个key是否应该加入到当前的sstable中。如果drop=true则说明这个当前key应该被丢弃,反则反之。

   if (!ParseInternalKey(key, &ikey)) { //把sequence,type,key都解析出来,放在ikey中// Do not hide error keyscurrent_user_key.clear();has_current_user_key = false;last_sequence_for_key = kMaxSequenceNumber;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

首先对key-value进行解析,前面我们分析过,sstable中存储的key-value是以如下的形式:

internal_key_size|key|sequence|type|value_size|value
  • 1

这里主要是把key,sequence,type解析出来。

如果发现这个key不合法,则设置一些标记变量。后面我们再讲解这些变量的作用。这些标记位的作用就是保证这个key一定会被写入到新合并生成的sstable文件中。并且它不对后面的key-value是否被丢弃产生影响。leveldb之所以选择不丢弃不合法的key-value,我想主要是为了不想隐藏系统可能的一些错误?

如果key-value形式合法,则走到下面的一个大else

else {if (!has_current_user_key ||user_comparator()->Compare(ikey.user_key,Slice(current_user_key)) != 0) { //如果等于0,表示这个key和之前度过的key相同,不必再添加了// First occurrence of this user keycurrent_user_key.assign(ikey.user_key.data(), ikey.user_key.size());has_current_user_key = true;last_sequence_for_key = kMaxSequenceNumber; //}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这个是判断当前迭代器的key和前面加入的一个key是否相等,如果相等的话,那说明这个key是一个过期的key,应该被丢弃,如果这个key是第一次出现,则将其设置为current_user_key,(1). 但是还不能确定把他加入到新的sstable中去,因为可能这个key的type是kTypeDeletion,表示这个key已经被用户删除了,因此自然应该把它丢弃掉;(2). 除此之外,对于过期的key,我们也不是一定会像前面说的那样把它丢弃掉,因为可能系统开启了快照,这样老旧的key也得保存下来。

所以后面有两个条件分支,分别处理这两种情况:

  1. 对于过期的key,通过检查这个key的序列号,判断它是否在系统快照中,如果在的话,即使它是过期key也不能丢弃。
  2. 对于非过期的key,检查这个key的type,看它是否是kTypeDeletion,即是否已经被用户删除了。
      if (last_sequence_for_key <= compact->smallest_snapshot) {// Hidden by an newer entry for same user keydrop = true;    // (A)} else if (ikey.type == kTypeDeletion &&ikey.sequence <= compact->smallest_snapshot &&compact->compaction->IsBaseLevelForKey(ikey.user_key)) {// For this user key:// (1) there is no data in higher levels// (2) data in lower levels will have larger sequence numbers// (3) data in layers that are being compacted here and have//     smaller sequence numbers will be dropped in the next//     few iterations of this loop (by rule (A) above).// Therefore this deletion marker is obsolete and can be dropped.drop = true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

对于第一个if,从最前面的if我们看到,当我们在发现当前key是第一次出现时会设置last_sequence_for_key = kMaxSequenceNumber;因此走这个if说明该key肯定是一个过期key了。所以判断它的序列号是否在快照序列号中,不是的话就标记drop = true,将其丢弃。


至此为止,当前key要么是第一次出现,要么是有快照保护,好像不能丢弃。但是事情还没完,我们还不能据此判断该key是否应该被保存下来,我们还要判断它的type,但是事情远没有我们想得那么简单,除了判断key的type之外,我们还要做其他判断.也就是说,当一个key为kTypeDeletion时它还不一定是要被删除的。为什么呢

想象一个场景:用户在调用delete删除一个key时,这个key在数据库中有一个过期的key存在,而这个过期key还来不及和这个被删除的key合并,如果在这种情况下,我们直接将这个被标记为删除的key丢弃,那数据库中还会存在一个过期的key,而这个过期的key在丢弃那个被删除的key时瞬间就变成正常不过期的key了,于是下次读key时,会读到这个本应该过期的key,按道理应该是找不到key才对。所以为了系统正常运行,我们每次丢弃一个标记为kTypeDeletion的key时,必须保证数据库中不存在它的过期key,否则就得将它保留,直到后面它和这个过期的key合并为止,合并之后再丢弃

所以这里会调用IsBaseLevelForKey函数判断level+2及level+2之后的所有level中没有和这个标记为删除的key相同的key,只要有,就肯定是过期key了。

last_sequence_for_key = ikey.sequence;}
  • 1
  • 2

这里就是标记last_sequence_for_key 变量,用它来标记当前key的序列号。

    if (!drop) {// Open output file if necessaryif (compact->builder == NULL) {status = OpenCompactionOutputFile(compact);if (!status.ok()) {break;}}if (compact->builder->NumEntries() == 0) {compact->current_output()->smallest.DecodeFrom(key);}compact->current_output()->largest.DecodeFrom(key);compact->builder->Add(key, input->value()); //把这个键值加入新建的sstable文件中// Close output file if it is big enough  // 如果当前结果文件已经足够大,则关闭文件,以后的compaction结果再放到新的文件中if (compact->builder->FileSize() >=compact->compaction->MaxOutputFileSize()) {status = FinishCompactionOutputFile(compact, input);if (!status.ok()) {break;}}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

OK,如果drop为false,说明当前key应该被保留下来。下面就将当前迭代器对应的key-value加入到sstable中,就是通过TableBuilder完成这些工作。前面我们讲过了。

加入这个key-value到sstable后,还要判断当前的sstable是不是满足写盘条件,即满足生成一个完整sstable的文件。

input->Next();
  • 1

继续下一个key-value。迭代器封装了所有细节。后面我们会专门介绍leveldb中的各种迭代器。

大循环之后就完成了文件合并的核心工作,循环之后是一些设置版本信息和写日志的工作,这个我们后面再介绍了。


总结

compaction是leveldb中最核心的东西了。(1). 前面我们说过,当用户调用delete删除一个键值时,leveldb并没有真正把它删除掉,而只是简单将这个键值对的type标记为kTypeDeletion,然后和正常的键值对一样写盘。这个特点使得leveldb的写操作很快,但是问题也是很显然的,就是会造成大量无效数据,占用磁盘空间。(2). 除此之外,leveldb添加数据时并不会将过期的key-value覆盖,而是通过序列号将其当成完全不同的key写入进去,因此会使得系统中存在很多过期数据。,毫无疑问这也是很占用空间的。

而compaction操作可以解决这些问题。通过compaction,leveldb可以将过期的key丢弃,而且在一定条件下丢弃标记为kTypeDeletion的数据。同时通过compaction控制了每层的文件数目。可以说compaction是leveldb的写操作得以高效的主要原因。

每次compaction操作是根据版本统计信息找到最适合进行compaction的level,然后从这个level中选择一个最合适的compaction文件,再去level+1中找出所有和这个文件的key重合的文件,最后将level中的这个文件和level+1中的那些文件进行合并,并将合并生成的所有sstable文件都放入到level+1层中。当然当level=0时,因为level0中的文件可能存在key重合,因此需要一些特殊处理。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Swartz2015/article/details/67633724

这篇关于leveldb源码剖析----compaction的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/501159

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

red5-server源码

red5-server源码:https://github.com/Red5/red5-server

TL-Tomcat中长连接的底层源码原理实现

长连接:浏览器告诉tomcat不要将请求关掉。  如果不是长连接,tomcat响应后会告诉浏览器把这个连接关掉。    tomcat中有一个缓冲区  如果发送大批量数据后 又不处理  那么会堆积缓冲区 后面的请求会越来越慢。

Windows环境利用VS2022编译 libvpx 源码教程

libvpx libvpx 是一个开源的视频编码库,由 WebM 项目开发和维护,专门用于 VP8 和 VP9 视频编码格式的编解码处理。它支持高质量的视频压缩,广泛应用于视频会议、在线教育、视频直播服务等多种场景中。libvpx 的特点包括跨平台兼容性、硬件加速支持以及灵活的接口设计,使其可以轻松集成到各种应用程序中。 libvpx 的安装和配置过程相对简单,用户可以从官方网站下载源代码