leveldb深度剖析-压缩流程(2)

本文深入剖析LevelDB的跨层压缩处理机制,介绍压缩预计算流程及打分函数原理,探讨无效Seek次数过多的情况,解释如何通过简单算法确定压缩层次与文件。同时,详细解读BackgroundCompaction压缩入口函数及PickCompaction函数的实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继续上一篇内容,来分析一下跨层压缩处理。leveldb不会轻易进行压缩流程(压缩流程影响性能),所以为了提升性能leveldb设计两个简单算法来决定压缩哪一层的哪一个文件

一、压缩预计算

1.1、打分函数

leveldb每次进行文件压缩并没有随意压缩的,而是通过压缩预计算流程(打分函数)计算出一下值,在压缩的时候根据预计算结果选择压缩的层次,那么打分函数是如何实现的呢?

/**
 * 预计算下次压缩Level层次
 * @param v 新版本信息
 */
void VersionSet::Finalize(Version* v) {
  // Precomputed best level for next compaction
  int best_level = -1;
  double best_score = -1;
  // 为什么少一层呢? 因为最后一层不需要压缩
  for (int level = 0; level < config::kNumLevels-1; level++) {
    double score;
    if (level == 0) {
      // 对于level 0以文件个数计算,kL0_CompactionTrigger默认配置为4,
      // 因为如果0层文件多了会影响合并效率
      // We treat level-0 specially by bounding the number of files
      // instead of number of bytes for two reasons:
      //
      // (1) With larger write-buffer sizes, it is nice not to do too
      // many level-0 compactions.
      //
      // (2) The files in level-0 are merged on every read and
      // therefore we wish to avoid too many files when the individual
      // file size is small (perhaps because of a small write-buffer
      // setting, or very high compression ratios, or lots of
      // overwrites/deletions).
      score = v->files_[level].size() /
          static_cast<double>(config::kL0_CompactionTrigger);
    } else {
      // Compute the ratio of current size to size limit.
      //对于level>0,根据level内的文件总大小计算
      //其中函数MaxBytesForLevel根据level返回其本层文件总大小的预定最大值。
      //计算规则为:1048576.0* level^10。
      const uint64_t level_bytes = TotalFileSize(v->files_[level]);
      score =
          static_cast<double>(level_bytes) / MaxBytesForLevel(options_, level);
    }
    //始终保存 分数最大以及level层次
    if (score > best_score) {
      best_level = level;
      best_score = score;
    }
  }
  /* 表示下次压缩层次以及计算出的分数  如果分数<1 则不是必须进行压缩  */
  v->compaction_level_ = best_level;
  v->compaction_score_ = best_score;
}

leveldb通过这样一个简单算法,设定压缩层次以及分数,后面在真正压缩的时候需要用到这两个数据。

1.2、无效Seek次太多

比如有两个文件A[aaa, mmm] ,B[ddd, hhh],如果我们要查询关键字为eee,通过上面可知,这两个文件都符合我们的要求,因此需要先查找A文件,如果A文件不存在eee则对B文件进行查找。很不幸我们需要eee这条记录就在B文件中,那么对于A文件的查找我们称做一次无效的Seek。当我们对于一个文件无效Seek次数过多,说明我们的数据不够和谐,因此就需要对这个文件(A文件)重新整理(即压缩)。leveldb设置的无效Seek次数为2^30。这部分内容在查询流程中在详细说明。

通过上面两种简单的算法,leveldb来确定对哪个层次中的哪个文件进行压缩。我们只需要了解是如何确定出来的即可。

二、BackgroundCompaction压缩入口函数

/**
 * 压缩真正入口函数
 * 1、imm_ MemTable不空则压缩MemTable 生成ldb(sstable)文件
 * 2、对现有level文件进行压缩,避免各个level文件过多以及清理掉标记为删除的数据
 */
