8. LevelDB源碼剖析之日志文件

8.1 基本原理

"LOG文件在LevelDb中的主要作用是系統(tǒng)故障恢復(fù)時(shí),能夠保證不會(huì)丟失數(shù)據(jù)。因?yàn)樵趯⒂涗泴懭雰?nèi)存的Memtable之前,會(huì)先寫入Log文件,這樣即使系統(tǒng)發(fā)生故障,Memtable中的數(shù)據(jù)沒(méi)有來(lái)得及Dump到磁盤的SSTable文件,LevelDB也可以根據(jù)log文件恢復(fù)內(nèi)存的Memtable數(shù)據(jù)結(jié)構(gòu)內(nèi)容,不會(huì)造成系統(tǒng)丟失數(shù)據(jù),在這點(diǎn)上LevelDb和Bigtable是一致的。" --- 數(shù)據(jù)分析與處理之二(Leveldb 實(shí)現(xiàn)原理)

8.2. 日志文件

8.2.1 數(shù)據(jù)結(jié)構(gòu)

日志中的每條記錄由Record Header + Record Content組成,其中Header大小為kHeaderSize(7字節(jié)),由CRC(4字節(jié)) + Size(2字節(jié)) + Type(1字節(jié))三部分組成。除此之外才是content的真正內(nèi)容:

日志記錄

日志文件的基礎(chǔ)部件很簡(jiǎn)單,只需要能夠創(chuàng)建文件、追加操作、實(shí)時(shí)刷新數(shù)據(jù)即可。為了做到跨平臺(tái)、解耦,LevelDB還是對(duì)此做了封裝。Leveldb命名空間下,有一個(gè)名為log的子命名空間,其下有Writer、Reader兩個(gè)實(shí)現(xiàn)類。按前幾節(jié)的命名規(guī)則,Writer其實(shí)是一個(gè)Builder,它對(duì)外提供了唯一的AddRecord方法用于追加操作記錄。

8.2.2 Writer

在log命名空間中,包含一個(gè)Writer用于日志操作,其只有一個(gè)Append方法,這和日志的定位相同,定義如下:

class Writer
{
  explicit Writer(WritableFile *dest);
  Writer(WritableFile *dest, uint64_t dest_length);
  ...
  Status AddRecord(const Slice &slice);
  ...
};

外部創(chuàng)建一個(gè)WritableFile,通過(guò)構(gòu)造函數(shù)傳遞給Writer。AddRecord按上述的結(jié)構(gòu)完善record并添加到日志文件中。

Status Writer::AddRecord(const Slice& slice) {
           const char* ptr = slice.data();
           size_t left = slice.size();

           // Fragment the record if necessary and emit it.  Note that if slice
           // is empty, we still want to iterate once to emit a single
           // zero-length record
           Status s;
           bool begin = true;
           do {
               //1. 當(dāng)前塊剩余大小
               const int leftover = kBlockSize - block_offset_;    
               assert(leftover >= 0);
               //2. 剩余大小不足,占位
               if (leftover < kHeaderSize)                        
               {
                   // Switch to a new block
                   if (leftover > 0) 
                   {
                       // Fill the trailer (literal below relies on kHeaderSize being 7)
                       assert(kHeaderSize == 7);
                       dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
                   }
                   block_offset_ = 0;
               }

               // Invariant: we never leave < kHeaderSize bytes in a block.
               assert(kBlockSize - block_offset_ - kHeaderSize >= 0);

               const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
               //3. 當(dāng)前塊存儲(chǔ)的空間大小
               const size_t fragment_length = (left < avail) ? left : avail;    

               //4. Record Type
               RecordType type;                                                
               const bool end = (left == fragment_length);                        
               if (begin && end) {
                   type = kFullType;
               }
               else if (begin) {
                   type = kFirstType;
               }
               else if (end) {
                   type = kLastType;
               }
               else {
                   type = kMiddleType;
               }
               //5. 寫入文件
               s = EmitPhysicalRecord(type, ptr, fragment_length);            
               ptr += fragment_length;
               left -= fragment_length;
               begin = false;
           } while (s.ok() && left > 0);
           return s;
       }
  • 當(dāng)前Block剩余大小不足以填充Record Header時(shí),以"\x00\x00\x00\x00\x00\x00"占位。
  • 當(dāng)Block無(wú)法完整記錄一條Record時(shí),通過(guò)type信息標(biāo)識(shí)該record在當(dāng)前block中的區(qū)塊信息,以便讀取時(shí)可根據(jù)type拼接出完整的record。
  • EmitPhysicalRecord向Block中插入Record數(shù)據(jù),每條記錄append之后會(huì)執(zhí)行一次flush。

8.3 創(chuàng)建日志的時(shí)機(jī)

在LevelDB中,日志文件和memtable是配對(duì)的,在任何數(shù)據(jù)寫入Memtable之前都會(huì)先寫入日志文件。除此之外,日志文件別無(wú)它用。

