天天看點

Leveldb源碼解析第二篇【Meta Block】

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。

上一章中詳細講解了

table

中的

data block

的結構以及涉及的源碼,本章中将講解

table

結構中的

meta block

table

結構

<beginning_of_file>
    [data block 1]
    [data block 2]
    ...
    [data block N]
    [meta block 1]
    ...
    [meta block K]
    [metaindex block]
    [index block]
    [Footer]        (fixed size; starts at file_size - sizeof(Footer))
    <end_of_file>
           
先說說

meta block

table

中的作用

一個meta block對應一個data block,meta block的作用是快速判斷對應的data block中是否存在某個key,詳情可以搜尋“Bloom Filter”

原理是這樣的,首先需要定義一個大的bitmap,實際就是一個字元串,bitmap中初始時每一位都是0,當往data block中添加key時,會根據這個key值算出一組hash值,hash值對bitmap位數取模後将bitmap中對應的位置設定為1;當需要查詢data block中是否存在某個key時,隻需通過這個key計算一組hash值,然後檢視hash值在bitmap中對應的位置的值是否為1,隻有有一個位置不為1,說明這個data block中不存在這個key

上面所說的算法中會存在hash沖突,如果bitmap中已經存了兩個key

key1 計算出的位置為 [1,3]
    key2 計算出的位置為 [2,4]
           

當我們要查詢的

key3

計算出來的位置為[1,4]時,在

bitmap

中[1,4]兩個位置都是1,這個隻能說明data block中有可能存在key3,是以說bitmap是用來快速判斷key不在data block中,我們需要做的是使出現誤判的機率降到最低,可以得到一個公式

假設一個key要對應k個hash值,總共有n個key,bitmap的位數為m,那麼出現誤判的機率為

(1-(1-1/m)^(kn))^k
           

上面的公式是怎麼得到的呢?

假設我們現在要在bitmap中判斷某個key是否存在,先要算出k個hash值,而這k個hash值對應的位置上面恰恰都有1的機率是多少呢

假設一個位置上面恰恰為1的機率為p,那麼k個位置上面都為1的機率為p^k

p要怎麼得到呢?

p代表的是一個位置上面恰恰為1的機率,我們可以先得到這個位置不為1的機率為q,那麼,p=(1-q)

q要怎麼得到呢?

q代表的是一個位置上面不為1的機率,說明n個key在計算k個hash的時候都沒有落在這個點上,一次沒有落在這個點上的機率為1-1/m,kn次沒有落在這個點上的機率為(1-1/m)(kn),那麼kn次落在這個點上的機率為1-(1-1/m)(kn),一共有k個點,k個點都是1的機率就為(1-(1-1/m)(kn))k

好多年不搞數學,上面的公式解釋的好痛苦(-_-!!!)

為了保證誤判的機率最低,如果m和n固定的話,可以得到k的最優解為k=m/n*ln2,我也不知道怎麼算出來的,網上抄的(-_-!!!)

上面說了這麼多理論,接下來要開始撸代碼啦

搞懂

meta block

需要閱讀如下源碼檔案
1 table/filter_block.h             // [非常重要|難度:4  級] filter_block的結構
2 table/filter_block.cc

3 include/leveldb/filter_policy.h       // [重要|難度:2級] 過濾政策
4 util/bloom.cc                         // [重要|難度:2級] 過濾政策具體實作
           

filter_policy.h

先介紹

filter_policy

,中文翻譯為過濾政策,這個地方隻是定義了一個接口,使用者可以重寫這個接口

class FilterPolicy {
 public:
  virtual ~FilterPolicy();
  virtual const char* Name() const = 0;       //傳回目前政策的名字

  // dst就是上面講的bitmap,n表示一個key在bitmap中占多少位,這個函數就是通過傳進來的keys,來建構一個bitmap
  virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const = 0;

  // bitmap中有沒有key
  virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const = 0;
};

