leveldb源码剖析---filter block

本文详细剖析了leveldb的filter block,解释了其作为metadata block的角色,用于快速判断key是否存在于data block中。FilterBlockBuilder的AddKey、StartBlock和Finish函数是构建filter block的核心,其中GenerateFilter函数生成过滤器。filter block的每个条目对应2kb大小的数据块范围,通过filter可以高效过滤key是否存在。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面我们分析了leveldb中的index block和data block。这里主要是分析filter block。filter block属于一种metadata block,它的构造主要由FilterBlockBuilder完成。filter block的作用就是提供快速判断一个key是否存在于某个datablock中,以加速查找的过滤器。

回忆在分析TableBuilder的Add函数时,每次向sstable中添加一个key-value,都会执行

  if (r->filter_block != NULL) { 
    r->filter_block->AddKey(key);
  }

以将key加入到filter block中。从判断语句我们也可以看到,filter block是可选项,不是必须要有的。

而且每写完一个data block,并在TableBuilder中的Flush函数中将data block写盘时,都会执行

  if (r->filter_block != NULL) { 
    r->filter_block->StartBlock(r->offset);
  }

而且在写完整个sstable时,还会在TableBuilder.Finish函数中执行以下代码

  if (ok() && r->filter_block != NULL) {
    WriteRawBlock(r->filter_block->Finish(), kNoCompression,
                  &filter_block_handle); 
  }

当然,这里的Finish函数,根据前面的分析可以知道,它返回一个字符串,供TableBuilder写盘。本文的主要重点就是这个字符串是怎么生成的,它具有什么样的格式。

所以我们可以看出构建filter block的核心函数是Addkey,Startblock,Finish。下面我们将逐个分析这些函数的源代码


FilterBlockBuilder.AddKey函数

void FilterBlockBuilder::AddKey(const Slice& key) {
  Slice k = key;
  start_.push_back(keys_.size());
  keys_.append(k.data(), k.size());
}

这里面的keys就是一个字符串,start_是一个整形数组。所以加入一个key就是把这个key append到keys字符串中,在此之前记录该字符串未加入key时的长度。这里的keys和start_都是类成员数据。


FilterBlockBuilder::StartBlock函数

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();
  }
}

其中kFilterBase是2的11次方,表示2kb。
block_offset是data block在sstable中的偏移位置。这个看TableBuilder.Flush函数就可以清楚知道。

所以从计算filter index我们可以看到:
并不是每个data block严格对应filter block中的一项。我们可以理解为对于filter i,它对应block offset在[i*2kb ~ (i+1)*2kb]中的所有block的key,所以可能有多个block

下面我们看一下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;
  }

前面我们讲过start_中存储的是keys的长度,每次向keys字符串中新加入一个key就更新以下start_中当前keys的长度,我们可以用下图形象表示:


这里写图片描述

start_[0]表示原始keys的长度,start_[1]表示加入key1后keys的长度,以此类推。所以如果num_keys==0,则说明当前keys中没有key,则直接返回,这里的 filter_offsets_.push_back表示直接用上一次生成的filter作为过滤器。

  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);
  }

理解了上面的keys和start_,我们就可以很好理解这部分的代码了。无非就是将keys中的key分别放入tmp_keys_数组中。我们的filter就是根据这些key生成的。

 filter_offsets_.push_back(result_.size());
  policy_->CreateFilter(&tmp_keys_[0], static_cast<int>(num_keys), &result_);

这就是生成过滤器的算法了。leveldb中提供了几种生成过滤器的算法,我们这里不深究。只需要看到,CreateFilter函数根据tmp_keys_生成一个过滤器,过滤器就是一个字符串,并将过滤器append到result中。所以result具有如下形式:


result由filter拼接而成

这里的result是一个字符串

  tmp_keys_.clear();
  keys_.clear();
  start_.clear();

最后面就是收尾了,从start_clear()和keys.clear()我们可以看到,每个filter所用的key都不会有重叠。

综上,start_block函数本质上就是根据keys_中的key生成一个filter,filter是一个字符串,并将这个filter拼接到result中。所以,所有的filter都会在result中,filter_offsets_数组则是记录各个filter在result中的偏移量。比如filter[i]在result中的偏移量为filter_offsets_[i],它对应的是所有block offset在[i*2kb ~ (i+1)*2kb]中的block包含的key。


FilterBlockBuilder::Finish

Finish函数和其他block一样,将会返回一个string给上层的TableBuilder,供TableBuilder写入磁盘。这里看一下filter block所代表的字符串是什么样子的

Slice FilterBlockBuilder::Finish() {
  if (!start_.empty()) {
    GenerateFilter();
  }

如果start_不是空,则再为结尾这些残余key生成一个filter。

  const uint32_t array_offset = result_.size();
  for (size_t i = 0; i < filter_offsets_.size(); i++) {
    PutFixed32(&result_, filter_offsets_[i]); 
  }

这里将filter_offsets_数组中的每一项拼接到filter数组result_中。

  PutFixed32(&result_, array_offset);
  result_.push_back(kFilterBaseLg);  // Save encoding parameter in result,    void push_back (char c); 
  return Slice(result_);

这里将这个filter block包含的所有filter的总字节数(array_offset)拼接到result中,通过它可以找到offset数组在filter block中的偏移位置。最后把kFilterBaseLg参数拼接进去,前面我们经常看到的2kb就是通过这个参数计算来的,我们可以通过改变这个参数来调节每个filter 对应的block offset区间。

最后即使返回result字符串供TableBuilder写盘了。

综上我们可以看到filter block的空间布局如下所示:


这里写图片描述


总结
filter block中的filter i对应block offset在{i*2kb ~ (i+1)*2kb}中的所有block包含的key。通过这个filter我们可以快速判断给定的一个key是否在这些block中。从filter block的组织中,可以知道,我们可以通过offset of filter array找到filter的偏移数组,通过这些偏移数组,我们可以找到具体的filter。本文没有介绍具体的filter生成算法,我们只简单地认为一个生成过滤器的算法将为一组key生成一个字符串。后面有机会再介绍,比如布隆过滤器等,还是挺有趣的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值