Leveldb源碼分析--6

5 操作Log 2

 

5.3 讀日誌

日誌讀取顯然比寫入要複雜,要檢查checksum,檢查是否有損壞等等,處理各種錯誤。

5.3.1 類層次

先來看看讀取涉及到的類圖,如圖5.3-1。

Reader主要用到了兩個接口,一個是彙報錯誤的Reporter,另一個是log文件讀取類SequentialFile。

> Reporter的接口只有一個:void Corruption(size_t bytes,const Status& status);

> SequentialFile有兩個接口:

    Status Read(size_t n, Slice* result, char* scratch);

    Status Skip(uint64_t n);

說明下,Read接口有一個*result參數傳遞結果就行了,爲何還有一個*scratch呢,這個就和Slice相關了。它的字符串指針是傳入的外部char*指針,自己並不負責內存的管理與分配。因此Read接口需要調用者提供一個字符串指針,實際存放字符串的地方。


圖5.3-1

Reader類有幾個成員變量,需要注意:

  bool eof_;   // 上次Read()返回長度< kBlockSize,暗示到了文件結尾EOF

  uint64_t last_record_offset_; // 函數ReadRecord返回的上一個record的偏移

  uint64_t end_of_buffer_offset_; // 當前的讀取偏移

  uint64_t const initial_offset_; // 偏移,從哪裏開始讀取第一條record

  Slice   buffer_; // 讀取的內容

5.3.2日誌讀取流程

Reader只有一個接口,那就是ReadRecord,下面來分析下這個函數。

S1 根據initial offset跳轉到調用者指定的位置,開始讀取日誌文件。跳轉就是直接調用SequentialFile的Seek接口。

另外,需要先調整調用者傳入的initialoffset參數,調整和跳轉邏輯在SkipToInitialBlock函數中。

  if (last_record_offset_ <initial_offset_) { // 當前偏移 < 指定的偏移,需要Seek

     if (!SkipToInitialBlock()) returnfalse;

  }

下面的代碼是SkipToInitialBlock函數調整read offset的邏輯:

  // 計算在block內的偏移位置,並圓整到開始讀取block的起始位置

  size_t offset_in_block =initial_offset_ % kBlockSize;

  uint64_t block_start_location =initial_offset_ - offset_in_block;

  // 如果偏移在最後的6byte裏,肯定不是一條完整的記錄,跳到下一個block

  if (offset_in_block >kBlockSize - 6) {

    offset_in_block = 0;

    block_start_location +=kBlockSize;

  }

  end_of_buffer_offset_ =block_start_location; // 設置讀取偏移

  if (block_start_location > 0)  file_->Skip(block_start_location); // 跳轉

首先計算出在block內的偏移位置,然後圓整到要讀取block的起始位置。開始讀取日誌的時候都要保證讀取的是完整的block,這就是調整的目的。

同時成員變量end_of_buffer_offset_記錄了這個值,在後續讀取中會用到。

S2在開始while循環前首先初始化幾個標記:

// 當前是否在fragment內,也就是遇到了FIRST 類型的record

bool in_fragmented_record = false;

uint64_t prospective_record_offset = 0; // 我們正在讀取的邏輯record的偏移

S3 進入到while(true)循環,直到讀取到KLastType或者KFullType的record,或者到了文件結尾。從日誌文件讀取完整的record是ReadPhysicalRecord函數完成的。

讀取出現錯誤時,並不會退出循環,而是彙報錯誤,繼續執行,直到成功讀取一條user record,或者遇到文件結尾。

S3.1 從文件讀取record

uint64_t physical_record_offset = end_of_buffer_offset_ -buffer_.size();

const unsigned int record_type = ReadPhysicalRecord(&fragment);

physical_record_offset存儲的是當前正在讀取的record的偏移值。接下來根據不同的record_type類型,分別處理,一共有7種情況:

S3.2 FULL type(kFullType),表明是一條完整的log record,成功返回讀取的user record數據。另外需要對早期版本做些work around,早期的Leveldb會在block的結尾生產一條空的kFirstType log record。

        if (in_fragmented_record) {

          if (scratch->empty())in_fragmented_record = false;

          else ReportCorruption(scratch->size(),"partial record without end(1)");

        }

        prospective_record_offset= physical_record_offset;

        scratch->clear(); // 清空scratch,讀取成功不需要返回scratch數據

        *record = fragment;

        last_record_offset_ =prospective_record_offset; // 更新last record offset

        return true;

S3.3 FIRST type(kFirstType),表明是一系列logrecord(fragment)的第一個record。同樣需要對早期版本做work around。

把數據讀取到scratch中,直到成功讀取了LAST類型的log record,才把數據返回到result中,繼續下次的讀取循環。

如果再次遇到FIRSTor FULL類型的log record,如果scratch不爲空,就說明日誌文件有錯誤。

       if (in_fragmented_record) {

          if (scratch->empty())in_fragmented_record = false;

          else ReportCorruption(scratch->size(),"partial record without end(2)");

        }

        prospective_record_offset =physical_record_offset;

        scratch->assign(fragment.data(), fragment.size()); //賦值給scratch

        in_fragmented_record =true; // 設置fragment標記爲true