// Return a new filter policy that uses a bloom filter with approximately
// the specified number of bits per key.  A good value for bits_per_key
// is 10, which yields a filter with ~ 1% false positive rate.
//
// Callers must delete the result after any database that is using the
// result has been closed.
//
// Note: if you are using a custom comparator that ignores some parts
// of the keys being compared, you must not use NewBloomFilterPolicy()
// and must provide your own FilterPolicy that also ignores the
// corresponding parts of the keys.  For example, if the comparator
// ignores trailing spaces, it would be incorrect to use a
// FilterPolicy (like NewBloomFilterPolicy) that does not ignore
// trailing spaces in keys.
extern const FilterPolicy* NewBloomFilterPolicy(int bits_per_key);
}
           

bloom.cc

filter_policy

的具體實作

class BloomFilterPolicy : public FilterPolicy {
 private:
  size_t bits_per_key_;   // 前文中講到的n/m,bitmap的位數除以key的個數
  size_t k_;              // 前文中講到的k,一個key在bitmap中要算出多少個hash

 public:
  explicit BloomFilterPolicy(int bits_per_key)
      : bits_per_key_(bits_per_key) {
    // 構造方法,直接給bits_pre_key_指派
    // 根據前文講到的k的最優解,k=n/m * ln2,ln2=~ 0.69,是以k的最優解為bits_per_key*0.69
    // We intentionally round down to reduce probing cost a little bit
    k_ = static_cast<size_t>(bits_per_key * 0.69);  // 0.69 =~ ln(2)
    // k_肯定不能小于1,如果k_太大的話,每次計算hash會消耗計算資源,劃不來,這個地方對誤判率和計算資源做了權衡,k_最大為30
    if (k_ < 1) k_ = 1;
    if (k_ > 30) k_ = 30;
  }

  // 過濾政策的名稱
  virtual const char* Name() const {
    return "leveldb.BuiltinBloomFilter2";
  }

  // dst就是上面講的bitmap,n表示一個key在bitmap中占多少位,這個函數就是通過傳進來的keys,來建構一個bitmap
  virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const {
    // 計算出bitmap有多少位
    size_t bits = n * bits_per_key_;

    // For small n, we can see a very high false positive rate.  Fix it
    // by enforcing a minimum bloom filter length.
    // 如果n很小的話,誤判率會很高,是以設定了一個最小值,64位,也就是8個位元組
    if (bits < 64) bits = 64;

    // 加上7然後除以8,隻要不是整除,就會進一位
    size_t bytes = (bits + 7) / 8;
    // 算出實際的bitmap有多少位
    bits = bytes * 8;

    const size_t init_size = dst->size();
    // 把bitmap放到dst後面,并用0填充,dst存放不止一個bitmap
    dst->resize(init_size + bytes, 0);
    // 将k_添加到dst的末尾,k_表示的是一個key算多少次hash
    dst->push_back(static_cast<char>(k_));  // Remember # of probes in filter
    // init_size是dst的初始長度,array指向目前bitmap的起始位置
    char* array = &(*dst)[init_size];
    // 又到了劃重點的時間,這個地方就是将key轉為hash值,然後将bitmap對應的點這是為1
    // n是key的個數
    for (int i = 0; i < n; i++) {
      // Use double-hashing to generate a sequence of hash values.
      // See analysis in [Kirsch,Mitzenmacher 2006].
      // 關于bloomHash這裡就不多解釋了,有時間我會專門寫一篇關于hash的文章
      // 這裡隻需要知道BloomHash就是闖進去一個key,傳回一個hash值,h就是傳回的hash值
      uint32_t h = BloomHash(keys[i]);
      // (h >> 17) | (h << 15) 這個和hash算法有關,通過一系列的位運算來随機種子
      const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
      // k_表示的是一個key算多少次hash,如果用BloomHash來算hash的話,計算量太大,這裡用到了一個簡單的方法
      // 就是前幾行中得到的随機種子再與h相加
      for (size_t j = 0; j < k_; j++) {
        // 得到要修改的位在第幾個位元組上
        const uint32_t bitpos = h % bits;
        // 取模 然後位移 再或上 定位到的位元組
        array[bitpos/8] |= (1 << (bitpos % 8));
        h += delta;
      }
    }
  }

// 判斷bitmap中有沒有key
  virtual bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const {
    const size_t len = bloom_filter.size();
    if (len < 2) return false;

    const char* array = bloom_filter.data();
    // len-1是因為bitmap的最後一位存放的是k_,也就是一個key要計算多少次hash
    const size_t bits = (len - 1) * 8;

    // 得到最後一個自己裡面存放的值
    const size_t k = array[len-1];
    if (k > 30) {
      // k的計算方法是,k=m/n*ln2,m表示bitmap的位數,n表示key的個數,如果k大于30,說明key很少
      // 既然key很少,周遊一遍data block也不會很耗資源
      // 但是得到30多個hash的話就比較費資源了,那還不如認為目前data block中有可能有這個key
      return true;
    }

    // 下面的代碼就和CreateFilter中的差不多了,隻要找到一個hash值對應的點不為1,那就說明key在這個data block中不存在
    uint32_t h = BloomHash(key);
    const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
    for (size_t j = 0; j < k; j++) {
      const uint32_t bitpos = h % bits;
      if ((array[bitpos/8] & (1 << (bitpos % 8))) == 0) return false;
      h += delta;
    }
    return true;
  }
};
}

