Leveldb源碼分析--8

6 SSTable之2

6.4 創建sstable文件

瞭解了sstable文件的存儲格式,以及Data Block的組織,下面就可以分析如何創建sstable文件了。相關代碼在table_builder.h/.cc以及block_builder.h/.cc(構建Block)中。

6.4.1 TableBuilder類

構建sstable文件的類是TableBuilder,該類提供了幾個有限的方法可以用來添加k/v對,Flush到文件中等等,它依賴於BlockBuilder來構建Block。

TableBuilder的幾個接口說明下:

> void Add(const Slice& key, const Slice& value),向當前正在構建的表添加新的{key, value}對,要求根據Option指定的Comparator,key必須位於所有前面添加的key之後;

> void Flush(),將當前緩存的k/v全部flush到文件中,一個高級方法,大部分的client不需要直接調用該方法;

> void Finish(),結束表的構建,該方法被調用後,將不再會使用傳入的WritableFile;

> void Abandon(),結束表的構建,並丟棄當前緩存的內容,該方法被調用後,將不再會使用傳入的WritableFile;【只是設置closed爲true,無其他操作】

一旦Finish()/Abandon()方法被調用,將不能再次執行Flush或者Add操作。

下面來看看涉及到的類,如圖6.3-1所示。

圖6.3-1

其中WritableFile和op log一樣,使用的都是內存映射文件。Options是一些調用者可設置的選項。

TableBuilder只有一個成員變量Rep* rep_,實際上Rep結構體的成員就是TableBuilder所有的成員變量;這樣做的目的,可能是爲了隱藏其內部細節。Rep的定義也是在.cc文件中,對外是透明的。

簡單解釋下成員的含義:

  Options options;   // data block的選項
  Options index_block_options; // index block的選項
  WritableFile* file;  // sstable文件
  uint64_t offset; // 要寫入data block在sstable文件中的偏移,初始0
  Status status; //當前狀態-初始ok
  BlockBuilder data_block; //當前操作的data block
  BlockBuilder index_block; // sstable的index block
  std::string last_key; //當前data block最後的k/v對的key
  int64_t num_entries; //當前data block的個數,初始0
  bool closed;          //調用了Finish() or Abandon(),初始false
  FilterBlockBuilder*filter_block; //根據filter數據快速定位key是否在block中
  bool pending_index_entry; //見下面的Add函數,初始false
  BlockHandle pending_handle; //添加到index block的data block的信息
  std::string compressed_output;//壓縮後的data block,臨時存儲,寫入後即被清空

Filter block是存儲的過濾器信息,它會存儲{key, 對應data block在sstable的偏移值},不一定是完全精確的,以快速定位給定key是否在data block中。

下面分析如何向sstable中添加k/v對,創建並持久化sstable。其它函數都比較簡單,略過。另外對於Abandon,簡單設置closed=true即返回。

6.4.2 添加k/v對

這是通過方法Add(constSlice& key, const Slice& value)完成的,沒有返回值。下面分析下函數的邏輯:

S1 首先保證文件沒有close,也就是沒有調用過Finish/Abandon,以及保證當前status是ok的;如果當前有緩存的kv對,保證新加入的key是最大的。

  Rep* r = rep_;
  assert(!r->closed);
  if (!ok()) return;
  if (r->num_entries > 0) {
     assert(r->options.comparator->Compare(key, Slice(r->last_key))> 0);
  }

S2 如果標記r->pending_index_entry爲true,表明遇到下一個data block的第一個k/v,根據key調整r->last_key,這是通過Comparator的FindShortestSeparator完成的。

  if (r->pending_index_entry) {
     assert(r->data_block.empty());
     r->options.comparator->FindShortestSeparator(&r->last_key,key);
     std::string handle_encoding;
     r->pending_handle.EncodeTo(&handle_encoding);
     r->index_block.Add(r->last_key, Slice(handle_encoding));
     r->pending_index_entry =false;
  }

