12 DB的打開
先分析LevelDB是如何打開db的,萬物始於創建。在打開流程中有幾個輔助函數:DBImpl(),DBImpl::Recover, DBImpl::DeleteObsoleteFiles, DBImpl::RecoverLogFile, DBImpl::MaybeScheduleCompaction。
12.1 DB::Open()
函數聲明爲:
Status DB::Open(const Options& options, const std::string&dbname, DB** dbptr);
分解來看,Open()函數主要有以下5個執行步驟。
S1 創建DBImpl對象,其後進入DBImpl::Recover()函數執行S2和S3。
S2 從已存在的db文件恢復db數據,根據CURRENT記錄的MANIFEST文件讀取db元信息;這通過調用VersionSet::Recover()完成。
S3 然後過濾出那些最近的更新log,前一個版本可能新加了這些log,但並沒有記錄在MANIFEST中。然後依次根據時間順序,調用DBImpl::RecoverLogFile()從舊到新回放這些操作log。回放log時可能會修改db元信息,比如dump了新的level 0文件,因此它將返回一個VersionEdit對象,記錄db元信息的變動。
S4 如果DBImpl::Recover()返回成功,就執行VersionSet::LogAndApply()應用VersionEdit,並保存當前的DB信息到新的MANIFEST文件中。
S5 最後刪除一些過期文件,並檢查是否需要執行compaction,如果需要,就啓動後臺線程執行。
下面就來具體分析Open函數的代碼,在Open函數中涉及到上面的3個流程。
S1 首先創建DBImpl對象,鎖定並試圖做Recover操作。Recover操作用來處理創建flag,比如存在就返回失敗等等,嘗試從已存在的sstable文件恢復db。並返回db元信息的變動信息,一個VersionEdit對象。
DBImpl* impl = newDBImpl(options, dbname);
impl->mutex_.Lock(); // 鎖db
VersionEdit edit;
Status s =impl->Recover(&edit); // 處理flag&恢復:create_if_missing,error_if_exists
S2 如果Recover返回成功,則調用VersionSet取得新的log文件編號——實際上是在當前基礎上+1,準備新的log文件。如果log文件創建成功,則根據log文件創建log::Writer。然後執行VersionSet::LogAndApply,根據edit記錄的增量變動生成新的current version,並寫入MANIFEST文件。函數NewFileNumber(){returnnext_file_number_++;},直接返回next_file_number_。
uint64_t new_log_number =impl->versions_->NewFileNumber();
WritableFile* lfile;
s =options.env->NewWritableFile(LogFileName(dbname, new_log_number),&lfile);
if (s.ok()) {
edit.SetLogNumber(new_log_number);
impl->logfile_ = lfile;
impl->logfile_number_ =new_log_number;
impl->log_ = newlog::Writer(lfile);
s =impl->versions_->LogAndApply(&edit, &impl->mutex_);
}
S3 如果VersionSet::LogAndApply返回成功,則刪除過期文件,檢查是否需要執行compaction,最終返回創建的DBImpl對象。
if (s.ok()) {
impl->DeleteObsoleteFiles();
impl->MaybeScheduleCompaction();
}
impl->mutex_.Unlock();
if (s.ok()) *dbptr = impl;
return s;
以上就是DB::Open的主題邏輯。12.2 DBImpl::DBImpl()
首先是初始化列表中,直接根據參數賦值,或者直接初始化。Comparator和filter policy都是參數傳入的。在傳遞option時會首先將option中的參數合法化,logfile_number_初始化爲0,指針初始化爲NULL。
創建MemTable,並增加引用計數,創建WriteBatch。
mem_(newMemTable(internal_comparator_)),
tmp_batch_(new WriteBatch),
mem_->Ref();
// 然後在函數體中,創建TableCache和VersionSet。
// 爲其他預留10個文件,其餘的都給TableCache.
const int table_cache_size =options.max_open_files - 10;
table_cache_ = newTableCache(dbname_, &options_, table_cache_size);
versions_ = newVersionSet(dbname_, &options_, table_cache_, &internal_comparator_);
12.3 DBImp::NewDB()
當外部在調用DB::Open()時設置了option指定如果db不存在就創建,如果db不存在leveldb就會調用函數創建新的db。判斷db是否存在的依據是<db name>/CURRENT文件是否存在。其邏輯很簡單。
// S1首先生產DB元信息,設置comparator名,以及log文件編號、文件編號,以及seq no。
VersionEdit new_db;
new_db.SetComparatorName(user_comparator()->Name());
new_db.SetLogNumber(0);
new_db.SetNextFile(2);
new_db.SetLastSequence(0);
// S2 生產MANIFEST文件,將db元信息寫入MANIFEST文件。
const std::string manifest =DescriptorFileName(dbname_, 1);
WritableFile* file;
Status s =env_->NewWritableFile(manifest, &file);
if (!s.ok()) return s;
{
log::Writer log(file);
std::string record;
new_db.EncodeTo(&record);
s = log.AddRecord(record);
if (s.ok()) s =file->Close();
}
delete file;
// S3 如果成功,就把MANIFEST文件名寫入到CURRENT文件中
if (s.ok()) s =SetCurrentFile(env_, dbname_, 1);
elseenv_->DeleteFile(manifest);
return s;
這就是創建新DB的邏輯,很簡單。12.4 DBImpl::Recover()
S1 創建目錄,目錄以db name命名,忽略任何創建錯誤,然後嘗試獲取db name/LOCK文件鎖,失敗則返回。
env_->CreateDir(dbname_);
Status s =env_->LockFile(LockFileName(dbname_), &db_lock_);
if (!s.ok()) return s;
S2 根據CURRENT文件是否存在,以及option參數執行檢查。
如果文件不存在&create_is_missing=true,則調用函數NewDB()創建;否則報錯。
如果文件存在& error_if_exists=true,則報錯。
S3 調用VersionSet的Recover()函數,就是從文件中恢復數據。如果出錯則打開失敗,成功則向下執行S4。
s = versions_->Recover();
S4嘗試從所有比manifest文件中記錄的log要新的log文件中恢復(前一個版本可能會添加新的log文件,卻沒有記錄在manifest中)。另外,函數PrevLogNumber()已經不再用了,僅爲了兼容老版本。
// S4.1 這裏先找出所有滿足條件的log文件:比manifest文件記錄的log編號更新。
SequenceNumber max_sequence(0);
const uint64_t min_log =versions_->LogNumber();
const uint64_t prev_log =versions_->PrevLogNumber();
std::vector<std::string>filenames;
s =env_->GetChildren(dbname_, &filenames); // 列出目錄內的所有文件
uint64_t number;
FileType type;
std::vector<uint64_t>logs;
for (size_t i = 0; i <filenames.size(); i++) { // 檢查log文件是否比min log更新
if(ParseFileName(filenames[i], &number, &type) && type ==kLogFile
&& ((number >=min_log) || (number == prev_log))) {
logs.push_back(number);
}
}
// S4.2 找到log文件後,首先排序,保證按照生成順序,依次回放log。並把DB元信息的變動(sstable文件的變動)追加到edit中返回。
std::sort(logs.begin(),logs.end());
for (size_t i = 0; i <logs.size(); i++) {
s = RecoverLogFile(logs[i],edit, &max_sequence);
// 前一版可能在生成該log編號後沒有記錄在MANIFEST中,
//所以這裏我們手動更新VersionSet中的文件編號計數器
versions_->MarkFileNumberUsed(logs[i]);
}
// S4.3 更新VersionSet的sequence
if (s.ok()) {
if(versions_->LastSequence() < max_sequence)
versions_->SetLastSequence(max_sequence);
}
上面就是Recover的執行流程。12.5 DBImpl::DeleteObsoleteFiles()
其調用點包括:DBImpl::CompactMemTable,DBImpl::BackgroundCompaction, 以及DB::Open的recovery步驟之後。
它會刪除所有過期的log文件,沒有被任何level引用到、或不是正在執行的compaction的output的sstable文件。
該函數沒有參數,其代碼邏輯也很直觀,就是列出db的所有文件,對不同類型的文件分別判斷,如果是過期文件,就刪除之,如下:
// S1 首先,確保不會刪除pending文件,將versionset正在使用的所有文件加入到live中。
std::set<uint64_t> live =pending_outputs_;
versions_->AddLiveFiles(&live); //該函數其後分析
// S2 列舉db的所有文件
std::vector<std::string>filenames;
env_->GetChildren(dbname_,&filenames);
// S3 遍歷所有列舉的文件,根據文件類型,分別處理;
uint64_t number;
FileType type;
for (size_t i = 0; i <filenames.size(); i++) {
if (ParseFileName(filenames[i], &number,&type)) {
bool keep = true; //false表明是過期文件
// S3.1 kLogFile,log文件,根據log編號判斷是否過期
keep = ((number >=versions_->LogNumber()) ||
(number ==versions_->PrevLogNumber()));
// S3.2 kDescriptorFile,MANIFEST文件,根據versionset記錄的編號判斷
keep = (number >=versions_->ManifestFileNumber());
// S3.3 kTableFile,sstable文件,只要在live中就不能刪除
// S3.4 kTempFile,如果是正在寫的文件,只要在live中就不能刪除
keep = (live.find(number) != live.end());
// S3.5 kCurrentFile,kDBLockFile, kInfoLogFile,不能刪除
keep = true;
// S3.6 如果keep爲false,表明需要刪除文件,如果是table還要從cache中刪除
if (!keep) {
if(type == kTableFile) table_cache_->Evict(number);
Log(options_.info_log, "Delete type=%d #%lld\n",type, number);
env_->DeleteFile(dbname_ + "/" +filenames[i]);
}
}
}
這就是刪除過期文件的邏輯,其中調用到了VersionSet::AddLiveFiles函數,保證不會刪除active的文件。
函數DbImpl::MaybeScheduleCompaction()放在Compaction一節分析,基本邏輯就是如果需要compaction,就啓動後臺線程執行compaction操作。12.6 DBImpl::RecoverLogFile()
參數說明:
@log_number是指定的log文件編號
@edit記錄db元信息的變化——sstable文件變動
@max_sequence 返回max{log記錄的最大序號, *max_sequence}
該函數打開指定的log文件,回放日誌。期間可能會執行compaction,生產新的level 0sstable文件,記錄文件變動到edit中。
它聲明瞭一個局部類LogReporter以打印錯誤日誌,沒什麼好說的,下面來看代碼邏輯。
// S1 打開log文件返回SequentialFile*file,出錯就返回,否則向下執行S2。
// S2 根據log文件句柄file創建log::Reader,準備讀取log。
log::Reader reader(file,&reporter, true/*checksum*/,0/*initial_offset*/);
// S3 依次讀取所有的log記錄,並插入到新生成的memtable中。這裏使用到了批量更新接口WriteBatch,具體後面再分析。
std::string scratch;
Slice record;
WriteBatch batch;
MemTable* mem = NULL;
while(reader.ReadRecord(&record, &scratch) && status.ok()) { // 讀取全部log
if (record.size() < 12) { // log數據錯誤,不滿足最小長度12
reporter.Corruption(record.size(), Status::Corruption("log recordtoo small"));
continue;
}
WriteBatchInternal::SetContents(&batch, record); // log內容設置到WriteBatch中
if (mem == NULL) { // 創建memtable
mem = new MemTable(internal_comparator_);
mem->Ref();
}
status =WriteBatchInternal::InsertInto(&batch, mem); // 插入到memtable中
MaybeIgnoreError(&status);
if (!status.ok()) break;
const SequenceNumber last_seq=
WriteBatchInternal::Sequence(&batch) + WriteBatchInternal::Count(&batch)- 1;
if (last_seq >*max_sequence) *max_sequence = last_seq; // 更新max sequence
// 如果mem的內存超過設置值,則執行compaction,如果compaction出錯,
// 立刻返回錯誤,DB::Open失敗
if(mem->ApproximateMemoryUsage() > options_.write_buffer_size) {
status =WriteLevel0Table(mem, edit, NULL);
if (!status.ok()) break;
mem->Unref(); // 釋放當前memtable
mem = NULL;
}
}
// S4 掃尾工作,如果mem != NULL,說明還需要dump到新的sstable文件中。
if (status.ok() && mem!= NULL) {// 如果compaction出錯,立刻返回錯誤
status = WriteLevel0Table(mem,edit, NULL);
}
if (mem != NULL)mem->Unref();
delete file;
return status;
把MemTabledump到sstable是函數WriteLevel0Table的工作,其實這是compaction的一部分,準備放在compaction一節來分析。12.7 小結
如上DB打開的邏輯就已經分析完了,打開邏輯參見DB::Open()中描述的5個步驟。此外還有兩個東東:把Memtable dump到sstable的WriteLevel0Table()函數,以及批量修改WriteBatch。第一個放在後面的compaction一節,第二個放在DB更新操作中。接下來就是db的關閉。
13 DB的關閉&銷燬
13.1 DB關閉
析構依次執行如下的5個邏輯:
S1 等待後臺compaction任務結束
S2 釋放db文件鎖,<dbname>/lock文件
S3 刪除VersionSet對象,並釋放MemTable對象
S4 刪除log相關以及TableCache對象
S5 刪除options的block_cache以及info_log對象
13.2 DB銷燬
該函數會刪除掉db的數據內容,要謹慎使用。函數邏輯爲:
S1 獲取dbname目錄的文件列表到filenames中,如果爲空則直接返回,否則進入S2。
S2 鎖文件<dbname>/lock,如果鎖成功就執行S3
S3 遍歷filenames文件列表,過濾掉lock文件,依次調用DeleteFile刪除。
S4 釋放lock文件,並刪除之,然後刪除文件夾。
Destory就執行完了,如果刪除文件出現錯誤,記錄之,依然繼續刪除下一個。最後返回錯誤代碼。
看來這一章很短小。DB的打開關閉分析完畢。