// 暫時還不知道有啥用,後面補充
const FilterPolicy* NewBloomFilterPolicy(int bits_per_key) {
  return new BloomFilterPolicy(bits_per_key);
}
           

上面介紹一下bloom的具體實作,現在來介紹在table中的filter block怎麼建構了,這個地方的命名我個人覺得有點奇怪,block.h中介紹的是block的結構,真正來建構一個block的類是block_builder,而filter_block.h就是用來建構filter_block的

filter_block.h中有兩個類,一個是建構一個filter_block的,還有一個是來解析filter_block的

// filter_block.h
// filter block建構類,一個FilterBlockBuilder對象并不是隻存放一個
class FilterBlockBuilder {
 public:
  explicit FilterBlockBuilder(const FilterPolicy*);

  // 開始建構filter block
  void StartBlock(uint64_t block_offset);
  // 在table添加key的時候,有一個地方會專門存放key,當一個data block完成時,就會用存儲的key來建構filter block
  void AddKey(const Slice& key);
  // 當一個filter block建構完成後,還需要做一些處理
  Slice Finish();

 private:
 // 真正建構filter block的地方
  void GenerateFilter();

  // 上面講了那麼多就是說的這個,裡面有k_、bits_per_key_、filter block中是否存在key的方法和建立filter block的方法
  const FilterPolicy* policy_;
  // 把key通過追加的方式全部放在keys_中
  std::string keys_;              // Flattened key contents
  // 存放了每個key的索引,和keys_一起用可以得到每個key
  std::vector<size_t> start_;     // Starting index in keys_ of each key
  // 真正放bitmap的地方,table裡面所有data block的bitmap全部都在這裡面喲
  std::string result_;            // Filter data computed so far
  // 臨時存放keys的地方,後面會提到
  std::vector<Slice> tmp_keys_;   // policy_->CreateFilter() argument
  // 和start_類似,用來存放每個bitmap的索引,其實就是在result_中的起始位址,和result一起使用可以得到具體的bitmap
  std::vector<uint32_t> filter_offsets_;

  // 隻允許引用,不允許隐式構造
  FilterBlockBuilder(const FilterBlockBuilder&);
  void operator=(const FilterBlockBuilder&);
};

// filter block解析類
class FilterBlockReader {
 public:
 // REQUIRES: "contents" and *policy must stay live while *this is live.
  FilterBlockReader(const FilterPolicy* policy, const Slice& contents);
  bool KeyMayMatch(uint64_t block_offset, const Slice& key);

 private:
  const FilterPolicy* policy_;
  const char* data_;    // Pointer to filter data (at block-start)
  const char* offset_;  // Pointer to beginning of offset array (at block-end)
  size_t num_;          // Number of entries in offset array
  size_t base_lg_;      // Encoding parameter (see kFilterBaseLg in .cc file)
};
           
// filter_block.cc
static const size_t kFilterBaseLg = 11;
static const size_t kFilterBase = 1 << kFilterBaseLg;