接下來將pending_handle加入到index block中{r->last_key, r->pending_handle’sstring}。最後將r->pending_index_entry設置爲false。

值得講講pending_index_entry這個標記的意義,見代碼註釋:

直到遇到下一個databock的第一個key時,我們才爲上一個datablock生成index entry,這樣的好處是:可以爲index使用較短的key;比如上一個data block最後一個k/v的key是"the quick brown fox",其後繼data block的第一個key是"the who",我們就可以用一個較短的字符串"the r"作爲上一個data block的index block entry的key。

簡而言之,就是在開始下一個datablock時,Leveldb纔將上一個data block加入到index block中。標記pending_index_entry就是幹這個用的,對應data block的index entry信息就保存在(BlockHandle)pending_handle。

S3 如果filter_block不爲空,就把key加入到filter_block中。

  if (r->filter_block != NULL) {
     r->filter_block->AddKey(key);
  }

S4 設置r->last_key = key,將(key, value)添加到r->data_block中,並更新entry數。

  r->last_key.assign(key.data(), key.size());
  r->num_entries++;
  r->data_block.Add(key,value);

S5 如果data block的個數超過限制,就立刻Flush到文件中。

  const size_testimated_block_size = r->data_block.CurrentSizeEstimate();
  if (estimated_block_size >=r->options.block_size) Flush();

6.4.3 Flush文件

該函數邏輯比較簡單,直接見代碼如下:

  Rep* r = rep_;
  assert(!r->closed); // 首先保證未關閉,且狀態ok
  if (!ok()) return;
  if (r->data_block.empty())return; // data block是空的
  // 保證pending_index_entry爲false,即data block的Add已經完成
  assert(!r->pending_index_entry);
  // 寫入data block,並設置其index entry信息—BlockHandle對象
  WriteBlock(&r->data_block, &r->pending_handle);
  //寫入成功,則Flush文件,並設置r->pending_index_entry爲true,
  //以根據下一個data block的first key調整index entry的key—即r->last_key
  if (ok()) {
    r->pending_index_entry =true;
    r->status =r->file->Flush();
  }
  if (r->filter_block != NULL){ //將data block在sstable中的便宜加入到filter block中
    r->filter_block->StartBlock(r->offset); // 並指明開始新的data block
  }

6.4.4 WriteBlock函數

在Flush文件時,會調用WriteBlock函數將data block寫入到文件中,該函數同時還設置data block的index entry信息。原型爲:

void WriteBlock(BlockBuilder* block, BlockHandle* handle)

該函數做些預處理工作,序列化要寫入的data block,根據需要壓縮數據,真正的寫入邏輯是在WriteRawBlock函數中。下面分析該函數的處理邏輯。

S1 獲得block的序列化數據Slice,根據配置參數決定是否壓縮,以及根據壓縮格式壓縮數據內容。對於Snappy壓縮,如果壓縮率太低<12.5%,還是作爲未壓縮內容存儲。

BlockBuilder的Finish()函數將data block的數據序列化成一個Slice。

  Rep* r = rep_;
  Slice raw = block->Finish(); // 獲得data block的序列化字符串
  Slice block_contents;
  CompressionType type =r->options.compression;

  switch (type) {
    case kNoCompression: block_contents= raw; break; // 不壓縮
    case kSnappyCompression: { // snappy壓縮格式
      std::string* compressed =&r->compressed_output;
      if(port::Snappy_Compress(raw.data(), raw.size(), compressed) &&
          compressed->size()< raw.size() - (raw.size() / 8u)) {
          block_contents =*compressed;
      } else { // 如果不支持Snappy,或者壓縮率低於12.5%,依然當作不壓縮存儲
        block_contents = raw;
        type = kNoCompression;
      }
      break;
    }
  }

S2 將data內容寫入到文件,並重置block成初始化狀態,清空compressedoutput。

  WriteRawBlock(block_contents,type, handle);
  r->compressed_output.clear();
  block->Reset();