S3.4 MIDDLE type(kMiddleType),這個處理很簡單,如果不是在fragment中,報告錯誤,否則直接append到scratch中就可以了。

        if (!in_fragmented_record){

             ReportCorruption(fragment.size(),"missing start of fragmentedrecord(1)");

        }else {scratch->append(fragment.data(),fragment.size());}

S3.5 LAST type(kLastType),說明是一系列log record(fragment)中的最後一條。如果不在fragment中,報告錯誤。

        if (!in_fragmented_record) {

               ReportCorruption(fragment.size(),"missing start of fragmentedrecord(2)");

        } else {

              scratch->append(fragment.data(), fragment.size());

              *record = Slice(*scratch);

              last_record_offset_ =prospective_record_offset;

              return true;

        }

至此,4種正常的log record type已經處理完成,下面3種情況是其它的錯誤處理,類型聲明在Logger類中:

  enum {

     kEof = kMaxRecordType + 1, // 遇到文件結尾

     // 非法的record,當前有3中情況會返回bad record

     // * CRC校驗失敗 (ReadPhysicalRecord reports adrop)

     // * 長度爲0 (No drop is reported)

     // * 在指定的initial_offset之外 (No drop is reported)

    kBadRecord = kMaxRecordType +2

  };

S3.6 遇到文件結尾kEof,返回false。不返回任何結果。

       if (in_fragmented_record) {

           ReportCorruption(scratch->size(), "partial record withoutend(3)");

           scratch->clear();

        }

        return false;

S3.7 非法的record(kBadRecord),如果在fragment中,則報告錯誤。

        if (in_fragmented_record){

           ReportCorruption(scratch->size(), "error in middle ofrecord");

           in_fragmented_record = false;

           scratch->clear();

        }

S3.8 缺省分支,遇到非法的record 類型,報告錯誤,清空scratch。

        ReportCorruption(…, "unknownrecord type %u", record_type);

        in_fragmented_record = false; // 重置fragment標記

        scratch->clear();// 清空scratch

上面就是ReadRecord的全部邏輯,解釋起來還有些費力。

5.3.3 從log文件讀取record

就是前面講過的ReadPhysicalRecord函數,它調用SequentialFile的Read接口,從文件讀取數據。

該函數開始就進入了一個while(true)循環,其目的是爲了讀取到一個完整的record。讀取的內容存放在成員變量buffer_中。這樣的邏輯有些奇怪,實際上,完全不需要一個while(true)循環的。

函數基本邏輯如下:

S1 如果buffer_小於block header大小kHeaderSize,進入如下的幾個分支:

S1.1 如果eof_爲false,表明還沒有到文件結尾,清空buffer,並讀取數據。

        buffer_.clear(); // 因爲上次肯定讀取了一個完整的record

        Status status =file_->Read(kBlockSize, &buffer_, backing_store_);

        end_of_buffer_offset_ +=buffer_.size(); // 更新buffer讀取偏移值

         if (!status.ok()) { // 讀取失敗,設置eof_true,報告錯誤並返回kEof

            buffer_.clear();

            ReportDrop(kBlockSize,status);

            eof_ = true;

            return kEof;

        } else if (buffer_.size()< kBlockSize){

            eof_ = true; // 實際讀取字節<指定(Block Size),表明到了文件結尾

        }

        continue; // 繼續下次循環

S1.2 如果eof_爲true並且buffer爲空,表明已經到了文件結尾,正常結束,返回kEof。

S1.3 否則,也就是eof_爲true,buffer不爲空,說明文件結尾包含了一個不完整的record,報告錯誤,返回kEof。

        size_t drop_size =buffer_.size();

        buffer_.clear();

        ReportCorruption(drop_size,"truncated record at end of file");

        return kEof;

S2 進入到這裏表明上次循環中的Read讀取到了一個完整的log record,continue後的第二次循環判斷buffer_.size() >= kHeaderSize將執行到此處。

解析出log record的header部分,判斷長度是否一致。

根據log的格式,前4byte是crc32。後面就是length和type,解析如下:

    const char* header = buffer_.data();

    const uint32_t length = ((header[4])& 0xff) | ((header[5]&0xff)<<8)

    const uint32_t type = header[6];

    if (kHeaderSize + length >buffer_.size()) { // 長度超出了,彙報錯誤

      size_t drop_size =buffer_.size();

      buffer_.clear();

      ReportCorruption(drop_size,"bad record length");

      return kBadRecord; // 返回kBadRecord

    }

    if (type == kZeroType&& length == 0) { // 對於Zero Type類型,不彙報錯誤

      buffer_.clear();

      return kBadRecord; // 依然返回kBadRecord

    }

S3 校驗CRC32,如果校驗出錯,則彙報錯誤,並返回kBadRecord。

S4 如果record的開始位置在initial offset之前,則跳過,並返回kBadRecord,否則返回record數據和type。

    buffer_.remove_prefix(kHeaderSize+ length);

    if (end_of_buffer_offset_ -buffer_.size() - kHeaderSize - length < initial_offset_) {

      result->clear();

      return kBadRecord;

    }

    *result = Slice(header +kHeaderSize, length);

    return type;

從log文件讀取record的邏輯就是這樣的。至此,讀日誌的邏輯也完成了。接下來將進入磁盤存儲的sstable部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章