void DBImpl::BackgroundCompaction() {
  mutex_.AssertHeld();
  //压缩Immutable MemTable 不是空 说明存在则进行dump到文件中
  if (imm_ != NULL) {
    CompactMemTable();
    return;
  }

  Compaction* c;//保存压缩相关数据
  bool is_manual = (manual_compaction_ != NULL);
  InternalKey manual_end;
  if (is_manual) {//手动压缩 一般用于测试debug
    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()));
  } else {//选出 最适合压缩的level以及文件元数据 此函数并没有进行压缩
    c = versions_->PickCompaction();
  }

  Status status;
  if (c == NULL) {
    // Nothing to do
  } else if (!is_manual && c->IsTrivialMove()) {//文件不需要合并 只需要修改所属层次
    // Move file to next level
    assert(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));
  } else {//level和level+1中有key值重合 因此需要合并
    CompactionState* compact = new CompactionState(c);
    status = DoCompactionWork(compact);//执行压缩
    if (!status.ok()) {
      RecordBackgroundError(status);
    }
    CleanupCompaction(compact);
    c->ReleaseInputs();
    DeleteObsoleteFiles();
  }
  delete c;

  if (status.ok()) {
    // Done
  } else if (shutting_down_.Acquire_Load()) {
    // Ignore compaction errors found during shutting down
  } else {
    Log(options_.info_log,
        "Compaction error: %s", status.ToString().c_str());
  }

  if (is_manual) {
    ManualCompaction* m = manual_compaction_;
    if (!status.ok()) {
      m->done = true;
    }
    if (!m->done) {
      // We only compacted part of the requested range.  Update *m
      // to the range that is left to be compacted.
      m->tmp_storage = manual_end;
      m->begin = &m->tmp_storage;
    }
    manual_compaction_ = NULL;
  }
}

上面压缩函数比较简单,这里简要说明一下:

1) 如果启动压缩流程,优先对MemTable进行压缩

2) 手动压缩一般用于测试环境,比如测试leveldb性能或者是leveldb有没有bug之类,所以对于这个分支可以不用关心

3) 通过调用PickCompaction选择出要进行压缩文件的层次,返回压缩信息

4) 有些时候文件中key与高一层文件没有冲突,这种常见只需要对文件level进行迁移即可,所以通过方法IsTrivialMove进行判断

5) 执行DoCompactionWork对文件进行压缩处理

三、PickCompaction

/**
 * 选择出要压缩的level以及文件元数据
 */
Compaction* VersionSet::PickCompaction() {
  Compaction* c;
  int level;

  // We prefer compactions triggered by too much data in a level over
  // the compactions triggered by seeks.
  const bool size_compaction = (current_->compaction_score_ >= 1);
  const bool seek_compaction = (current_->file_to_compact_ != NULL);

  /*  
   * 压缩场景原因:
   *    1、一种是某一层级的文件数过多或者文件总大小超过预定门限,
   *    2、level n 和level n+1重叠严重,无效seek次数太多。(level n 和level n+1的文件,key的范围可能交叉导致)
   */
  if (size_compaction) {//由于文件大小超过门限值 
    level = current_->compaction_level_;//代表 当前要压缩合并的层次
    assert(level >= 0);
    assert(level+1 < config::kNumLevels);
    c = new Compaction(options_, level);

    // Pick the first file that comes after compact_pointer_[level]
    for (size_t i = 0; i < current_->files_[level].size(); i++) {
      FileMetaData* f = current_->files_[level][i];
      //compact_pointer_压缩点  保存上次压缩最大key值 本次largest key大于上次 则进行压缩
      if (compact_pointer_[level].empty() ||
          icmp_.Compare(f->largest.Encode(), compact_pointer_[level]) > 0) {
        c->inputs_[0].push_back(f);//保存文件元数据信息
        break;
      }
    }
    if (c->inputs_[0].empty()) {
      // Wrap-around to the beginning of the key space
      c->inputs_[0].push_back(current_->files_[level][0]); //保存文件元数据信息
    }
  } else if (seek_compaction) {//无效seek次数太多
    level = current_->file_to_compact_level_;//代表 当前要压缩合并的层次
    c = new Compaction(options_, level);
    c->inputs_[0].push_back(current_->file_to_compact_);
  } else {
    return NULL;
  }

  c->input_version_ = current_;
  c->input_version_->Ref();

  // Files in level 0 may overlap each other, so pick up all overlapping ones
  // 如果要压缩文件层次是0  那么key值可能有重复 需要分辨出来
  if (level == 0) {
    InternalKey smallest, largest;
    GetRange(c->inputs_[0], &smallest, &largest);
    // Note that the next call will discard the file we placed in
    // c->inputs_[0] earlier and replace it with an overlapping set
    // which will include the picked file.
    // 遍历level-0层所有文件 重新确定输入集合input_[0]
    current_->GetOverlappingInputs(0, &smallest, &largest, &c->inputs_[0]);
    assert(!c->inputs_[0].empty());
  }
  /* 当前level要合并的文件统一保存到inputs_[0] 下面这个函数是设置inputs_[1] */
  SetupOtherInputs(c);

  return c;
}

说明:

1)上面主体if判断是针对两种场景压缩场景判断,通过代码注释可以方便理解。

2)这段代码提出了一个概念压缩点。压缩点实际为当前层次最近一次压缩后文件中最大值key。compact_pointer成员变量保存着每一层压缩点信息。这里涉及到一个概率性问题: 当前层发生压缩流程一定是有新文件加入(无效seek场景不处理压缩点),那么我们期盼新加入的文件比压缩点保存的最大值要大,这样只需要处理新的文件,将新文件与level+1层文件进行合并。那么level层小于压缩点的文件基本不用动(提升性能)。当然这只是一种期望,我们并不能保证每次都有会新的最大值文件出现。当没有大于的压缩点,只能随便选择一个文件,即代码。

    if (c->inputs_[0].empty()) {
      // Wrap-around to the beginning of the key space
      c->inputs_[0].push_back(current_->files_[level][0]); //保存文件元数据信息
    }

3)因为level0中的文件比较特殊,该层文件中的key不是有序的,因此需要特殊处理一下,保存整层文件中最小和最大值。注意这里调用了方法GetOverlappingInputs后,inputs_[0]可能保存多个文件。

4)调用方法SetupOtherInputs,实际是设置inputs_[1]。

四、 SetupOtherInputs

/**
 * 压缩最主要过程 是剔除掉重复数据
 * 他的作用是为了在不影响性能的情况下尽可能多的compaction当前level的文件
 */
void VersionSet::SetupOtherInputs(Compaction* c) {
  const int level = c->level();
  InternalKey smallest, largest;
  GetRange(c->inputs_[0], &smallest, &largest);
  //从level+1层中挑出 与 level层key重叠的 文件  inputs_[1]可能保存多个
  current_->GetOverlappingInputs(level+1, &smallest, &largest, &c->inputs_[1]);

  // Get entire range covered by compaction 
  // 先合并 在获取最大值、最小值 相当于确定最终区间
  InternalKey all_start, all_limit;
  GetRange2(c->inputs_[0], c->inputs_[1], &all_start, &all_limit);

  // See if we can grow the number of inputs in "level" without
  // changing the number of "level+1" files we pick up.
  if (!c->inputs_[1].empty()) {//如果进入此分支 表明level 和 level+1有key重复
    // 根据新的集合[all_start, all_limit]在level中进行过滤 可能又有符合条件的文件加入到其中
    std::vector<FileMetaData*> expanded0;
    current_->GetOverlappingInputs(level, &all_start, &all_limit, &expanded0);
    
    // 计算所有文件大小
    const int64_t inputs0_size = TotalFileSize(c->inputs_[0]);// level层待合并的文件
    const int64_t inputs1_size = TotalFileSize(c->inputs_[1]); // level + 1层待合并文件
    const int64_t expanded0_size = TotalFileSize(expanded0);
    // expanded0集合 大于 input_[0]表示有新文件加入  因此需要以expanded0为基准
    if (expanded0.size() > c->inputs_[0].size() &&
        inputs1_size + expanded0_size <
            ExpandedCompactionByteSizeLimit(options_)) {
      InternalKey new_start, new_limit;
      GetRange(expanded0, &new_start, &new_limit);//从扩展0 中挑出最小值和最大值
      std::vector<FileMetaData*> expanded1;
      current_->GetOverlappingInputs(level+1, &new_start, &new_limit,
                                     &expanded1);//从level+1中挑出 重叠key的文件信息保存到扩展1中
      if (expanded1.size() == c->inputs_[1].size()) {//表示level+1集合没有变化,则使用expanded0和expanded1作为input集合
        Log(options_->info_log,
            "Expanding@%d %d+%d (%ld+%ld bytes) to %d+%d (%ld+%ld bytes)\n",
            level,
            int(c->inputs_[0].size()),
            int(c->inputs_[1].size()),
            long(inputs0_size), long(inputs1_size),
            int(expanded0.size()),
            int(expanded1.size()),
            long(expanded0_size), long(inputs1_size));
        smallest = new_start;
        largest = new_limit;
        c->inputs_[0] = expanded0;
        c->inputs_[1] = expanded1;
        GetRange2(c->inputs_[0], c->inputs_[1], &all_start, &all_limit);
      }
    }
  }

  // Compute the set of grandparent files that overlap this compaction
  // (parent == level+1; grandparent == level+2)
  if (level + 2 < config::kNumLevels) {
    current_->GetOverlappingInputs(level + 2, &all_start, &all_limit,
                                   &c->grandparents_);
  }

  if (false) {
    Log(options_->info_log, "Compacting %d '%s' .. '%s'",
        level,
        smallest.DebugString().c_str(),
        largest.DebugString().c_str());
  }

  // Update the place where we will do the next compaction for this level.
  // We update this immediately instead of waiting for the VersionEdit
  // to be applied so that if the compaction fails, we will try a different
  // key range next time.
  compact_pointer_[level] = largest.Encode().ToString();//保存本次压缩 最大值
  c->edit_.SetCompactPointer(level, largest);
}

