Leveldb源碼分析--18

11 VersionSet分析之1

Version之後就是VersionSet,它並不是Version的簡單集合,還肩負了不少的處理邏輯。這裏的分析不涉及到compaction相關的部分,這部分會單獨分析。包括log等各種編號計數器,compaction點的管理等等。

11.1 VersionSet接口

1 首先是構造函數,VersionSet會使用到TableCache,這個是調用者傳入的。TableCache用於Get k/v操作。
VersionSet(const std::string& dbname, const Options* options,
TableCache*table_cache, const InternalKeyComparator*)
VersionSet的構造函數很簡單,除了根據參數初始化,還有兩個地方值得注意:
N1 next_file_number_從2開始;
N2 創建新的Version並加入到Version鏈表中,並設置CURRENT=新創建version;
其它的數字初始化爲0,指針初始化爲NULL。
2 恢複函數,從磁盤恢復最後保存的元信息
Status Recover();
3 標記指定的文件編號已經被使用了
void MarkFileNumberUsed(uint64_t number)
邏輯很簡單,就是根據編號更新文件編號計數器:
if (next_file_number_ <= number) next_file_number_ = number + 1;
4 在current version上應用指定的VersionEdit,生成新的MANIFEST信息,保存到磁盤上,並用作current version。
要求:沒有其它線程併發調用;要用於mu;
Status LogAndApply(VersionEdit* edit, port::Mutex* mu)EXCLUSIVE_LOCKS_REQUIRED(mu)
5 對於@v中的@key,返回db中的大概位置
uint64_t ApproximateOffsetOf(Version* v, const InternalKey& key);
6 其它一些簡單接口,信息獲取或者設置,如下:
Version* current() const { return current_; } //返回current version
// 當前的MANIFEST文件號
uint64_t ManifestFileNumber() const { return manifest_file_number_; }
uint64_t NewFileNumber() { return next_file_number_++; } // 分配並返回新的文件編號
uint64_t LogNumber() const { return log_number_; } // 返回當前log文件編號
// 返回正在compact的log文件編號,如果沒有返回0
uint64_t PrevLogNumber() const { return prev_log_number_; }
// 獲取、設置last sequence,set時不能後退
uint64_t LastSequence() const { return last_sequence_; }
void SetLastSequence(uint64_t s) {
    assert(s >=last_sequence_);
    last_sequence_ = s;
}
// 返回指定level中所有sstable文件大小的和
int64_t NumLevelBytes(int level) const;
// 返回指定level的文件個數
int NumLevelFiles(int level) const;
// 重用@file_number,限制很嚴格:@file_number必須是最後分配的那個
// 要求: @file_number是NewFileNumber()返回的.
void ReuseFileNumber(uint64_t file_number) {
  if (next_file_number_ ==file_number + 1) next_file_number_ = file_number;
}
// 對於所有level>0,遍歷文件,找到和下一層文件的重疊數據的最大值(in bytes)
// 這個就是Version:: GetOverlappingInputs()函數的簡單應用
int64_t MaxNextLevelOverlappingBytes();
// 獲取函數,把所有version的所有level的文件加入到@live中
void AddLiveFiles(std::set<uint64_t>* live)
// 返回一個可讀的單行信息——每個level的文件數,保存在*scratch中
struct LevelSummaryStorage {char buffer[100]; };
const char* LevelSummary(LevelSummaryStorage* scratch) const;

下面就來分析這兩個接口Recover、LogAndApply以及ApproximateOffsetOf。

11.2 VersionSet::Builder類

Builder是一個內部輔助類,其主要作用是:
1 把一個MANIFEST記錄的元信息應用到版本管理器VersionSet中;
2 把當前的版本狀態設置到一個Version對象中。

11.2.1 成員與構造

Builder的vset_與base_都是調用者傳入的,此外它還爲FileMetaData定義了一個比較類BySmallestKey,首先依照文件的min key,小的在前;如果min key相等則file number小的在前。

  typedefstd::set<FileMetaData*, BySmallestKey> FileSet;
  struct LevelState { // 這個是記錄添加和刪除的文件
    std::set<uint64_t>deleted_files;
    FileSet* added_files; // 保證添加文件的順序是有效定義的
  };
  VersionSet* vset_;
  Version* base_;
  LevelStatelevels_[config::kNumLevels];
