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

项目是按照“Tair LDB基于Prefixkey的范围查找性能优化项目提议方案”的步骤一步步完成的,目前方案中提出的三个重点问题已经全部解决,如下所示:


虽然已经使用prefix bloomfilter对prefix key进行过滤来提升范围查找的性能,经过实验测试也达到了一定的效果。但是这个问题的解决方案又导致了新问题的产生,即产生了下面两个问题:

问题1. ldb_instance.cpp(客户端和ldb底层交互的接口)中还有类似begin_scan(扫描某一个bucket)的接口。这些scan的key是没有prefixkey的,且编码信息也不完整(并非是前几次文章所说的前面9字节元信息和area信息编码+后面8字节sequence和valueType编码)。如果这时候也按之前的方法提取prefix key必然会产生错误,而且这种情况也不需要prefix key和bloomfilter,因此需要这块需要考虑。

问题2. delete的时候也需要带入prefix的相关信息。 如果不带,那么在get_range的时候,可能会出现已经delete了,但是依然被扫描出来了。 这个怎么理解呢?一开始我也没搞明白,因为觉得delete操作是根据delete标记来判断是否查找到的数据是否是delete过的,如果是就不取就是了,跟prefix key没什么关系。后来在导师的指导下才彻底搞懂,原来自己忽略了一个非常重要的事实:delete和put相同的某个key可能存储在sst不同的block上,甚至在不同的sst文件里。下面是导师的指导:
1. bloomfilter是在sst的block中的。
2. prefix_put时,会在某个sst(比如B)中写入该prefixkey bloomfilter
3. prefix_remove的时候(即delete时),该delete key在其他sst的可能性非常大。 而其它sst中是没有该prefix key的bloomfilter。 而其它sst(比如A)一般会在低层level
4. get_range中用merge iterator遍历时,遍历到A时,先判断prefixkey bloomfilter,结果为无(因为A的prefix bloomfilter中没有存储delete key的prefix,因此这次判断实际上是错误的),则遍历B,判断prefixkey bloomfilter,结果为有。最终结果暴露了本该删除的数据


问题来了就要解决,下面是在导师指导及自己思考下提供的解决方案,方案不唯一,这里提供一个参考。

问题1解决方案

对于类似begin_scan里的key没有prefix的情况,需要在prefix bloomfilter过滤之前进行判断,如果是这种情况的key则不需要经过bloomfilter。对于这个问题,有两种解决方案:

方法1:通过每次操作的option传入参数,这里的option指的是ReadOptions,在里面添加参数bool use_prefix_bloom;默认值是true,然后在begin_scan等方法中在扫描之前设置该参数为false,下面是添加了use_prefix_bloom参数的ReadOptions:

// Options that control read operations
struct ReadOptions {
  // If true, all data read from underlying storage will be
  // verified against corresponding checksums.
  // Default: false
  bool verify_checksums;

  // Should the data read for this iteration be cached in memory?
  // Callers may wish to set this field to false for bulk scans.
  // Default: true
  bool fill_cache;

  // If "snapshot" is non-NULL, read as of the supplied snapshot
  // (which must belong to the DB that is being read and which must
  // not have been released).  If "snapshot" is NULL, use an impliicit
  // snapshot of the state at the beginning of this read operation.
  // Default: NULL
  const Snapshot* snapshot;

  bool use_prefix_bloom;

  ReadOptions()
      : verify_checksums(false),
        fill_cache(true),
        snapshot(NULL),
        use_prefix_bloom(true){
  }
};

然后在begin_scan中设置use_prefix_bloom参数:

leveldb::ReadOptions scan_read_options = read_options_;
scan_read_options.fill_cache = false; // not fill cache
scan_read_options.use_prefix_bloom = false; // not use prefix bloomfilter

最后在使用prefix bloomfilter进行过滤之前进行判断即可,如下:

if (options_.use_prefix_bloom) {
    // 使用prefix bloomfilter进行过滤
}

具体怎么使用prefix bloomfilter进行过滤可以查看这篇文章:
Tair LDB基于Prefixkey的范围查找性能优化项目之如何使用prefix bloomfilter进行过滤

方法2:第二种方法是一种取巧的方法,前面说过key的编码格式,含有prefix_size的key或一般的key在经过编码之后至少不小于7(元信息) + 2(area信息) + 8(sequence/valueType信息)个字节的编码信息,而类似begin_scan里的key编码之后没有这么多信息,可以说是特殊的key,因此我可以通过判断key的大小是否超过了7 + 2 + 8个字节来决定是否使用prefix bloomfilter,如下:

if (target.size() > 7 + 2 + 8) {
    // 使用prefix bloomfilter进行过滤
}

