代碼精讀 LevelDB 布隆濾波器

簡介

布隆濾波器 BloomFilter 是一種常用的「使用少量字節數判斷鍵值存在性」的手段。其大致原理爲:使用多個哈希函數,對同一個鍵值做哈希運行,並將其哈希值取模設置比特數值。當給定一個鍵值時,如果計算上述多個哈希函數,存在一個哈希值對應的比特位爲零,則說明該鍵值不存在。

從上可以發現,BloomFilter 常用於判斷鍵的不存在性。由於哈希碰撞的原因,可能存在假陽性 (False Positive),即:所有哈希函數計算出結果對應的比特位全爲 1,但該鍵仍然不存在。

爲了降低假陽性的概率,存在一個權衡,即使用更少的哈希函數,和更少的比特位,使得判別錯誤率在可接受範圍內。其理論推導的結果如下。其中,k 爲哈希函數個數,m 爲比特數組容量,n 爲鍵的個數,x 爲每個鍵佔用的比特位數,即 m/n。

最佳哈希函數個數爲:(m/n) * ln2 ~= 0.69 x
在最佳哈希個數前提下,使得錯誤率不超過 y,則:m >= 1.44n * log2(1/y),即 x >= 1.44 * log2(1/y)

實現

在 LevelDB 中,爲使得假陽性概率在 1%,則每鍵佔用比特數:x >= 9.57。因此 LevelDB 推薦每鍵佔用比特位爲 10。

哈希函數

每輪 hash 計算處理 4 字節。可以考慮拓展成 64 位哈希函數。

static uint32_t BloomHash(const Slice& key) {
  return Hash(key.data(), key.size(), 0xbc9f1d34);
}

uint32_t Hash(const char* data, size_t n, uint32_t seed) {
  // Similar to murmur hash
  const uint32_t m = 0xc6a4a793;
  const uint32_t r = 24;
  const char* limit = data + n;
  uint32_t h = seed ^ (n * m);

  // Pick up four bytes at a time
  while (data + 4 <= limit) {
    uint32_t w = DecodeFixed32(data);
    data += 4;
    h += w;
    h *= m;
    h ^= (h >> 16);
  }

  // Pick up remaining bytes
  switch (limit - data) {
    case 3:
     h += static_cast<uint8_t>(data[2]) << 16;
     FALLTHROUGH_INTENDED;
    case 2:
     h += static_cast<uint8_t>(data[1]) << 8;
     FALLTHROUGH_INTENDED;
    case 1:
     h += static_cast<uint8_t>(data[0]);
     h *= m;
     h ^= (h >> r);
     break;
  }
  return h;
}

FilterPolicy

FilterPolicy 是一個純虛基類,聲明瞭 3 個接口。其主要用於判斷 key 的存在性,以減少 disk IO 的次數。在 LevelDB 中,僅實現了 BloomFilterPolicy 一種過濾器,其計算出來的位圖將存儲在 sst FilterBlock 中。

struct FilterPolicy {
 public:
  virtual ~FilterPolicy();

  // Return the name of this policy.  Note that if the filter encoding
  // changes in an incompatible way, the name returned by this method
  // must be changed.  Otherwise, old incompatible filters may be
  // passed to methods of this type.
  virtual const char* Name() const = 0;

  // keys[0,n-1] contains a list of keys (potentially with duplicates)
  // that are ordered according to the user supplied comparator.
  // Append a filter that summarizes keys[0,n-1] to *dst.
  //
  // Warning: do not change the initial contents of *dst.  Instead,
  // append the newly constructed filter to *dst.
  virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const = 0;

  // "filter" contains the data appended by a preceding call to
  // CreateFilter() on this class.  This method must return true if
  // the key was in the list of keys passed to CreateFilter().
  // This method may return true or false if the key was not on the
  // list, but it should aim to return false with a high probability.
  virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const = 0;
};

BloomFilterPolicy 僅包含每鍵佔用比特數和哈希函數個數成員。其中哈希個數,按照上述公式推導,採用 ln(2) * bits_per_key。

struct BloomFilterPolicy : public FilterPolicy {
  size_t bits_per_key_; // 每個 key 所需的比特數
  size_t k_; // 哈希函數個數
};

explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key) {
  // 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)
  if (k_ < 1) k_ = 1;
  if (k_ > 30) k_ = 30;
}

構造 BloomFilter 流程

void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
  // Compute bloom filter size (in both bits and bytes)
  size_t bits = n * bits_per_key_; // 計算 bloom filter 總比特數

  // For small n, we can see a very high false positive rate.  Fix it
  // by enforcing a minimum bloom filter length.
  if (bits < 64) bits = 64; // 最小佔用比特數

  size_t bytes = (bits + 7) / 8;
  bits = bytes * 8; // 按字節對齊後的比特數

  const size_t init_size = dst->size(); // 初始字節,保持不動
  dst->resize(init_size + bytes, 0);
  dst->push_back(static_cast<char>(k_));  // Remember # of probes in filter,持久化了 k
  char* array = &(*dst)[init_size];
  for (int i = 0; i < n; i++) {
   // Use double-hashing to generate a sequence of hash values.
   // See analysis in [Kirsch,Mitzenmacher 2006].
   uint32_t h = BloomHash(keys[i]); // 僅使用一個哈希函數
   const uint32_t delta = (h >> 17) | (<< 15);  // Rotate right 17 bits
   for (size_t j = 0; j < k_; j++) { // 一個哈希函數,經過 k 次變換
     const uint32_t bitpos = h % bits;
     array[bitpos / 8] |= (1 << (bitpos % 8)); // 通過哈希值,設置比特位
     h += delta;
   }
  }
}

驗證 BloomFilter 流程:

bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const override {
  const size_t len = bloom_filter.size();
  if (len < 2) return false;

  const char* array = bloom_filter.data();
  const size_t bits = (len - 1) * 8; // 比特總數

  // Use the encoded k so that we can read filters generated by
  // bloom filters created using different parameters.
  const size_t k = array[len - 1]; // 還原出 k_ 參數
  if (k > 30) {
   // Reserved for potentially new encodings for short bloom filters.
   // Consider it a match.
   return true;
  }

  uint32_t h = BloomHash(key);
  const uint32_t delta = (h >> 17) | (<< 15);  // Rotate right 17 bits
  for (size_t j = 0; j < k; j++) { // 如果 k 次哈希計算,有一次不命中 bitmap,就認爲 key 不存在
   const uint32_t bitpos = h % bits;
   if ((array[bitpos / 8] & (1 << (bitpos % 8))) == 0) return false;
   h += delta;
  }
  return true;
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eXutrgBfXjIErdbocNntiA