说明:

1)input_[0]中保存文件数目:如果是level0层可能保存多个文件,如果level > 0则保存一个文件。确定input_[0]中最小/大值并且以此来确定input_[1]中符合范围的文件。

2)确定input_[0]和input_[1]文件中的最小/大值,保存在all_start,all_limit

3)如果input_[1]不空,则表明level+1层有冲突文件,这些冲突文件就是要合并的。因为在第2)步骤中确定了最终的all_start和all_limit,为了避免遗漏文件,必须再次对level层中文件进行遍历,查找符合条件的文件,将这些文件定义为expanded0。这里举例详细说明:

假设,level中有两个文件:A0,范围为[fff,kkk],A1,范围为[mmm,ppp],将文件A1作为input_[0]
           level+1中有两个文件,F0,范围为[hhh,nnn],F1范围为[ooo,sss],可以得出input_[1] = {F0, F1},集合[hhh,sss]
           那么对input_[0] ∪ input_[1] (并集)可得all_start=hhh,all_limit=sss。由于新的集合确定出来,为了避免遗漏文件需要再次遍历level层中的文件,并将新集合称之为expanded0,由上面的例子可知,expanded0包含文件A0和A1

如果expaned0_size大于input0_size则表示有新的文件加入压缩流程,需要再次进行处理。

4)因为expanded集合大于input_[0],因此需要再次确定level中最小/大值,由上面例子可以范围:new_start=fff,new_limit = ppp,所以需要再次对level+1层文件进行遍历,确定expanded1,如果和expended1集合与input_[1]集合相等,则确定最终集合expanded0作为input_[0],expaneded[1]作为input_[1]。

5)注意input_[1],是有可能为空的,如果input_[1]为空,那么说明level和level+1没有冲突,只需要将level相应文件修改层次即可,不需要进行压缩处理。如下函数处理逻辑:

//是否只需要移动文件进行compaction,不需要merge和split
bool Compaction::IsTrivialMove() const {
  const VersionSet* vset = input_version_->vset_;
  // Avoid a move if there is lots of overlapping grandparent data.
  // Otherwise, the move could create a parent file that will require
  // a very expensive merge later on.
  return (num_input_files(0) == 1 && num_input_files(1) == 0 &&
          TotalFileSize(grandparents_) <=
              MaxGrandParentOverlapBytes(vset->options_));
}

五、总结

本篇的重点方法有很多,leveldb处理逻辑比较复杂,好在自认为分析的比较清楚,但是不知道读者是否能看清楚。若有不清楚的可以看一下代码注释,在不清楚可以留言进行讨论。下一篇分析DoCompactionWork流程

根据引用,你提到了pip show jsonpath的安装成功。根据这个引用,我可以确认jsonpath已经成功安装在你的Python环境中。 至于你提到的"windows python LevelDB安装 metadata-generation-failed",我没有找到相关的引用来回答这个问题。但是我可以给你一些建议来解决这个问题。 首先,确保你已经正确地安装了LevelDB以及与之相关的依赖包。你可以尝试使用pip install命令来安装LevelDB。如果出现metadata generation failed的错误,可能是由于网络问题或者软件包源的问题导致的。你可以尝试使用其他的软件包源或者使用代理来解决这个问题。 另外,你还可以尝试使用conda来安装LevelDB,它是一个流行的Python包管理器,可以管理Python包及其依赖项。你可以在Anaconda官方网站上找到更多关于conda的信息和安装指南。 最后,如果上述方法都没有解决你的问题,我建议你检查一下你的操作系统和Python版本是否与LevelDB兼容。有时候,特定的软件包可能只能在特定的操作系统或者Python版本上正常工作。 希望这些建议对你有帮助,如果你还有其他问题,请随时提问。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python38安装jsonpath失败问题解决](https://blog.youkuaiyun.com/qq_27371025/article/details/125855179)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [The Log: What every software engineer should know about real-time data's unifying abstraction](https://blog.youkuaiyun.com/iloveu8780/article/details/80097101)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值