// 其接口有3個:
  void Apply(VersionEdit* edit)
  void SaveTo(Version* v)
  void MaybeAddFile(Version* v, int level, FileMetaData* f)
構造函數執行簡單的初始化操作,在析構時,遍歷檢查LevelState::added_files,如果文件引用計數爲0,則刪除文件。

11.2.2 Apply()

函數聲明:voidApply(VersionEdit* edit),該函數將edit中的修改應用到當前狀態中。注意除了compaction點直接修改了vset_,其它刪除和新加文件的變動只是先存儲在Builder自己的成員變量中,在調用SaveTo(v)函數時才施加到v上。
S1 把edit記錄的compaction點應用到當前狀態
edit->compact_pointers_ => vset_->compact_pointer_
S2 把edit記錄的已刪除文件應用到當前狀態
edit->deleted_files_ => levels_[level].deleted_files
S3把edit記錄的新加文件應用到當前狀態,這裏會初始化文件的allowed_seeks值,以在文件被無謂seek指定次數後自動執行compaction,這裏作者闡述了其設置規則。
   for (size_t i = 0; i <edit->new_files_.size(); i++) {
      const int level =edit->new_files_[i].first;
      FileMetaData* f = newFileMetaData(edit->new_files_[i].second);
      f->refs = 1;
      f->allowed_seeks = (f->file_size /16384); // 16KB-見下面
      if (f->allowed_seeks <100) f->allowed_seeks = 100;
      levels_[level].deleted_files.erase(f->number); // 以防萬一
      levels_[level].added_files->insert(f);
}
值allowed_seeks事關compaction的優化,其計算依據如下,首先假設:
>1 一次seek時間爲10ms
>2 寫入10MB數據的時間爲10ms(100MB/s)
>3 compact 1MB的數據需要執行25MB的IO
->從本層讀取1MB
->從下一層讀取10-12MB(文件的key range邊界可能是非對齊的)
->向下一層寫入10-12MB
這意味這25次seek的代價等同於compact 1MB的數據,也就是一次seek花費的時間大約相當於compact 40KB的數據。基於保守的角度考慮,對於每16KB的數據,我們允許它在觸發compaction之前能做一次seek。

11.2.3 MaybeAddFile()

函數聲明:voidMaybeAddFile(Version* v, int level, FileMetaData* f)
該函數嘗試將f加入到levels_[level]文件set中。
要滿足兩個條件:
>1 文件不能被刪除,也就是不能在levels_[level].deleted_files集合中;
>2 保證文件之間的key是連續的,即基於比較器vset_->icmp_,f的min key要大於levels_[level]集合中最後一個文件的max key;

11.2.4 SaveTo()

把當前的狀態存儲到v中返回,函數聲明:void SaveTo(Version* v)
函數邏輯:For循環遍歷所有的level[0, config::kNumLevels-1],把新加的文件和已存在的文件merge在一起,丟棄已刪除的文件,結果保存在v中。對於level> 0,還要確保集合中的文件沒有重合。
S1 merge流程

  conststd::vector<FileMetaData*>& base_files = base_->files_[level]; // 原文件集合
  std::vector<FileMetaData*>::const_iterator base_iter =base_files.begin();
  std::vector<FileMetaData*>::const_iterator base_end =base_files.end();
   const FileSet* added =levels_[level].added_files;
   v->files_[level].reserve(base_files.size()+ added->size());
   for (FileSet::const_iteratoradded_iter = added->begin(); added_iter !=added->end(); ++added_iter) {
      //加入base_中小於added_iter的那些文件
      for(std::vector<FileMetaData*>::const_iterator bpos = std::upper_bound(base_iter,base_end, *added_iter, cmp);
           base_iter != bpos;++base_iter) { // base_iter逐次向後移到
        MaybeAddFile(v, level,*base_iter);
      }
      MaybeAddFile(v, level,*added_iter); // 加入added_iter
   }
   // 添加base_剩餘的那些文件
   for (; base_iter != base_end;++base_iter) MaybeAddFile(v, level, *base_iter);
