[LevelDB]Block系统内幕解析-过滤块(Filter Block)

Block的基本信息

Block 是组成SSTable文件中的基本单元,主要有以下类型

  1. 数据块(Data Block):存储实际的键值对数据,按键排序并使用前缀压缩减少空间占用。
  2. 过滤块(Filter Block):包含布隆过滤器,用于快速判断一个键是否可能存在,避免不必要的磁盘读取。
  3. 元数据块(Meta Block):存储关于SSTable文件的额外元数据信息,如统计数据或特定功能的配置。
  4. 元数据索引块(Metaindex Block):保存指向各个元数据块的索引,方便查找特定类型的元数据。
  5. 索引块(Index Block):存储数据块的索引信息,记录每个数据块的最大键和偏移量,用于定位特定键所在的数据块。

核心逻辑

目的:让Table在读取对应的data block,能低成本的skip掉一些数据块,从而降低整个系统磁盘IO的开销。类似将所有的key使用64位来做一个指纹出来,然后每次对数据块开始检索前,先匹配指纹,减少开销。

核心逻辑-布隆过滤器-CreateFilter&KeyMayMatch

以上的所有逻辑都是职能逻辑,接下来,我们来看常规情况下在 leveldb中内置的key和特征值之间的映射生成和读取的逻辑。

  1. CreateFilter 对应上面的GenerateFilter
  2. KeyMayMatch 对应上面的KeyMayMatch

CreateFilter

  1. 计算布隆过滤器大小:根据键的数量(n)和每个键的位数(bits_per_key_)计算总位数
  2. 确保最小大小:如果计算出的位数小于64,强制设为64位,确保最低限度的有效性
  3. 计算字节数和位数:将位数转换为字节数,使用(bits + 7) / 8的整数除法技巧,然后重新校准位数
  4. 准备存储空间:调整目标字符串大小并初始化为0,同时存储探测次数k_供后续查询使用
  5. 循环处理每个键:遍历所有输入键,为每个键计算哈希值并设置对应的位
  6. 计算哈希值:使用BloomHash函数为当前键生成哈希值
  7. 计算哈希增量:通过位运算(h >> 17) | (h << 15)生成增量值,用于后续的双重哈希
  8. 循环设置位:对每个键设置k个位,使用模运算h % bits确定位置,然后通过位运算设置相应的位,每次迭代后哈希值加上增量值

源代码

cpp 复制代码
void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
    // Compute bloom filter size (in both bits and bytes)
    size_t bits = n * bits_per_key_;

    // 如果bits小于64,则强制设置为64, 至少保障64位来防止false positive
    if (bits < 64) bits = 64;
    // 这里有个trick, 为了避免浮点数除法, 使用(bits + 7) / 8 来计算bytes 这样只有整数除法
    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<char>(k_));  // 存入探测数量 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);  // 这里又是一个新trick, 使用位移操作来代替乘法 这样能够使增量与原哈希值差异大,但保持了良好的随机性
      for (size_t j = 0; j < k_; j++) {
        const uint32_t bitpos = h % bits;// 
        array[bitpos / 8] |= (1 << (bitpos % 8));
        h += delta;
      }
    }
  }

KeyMayMatch

本质上就是上述方法CreateFilter的逆运算,这里就直接放代码了,就不赘述了

cpp 复制代码
  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;

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

    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;// 如果位运算不一致就 直接返回false
      h += delta;
    }
    return true;
  }

举例说明

对外提供服务

  1. 开始查找键k
  2. 首先通过索引块找到可能包含目标键的数据块位置
  3. 过滤器是否存在 (filter != nullptr),如果数据库没有配置过滤策略,过滤器就不存在
  4. handle解析是否成功 (handle.DecodeFrom(...).ok()),确保从索引块中能正常解析出数据块的位置信息
  5. 键是否肯定不存在 (!filter->KeyMayMatch(...)),使用布隆过滤器检查键是否可能存在
  6. 如果三个条件都满足(过滤器存在、handle解析成功、键肯定不存在),则可以跳过数据块读取,直接返回Not Found, 否则,需要读取数据块并在数据块中搜索目标键

作用: 这种优化可以避免对不存在键的磁盘I/O操作,大幅提升查询性能 以下是源代码:

