Leveldb源碼分析--20

12 DB的打開

先分析LevelDB是如何打開db的,萬物始於創建。在打開流程中有幾個輔助函數:DBImpl(),DBImpl::Recover, DBImpl::DeleteObsoleteFiles, DBImpl::RecoverLogFile, DBImpl::MaybeScheduleCompaction。

12.1 DB::Open()

打開一個db,進行PUT、GET操作,就是前面的靜態函數DB::Open的工作。如果操作成功,它就返回一個db指針。前面說過DB就是一個接口類,其具體實現在DBImp類中,這是一個DB的子類。
函數聲明爲:
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()

構造函數做的都是初始化操作,DBImpl::DBImpl(const Options& options, const std::string&dbname)
首先是初始化列表中,直接根據參數賦值,或者直接初始化。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()

函數聲明爲:StatusDBImpl::Recover(VersionEdit* edit),如果調用成功則設置VersionEdit。Recover的基本功能是:首先是處理創建flag,比如存在就返回失敗等等;然後是嘗試從已存在的sstable文件恢復db;最後如果發現有大於原信息記錄的log編號的log文件,則需要回放log,更新db數據。回放期間db可能會dump新的level 0文件,因此需要把db元信息的變動記錄到edit中返回。函數邏輯如下:
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()

這個是垃圾回收函數,如前所述,每次compaction和recovery之後都會有文件被廢棄。DeleteObsoleteFiles就是刪除這些垃圾文件的,它在每次compaction和recovery完成之後被調用。
其調用點包括: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()

函數聲明:StatusRecoverLogFile(uint64_t log_number, VersionEdit* edit,SequenceNumber* max_sequence)
參數說明:
@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關閉

外部調用者通過DB::Open()獲取一個DB*對象,如果要關閉打開的DB*db對象,則直接delete db即可,這會調用到DBImpl的析構函數。
析構依次執行如下的5個邏輯:
S1 等待後臺compaction任務結束
S2 釋放db文件鎖,<dbname>/lock文件
S3 刪除VersionSet對象,並釋放MemTable對象
S4 刪除log相關以及TableCache對象
S5 刪除options的block_cache以及info_log對象

13.2 DB銷燬

函數聲明:StatusDestroyDB(const std::string& dbname, const Options& options)
該函數會刪除掉db的數據內容,要謹慎使用。函數邏輯爲:
S1 獲取dbname目錄的文件列表到filenames中,如果爲空則直接返回,否則進入S2。
S2 鎖文件<dbname>/lock,如果鎖成功就執行S3
S3 遍歷filenames文件列表,過濾掉lock文件,依次調用DeleteFile刪除。
S4 釋放lock文件,並刪除之,然後刪除文件夾。
Destory就執行完了,如果刪除文件出現錯誤,記錄之,依然繼續刪除下一個。最後返回錯誤代碼。
看來這一章很短小。DB的打開關閉分析完畢。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章