因此,日志文件的創(chuàng)建時(shí)和Memtable的創(chuàng)建時(shí)機(jī)也必然一致,這點(diǎn)對(duì)于我們理解日志文件至關(guān)重要。那么,Memtable在何時(shí)會(huì)創(chuàng)建呢?

8.3.1 數(shù)據(jù)庫(kù)啟動(dòng)

如果我們創(chuàng)建了一個(gè)新數(shù)據(jù)庫(kù),或者數(shù)據(jù)庫(kù)上次運(yùn)行的所有日志都已經(jīng)歸檔到Level0狀態(tài)。此時(shí),需要為本次數(shù)據(jù)庫(kù)進(jìn)程創(chuàng)建新的Memtable以及日志文件,代碼邏輯如下:

Status DB::Open(const Options &options, const std::string &dbname,
                DB **dbptr)
{
  *dbptr = NULL;

  //創(chuàng)建新的數(shù)據(jù)庫(kù)實(shí)例
  DBImpl *impl = new DBImpl(options, dbname);
  impl->mutex_.Lock();
  VersionEdit edit;

  //恢復(fù)到上一次關(guān)閉時(shí)的狀態(tài)
  // Recover handles create_if_missing, error_if_exists
  bool save_manifest = false;
  Status s = impl->Recover(&edit, &save_manifest);
  if (s.ok() && impl->mem_ == NULL)
  {
    //創(chuàng)建新的memtable及日志文件
    // Create new log and a corresponding memtable.
    
    //分配日志文件編號(hào)及創(chuàng)建日志文件
    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;
      //文件交由log::Writer做追加操作
      impl->log_ = new log::Writer(lfile);  
      //創(chuàng)建MemTable
      impl->mem_ = new MemTable(impl->internal_comparator_);
      impl->mem_->Ref();
    }
  }
  ......
}

創(chuàng)建日志文件前,需要先給日志文件起一個(gè)名字,此處使用日志編號(hào)及數(shù)據(jù)庫(kù)名稱拼接而成,例如:

數(shù)據(jù)庫(kù)名稱為AiDb,編號(hào)為324時(shí),日志文件名稱為AiDb000324.log

8.3.2 插入數(shù)據(jù)

如果插入數(shù)據(jù)時(shí),當(dāng)前的memtable容量達(dá)到設(shè)定的options_.write_buffer_size,此時(shí)觸發(fā)新的memtable創(chuàng)建,并將之前的memtable轉(zhuǎn)為imm,同時(shí)構(gòu)建新的日志文件。

      uint64_t new_log_number = versions_->NewFileNumber();
      WritableFile *lfile = NULL;
      s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
      if (!s.ok())
      {
        // Avoid chewing through file number space in a tight loop.
        versions_->ReuseFileNumber(new_log_number);
        break;
      }
      delete log_;
      delete logfile_;
      logfile_ = lfile;
      logfile_number_ = new_log_number;

      //創(chuàng)建日志文件
      log_ = new log::Writer(lfile);
      imm_ = mem_;
      has_imm_.Release_Store(imm_);

      //創(chuàng)建memtable
      mem_ = new MemTable(internal_comparator_);
      mem_->Ref();
      force = false; // Do not force another compaction if have room
      MaybeScheduleCompaction();

8.3.3 數(shù)據(jù)庫(kù)恢復(fù)

數(shù)據(jù)庫(kù)啟動(dòng)時(shí)首先完成數(shù)據(jù)庫(kù)狀態(tài)恢復(fù),日志恢復(fù)過(guò)程中,如果為最后一個(gè)日志文件,且配置為日志重用模式(options_.reuse_logs=true)時(shí),創(chuàng)建新的日志文件。但和其他場(chǎng)景不同的是,這里的日志文件是“繼承性”的,也就是說(shuō)部分內(nèi)容是上次遺留下來(lái)的。來(lái)看實(shí)現(xiàn):

  // See if we should keep reusing the last log file.
  if (status.ok() && options_.reuse_logs && last_log && compactions == 0)
  {
    assert(logfile_ == NULL);
    assert(log_ == NULL);
    assert(mem_ == NULL);
    uint64_t lfile_size;
    if (env_->GetFileSize(fname, &lfile_size).ok() &&
        env_->NewAppendableFile(fname, &logfile_).ok())
    {
      Log(options_.info_log, "Reusing old log %s \n", fname.c_str());
      log_ = new log::Writer(logfile_, lfile_size);
      logfile_number_ = log_number;
      if (mem != NULL)
      {
        mem_ = mem;
        mem = NULL;
      }
      else
      {
        // mem can be NULL if lognum exists but was empty.
        mem_ = new MemTable(internal_comparator_);
        mem_->Ref();
      }
    }
  }

8.5 總結(jié)

日志文件本身的定位是清晰的,實(shí)現(xiàn)也不復(fù)雜。原本Current、Manifest與Log打算一起備注,但要搞清楚Manifest,LevelDB的版本機(jī)制必定要搞清楚,而這本身又是很豐富的內(nèi)容。


轉(zhuǎn)載請(qǐng)注明;【隨安居士】http://www.itdecent.cn/p/d1bb2e2ceb4c

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容