11 VersionSet分析之1
Version之後就是VersionSet,它並不是Version的簡單集合,還肩負了不少的處理邏輯。這裏的分析不涉及到compaction相關的部分,這部分會單獨分析。包括log等各種編號計數器,compaction點的管理等等。
11.1 VersionSet接口
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類
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()
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()
該函數嘗試將f加入到levels_[level]文件set中。
要滿足兩個條件:
>1 文件不能被刪除,也就是不能在levels_[level].deleted_files集合中;
>2 保證文件之間的key是連續的,即基於比較器vset_->icmp_,f的min key要大於levels_[level]集合中最後一個文件的max key;
11.2.4 SaveTo()
函數邏輯: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()
該函數依照規則爲下次的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()
把v加入到versionset中,並設置爲current version。並對老的current version執行Uref()。
在雙向循環鏈表中的位置在dummy_versions_之前。