Leveldb源碼分析--12

8 FilterPolicy&Bloom之1

8.1 FilterPolicy

因名知意,FilterPolicy是用於key過濾的,可以快速的排除不存在的key。前面介紹Table的時候,在Table::InternalGet函數中有過一面之緣。
FilterPolicy有3個接口:
virtual const char* Name() const = 0; // 返回filter的名字
virtual void CreateFilter(const Slice* keys, int n, std::string* dst)const = 0;
virtual bool KeyMayMatch(const Slice& key, const Slice& filter)const = 0;
> CreateFilter接口,它根據指定的參數創建過濾器,並將結果append到dst中,注意:不能修改dst的原始內容,只做append。
參數@keys[0,n-1]包含依據用戶提供的comparator排序的key列表--可重複,並把根據這些key創建的filter追加到@*dst中。
> KeyMayMatch,參數@filter包含了調用CreateFilter函數append的數據,如果key在傳遞函數CreateFilter的key列表中,則必須返回true。

注意,它不需要精確,也就是即使key不在前面傳遞的key列表中,也可以返回true,但是如果key在列表中,就必須返回true。
涉及到的類如圖8.1-1所示。

圖8.1-1

8.2InternalFilterPolicy

這是一個簡單的FilterPolicy的wrapper,以方便的把FilterPolicy應用在InternalKey上,InternalKey是Leveldb內部使用的key,這些前面都講過。它所做的就是從InternalKey拆分得到user key,然後在user key上做FilterPolicy的操作。

它有一個成員:constFilterPolicy* const user_policy_;
其Name()返回的是user_policy_->Name();
bool InternalFilterPolicy::KeyMayMatch(const Slice& key, constSlice& f) const {
  returnuser_policy_->KeyMayMatch(ExtractUserKey(key), f);
}

void InternalFilterPolicy::CreateFilter(const Slice* keys, int n,std::string* dst) const {
  Slice* mkey =const_cast<Slice*>(keys);
  for (int i = 0; i < n; i++)mkey[i] = ExtractUserKey(keys[i]);
  user_policy_->CreateFilter(keys, n, dst);
}

8.3 BloomFilter

8.3.1 基本理論

Bloom Filter實際上是一種hash算法,數學之美系列有專門介紹。它是由巴頓.布隆於一九七零年提出的,它實際上是一個很長的二進制向量和一系列隨機映射函數。
Bloom Filter將元素映射到一個長度爲m的bit向量上的一個bit,當這個bit是1時,就表示這個元素在集合內。使用hash的缺點就是元素很多時可能有衝突,爲了減少誤判,就使用k個hash函數計算出k個bit,只要有一個bit爲0,就說明元素肯定不在集合內。下面的圖8.3-1是一個示意圖。

圖8.3-1

在leveldb的實現中,Name()返回"leveldb.BuiltinBloomFilter",因此metaindex block 中的key就是”filter.leveldb.BuiltinBloomFilter”。Leveldb使用了double hashing來模擬多個hash函數,當然這裏不是用來解決衝突的。
和線性再探測(linearprobing)一樣,Double hashing從一個hash值開始,重複向前迭代,直到解決衝突或者搜索完hash表。不同的是,double hashing使用的是另外一個hash函數,而不是固定的步長。

給定兩個獨立的hash函數h1和h2,對於hash表T和值k,第i次迭代計算出的位置就是:h(i, k) = (h1(k) + i*h2(k)) mod |T|。

對此,Leveldb選擇的hash函數是:
Gi(x)=H1(x)+iH2(x)
H2(x)=(H1(x)>>17) | (H1(x)<<15)

H1是一個基本的hash函數,H2是由H1循環右移得到的,Gi(x)就是第i次循環得到的hash值。【理論分析可參考論文Kirsch,Mitzenmacher2006

在bloom_filter的數據的最後一個字節存放的是k_的值,k_實際上就是G(x)的個數,也就是計算時採用的hash函數個數。

8.3.2 BloomFilter參數

