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檔案編號,如果沒有返回0uint64_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_之前。