[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...

相关推荐
程序员岳焱2 小时前
Java 与 MySQL 性能优化:Java 实现百万数据分批次插入的最佳实践
后端·mysql·性能优化
麦兜*3 小时前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
大只鹅3 小时前
解决 Spring Boot 对 Elasticsearch 字段没有小驼峰映射的问题
spring boot·后端·elasticsearch
ai小鬼头3 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
IT_10244 小时前
Spring Boot项目开发实战销售管理系统——数据库设计!
java·开发语言·数据库·spring boot·后端·oracle
bobz9654 小时前
动态规划
后端
stark张宇4 小时前
VMware 虚拟机装 Linux Centos 7.9 保姆级教程(附资源包)
linux·后端
亚力山大抵5 小时前
实验六-使用PyMySQL数据存储的Flask登录系统-实验七-集成Flask-SocketIO的实时通信系统
后端·python·flask
超级小忍5 小时前
Spring Boot 中常用的工具类库及其使用示例(完整版)
spring boot·后端
CHENWENFEIc6 小时前
SpringBoot论坛系统安全测试实战报告
spring boot·后端·程序人生·spring·系统安全·安全测试