這裏先來說下其兩個成員變量:bits_per_key_和key_;其實這就是Bloom Hashing的兩個關鍵參數。
變量k_實際上就是模擬的hash函數的個數;
關於變量bits_per_key_,對於n個key,其hash table的大小就是bits_per_key_。它的值越大,發生衝突的概率就越低,那麼bloom hashing誤判的概率就越低。因此這是一個時間空間的trade-off。
對於hash(key),在平均意義上,發生衝突的概率就是1/ bits_per_key_。
它們在構造函數中根據傳入的參數bits_per_key初始化。
    bits_per_key_ = bits_per_key;
    k_ =static_cast<size_t>(bits_per_key * 0.69); // 0.69 =~ ln(2)
    if (k_ < 1) k_ = 1;
    if (k_ > 30) k_ = 30;

模擬hash函數的個數k_取值爲bits_per_key_*ln(2),爲何不是0.5或者0.4了,可能是什麼理論推導的結果吧,不瞭解了。

8.3.3 建立BloomFilter

瞭解了上面的理論,再來看leveldb對Bloom Filter的實現就輕鬆多了,先來看Bloom Filter的構建。這就是FilterPolicy::CreateFilter接口的實現:
void CreateFilter(const Slice* keys, int n, std::string* dst) const
下面分析其實現代碼,大概有如下幾個步驟:
S1 首先根據key個數分配filter空間,並圓整到8byte。
    size_t bits = n * bits_per_key_;
    if (bits < 64) bits = 64; // 如果n太小FP會很高,限定filter的最小長度
    size_t bytes = (bits + 7) / 8;// 圓整到8byte
    bits = bytes * 8; // bit計算的空間大小
    const size_t init_size =dst->size();
    dst->resize(init_size +bytes, 0); // 分配空間
S2 在filter最後的字節位壓入hash函數個數
dst->push_back(static_cast<char>(k_));// Remember # of probes in filter
S3 對於每個key,使用double-hashing生產一系列的hash值h(K_個),設置bits array的第h位=1。
    char* array =&(*dst)[init_size];
    for (size_t i = 0; i < n;i++) {
      // double-hashing,分析參見[Kirsch,Mitzenmacher 2006]
      uint32_t h =BloomHash(keys[i]); // h1函數
      const uint32_t delta = (h>> 17) | (h << 15); // h2函數、由h1 Rotate right 17 bits
      for (size_t j = 0; j <k_; j++) { // double-hashing生產k_個的hash值
        const uint32_t bitpos = h% bits; // 在bits array上設置第bitpos位
        array[bitpos/8] |= (1<< (bitpos % 8));
        h += delta;
      }
    }
Bloom Filter的創建就完成了。

8.3.4 查找BloomFilter

在指定的filer中查找key是否存在,這就是bloom filter的查找函數:
bool KeyMayMatch(const Slice& key, const Slice& bloom_filter),函數邏輯如下:
S1 準備工作,並做些基本判斷。
    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;
    const size_t k = array[len-1];// 使用filter的k,而不是k_,這樣更靈活
    if (k > 30) return true; // 爲短bloom filter保留,當前認爲直接match

S2 計算key的hash值,重複計算階段的步驟,循環計算k個hash值,只要有一個結果對應的bit位爲0,就認爲不匹配,否則認爲匹配。

    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; // notmatch
      h += delta;
    }
    return true; // match

8.4 Filter Block格式

Filter Block也就是前面sstable中的meta block,位於data block之後。
如果打開db時指定了FilterPolicy,那麼每個創建的table都會保存一個filter block,table中的metaindex就包含一條從”filter.<N>到filter block的BlockHandle的映射,其中”<N>”是filter policy的Name()函數返回的string。
Filter block存儲了一連串的filter值,其中第i個filter保存的是block b中所有的key通過FilterPolicy::CreateFilter()計算得到的結果,block b在sstable文件中的偏移滿足[ i*base ... (i+1)*base-1 ]。

當前base是2KB,舉個例子,如果block X和Y在sstable的起始位置都在[0KB, 2KB-1]中,X和Y中的所有key調用FilterPolicy::CreateFilter()的計算結果都將生產到同一個filter中,而且該filter是filter block的第一個filter。

Filter block也是一個block,其格式遵從block的基本格式:|block data| type | crc32|。其中block dat的格式如圖8.4-1所示。

圖8.4-1 filter block data

瞭解了格式,再分析構建和讀取filter的代碼就很簡單了。

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