Leveldb源碼分析–6

來源:互聯網
上載者:User
文章目錄
  • 5.3 讀日誌
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部分。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.