6.4.5 WriteRawBlock函數

在WriteBlock把準備工作都做好後,就可以寫入到sstable文件中了。來看函數原型:

void WriteRawBlock(const Slice& data, CompressionType, BlockHandle*handle);

函數邏輯很簡單,見代碼。

  Rep* r = rep_;
  handle->set_offset(r->offset); // 爲index設置data block的handle信息
  handle->set_size(block_contents.size());

  r->status =r->file->Append(block_contents); // 寫入data block內容
  if (r->status.ok()) {// 寫入1byte的type和4bytes的crc32
    chartrailer[kBlockTrailerSize];
    trailer[0] = type;
    uint32_t crc = crc32c::Value(block_contents.data(),block_contents.size());
    crc = crc32c::Extend(crc, trailer, 1);  // Extend crc tocover block type
    EncodeFixed32(trailer+1, crc32c::Mask(crc));
    r->status =r->file->Append(Slice(trailer, kBlockTrailerSize));
    if (r->status.ok()) { // 寫入成功更新offset-下一個data block的寫入偏移
      r->offset +=block_contents.size() + kBlockTrailerSize;
    }
  }

6.4.6 Finish函數

調用Finish函數,表明調用者將所有已經添加的k/v對持久化到sstable,並關閉sstable文件。

該函數邏輯很清晰,可分爲5部分。

S1 首先調用Flush,寫入最後的一塊data block,然後設置關閉標誌closed=true。表明該sstable已經關閉,不能再添加k/v對。

  Rep* r = rep_;
  Flush();
  assert(!r->closed);
  r->closed = true;

BlockHandle filter_block_handle,metaindex_block_handle, index_block_handle;

S2 寫入filter block到文件中

  if (ok() &&r->filter_block != NULL) {
    WriteRawBlock(r->filter_block->Finish(), kNoCompression,&filter_block_handle);
  }

S3 寫入meta index block到文件中

如果filterblock不爲NULL,則加入從"filter.Name"到filter data位置的映射。通過meta index block,可以根據filter名字快速定位到filter的數據區。

  if (ok()) {
    BlockBuildermeta_index_block(&r->options);
    if (r->filter_block !=NULL) {
      //加入從"filter.Name"到filter data位置的映射
      std::string key ="filter.";
      key.append(r->options.filter_policy->Name());
      std::string handle_encoding;
      filter_block_handle.EncodeTo(&handle_encoding);
      meta_index_block.Add(key,handle_encoding);
    }
    // TODO(postrelease): Add stats and other metablocks
    WriteBlock(&meta_index_block, &metaindex_block_handle);
  }

S4 寫入index block,如果成功Flush過data block,那麼需要爲最後一塊data block設置index block,並加入到index block中。

  if (ok()) {
    if (r->pending_index_entry){ // Flush時會被設置爲true
      r->options.comparator->FindShortSuccessor(&r->last_key);
      std::string handle_encoding;
      r->pending_handle.EncodeTo(&handle_encoding);
      r->index_block.Add(r->last_key, Slice(handle_encoding)); // 加入到index block中
      r->pending_index_entry =false;
    }
    WriteBlock(&r->index_block, &index_block_handle);
  }

S5 寫入Footer。

  if (ok()) {
    Footer footer;
    footer.set_metaindex_handle(metaindex_block_handle);
    footer.set_index_handle(index_block_handle);
    std::string footer_encoding;
    footer.EncodeTo(&footer_encoding);

    r->status =r->file->Append(footer_encoding);
    if (r->status.ok()) {
      r->offset +=footer_encoding.size();
    }
  }

整個寫入流程就分析完了,對於Datablock和Filter Block的操作將在Data block和Filter Block中單獨分析,下面的讀取相同。


發佈了90 篇原創文章 · 獲贊 99 · 訪問量 164萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章