天天看點

leveldb源碼分析--SSTable之block

在SSTable中主要存儲資料的地方是data block,block_builder就是這個專門進行block的組織的地方,我們來詳細看看其中的内容,其主要有Add,Finish和CurrentSizeEstimate三個函數。Finish的邏輯十分簡單就是簡單的将restart點資訊和restart點個數分别以PutFixed32的格式寫入資料最後;CurrentSizeEstimate則是簡單的計算目前塊需要的存儲大小 = 已插入的KV對的大小 + 重新開機點個數 * 4 + 1 * 4(重新開機點計數器)。需要注意的是Datablock,Metablock, MetaIndex block, Indexblock,Footer這幾個資料塊中Datablock, MetaIndex block, Indexblock都是以KV對的形式組成的,是以都由BlockBuilder負責管理。

我們詳細分析一下BlockBuilder的Add函數

leveldb源碼分析--SSTable之block
void BlockBuilder::Add(const Slice& key, const Slice& value) {
  //一些基本的狀态判斷,如上次插入的key是否比目前key小等
  if (counter_ < options_->block_restart_interval) {
    // 不需要restart時取得跟上一個restart點的key的相同部分
    const size_t min_length = std::min(last_key_piece.size(), key.size());
    while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
      shared++;
    }
  } else {
    // 否則,完整存儲key,并記錄restart點
    restarts_.push_back(buffer_.size());
    counter_ = 0;
  }
  const size_t non_shared = key.size() - shared;
  // 按“共享長度 | 非共享長度| 值長度| 非共享資料| value值寫入buffer
  PutVarint32(&buffer_, shared);
  PutVarint32(&buffer_, non_shared);
  PutVarint32(&buffer_, value.size());
  // Add string delta to buffer_ followed by value
  buffer_.append(key.data() + shared, non_shared);
  buffer_.append(value.data(), value.size());
  // Update state
  last_key_.resize(shared);
  last_key_.append(key.data() + shared, non_shared);
  assert(Slice(last_key_) == key);
  counter_++;
}      
leveldb源碼分析--SSTable之block

了解了BlockBuilder之後接下來我們再看看filter_block,首先看看他的幾個成員變量

leveldb源碼分析--SSTable之block
const FilterPolicy* policy_;         //  filter,即hash算法實作類
  std::string keys_;                     //  所有key,一個接一個一直添加在一起
  std::vector<size_t> start_;            //  每個key在keys_中的開始位置
  std::string result_;                   // 已經産生的Filter資料
  std::vector<Slice> tmp_keys_;          // 用來還原keys的一個臨時數組
  std::vector<uint32_t> filter_offsets_; //每次生成的filter值偏移      
leveldb源碼分析--SSTable之block

了解了基本成員以後我們描述一下FilterBlockBuilder的邏輯:

Add:當程式向一個block添加資料時就調用Add将key的内容添加到keys_,并記錄這個key在keys_中的開始下标;

StartBlock: 當該block達到指定的域大小以後就調用StartBlock根據block最後寫入檔案的大小生成一個filter值并添加到result中,同時記錄該開始位址;

Finish: 當SSTable寫入結束後(TableBuilder.Finish)會調用Finish() 将每次生成的filter的偏移量(數組filter_offsets_)和總的Filter的大小寫入Metablock的最後。

需要注意的是在生成過程中從函數StartBlock的邏輯可以看出當一個資料塊大小/2k >=2 時,為了友善處理會再次記錄空偏移,在讀取的時候處理就可以簡單的讀取目前的block_offset / kFilterBase 和其後的一個數值就可以得到兩個偏移量(開始和結束)。我們看一下代碼邏輯

