Leveldb BloomFilter 解析
一个设计的非常巧妙的数据结构
BloomFilter 原理
布隆过滤器由巴顿布隆于1970年提出,由一个很长的bit数组以及一系列hash函数组成。bloomfilter可以用于检索一个元素是否出现在一个集合中,bloomfilter的优点是相比hash表拥有极大的空间效率,缺点是会出现一定的错误概率(False positive,一个不在集合中的元素被误认为处于集合中)。
bloomfilter 的原理是,当一个元素在加入集合时,通过k个hash 函数映射到bit数组的k个位置,并将相应的位置置1。在查找一个元素是否存在于集合中时,使用相同的k个hash 函数查看bit数组中的位置是否为全为1,如果出现一个0,则该key 一定不存在集合中,否则有可能出现在集合中。
优点
Bloomfilter 可以使用较少的空间存储大量数据的全集,并且存储的不是每个元素的原本的数值,只是设置对应的bit位,一定程度上实现了加密的效果。除空间效率之外,bloomfilter的时间效率都是常数级别O(k),其中k 代表使用的hash 函数个数,由于k个hash 函数式相互独立的性质,在进行k个hash 计算时可以并行计算进一步加速插入查找。
缺点
Bloomfilter 的缺点是会出现False positive 的误判率,而且随着元素的增加,误算率也随之增加。因此尽可能降低误判率需要一些额外工作。
BloomFilter参数选择
以下均参考wikipedia bloomfilter 中关于误差率的计算章节。直接说结论,当k(hash 函数个数),m(bit数组大小),n(插入元素个数)满足下式的时候,可以保证最低的误差率。$$k=\cfrac{m}{n}ln2$$下图(摘自wikipedia) 表示在最优hash函数个数的情况下,不同m,n之间误差率关系。
从上图可以看到当存储10亿个元素时使用4GB的存储空间可以保证不到1e-06的错误率。可以看到bloomfilter在实现高空间利用率的同时可以保证较低误差率。
Leveldb 实现
leveldb bloomfilter 实现在util/bloom.cc
成员变量以及构造函数
class BloomFilterPolicy : public FilterPolicy {
private:
// 平均每个key拥有的bit 数目
size_t bits_per_key_;
// hash function 的个数
size_t k_;
public:
explicit BloomFilterPolicy(int bits_per_key)
: bits_per_key_(bits_per_key) {
// We intentionally round down to reduce probing cost a little bit
// 按照上面的公式设定hash函数的个数
k_ = static_cast<size_t>(bits_per_key * 0.69); // 0.69 =~ ln(2)
if (k_ < 1) k_ = 1;
if (k_ > 30) k_ = 30;
}
};
CreateFilter
将传入的n个key 存储到bloomfilter 中,bloomfilter结果使用string存储。
virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// Compute bloom filter size (in both bits and bytes)
// bloomfilter 需要多少bit
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.
if (bits < 64) bits = 64;
// 对齐,方便内存读写以及后续位置索引
size_t bytes = (bits + 7) / 8;
bits = bytes * 8;
// 在string 中分配空间
const size_t init_size = dst->size();
dst->resize(init_size + bytes, 0);
// string 的最后一个byte存储使用的hash 函数的个数
dst->push_back(static_cast<char>(k_)); // Remember # of probes in filter
// 获得string内部的char 型数组
char* array = &(*dst)[init_size];
// 逐个将每个key 写入bloom fliter
for (int i = 0; i < n; i++) {
// Use double-hashing to generate a sequence of hash values.
// See analysis in [Kirsch,Mitzenmacher 2006].
// leveldb 使用一个hash 函数,每次对hash值向右循环移位17个bit来模拟实现多个hash 函数的效果
uint32_t h = BloomHash(keys[i]);
// 每次向右循环移位17个bit
const uint32_t delta = (h >> 17) | (h << 15); // Rotate right 17 bits
for (size_t j = 0; j < k_; j++) {
// 在整个bit 数组的位置
const uint32_t bitpos = h % bits;
// 在char型数组的位置
array[bitpos/8] |= (1 << (bitpos % 8));
// 更新获得一个新的hash 数值
h += delta;
}
}
}
KeyMayMatch
判断一个 key 在bloomfilter中是否存在。
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();
// 最后一个byte数值代表使用了多少hash函数
// 除最后一个byte之外代表bit数组
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];
if (k > 30) {
// Reserved for potentially new encodings for short bloom filters.
// Consider it a match.
return true;
}
// 使用相同的方法模拟多个hash函数计算的hash值
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;
// 找到一个bit位置不匹配,提前返回false
// 在bit数组中位置的索引和设置时的方法一致
if ((array[bitpos/8] & (1 << (bitpos % 8))) == 0) return false;
// 更新获得下一个hash value
h += delta;
}
// 全部匹配return true
return true;
}
BloomFilter 应用场景
由于其高效的空间效率,bloomfilter 可以应用于以下场景:
- 爬虫系统记录以经爬取过的url
- 垃圾邮件过滤
- p2p 网络中查找资源操作: 使用一个bloomfilter 保存拥有此资源的网络通路
- 广播信息时检查某个ip是否发包
- 字典纠错:将所有单词存储到bloomfilter中,如果不存在则认为是一个错误拼写
- CDN 代理缓存: 每个cache 服务器上使用bloomfilter 存储兄弟cache 服务器上是否有缓存关键字,如果没有则可以避免一次查找
总结
Bloomfilter 是一种设计巧妙的数据结构,由于其良好的空间效率,可以用于判断一个元素是否包含于海量元素集合的场景。
Leveldb 的实现的bloomfilter 可以灵活配置hash 函数的个数,使用一个hash 函数模拟任意多个hash 函数的场景,并将使用hash函数的个数存储到bloomfilter编码结果中。除此之外,leveldb bloomfilter 实现bit数组个数,hash函数个数以及存储元素个数的最优配置,保证最低的误差率。