两种方案经过测试都是正确的,导师建议“用确定的方式去实现(即第一种方法)而不是约定俗成的方式(即第二种方案)”,因此源码中采用第一种方案实现。

问题2解决方案

问题2需要在delete的时候也需要带入prefix的信息,这个问题解决的时间比较久,期间也理解错了一些东西,可能之前对Delete接口的研究不够深入。目前我的方案是这样的:

(1)添加了一个ValueType:kTypeDeletionWithPrefix,表示delete的时候携带Prefix key信息。之前的valueType有三个:kTypeValue ,kTypeDeletion,kTypeDeletionWithTailer,新的Type需要支持delete时候带入value,value中含有Prefix key信息。

(2)修改或添加DB存储层关于delete的一些函数
由于delete的过程和put的过程一样,只是valueType不同。首先都要将数据写入memtable,写入过程中调用的一系列delete基础实现需要修改。但为了不改动原有delete相关函数的实现,我都是通过新增另外的delete函数来实现带value的delete操作,也就是说所有实现Delete的地方,我都要增加相应的支持带value的Delete实现。

(3)修改客户端
客户端Delete操作的底层实现是LdbInstance::remove(),该函数首先会判断是否需要先从DB中查询key所对应的value,看DB中是否存在,如果存在就将查询到的value封装成LdbItem,并调用LdbInstance::do_remove()完成,如下:

 if ((db_version_care_ && version_care) || mtime_care)
 {    
   rc = do_get(ldb_key, db_value, true/* get from cache */,
              false/* not fill cache */, false/* not update cache stat */);
   ...

因为我们delete时候需要value信息,而这里又给我们查询到了,那么我们就可以使用这个value了。这样真的可以吗?
嗯,仔细的人会说不可以,
因为这里不一定会查询DB得到value,因为在查询之前有个if判断,既然是判断就有else的情况,如果是else不就不会从DB中查了吗?那还怎么得到value?有人可能会说直接把do_get()这句查询DB放到if条件外面不就行了,这样就保证每次都能查找到value了。嗯,这样的确可以,但仔细想一下,这样好吗?DB的底层查询操作是比较昂贵的操作,这涉及到效率和性能问题。再仔细一想,有必要获取完整的value吗?delete操作所需的value只要包含prefix size信息即可,并不需要完整的value。由于这里prefix size信息是已知的,而通过前面的分析知道在从value中提取prefix size信息时只需要用到value前面的ldb_meta_item部分,因此我们完全可以根据ldb_meta_item和prefix_size信息构造一个无真实value信息的只含有meta数据(内含prefix_size)的ldb_item,从而不需要读取DB,减少了开销。

  // construct a ldb item only contains meta data
  LdbItem ldb_meta_item;
  ldb_meta_item.meta().base_.meta_version_ = META_VER_PREFIX;
  ldb_meta_item.meta().base_.flag_ = TAIR_ITEM_FLAG_NEWMETA;
  ldb_meta_item.set_prefix_size(key.get_prefix_size());
  ldb_meta_item.set("", 1);

然后再调用具体的do_remove()方法完成delete操作,源码中do_remove()方法没有携带value信息,这样在接下来调用db的Delete操作也不能带value信息。因此我这里修改了do_remove()函数,使其带有参数value,方便后面操作。

LdbInstance::do_remove(LdbKey& ldb_key, LdbItem& ldb_item, bool synced, 
    tair::common::entry_tailer* tailer)

最后在调用db底层的Delete操作之前判断ldb_item里是否设置了key的prefix_size信息,如果设置了则调用我们添加的带value支持的Delete操作,如果没有设置(即prefix size为0)则继续调用原来的Delete操作。

if(ldb_item.prefix_size() > 0) 
{                                                                       
  status = db_->DeleteWithPrefix(write_options_, leveldb::Slice(ldb_key.data(), ldb_key.size()), 
     leveldb::Slice(ldb_item.data(), ldb_item.size()), synced);
}
else 
{
  // 继续调用原来的Delete操作
}

到此Delete问题就基本解决了。

后记

整个项目下来,问题多多,但有问题不是难事,有问题没解决方案才是真正的难题。好在这个项目的几个提案步骤及后续产生的问题都基本解决,也许解决方案不是最好的,但至少是解决了,而且解决问题的过程收获了很多东西,不仅是技术上的,在项目整体架构上也有了一个更清晰的认识。如果后续能想到更好的解决方案或经测试发现新的问题,我都会去及时加以解决。

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

上一篇:ZYNQ xilinx_axienet CPU软中断占用过高问题解决记录


下一篇:.NET Core:处理全局异常