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

继续上一篇遗留问题,本篇介绍对MemTable压缩,在介绍之前先普及一下其他内容。

在存储流程第一篇博客中,有一个方法没有详细说明--MakeRoomForWrite,该函数是保证新插入的数据有足够空间,那么该方法是如何保证的呢?本篇就详细介绍一下该方法。

一、MakeRoomForWrite

/*
 * 确保有足够空间可写
 * @param force true表示强制立刻写入  false表示延迟写入
 * 如果mem_没有可用空间可写,则会重新生成mem_ 旧的mem_转成imm_ 然后启动后台线程
 * 进行压缩处理等相关操作
 */
Status DBImpl::MakeRoomForWrite(bool force) {
  mutex_.AssertHeld();
  assert(!writers_.empty());
  bool allow_delay = !force; //是否延迟写入
  Status s;
  while (true) {
    if (!bg_error_.ok()) {//后台线程background 在将imm_写入磁盘(level0时)发生错误
      // Yield previous error
      s = bg_error_;
      break;
    } else if (
        allow_delay &&
        versions_->NumLevelFiles(0) >= config::kL0_SlowdownWritesTrigger) {
      // We are getting close to hitting a hard limit on the number of
      // L0 files.  Rather than delaying a single write by several
      // seconds when we hit the hard limit, start delaying each
      // individual write by 1ms to reduce latency variance.  Also,
      // this delay hands over some CPU to the compaction thread in
      // case it is sharing the same core as the writer.
      // 当文件数目达到8个则进行延迟写入  延迟写入只进行一次
      mutex_.Unlock();
      env_->SleepForMicroseconds(1000);//直接睡眠1000ms
      allow_delay = false;  // Do not delay a single write more than once
      mutex_.Lock();
    } else if (!force &&
               (mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
      // There is room in current memtable
      // MemTable的内存是动态扩展,
      //    当MemTable已经使用的空间达到了阈值(4M),则不再继续向当前MemTable对象追加数据(需要重新创建)  
      //    当MemTable已经使用的空间没有达到阈值(4M), 则继续使用
      break;
    } else if (imm_ != NULL) {/* 表示mem_已满 需要等待imm_持久化到磁盘 */
      // We have filled up the current memtable, but the previous
      // one is still being compacted, so we wait.
      Log(options_.info_log, "Current memtable full; waiting...\n");
      bg_cv_.Wait();
    } else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
      // There are too many level-0 files.   level-0文件数目太多需要等待压缩
      Log(options_.info_log, "Too many L0 files; waiting...\n");
      bg_cv_.Wait();
    } else {//imm_为空,mem_没有空间可写 else中没有break语句
      // Attempt to switch to a new memtable and trigger compaction of old
      assert(versions_->PrevLogNumber() == 0);
      uint64_t new_log_number = versions_->NewFileNumber();
      WritableFile* lfile = NULL;
      s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);//创建新的log文件
      if (!s.ok()) {
        // Avoid chewing through file number space in a tight loop.
        versions_->ReuseFileNumber(new_log_number);
        break;
      }
      delete log_;
      delete logfile_;
      logfile_ = lfile;
      logfile_number_ = new_log_number;
      log_ = new log::Writer(lfile);
      imm_ = mem_;
      has_imm_.Release_Store(imm_);
      mem_ = new MemTable(internal_comparator_);//创建新的MemTable对象
      mem_->Ref();
      force = false;   // Do not force another compaction if have room
      MaybeScheduleCompaction();//启动后台线程 将imm_写到level0
    }
  }
  return s;
}

说明:

1) 该函数主要6个逻辑判断,每个逻辑判断并不是很复杂,在注释中已经明确给出。

2) 最后一个 else语句没有break语句说明,while循环并没有退出,而是进入上面最后一个else if分支然后被阻塞。

3) leveldb中提倡level0层的文件不应该过多,太多影响性能,主要原因是level0层中的文件key是有重叠的,并没有按照顺序存储。所以leveldb默认level0层文件数是8,即config::kL0_SlowdownWritesTrigger。

二、启动压缩流程

2.1、创建独立线程进行压缩

启动压缩流程入口函数为MaybeScheduleCompaction,函数实现如下:

/**
 * 尝试调度压缩流程
 * 压缩场景原因:
 *    一种是某一层级的文件数过多或者文件总大小超过预定门限,
 *    另一种是level n 和level n+1重叠严重,无效seek次数太多。(level n 和level n+1的文件,key的范围可能交叉导致)
 */