// 構造方法,policy_裡面有k_、bits_per_key_、filter block中是否存在key的方法和建立filter block的方法
FilterBlockBuilder::FilterBlockBuilder(const FilterPolicy* policy)
    : policy_(policy) {
}

// 開始建構filter block,上一章我了解錯了,不是一個data block建構一個filter block,而是2k的資料建構一個filter block
/* 
如果這一塊的邏輯讓我來寫的話,邏輯應該是這樣的
當一個data block建構完成後,調用StartBlock方法,配置設定一個的bitmap,
通過policy_裡面的CreateFilter方法将這個data block裡面的keys轉換成bitmap的對應的位;
上面的是寫入bitmap,怎麼讀取這個bitmap呢?
一個data block對應一個bitmap,如果要讀bitmap,就需要知道table中bitmap對應的是第k個data block
當周遊table裡面的data block時,就需要記錄k值
然後得到第k個bitmap
*/
/*
上面是我個人的邏輯,再看看大神的邏輯
上面我們是通過得到是第k個data block進而得到bitmap
大神的方法通過data block在table中的偏移量來得到是第幾個bitmap,方法如下
直覺上了解是每2k的data block就配置設定一個bitmap,通過data block的偏移量,然後除以2,就可以得到是第幾個bitmap了
那問題了,data block的大小門檻值是2k,但并不是說data block大小就是2k,如果data block中存放了一個7k的key-value,
那在得到bitmap的時候怎麼将這個data block分成多個2k呢?
【這裡我們補充一個變量filter_offsets_,它是用來存放每個bitmap的偏移量,如果要得到第n個bitmap,
隻需filter_offsets_[n],得到第n個bitmap的偏移,然後result_+filter_offsets_[n],就可以得到bitmap了】
還是上面7k的例子,如果除以2,需要配置設定3個bitmap,實際上第一個bitmap就是這個data block的索引了
那剩下的兩個bitmap呢?剩下的兩個bitmap是空的,這兩個bitmap在filter_offsets_中指向的還是第一個bitmap的位置
相當于在filter_offsets_中占了兩個位置,實際存放bitmap的result_啥也沒存
如果第1個data block大小是7k,第2個data block大小是5k
那麼第1個data block在table中的偏移量為7,7/2=3,在filter_offsets_中會有3條記錄,不過存放的都是第一個bitmap的偏移
第2個data block在table中的偏移量為12,12/2=6,在filter_offsets_中會加2條記錄,2條記錄存放的都是第二個bitmap的偏移
現在得到第1個data block的bitmap,偏移量/2=3,filter_offsets_的第3條記錄指向的是第1個bitmap的偏移,擷取成功
得到第2個data block的bitmap,偏移量/2=6,filter_offsets_的第6條記錄指向的是第2個bitmap的偏移,擷取成功
*/
void FilterBlockBuilder::StartBlock(uint64_t block_offset) {
// filter_index bitmap索引個數
uint64_t filter_index = (block_offset / kFilterBase);
assert(filter_index >= filter_offsets_.size());
// 隻要bitmap索引個數比目前索引個數大,就一直建構filter block,實際隻有第一次循環的時候在result_中放了一個bitmap
// 其他循環都隻是在filter_offsets_中存放第一次循環中得到的bitmap的偏移
  while (filter_index > filter_offsets_.size()) {
    GenerateFilter();
  }
}

// 将key放在keys_中,将key的大小放在start_中,建構filter data的時候用到
void FilterBlockBuilder::AddKey(const Slice& key) {
  Slice k = key;
  start_.push_back(keys_.size());
  keys_.append(k.data(), k.size());
}

