Tair LDB基于Prefixkey的范围查找性能优化项目之如何使用prefix bloomfilter进行过滤

项目是按照“Tair LDB基于Prefixkey的范围查找性能优化项目提议方案”的步骤一步步完成的,目前已经解决了前面两个问题:

今天来继续解决最后一个关键问题。在提案中有以下描述:

  1. 在get_range接口中,如果查找到了sstable这里(先查memtable和immutable memtable,两者没有磁盘IO操作),

(1)首先根据[pkey+skey,pkey+end]的范围查找可能的sstfiles。

(2)对于每一个file,对dataindex block里的信息继续进行范围查找,找到可能包含[pkey+skey,pkey+end]这个范围的blocks。

(3)在读每一个block之前,获取filter block中存储的filter,通过prefix的MayMatch方法判断该block是否包含前缀pkey,如果不包含,则直接跳过这个block,这样就通过prefix bloomfilter实现了block过滤,从而减少了不必要的磁盘IO操作。

从编码角度讲,这里有很多待解决的关键问题,下面列举几个:

如何正确定位“读每一个block之前”?
如何获取filter block中事先存储的prefix bloomfilter?
如何正确使用prefix bloomfilter?
……

首先解决的是第一个问题:get_range的过程中什么时候开始读block,我们要在读之前进行拦截,加上bloomfilter过滤代码。

由于get_range中key的查找遍历在存储层面上统一通过Iterator的方式处理。其查找过程为:memtable Iterator —> sstable Iterator —> block Iterator。我们需要在block Iterator之前使用prefix bloomfilter以实现block过滤。block Iterator的具体调用在sstable Iterator最后,由two_level_iterator.cc的Seek()函数实现,如下:

void TwoLevelIterator::Seek(const Slice& target) {
    index_iter_.Seek(target);
    InitDataBlock(target);
    if (data_iter_.iter() != NULL) {
        data_iter_.Seek(target);
    }
    SkipEmptyDataBlocksForward();
}

具体流程为:
a) index_iter_.Seek(),得到index_iter_.Value(),即key所在data的index信息data_block_handle_。
b) InitDataBlock(),根据index_block_handle_,调用hook函数,获得对应data的data_iter_。
c) data_iter_->Seek(),定位到要找的key。
d) SkipEmptyDataBlocksForward(),如果获得的data_iter是无效的,那么需要不断尝试下一个data并定位到其最开始(已经满足Seek条件),直到找到合法的data。

一开始我的想法是:block Iterator的获得在b步骤中,我们需要在其获得之前使用prefix bloomfilter过滤一下,因此我在a和b步骤之间添加了下面过滤语句:

if (index_iter_.Valid()) {
    Slice handle_value = index_iter_.value();
    const char* key_data = target.data();
    const char *ptr = strchr(key_data + LDB_KEY_META_SIZE + 2, 0);
    int prefix_size = ptr + 1 -  key_data;
    char* prefix_key = new char[prefix_size + 8];
    memcpy(prefix_key, key_data, prefix_size);
    memcpy(prefix_key + prefix_size, key_data + target.size() - 8, 8);
    BlockHandle handle;
    if (filter_ != NULL && handle.DecodeFrom(&handle_value).ok()
        && !filter_->KeyMayMatch(handle.offset(), Slice(prefix_key, prefix_size + 8))) {
        // Not found, prefix bloomfilter hit
    } else {
        // prefix bloomfilter miss
        InitDataBlock();
        ……
}

经过验证的确可以在某个时刻进行bloomfilter过滤,因为当prefix key不存在时,会发生“prefix bloomfilter hit”。但后来经过大量数据的测试发现有个问题:

  • filter block时而获取到,时而获取不到,获取不到时还是会进行block reader。

这是什么原因呢?一开始这个问题我是这么想的:
TwoLevelIterator是通过NewTwoLevelIterator()构造的,而构造该Iterator的地方有两个:
(a)DBIter的构造过程里关于sstable的Iterator建立(Version::AddIterators())
- level-0中所有sstable的iterator (TableCache::NewIterator(),作为单个sstable iterator的TwoLevelIterator)
- 每个非level-0的leve上sstable集合iterator((VersionSet::NewConcatenatingIterator(), 作为sstable集合 iterator的TwoLevelIterator)

其中level-0上的TwoLevelIterator最终通过在Table::NewIterator()(调用NewTwoLevelIterator()返回)中构造,而Table中有filter可供使用,因此我可以在Table::NewIterator()中通过NewTwoLevelIterator()调用将filter参数传递给TwoLevelIterator,方便在Seek中使用。而对于非level-0的sstable构造出的所有TwoLevelIterator没有可用的filter 参数,因此在level-0上能够命中,在非level-0上却不能命中(因为获取不到filter)。

(b)Compaction过程:VersionSet::MakeInputIterator()
- 对level-0的每个sstable,构造出对应的iterator:TwoLevelIterator(TableCache::NewIterator())。
- 对非level-0的sstable构造出sstable集合的iterator:TwoLevelIterator (NewTwoLevelIterator()) 与(a)同,需要解决同样的问题。

后来再仔细琢磨这个问题,发现自己理解有误,实际上level-0和非level-0上的TwoLevelIterator都调用了Table::NewIterator(),只是后一个调用的时间比较晚且是通过hook函数回调执行的,不容易发现。而这个hook函数的执行在TwoLevelIterator::InitDataBlock()方法中,从上面代码可以看出我的filter获取和过滤步骤都在InitDataBlock()方法之前进行,这时候可能还没获取到filter,当然会出现filter为NULL的情形了。那么就需要研究InitDataBlock()方法,并在hook函数执行之后根据返回值判断来获取相应的filter,该方法如下:

void TwoLevelIterator::InitDataBlock() {
  if (!index_iter_.Valid()) {
    SetDataIterator(NULL);
  } else {
    Slice handle = index_iter_.value();
    if (data_iter_.iter() != NULL && handle.compare(data_block_handle_) == 0) {
      // data_iter_ is already constructed with this iterator, so
      // no need to change anything
    } else {
      Iterator* iter = (*block_function_)(arg_, options_, handle);
      data_block_handle_.assign(handle.data(), handle.size());
      SetDataIterator(iter);
    }
  }
}

这里的block_function_就是具体要调用的hook函数,而且这里的block_function_并不是确定的某个函数,而是根据执行情况动态确定,经过调试发现block_function_主要调用了以下两个函数:

(1)table.cc中的Iterator* Table::BlockReader()方法,目的是为了根据index_block_handle_获取data的data_iter_,这一步的block reader是少不了的,因为你在用prefix bloomfilter过滤prefix key之前肯定要知道过滤的是哪个block,这个block的iterator即位置首先要找到,这就需要进行一次block reader。
(2)two_level_iterator.cc中的Iterator* NewTwoLevelIterator()方法,这是构造具体的TwoLevelIterator,block iterator就是通过TwoLevelIterator的相关函数来获得的,也是第一步要做的事情(没有TwoLevelIterator后面就不能获得data_iter_及进行查找key了)。

这里需要关心的就是当block_function_为NewTwoLevelIterator()方法时,在该方法执行过后我们就能获取filter了(从Table::NewIterator()传递过来的),而且filter的获取是在具体查找key(要进行block reader)之前,也就是可以在查找key之前进行prefix bloomfilter过滤了。这个过滤代码既可以放在InitDataBlock()方法之内也可以放在Seek()函数中InitDataBlock()方法之后。从之前写的过滤代码可以看出,如果放在Seek()中,很多操作都和InitDataBlock()方法中的操作重复了,比如判断index_iter_的有效性、获取handle等,重复执行这些操作必然影响效率。因此更好的做法是将filter的获取和过滤代码都放在InitDataBlock()方法里,如下所示:

void TwoLevelIterator::InitDataBlock(const Slice& target) {
  if (!index_iter_.Valid()) {
    SetDataIterator(NULL);
  } else {
    Slice handle = index_iter_.value();
    if (data_iter_.iter() != NULL && handle.compare(data_block_handle_) == 0) {
      // data_iter_ is already constructed with this iterator, so
      // no need to change anything
    } else {
      Iterator* iter = (*block_function_)(arg_, options_, handle);
      if(typeid(*iter) == typeid(TwoLevelIterator)) {
          filter_ = dynamic_cast<TwoLevelIterator*>(iter)->filter_;
      }
      if (options_.use_prefix_bloom) {
        BlockHandle block_handle;
        if (filter_ != NULL && block_handle.DecodeFrom(&handle).ok()) {
            const char* key_data = target.data();
            const char *ptr = strchr(key_data + 9, 0);
            int prefix_size = ptr + 1 -  key_data;
            if(prefix_size > 0) {
                char* prefix_key = new char[prefix_size + kInternalKeyBaseSize];
                memcpy(prefix_key, key_data, prefix_size);
                memcpy(prefix_key + prefix_size, key_data + target.size() - kInternalKeyBaseSize, kInternalKeyBaseSize);
                if(!filter_->KeyMayMatch(block_handle.offset(), Slice(prefix_key,
                  prefix_size + kInternalKeyBaseSize))) {
                    // Not found. prefix bloomfilter hit...
                    SetDataIterator(NULL);
                    delete[] prefix_key;
                    return;
                }
            }
        }
      }
      data_block_handle_.assign(handle.data(), handle.size());
      SetDataIterator(iter);
    }
  }
}

这里需要注意两个问题:
1. 在提取prefix key的过程中采取了一种取巧的方式而并不是通过PrefixFetcher类提取的。因为观察到真实key如果其中含有prefix key的话,那么prefix key和suffix key两部分不是直接连在一起的,而是通过一个‘\0‘连在一起的,因此我这里可以直接查找真实key中\0所在位置从而提取出\0之前的prefix key部分,这个方法可能不太好,但测试时没发现什么问题。

2. 这里的9之前讨论过,是元信息+area信息的长度,本类是写在option.h中,由于这里不能获取option(不能获取option也是上面为啥没有用PrefixFetcher提取prefix key的原因,因为PrefixFetcher类的工具也是写在option中),所以这里暂时直接用9代替,这也是个待改进的地方。

时间: 2024-11-08 23:12:02

Tair LDB基于Prefixkey的范围查找性能优化项目之如何使用prefix bloomfilter进行过滤的相关文章

Tair LDB基于Prefixkey的范围查找性能优化项目之如何建立prefix bloomfilter

项目是按照"Tair LDB基于Prefixkey的范围查找性能优化项目提议方案"的步骤一步步完成的,上次解决了如何获取key的prefix_size问题"Tair LDB基于Prefixkey的范围查找性能优化项目之如何提取key的prefix_size".今天来继续解决第二个问题.在提案中有以下描述: 提取到prefix_size信息后,我们对所有的keys实现prefix bloomfilter,为了实现简单,我们可以在原有的filter block里加入新的

Tair LDB基于Prefixkey的范围查找性能优化项目测试及完成总结报告

项目这周就截止了,这算是我第一个有导师指导的真正意义上的C++项目,项目基本完成,想要实现的功能也已经实现,并做了大量的性能测试.不过这对于业界来说,可能完成的还不够成熟,还有许多待改进的地方,还不能马上投入使用,还需要进行严格的考验,毕竟tair的应用场景太重要了,不容一丝疏忽.但于我个人而言,帮助还是挺大的,不仅是多了一次有价值的项目经验,更是学到了一些项目之外的东西,比如计划的重要性,惰性的控制,时间的分配管理(找工作与项目进度产生冲突)等.好了,不多说了,在这最后一篇总结报告里首先给出性

Tair LDB基于Prefixkey的范围查找性能优化项目之后续问题解决记录

项目是按照"Tair LDB基于Prefixkey的范围查找性能优化项目提议方案"的步骤一步步完成的,目前方案中提出的三个重点问题已经全部解决,如下所示: 如何获取key的prefix_size问题:Tair LDB基于Prefixkey的范围查找性能优化项目之如何提取key的prefix_size 如何建立prefix bloomfilter:Tair LDB基于Prefixkey的范围查找性能优化项目之如何建立prefix bloomfilter 如何在get_range过程中使用

Tair LDB基于Prefixkey的范围查找性能优化项目之如何提取key的prefix_size信息

New Document/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */ /* Author: Nicolas Hery - http://nicolashery.com */ /* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */ /* Source: https://github.com/nicolahery/markdownpad-github */ /*

Tair LDB基于Prefixkey的范围查找性能优化项目中期总结

"Tair LDB基于Prefixkey的范围查找性能优化"这个项目刚好进行了一个月,这一个月主要是熟悉项目.掌握项目和提出设计方案的过程,下面从几个方面总结下个人在该项目上所做的工作及自己的个人所得所感. 项目工作简单总结 下面是对阶段性的成果进行总结,并附有每个阶段的总结报告. 1. 项目实施计划的确定 不管什么类型的项目(大.小,难.易),在项目开展之前都应该有个可实施的计划,一方面能够确保项目的进度,另一方面也能防止有些人三天打鱼两天晒网的心态.在导师的细心指导下,我们确定了下

Tair LDB基于Prefixkey的范围查找性能优化项目提议方案

基于prefix bloomfilter的过滤思想和get_range接口数据的特点,在导师的指导下,提出如下的简单方案,对get_range接口的范围查找过程进行优化,使得能够根据prefix进行过滤,减少无效的磁盘IO. 待优化接口 int get_range(int area, const data_entry &pkey, const data_entry &start_key, const data_entry &end_key, int offset, int limi

手机淘宝 521 性能优化项目揭秘

又是一年双十一,亿万用户都会在这一天打开手机淘宝,高兴地在会场页面不断浏览,面对琳琅满目的商品图片,抢着添加购物车,下单付款.为了让用户更顺畅更方便地实现这一切,做到“如丝般顺滑”,双十一前夕手机淘宝成立了“521”(我爱你)性能优化项目,在日常优化基础之上进行三个方面的专项优化攻关,分别是1)H5页面的一秒法则:2)启动时间和页面帧率提升20%:3)Android内存占用降低50%.优化过程中遇到的困难,思考后找寻的方案,实施后提取的经验都会在下面详细地介绍给读者. 第一章 一秒法则的实现 “

性能优化项目的总结

在性能优化项目中,我只是一个协助参与的角色,但也正好给了我从外部参看项目运作的机会,需要优化的系统已经是运行了6年的老系统,数据从来没有做过分离,涉及全库查询等致命的优化问题.另外本次项目的业主也希望对优化工作进行指导,造成走了不少弯路,同时由于垂直数据库技术不足,从外部找了合作伙伴进行深入性能优化研究.总之这个项目虽小,但具备了复杂项目的各方面的内容,我也将会对这个项目进行初步的分析. 基础方向 SQL优化 首先要做的就是找出执行慢的SQL或业务模块,调查SQL的业务逻辑是否存在可优化的空间.

201506150846_《JavaScript权威指南(第六版)——性能优化10条、小写转大写、过滤、函数》(P162-168)

一. 权威指南 1. 对于类数组对象,我们不能用数组方法,但是我们可以用 Function.call(); 例如: Array.prototype.slice.call(arr,...); Array.prototype.silice.call(arr,...); Array.prototype.map.call(arr,...); 二. 性能优化10条 1.  var someId = document.getElementById('#someElem');  改成:  var docu =