目录
布隆过滤器原理
bloom filter的实现
bloom filter的使用
过滤器的格式
filter block的构造
使用过滤器的判别过程
在前面关于SST文件的章节中,我们提到SST文件中包含index block和data block,在查询数据中先利用index block定位到相应的data block,然后再到data block中去查询数据。但是如果该key在该data block中并不存在,此番查询便是浪费功夫。那有没有更快速地直接判断一个key在一个集合中是否存在、而无需遍历查询的方式呢?答案之一就是布隆过滤器。
相关代码文件:
- include/leveldb/filter_policy.h、util/filter_policy.cc:过滤器的抽象类接口
- util/bloom.cc:布隆过滤器的具体实现
- table/filter_block.h、table/filter_block.cc:读写filter block的类FilterBlockReader和FilterBlockBuilder
布隆过滤器用一个位图记录一个集合中元素的存在情况,该位图每一位的值只能为0或者1(好像说了句废话hhh anyway)。假设位图的大小为m,集合中的元素总数为n。
除此之外,布隆过滤器需要事先确定k个相互独立的哈希函数
位图的设置过程如下:每当有一个元素加入集合,该元素分别经过k个哈希函数的运算并将运算结果对m取模,得到k个下标,然后在位图中将这k个位置置为1
判断一个元素是否存在的逻辑:给定一个元素,依然用同样k个哈希函数运算得到k个下标,然后去看位图中k个下标处的值:
举个例子,如下图所示,假设位图大小m=10,元素个数n=3,哈希函数个数k=2。元素Peach因为计算出位置有0,说明该元素一定不存在;元素Grape虽然两个位置均为1,但是是虚警,其实元素并不存在
由此我们可以发现,布隆过滤器对于元素不存在的事实判断是准确的,但是对于元素的存在有虚警的情况。
那么虚警概率是多少呢?给定m,n和k,虚警概率的计算公式为:
可见,m越大、n越小、k越大,虚警概率就越低。
给定m和n,为了最小化虚警概率,k的取值应为:
布隆过滤器的实现在util/bloom.cc中的BloomFilterPolicy类
两个成员变量的含义分别为:
在构造函数中,可以看到给定m/n,哈希函数数目k被设置为能最小化虚警概率的m/n*ln2
class BloomFilterPolicy : public FilterPolicy {
public:
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(bits_per_key * 0.69); // 0.69 =~ ln(2)
if (k_ < 1) k_ = 1;
if (k_ > 30) k_ = 30;
}
void CreateFilter(const Slice* keys, int n, std::string* dst);
bool KeyMayMatch(const Slice& key, const Slice& bloom_filter);
private:
size_t bits_per_key_;
size_t k_;
}
BloomFilterPolicy类有两个核心方法:
下面看一下它们的实现逻辑:
BloomFilterPolicy::CreateFilter:
函数功能是:给定一组元素keys,计算出布隆过滤器位图并追加到已有的过滤器dst后面
大体逻辑就是遍历集合中的所有元素,利用哈希函数计算出需要置1的k个位图下标。这里采用了论文[Kirsch,Mitzenmacher 2006]的优化方法,可以只计算一次哈希函数并且利用移位操作得到k个下标。
需要注意要将bit下标转化为对于字符串的byte操作(第25行):bitpos是计算出的位图下标,bitpos/8是指该下标位于第几个字符,bitpos%8是指该下标在字符中的第几个bit
具体可以看下面的代码和注释
void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
// 计算位图的bit数
size_t bits = n * bits_per_key_;
// 如果位图位数太小,调大以降低虚警概率
if (bits < 64) bits = 64;
// 因为是用字符串来存,所以向上取到8的倍数
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(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) | (h << 15); // Rotate right 17 bits
for (size_t j = 0; j < k_; j++) {
const uint32_t bitpos = h % bits;
array[bitpos / 8] |= (1 << (bitpos % 8));
h += delta;
}
}
}
BloomFilterPolicy::KeyMayMatch
函数功能是:给定一个key,和布隆过滤器的位图,判断元素的存在与否
逻辑基本上就是CreateFilter函数的逆过程,
具体可以看下面的代码和注释
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;
// 从布隆过滤器的最后一个字符解码出哈希函数个数k
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;
}
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;
// 只要有一位为0就说明不存在,返回false
if ((array[bitpos / 8] & (1 << (bitpos % 8))) == 0) return false;
h += delta;
}
return true;
}
我们先介绍一下SST文件中和过滤器相关的模块,主要有两个
布隆过滤器的位图是写在SST文件中的filter block。这里用于构建filter block的工具类是FilterBlockBuilder。如果为SST文件维护一个全局的布隆过滤器,那么在保证较低的虚警率的情况下便需要一个较大的位图,因此LevelDB选择在一个SST文件内维护多个局部的布隆过滤器,事实上是每一个data block对应一个位图。
FilterBlockBuilder类的声明如下,主要成员变量的含义为:
class FilterBlockBuilder {
public:
void StartBlock(uint64_t block_offset);
void AddKey(const Slice& key);
Slice Finish();
private:
void GenerateFilter();
const FilterPolicy* policy_;
// 中间暂存每一轮次key的地方
std::string keys_; // Flattened key contents
std::vector start_; // Starting index in keys_ of each key
// 最终存放多个过滤器的地方
std::string result_; // Filter data computed so far
std::vector filter_offsets_;
std::vector tmp_keys_; // policy_->CreateFilter() argument
};
在TableBuilder::Add中,每次添加一个键值对到data block的同时,也会通过调用AddKey函数将该key添加至FilterBlockBuilder里
void TableBuilder::Add(const Slice& key, const Slice& value) {
...
if (r->filter_block != nullptr) {
r->filter_block->AddKey(key);
}
...
if (estimated_block_size >= r->options.block_size) {
Flush();
}
}
void FilterBlockBuilder::AddKey(const Slice& key) {
Slice k = key;
start_.push_back(keys_.size());
keys_.append(k.data(), k.size());
}
从FilterBlockBuilder::AddKey函数可以看到,AddKey只是把该key暂存到builder里,并没有真的构建过滤器,那么构建过滤器的时机是什么呢?答案在TableBuilder::Flush()里,当一个data block写满后,会调用Flush,而Flush函数里会调用StartBlock函数,如下:
void TableBuilder::Flush() {
...
if (r->filter_block != nullptr) {
r->filter_block->StartBlock(r->offset);
}
}
// Generate new filter every 2KB of data
static const size_t kFilterBaseLg = 11;
static const size_t kFilterBase = 1 << kFilterBaseLg;
void FilterBlockBuilder::StartBlock(uint64_t block_offset) {
uint64_t filter_index = (block_offset / kFilterBase);
assert(filter_index >= filter_offsets_.size());
while (filter_index > filter_offsets_.size()) {
GenerateFilter();
}
}
void FilterBlockBuilder::GenerateFilter() {
const size_t num_keys = start_.size();
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_.push_back(keys_.size()); // Simplify length computation
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_.
filter_offsets_.push_back(result_.size());
policy_->CreateFilter(&tmp_keys_[0], static_cast(num_keys), &result_);
tmp_keys_.clear();
keys_.clear();
start_.clear();
}
主要看一下GenerateFilter函数的逻辑:它会构造出这一轮暂存的元素集合,并且调用BloomFilterPolicy::CreateFilter接口计算出这一轮的布隆过滤器位图并追加到result_上
这里有一个比较迷惑的参数是kFilterBase,看起来它的本意是每攒2KB的数据就计算一轮新的布隆过滤器位图,然而事实上StartBlock的调用时机是随着TableBuilder::Flush的,也就是只有当一个data block被写满,才会发起一轮位图的计算
假设第一个data block的大小为6KB,那计算出来的filter_index是6/2=3,也就是下面GenerateFilter会循环3次,6KB数据都会被计算到第一轮的位图里,而后两次都是轮空。
但是在这种实现下,data block的offset和filter的index依然是可以严格对应起来的,所以没有问题。
filter block的落盘时机:在TableBuilder::Finish里
主要逻辑:
Status TableBuilder::Finish() {
...
BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;
// Write filter block
if (ok() && r->filter_block != nullptr) {
WriteRawBlock(r->filter_block->Finish(), kNoCompression,
&filter_block_handle);
}
// Write metaindex block
if (ok()) {
BlockBuilder meta_index_block(&r->options);
if (r->filter_block != nullptr) {
// Add mapping from "filter.Name" to location of filter data
std::string key = "filter.";
key.append(r->options.filter_policy->Name());
std::string handle_encoding;
filter_block_handle.EncodeTo(&handle_encoding);
meta_index_block.Add(key, handle_encoding);
}
// TODO(postrelease): Add stats and other meta blocks
WriteBlock(&meta_index_block, &metaindex_block_handle);
}
...
}
读取filter block分为两步,(1)从SST中读出metaindex block;(2)根据metaindex block中的handle读出filter block
读取过程的起点要追溯到读SST文件的地方,也就是Version::Get,调到TableCache::Get,
TableCache::Get里构造Table的时候,先读出footer、然后读出index block(和本节关联不大)。有一个关键是调用Table::ReadMeta,函数逻辑很简单,其实就是先把metaindex block读出来,然后根据里面的handle把filter block读出来,最后根据block contents构建FilterBlockReader
Status Table::Open(const Options& options, RandomAccessFile* file,
uint64_t size, Table** table) {
// 读取footer
*table = nullptr;
if (size < Footer::kEncodedLength) {
return Status::Corruption("file is too short to be an sstable");
}
char footer_space[Footer::kEncodedLength];
Slice footer_input;
Status s = file->Read(size - Footer::kEncodedLength, Footer::kEncodedLength,
&footer_input, footer_space);
if (!s.ok()) return s;
Footer footer;
s = footer.DecodeFrom(&footer_input);
if (!s.ok()) return s;
// Read the index block
BlockContents index_block_contents;
ReadOptions opt;
if (options.paranoid_checks) {
opt.verify_checksums = true;
}
s = ReadBlock(file, opt, footer.index_handle(), &index_block_contents);
if (s.ok()) {
// We've successfully read the footer and the index block: we're
// ready to serve requests.
Block* index_block = new Block(index_block_contents);
Rep* rep = new Table::Rep;
rep->options = options;
rep->file = file;
rep->metaindex_handle = footer.metaindex_handle();
rep->index_block = index_block;
rep->cache_id = (options.block_cache ? options.block_cache->NewId() : 0);
rep->filter_data = nullptr;
rep->filter = nullptr;
*table = new Table(rep);
//
(*table)->ReadMeta(footer);
}
return s;
}
然后从table里读取数据的函数是Table::InternalGet,我们来看一下这里和filter有关的逻辑
这里主要调用到了FilterBlockReader::KeyMayMatch
Status Table::InternalGet(const ReadOptions& options, const Slice& k, void* arg,
void (*handle_result)(void*, const Slice&,
const Slice&)) {
Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);
iiter->Seek(k);
// 找到key可能存在的data block
if (iiter->Valid()) {
Slice handle_value = iiter->value();
FilterBlockReader* filter = rep_->filter;
BlockHandle handle;
if (filter != nullptr && handle.DecodeFrom(&handle_value).ok() &&
!filter->KeyMayMatch(handle.offset(), k)) {
// Not found
}
...
}
}
FilterBlockReader::KeyMayMatch函数传入的offset参数指的是待查询的data block的起始偏移量,FilterBlockReader::KeyMayMatch会根据data block的偏移量计算出filter的index,然后从filter block里把相应的位图片段挑出来进行检测。