// finish是在table建構時調用的
Slice FilterBlockBuilder::Finish() {
  if (!start_.empty()) {
    GenerateFilter();
  }

  // Append array of per-filter offsets
  // 當所有bitmap全部建構完成後,将所有的偏移量放到result_中
  const uint32_t array_offset = result_.size();
  for (size_t i = 0; i < filter_offsets_.size(); i++) {
    PutFixed32(&result_, filter_offsets_[i]);
  }

  // 最後再将所有bitmap的大小放到result_的後面
  PutFixed32(&result_, array_offset);
  // 将 2k 這個數字放在最後
  // 那麼在解析result_的時候,讀取最後一個位元組,表示data block按多少k分割
  // 讀取倒數第5到倒數第2個位元組(size),表示所有bitmap的大小
  // result_+size就可以位元組偏移到記錄偏移點的地方,這有就可以一步一步解析了
  result_.push_back(kFilterBaseLg);  // Save encoding parameter in result
  
  return Slice(result_);
}

// 劃重點
void FilterBlockBuilder::GenerateFilter() {
  // 得到目前block中key的個數
  const size_t num_keys = start_.size();
  // 如果沒有key,說明之前已經建立過bitmap了,現在隻需要把目前偏移量放到filter_offsets_中
  if (num_keys == 0) {
    // Fast path if there are no keys for this filter
    filter_offsets_.push_back(result_.size());
    return;
  }

  // Make list of keys from flattened key structure
  // start_存放的是所有key的起始位置,下面一步是把結束的位置也放進去
  start_.push_back(keys_.size());  // Simplify length computation
  // tmp_keys_上面再定義的時候沒有解釋,這裡詳細解釋,就是用來存放目前data block中的key的,不過是以slice的形式存儲的
  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);
  }

  // Generate filter for current set of keys and append to result_.
  // 記錄result_的大小,實際是記錄下一個filter block的起始位置
  filter_offsets_.push_back(result_.size());
  // 得到filter block後放在result_裡面
  policy_->CreateFilter(&tmp_keys_[0], static_cast<int>(num_keys), &result_);

  tmp_keys_.clear();
  keys_.clear();
  start_.clear();
}
           
// 下面就是filter  block的解析過程了
FilterBlockReader::FilterBlockReader(const FilterPolicy* policy,
                                     const Slice& contents)
    : policy_(policy),
      data_(NULL),
      offset_(NULL),
      num_(0),
      base_lg_(0) {
  size_t n = contents.size();
  // contents的大小不可能小于5,最後一個位元組記錄kFilterBaseLg,然後4個位元組記錄所有bitmap的大小
  if (n < 5) return;  // 1 byte for base_lg_ and 4 for start of offset array
  // 這個在上面說過,最後一個位元組是kFilterBaseLg
  base_lg_ = contents[n-1];
  // 然後得到倒數第5到倒數第2個位元組,轉為uint32,就可以得到所有bitmap的大小
  uint32_t last_word = DecodeFixed32(contents.data() + n - 5);
  if (last_word > n - 5) return;
  data_ = contents.data();
  // offset_就是記錄偏移量的位置了
  offset_ = data_ + last_word;
  // num_記錄有多少個偏移量,總大小減去最後5個位元組,然後再減去bitmap的大小,剩下的就全是記錄偏移量了
  // 一個偏移量占4個位元組,(n - 5 - last_word) / 4 就表示有多少個偏移量了
  num_ = (n - 5 - last_word) / 4;
}

// 通過block的偏移量來判斷 key 在不在這個block中
bool FilterBlockReader::KeyMayMatch(uint64_t block_offset, const Slice& key) {
  // block_offset 往右移11位,意思就是除以2k,也就是得到是第幾個bitmap
  uint64_t index = block_offset >> base_lg_;
  if (index < num_) {
    // 然後再通過bitmap的偏移量得到真正的bitmap
    uint32_t start = DecodeFixed32(offset_ + index*4);
    uint32_t limit = DecodeFixed32(offset_ + index*4 + 4);
    if (start <= limit && limit <= static_cast<size_t>(offset_ - data_)) {
      Slice filter = Slice(data_ + start, limit - start);
      // 判斷bitmap中有沒有key
      return policy_->KeyMayMatch(key, filter);
    } else if (start == limit) {
      // Empty filters do not match any keys
      return false;
    }
  }
  return true;  // Errors are treated as potential matches
}
           

【作者:antonyxu https://antonyxux.github.io/ 】

繼續閱讀