leveldb源碼分析--SSTable之block
void FilterBlockBuilder::StartBlock(uint64_t block_offset) {
  uint64_t filter_index = (block_offset / kFilterBase); // block的偏移 / 2K
// 這裡上一個block時 filter_index == 上次 block_offset  / kFilterBase,
//是以這次block大小/kFilterBase >=2 時會調用兩次以上GenerateFilter,
//而連續調用時,隻是在filter_offsets_中添加一個目前大小,但并未擴充其空間
  while (filter_index > filter_offsets_.size()) {
    GenerateFilter();
  }
}      
leveldb源碼分析--SSTable之block
leveldb源碼分析--SSTable之block
void FilterBlockBuilder::GenerateFilter() {
  const size_t num_keys = start_.size();
  if (num_keys == 0) {
    // Fast path if there are no keys for this filter ,連續被第二次調用時進入此邏輯
    filter_offsets_.push_back(result_.size());
    return;
  }
  //還原為key數組
  start_.push_back(keys_.size()); // Simplify length computation
  tmp_keys_.resize(num_keys);
  for (size_t i = 0; i < num_keys; i++) {
    const char* base = keys_.data() + start_[i];
    size_t length = start_[i+1] - start_[i];
    tmp_keys_[i] = Slice(base, length);
  }
  // 記錄生成filter資訊的開始偏移量
  filter_offsets_.push_back(result_.size());
  policy_->CreateFilter(&tmp_keys_[0], num_keys, &result_);
//還原狀态
  tmp_keys_.clear();
  keys_.clear();
  start_.clear();
}      
leveldb源碼分析--SSTable之block

FilterBlockReader為從檔案中将FilterBlockBuilder生成filter資訊還原回來(讀取),隻包含一個構造函數FilterBlockReader和KeyMayMatch,根據名稱就能知道其功能而且邏輯較簡單,是以這裡就不再詳細解釋。

這裡引用Leveldb源碼分析--13的一個例子以便于了解:

8.5.5 簡單示例

讓我們根據TableBuilder對FilterBlockBuilder接口的調用範式: 

(StartBlock AddKey*)* Finish以及上面的函數實作,結合一個簡單例子看看leveldb是如何為data block建立filter block(也就是meta block)的。 

考慮兩個datablock,在sstable的範圍分别是:Block 1 [0, 7KB-1], Block 2 [7KB, 14.1KB] 

    S1 首先TableBuilder為Block 1調用FilterBlockBuilder::StartBlock(0),該函數直接傳回; 

    S2 然後依次向Block 1加入k/v,其中會調用FilterBlockBuilder::AddKey,FilterBlockBuilder記錄這些key。 

    S3 下一次TableBuilder添加k/v時,例行檢查發現Block 1的大小超過設定,則執行Flush操作,Flush操作在寫入Block 1後,開始準備Block 2并更新block offset=7KB,最後調用FilterBlockBuilder::StartBlock(7KB),開始為Block 2建構Filter。 

    S4 在FilterBlockBuilder::StartBlock(7KB)中,計算出filter index = 3,觸發3次GenerateFilter函數,為Block 1添加的那些key清單建立filter,其中第2、3次循環建立的是空filter。 

此時filter的結構如圖8.5-1所示。

leveldb源碼分析--SSTable之block

圖8.5-1

在StartBlock(7KB)時會向filter的偏移數組filter_offsets_壓入兩個包含空key set的元素,filter_offsets_[1]和filter_offsets_[2],它們的值都等于7KB-1。 

S5 Block 2建構結束,TableBuilder調用Finish結束table的建構,這會再次觸發Flush操作,在寫入Block 2後,為Block 2的key建立filter。最終的filter如圖8.5-2所示。

leveldb源碼分析--SSTable之block

圖8.5-2

這裡如果Block 1的範圍是[0, 1.8KB-1],Block 2從1.8KB開始,那麼Block 2将會和Block 1共用一個filter,它們的filter都被生成到filter 0中。 

當然在TableBuilder建構表時,Block的大小是根據參數配置的,也是基本均勻的。

至此我們就介紹完了Datablock,Metablock, MetaIndex block, Indexblock,Footer,即所有SSTable相關的資料塊的寫入邏輯了。