void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  if (bg_compaction_scheduled_) {
    // Already scheduled
  } else if (shutting_down_.Acquire_Load()) {
    // DB is being deleted; no more background compactions
  } else if (!bg_error_.ok()) {
    // Already got an error; no more changes
  } else if (imm_ == NULL &&
             manual_compaction_ == NULL &&
             !versions_->NeedsCompaction()) {
    // No work to be done
  } else {
    bg_compaction_scheduled_ = true;
    env_->Schedule(&DBImpl::BGWork, this); //创建线程 执行压缩
  }
}

/**
 * 压缩线程线程函数 回调函数
 */
void DBImpl::BGWork(void* db) {
  reinterpret_cast<DBImpl*>(db)->BackgroundCall();
}

2.2、执行压缩

通过上一小节可知,真正执行压缩处理的方法是BackgroundCall,下面分析一下该方法内部实现。

/**
 * 压缩处理
 */
void DBImpl::BackgroundCall() {
  MutexLock l(&mutex_);
  assert(bg_compaction_scheduled_);
  if (shutting_down_.Acquire_Load()) {
    // No more background work when shutting down.
  } else if (!bg_error_.ok()) {
    // No more background work after a background error.
  } else {
    BackgroundCompaction();//压缩处理
  }

  bg_compaction_scheduled_ = false; //表示压缩完成

  // Previous compaction may have produced too many files in a level,
  // so reschedule another compaction if needed.
  MaybeScheduleCompaction(); //再次尝试压缩 因为有可能上一次压缩产生的文件比较多 所以在此进行压缩
  bg_cv_.SignalAll();
}

说明:

1) leveldb是支持多线程并发访问的,所以需要判断各种关键状态

2) BackgroundCompaction方法是用于压缩处理

3) 执行完 BackgroundCompaction方法后需要再次调用MaybeScheduleCompaction方法,主要原因是可能由于本次压缩导致某一level层中的文件过多,需要再次压缩。这里是一个递归调用,压缩到最后一层就不在压缩了

2.3、流程图

三、MemTable压缩

3.1、流程图

MemTable压缩实际是将Immutable MemTable写入到ldb文件中,具体流程图如下所示:

3.2、函数实现

/**
 * 压缩MemTable
 * 将imm_数据压缩到level0中sstable文件里
 */
void DBImpl::CompactMemTable() {
  mutex_.AssertHeld();
  assert(imm_ != NULL);

  // Save the contents of the memtable as a new Table
  VersionEdit edit;
  Version* base = versions_->current();
  base->Ref();
  Status s = WriteLevel0Table(imm_, &edit, base); //将imm_写入到文件 将version信息写到MANIFEST中
  base->Unref();

  if (s.ok() && shutting_down_.Acquire_Load()) {
    s = Status::IOError("Deleting DB during memtable compaction");
  }

  // Replace immutable memtable with the generated Table
  if (s.ok()) {
    edit.SetPrevLogNumber(0);
    edit.SetLogNumber(logfile_number_);  // Earlier logs no longer needed
    s = versions_->LogAndApply(&edit, &mutex_);//更新到Manifest文件中
  }

  if (s.ok()) {//释放Immutable Table内存
    // Commit to the new state
    imm_->Unref();
    imm_ = NULL;
    has_imm_.Release_Store(NULL);
    DeleteObsoleteFiles();//删除残余文件
  } else {
    RecordBackgroundError(s);//后台压缩线程 遇到问题 发起通知
  }
}

该函数比较简单,主要实现逻辑在于WriteLevel0Table方法,该方法是将MemTable中的数据写入到文件中,下面我们来重点分析一下该函数具体实现。

四、写入到Level0文件

 WriteLeve0Table函数主要将MemTable数据写入到文件中,下面该函数流程图:

/**
 * 将MemTable写入到level0文件中
 * @param mem   MemTable对象
 * @param edit  用于写入到MANIFEST文件 版本信息  输出参数
 * @param base  当前db version信息
 */
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
                                Version* base) {
  mutex_.AssertHeld();
  const uint64_t start_micros = env_->NowMicros();
  FileMetaData meta;// 保存level0文件 元数据
  meta.number = versions_->NewFileNumber();// 要创建新文件 获取新文件编号
  pending_outputs_.insert(meta.number);
  Iterator* iter = mem->NewIterator();
  Log(options_.info_log, "Level-0 table #%llu: started",
      (unsigned long long) meta.number);

  Status s;
  {
    mutex_.Unlock();//将memtable中数据写入到ldb文件中 并返回元数据meta
    s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
    mutex_.Lock();
  }

  Log(options_.info_log, "Level-0 table #%llu: %lld bytes %s",
      (unsigned long long) meta.number,
      (unsigned long long) meta.file_size,
      s.ToString().c_str());
  delete iter;
  pending_outputs_.erase(meta.number);


  // Note that if file_size is zero, the file has been deleted and
  // should not be added to the manifest.
  int level = 0;//默认放到level0
  if (s.ok() && meta.file_size > 0) {
    /* 获取用户数据 key 非内部internal_key */
    const Slice min_user_key = meta.smallest.user_key();
    const Slice max_user_key = meta.largest.user_key();

    /**
     * 虽然函数名字是将数据写到level0中 但是最终是否写到level0中取决于
     * PickLevelForMemTableOutput返回值  该函数会根据key选择合适的层
     * 通常会保存在level0,本次存储的最小key和最大key不在level0范围内 就有可能
     * 存储到更高层 为什么要这样做呢? 如果可以放到更高层 可以减少压缩频率
     */
    if (base != NULL) {
      level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
    }
    //保存metafiledata 到了这里就确定了文件所属层次
    edit->AddFile(level, meta.number, meta.file_size,
                  meta.smallest, meta.largest);
  }

  /* 压缩统计信息 */
  CompactionStats stats;
  stats.micros = env_->NowMicros() - start_micros;
  stats.bytes_written = meta.file_size;
  stats_[level].Add(stats);
  return s;
}

4.1、BuildTable

BuildTable方法内部就是创建ldb文件并按照一定格式将数据写入到其中。具体数据格式可参考《leveldb深度剖析-存储结构(2)》。我们了解ldb是按照Block方式组织的,一个Block默认大小是4KB,所以当数据超过4KB就会创建一个Block。这里不在展示相关代码逻辑,只需要了解存储格式就能够比较轻松理解源码。

4.2、PickLevelForMemTableOutput

从MemTable中dump出的文件一定是level0吗?答案是不一定。leveldb中的ldb文件本身没有层次概念,所有的ldb文件都一样,那么如何确定这个文件是在哪一层呢?即由函数PickLevelForMemTableOutput决定。

在上面WriteLevel0Table方法中注释已经很明确给出说明,下面来看一下PickLevelForMemTableOutput内部实现:

/**
 * 根据最小key和最大key查找所在层次
 * @param smallest_user_key 最小用户数据key
 * @param largest_user_key  最大用户数据key
 */
int Version::PickLevelForMemTableOutput(
    const Slice& smallest_user_key,
    const Slice& largest_user_key) {
  int level = 0; //默认存储到level0

  /* 如果和level0中key不重叠 则可能保存到更高层中 */
  if (!OverlapInLevel(0, &smallest_user_key, &largest_user_key)) {
    // Push to next level if there is no overlap in next level,
    // and the #bytes overlapping in the level after that are limited.
    InternalKey start(smallest_user_key, kMaxSequenceNumber, kValueTypeForSeek);
    InternalKey limit(largest_user_key, 0, static_cast<ValueType>(0));
    std::vector<FileMetaData*> overlaps;
    while (level < config::kMaxMemCompactLevel) {
      //进入if分支 表示level层没有冲突 但是level+1层有冲突 则将文件保存在level层
      if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) {
        break;
      }
      if (level + 2 < config::kNumLevels) {
        // Check that file does not overlap too many grandparent bytes.
        GetOverlappingInputs(level + 2, &start, &limit, &overlaps);
        const int64_t sum = TotalFileSize(overlaps);//overlaps 保存冲突文件元信息
        //进入if分支 表示level层和level+1层都没有没有冲突 但是level+2层有冲突
        //如果冲突文件大小超过默认值20M则保存在level层中
        if (sum > MaxGrandParentOverlapBytes(vset_->options_)) {
          break;
        }
      }
      level++;
    }
  }
  return level;
}

说明:

1) 新文件中key与level0中文件key有重叠,则将新文件设置为level0,否则进入2

2) 新文件中key与level1中文件key有重叠,则将新文件设置为level0,否则进入3

3) 新文件中key与level2中文件key有重叠并且重叠文件大小超过20M,则将新文件设置为level0,否则level加1,重新进入2

leveldb为什么要这样设计呢?我的理解如下:

1) level0中文件key可能重叠,文件越多影响性能,所以新文件尽量不放到level0中

2) 为什么压缩MemTable的level最多是2呢?其实kMaxMemCompactLevel有注释,为了提升性能如果层次越大那么open的文件就多比较也就越多

五、总结

至此,MemTable生成ldb文件流程介绍完毕,压缩流程剩下一部分就是跨层进行文件压缩处理

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据引用,你提到了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.csdn.net/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.csdn.net/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、付费专栏及课程。

余额充值