對象cmp就是前面定義的比較仿函數BySmallestKey對象。
S2 檢查流程,保證level>0的文件集合無重疊,基於vset_->icmp_,確保文件i-1的max key < 文件i的min key。

11.3 Recover()

對於VersionSet而言,Recover就是根據CURRENT指定的MANIFEST,讀取db元信息。這是9.3介紹的Recovery流程的開始部分。

11.3.1 函數流程

下面就來分析其具體邏輯。

S1 讀取CURRENT文件,獲得最新的MANIFEST文件名,根據文件名打開MANIFEST文件。CURRENT文件以\n結尾,讀取後需要trim下。

std::string current; // MANIFEST文件名
ReadFileToString(env_, CurrentFileName(dbname_), ¤t)
std::string dscname = dbname_ + "/" + current;
SequentialFile* file;
env_->NewSequentialFile(dscname, &file);

S2 讀取MANIFEST內容,MANIFEST是以log的方式寫入的,因此這裏調用的是log::Reader來讀取。然後調用VersionEdit::DecodeFrom,從內容解析出VersionEdit對象,並將VersionEdit記錄的改動應用到versionset中。讀取MANIFEST中的log number, prev log number, nextfile number, last sequence。

Builder builder(this, current_);
while (reader.ReadRecord(&record, &scratch) && s.ok()) {
      VersionEdit edit;
      s = edit.DecodeFrom(record);
      if (s.ok())builder.Apply(&edit);
      if (edit.has_log_number_) { // log number, file number, …逐個判斷
        log_number =edit.log_number_;
        have_log_number = true;
      }
      … …
}
S3 將讀取到的log number, prev log number標記爲已使用。
MarkFileNumberUsed(prev_log_number);
MarkFileNumberUsed(log_number);
S4 最後,如果一切順利就創建新的Version,並應用讀取的幾個number。
  if (s.ok()) {
    Version* v = newVersion(this);
    builder.SaveTo(v);
    // 安裝恢復的version
    Finalize(v);
    AppendVersion(v);
    manifest_file_number_ =next_file;
    next_file_number_ = next_file+ 1;
    last_sequence_ = last_sequence;
    log_number_ = log_number;
    prev_log_number_ =prev_log_number;
  }
Finalize(v)和AppendVersion(v)用來安裝並使用version v,在AppendVersion函數中會將current version設置爲v。下面就來分別分析這兩個函數。

11.3.2 Finalize()

函數聲明:void Finalize(Version*v);
該函數依照規則爲下次的compaction計算出最適用的level,對於level 0和>0需要分別對待,邏輯如下。
S1 對於level 0以文件個數計算,kL0_CompactionTrigger默認配置爲4。
score =v->files_[level].size()/static_cast<double>(config::kL0_CompactionTrigger);
S2 對於level>0,根據level內的文件總大小計算
const uint64_t level_bytes = TotalFileSize(v->files_[level]);
score = static_cast<double>(level_bytes) /MaxBytesForLevel(level);
S3 最後把計算結果保存到v的兩個成員compaction_level_和compaction_score_中。
其中函數MaxBytesForLevel根據level返回其本層文件總大小的預定最大值。
計算規則爲:1048576.0* level^10。
這裏就有一個問題,爲何level0和其它level計算方法不同,原因如下,這也是leveldb爲compaction所做的另一個優化。
>1 對於較大的寫緩存(write-buffer),做太多的level 0 compaction並不好
>2 每次read操作都要merge level 0的所有文件,因此我們不希望level 0有太多的小文件存在(比如寫緩存太小,或者壓縮比較高,或者覆蓋/刪除較多導致小文件太多)。
看起來這裏的寫緩存應該就是配置的操作log大小。

11.3.3 AppendVersion()

函數聲明:void AppendVersion(Version*v)
把v加入到versionset中,並設置爲current version。並對老的current version執行Uref()。
在雙向循環鏈表中的位置在dummy_versions_之前。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章