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文件流程介紹完畢,壓縮流程剩下一部分就是跨層進行文件壓縮處理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章