8 FilterPolicy&Bloom之1
8.1 FilterPolicy
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將元素映射到一個長度爲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參數
變量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
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
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格式
如果打開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的代碼就很簡單了。