cpp 复制代码
Status Table::InternalGet(const ReadOptions& options, const Slice& k, void* arg,
                          void (*handle_result)(void*, const Slice&,
                                                const Slice&)) {
... // 逻辑省略
    FilterBlockReader* filter = rep_->filter;
    BlockHandle handle;
    // 1. filter != nullptr: 如果 db没有设计过滤策略就是nullptr 即:
    // 2. handle.DecodeFrom(&handle_value).ok(): 是否能够从索引块中正常解析出handle
    // 3. !filter->KeyMayMatch(handle.offset(), k))检查当前的数据块是否有可能存在对应的key
    // 组合起来就是为了判断当前的value 肯定不存在对应的数据块中
    if (filter != nullptr && handle.DecodeFrom(&handle_value).ok() &&
        !filter->KeyMayMatch(handle.offset(), k)) {
      // 通过 KeyMayMatch 方法提前判断不存在,直接跳过
    } else
    {
    ....// 遍历对应的数据块
    }
 ....// 逻辑省略
}

实现细节

基本数据结构

  1. FilterBlockBuilder:负责在写入阶段收集键并构建过滤器块 2. FilterBlockReader:负责在查询阶段解析过滤器块并快速判断键是否可能存在
调用入口GenerateFilter&&KeyMayMatch
  1. GenerateFilter: 根据当前的kv键来生成对应的方法
  2. KeyMayMatch:判断当前的块是否有可能存在对应的key
GenerateFilter
cpp 复制代码
void FilterBlockBuilder::GenerateFilter() {
...
  // ***************************************************************
  policy_->CreateFilter(&tmp_keys_[0], static_cast<int>(num_keys), &result_);         	
  // 这里是一个虚函数调用,如果正式工作就是具体的逻辑来进行调用,使用了策略模式进行实现
  // ***************************************************************
...
}

这里的核心方法是 CreateFilter 传入 将key打平的中间变量 tmp_keys ,然后将result_返回出来,result_保存将当前的所有key都使用hash压缩后的特征值

KeyMayMatch
  1. 计算过滤器索引:将数据块偏移量右移 base_lg_ 位(默认11位)得到对应的过滤器索引
  2. 验证索引有效性:检查索引是否在有效范围内(小于过滤器数量)
  3. 获取过滤器位置:从偏移量数组中解码出过滤器的起始位置和结束位置
  4. 检查范围有效性:确保过滤器的位置信息合理
  5. 使用布隆过滤器:对于有效的过滤器,调用布隆过滤器检查键是否可能存在

源代码

cpp 复制代码
bool FilterBlockReader::KeyMayMatch(uint64_t block_offset, const Slice& key) {
  uint64_t index = block_offset >> base_lg_;// 默认是 11位
  if (index < num_) {
    uint32_t start = DecodeFixed32(offset_ + index * 4);// 从当前的offset中解析出两个位置 一个是开头,另一个是结束
    uint32_t limit = DecodeFixed32(offset_ + index * 4 + 4);
    if (start <= limit && limit <= static_cast<size_t>(offset_ - data_)) {
      Slice filter = Slice(data_ + start, limit - start);
      //*******************************************************
      return policy_->KeyMayMatch(key, filter);// 使用 布隆过滤器来进行匹配
      //*******************************************************
    } else if (start == limit) {
      // 如果是空就没必要进行检索了
      return false;
    }
  }
  return true;  // 如果报错也是默认 是true,扫描当前的 data
}

policy_->KeyMayMatch(key, filter) 这个方法是核心方法,传入要匹配的key,和过滤区的内存指针,从而能够根据 filter 对key进行快速匹配

猜你喜欢

C++多线程: blog.csdn.net/luog_aiyu/a... 一文了解LevelDB数据库读取流程:blog.csdn.net/luog_aiyu/a... 一文了解LevelDB数据库写入流程:blog.csdn.net/luog_aiyu/a... 关于LevelDB存储架构到底怎么设计的:blog.csdn.net/luog_aiyu/a...

PS

你的赞是我很大的鼓励 我是darkchink,一个计算机相关从业者,加我免费领取计算机相关书籍和资料 vx 二维码见: www.cnblogs.com/DarkChink/p...

相关推荐
追逐时光者24 分钟前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友1 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧2 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧2 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧2 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
brzhang3 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang3 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
Roye_ack4 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis
AAA修煤气灶刘哥5 小时前
面试必问的CAS和ConcurrentHashMap,你搞懂了吗?
后端·面试
SirLancelot16 小时前
MinIO-基本介绍(一)基本概念、特点、适用场景
后端·云原生·中间件·容器·aws·对象存储·minio