漫谈HBase Filter


                                                          初衷


对数据库来讲,知足业务多样化的查询方式很是重要。若是说有人设计了一个KV数据库,只提供了Get/Put/Scan这三种接口,估计要被用户吐槽到死,毕竟现实的业务场景并不简单。就以订单系统来讲,查询给定用户最近三个月的历史订单,这里面的过滤条件就至少有2个:1. 查指定用户的订单;2. 订单必须是最近是三个月的。此外,这里的过滤条件还必须是用AND来链接的。若是经过Scan先把整个订单表信息加载到客户端,再按照条件过滤,这会给数据库系统形成极大压力。所以,在服务端实现一个数据过滤器是必须的。数据库

除了上例查询需求,相似小明或小黄最近三个月的历史订单这样的查询需求,一样很常见。这两个查询需求,本质上前者是一个AND链接的多条件查询,后者是一个OR链接的多条件查询,现实场景中AND和OR混合链接的多条件查询需求也不少。所以,HBase设计了Filter以及用AND或OR来链接Filter的FilterList。bash

例以下面的过滤器,表示用户将读到rowkey以abc为前缀且值为testA的那些cell。框架

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new ValueFilter(EQUAL, 
     new BinaryComparator(Bytes.toBytes("testA"))))复制代码

实际上,FilterList内部的子Filter也能够是一个FilterList。例以下面过滤器表示用户将读到那些rowkey以abc为前缀且值为testA或testB的f列cell列表。模块化

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new FamilyFilter(EQUAL, new BinaryComparator(Bytes.toBytes("f"))),     
     new FilterList(MUST_PASS_ONE,      
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testA"))),     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testB")))));复制代码

所以,FilterList的结构实际上是一颗多叉树。每个叶子节点都是一个具体的Filter,例如PrefixFilter、ValueFilter等;全部的非叶子节点都是一个FilterList,各个子树对应各自的子filter逻辑。对应的图示以下:性能

固然,HBase还提供了NOT语义的SkipFilter,例如用户想拿到那些rowkey以abc为前缀但value既不等于testA又不等于testB的f列的cell列表,可用以下FilterList来表示:测试

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new FamilyFilter(EQUAL, new BinaryComparator("f")),     
     new SkipFilter(     
     new FilterList(MUST_PASS_ONE,     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testA"))),     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testB"))))));复制代码


                                                    实现


Filter和FilterList做为一个通用的数据过滤框架,提供了一系列的接口,供用户来实现自定义的Filter。固然,HBase自己也提供了一系列的内置Filter,例如:PrefixFilter、RowFilter、FamilyFilter、QualifierFilter、ValueFilter、ColumnPrefixFilter等。优化

事实上,不少Filter都没有必要在服务端从Scan的startRow一直扫描到endRow,中间有不少数据是能够根据Filter具体的语义直接跳过,经过减小磁盘IO和比较次数来实现更高的性能的。以PrefixFilter(“333”)为例,须要返回的是rowkey以“333”为前缀的数据。spa

实际的扫描流程如上图所示:设计

(1)碰到rowkey=111的行时,发现111比前缀333小,所以直接跳过111这一行去扫下一行,返回状态码NEXT_ROW;
(2)下一个Cell的rowkey=222,仍然比前缀333小,所以继续扫下一行,返回状态NEXT_ROW;
(3)下一个Cell的rowkey=333,前缀和333匹配,返回column=f:ddd这个cell个用户,返回状态码为INCLUDE;
(4)下一个Cell的rowkey仍为333,前缀和333匹配,返回column=f:eee这个cell给用户,返回状态码为INCLUDE;
(5)下一个Cell的rowkey为444,前缀比333大,再也不继续扫描数据。code

这个流程中,每碰到一个Cell,返回的状态码NEXT_ROW、INCLUDE等,就告诉了RegionServer扫描框架下一个Cell的位置。例如在第2步中,返回状态码NEXT_ROW,那么下一个Cell的rowkey必须是比222大的,因而就跳过了column=f:ccc这个Cell,直接定位到了rowkey=333的行,继续扫描数据。

在实际的Filter设计中,共引入了INCLUDE、INCLUDE_AND_NEXT_COL、SKIP、NEXT_COL、NEXT_ROW、SEEK_NEXT_USING_HINT共6种状态码。其中INCLUDE表示当前Cell应该返回给用户,同时自动读下一个Cell;INCLUDE_AND_NEXT_COL相似,表示当前Cell返回给用户,同时须要切换到下一个Column的Cell,也就是跟当前Cell相同Column的Cell都被跳过。SEEK_NEXT_USING_HINT表示下一个待读取的Cell是用户根据Filter语义自定义的一个Cell,例如对PrefixFilter(333)来讲,碰到rowkey=111的行时,实际上是能够根据前缀为333直接定位到下一个rowkey=333的Cell,只是当前的PrefixFilter没有作这个优化。

FilterList在处理状态码时则要稍微复杂一点,由于对同一个Cell每一个子Filter的状态码均可能不同,所以须要对多个子Filter的状态码进行合并。例如:

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testA"))));复制代码

在碰到rowkey=abb且value=testA的Cell(记为Cell-A)时,PrefixFilter返回的状态码应该是NEXT_ROW,而ValueFilter返回的状态码应该是INCLUDE。对于用AND来链接的FilterList来讲,应该取各状态码中跳跃步数最大的状态码,所以对Cell-A来讲,fl这个FilterList获得的状态码将会是:max(NEXT_ROW, INCLUDE) = NEXT_ROW。

简单来讲,对某个Cell,用AND链接的FilterList,必须选各子Filter状态码跳跃步数最大的那个状态码;而用OR链接的FilterList,必须选各子Filter状态码跳跃步数最小的那个状态码。这实际上是FilterList对比正常Filter来讲,须要实现的一个最核心的工做,咱们很早以前就在HBASE-18410将这块代码进行模块化重构,HBase1.4.x以后的版本都是使用重构以后的代码,用户在使用新版本FilterList时能够得到更精准的语义保证了。


                                                      一些优化

1.经过设置StartRow和StopRow替换PrefixFilter

PrefixFilter是将rowkey前缀为指定字节串的数据都过滤出来并返回给用户。例如,以下scan会返回全部rowkey前缀为’def’的数据。

Scan scan = new Scan();
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));复制代码

注意,这个scan虽然能拿到预期的效果,但却并不高效。由于对于rowkey在区间(-oo, def)的数据,scan会一条条依次扫描一次,发现前缀不为def,就读下一行,直到找到第一个rowkey前缀为def的行为止。

这主要是由于目前HBase的PrefixFilter设计的相对简单粗暴,没有根据具体的Filter作过多的查询优化。这种问题其实很好解决,在scan中简单加一个startRow便可,RegionServer在发现scan设了StartRow,首先寻址定位到这个StartRow,而后从这个位置开始扫描数据,这样就跳过了大量的(-oo, def)的数据。代码以下:

Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("def"));
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));复制代码

固然,更简单直接的方式,就是将PrefixFilter直接展开成扫描[def, deg)这个区间的数据,这样效率是最高的,代码以下:

Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("def"));
scan.setStopRow(Bytes.toBytes("deg"));复制代码

2.用MultipleColumnPrefixFilter来替换掉FilterList(OR, ColumnPrefixFilter, ColumnPrefixFilter, …)

在HBASE-22448,有用户提到,写一个以下的FilterList会显的特别慢:

fl = new FilterList(MUST_PASS_ONE,      
     new ColumnPrefixFilter(Bytes.toBytes("aaa")),     
     new ColumnPrefixFilter(Bytes.toBytes("bbb")),    
     ...     
     new ColumnPrefixFilter(Bytes.toBytes("zzz")))复制代码

这是由于采用FilterList(OR, ColumnPrefixFilter,…)的比较次数以下图所示,每一个橙色的实心圆圈表示一次Cell的Compare操做。

通过讨论和测试后,发现实际上是能够用MultipleColumnPrefixFilter来替换上述FilterList(OR,ColumnPrefixFilter,…)的。代码以下:

fl = new MultipleColumnPrefixFilter(byte[][] {    
     Bytes.toBytes("aaa"),    
     Bytes.toBytes("bbb"), 
     ...,    
     Bytes.toBytes("zzz")});复制代码

经过评估,咱们发现Cell的比较次数以下图所示:

两者对比发现,采用MultipleColumnPrefixFilter以后能够减小大量的比较次数。事实上,用HBASE-22448上的测试数据对比,发现优化后的性能快20倍:

这个案例带给咱们的启发是,若是发现某些场景下采用通用的FilterList框架没法知足业务的性能需求,那么实际上能够尝试采用自定义Filter的方式来知足更高的性能需求。由于在自定义的Filter中,咱们能够经过更少的比较次数来实现优化,而FilterList框架为了保证通用逻辑的正确性则没法实现。

3.关于SingleColumnValueFilter的语义

这个Filter的定义比较复杂,让人有点难以理解。举例来讲:

Scan scan = new Scan();
SingleColumnValueFilter scvf = new SingleColumnValueFilter(
Bytes.toBytes("family"),
Bytes.toBytes("qualifier"), 
CompareOp.EQUAL, 
Bytes.toBytes("value"));
scan.setFilter(scvf);复制代码

这个例子表面上是将列簇为family、列为qualifier且值为value的cell返回给用户。但事实上,对那些不包含family:qualifier这一列的行,也会被默认返回给用户。若是用户不但愿读取那些不包含family:qualifier的数据,须要设计以下scan:

Scan scan = new Scan();
SingleColumnValueFilter scvf = new SingleColumnValueFilter(
Bytes.toBytes("family"),
Bytes.toBytes("qualifier"), 
CompareOp.EQUAL, 
Bytes.toBytes("value"));
scvf.setFilterIfMissing(true); // 跳过不包含对应列的数据
scan.setFilter(scvf);复制代码

另外,当SingleColumnValueFilter设置filterIfMissing为true时,和其余Filter组合成FilterList时,可能致使返回结果不正确(参见HBASE-20151)。由于filterIfMissing设为true时,SingleColumnValueFilter必需要遍历一行数据中的每个cell, 才能肯定是否过滤,但在filterList中,若是其余的Filter返回NEXT_ROW会直接跳过某个列簇的数据,致使SingleColumnValueFilter没法遍历一行全部的cell,从而致使返回结果不符合预期。对于这个问题,我的建议是:不要使用SingleColumnValueFilter和其余Filter组合成FilterList。尽可能经过ValueFilter来替换掉SingleColumnValueFilter。

4.关于PageFilter

在HBASE-21332中,有一位用户说,有一个表,表里面有5个Region,分别为(-oo, 111), [111, 222), [222, 333), [333, 444), [444, +oo)。表中这5个Region,每一个Region都有超过10000行的数据。他发现经过以下scan扫描出来的数据竟然超过了3000行:


Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("111"));
scan.withStopRow(Bytes.toBytes("4444"));
scan.setFilter(new PageFilter(3000));复制代码

乍一看确实很诡异,由于PageFilter就是用来作数据分页功能的,应该要保证每一次扫描最多返回不超过3000行。可是须要注意的是,HBase里面Filter状态所有都是Region内有效的,也就是说,Scan一旦从一个Region切换到另外一个Region以后, 以前那个Filter的内部状态就无效了,新Region内用的实际上是一个全新的Filter。具体这个问题来讲,就是PageFilter内部计数器从一个Region切换到另外一个Region以后,计数器已经被清0。所以,这个Scan扫描出来的数据将会是:

在[111,222)区间内扫描3000行数据,切换到下一个region [222, 333)。

在[222,333)区间内扫描3000行数据,切换到下一个region [333, 444)。

在[333,444)区间内扫描3000行数据,发现已经到达stopRow,终止。

所以,最终将返回9000行数据。理论上说,这应该算是HBase的一个缺陷,PageFilter并无实现全局的分页功能,由于Filter没有全局的状态。我我的认为,HBase也是考虑到了全局Filter的复杂性,因此暂时没有提供这样的实现。固然若是想实现分页功能,能够不经过Filter,而直接经过limit来实现,代码以下:

Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("111"));
scan.withStopRow(Bytes.toBytes("4444"));
scan.setLimit(1000);复制代码

因此,正常状况下对用户来讲,PageFilter并无太多存在的价值。


                                                         总结


这些年社区的Filter和FilterList模块基本上是由小米HBase团队来负责维护和改进的。总结起来,一方面,自己HBase读路径上的概念繁多,诸如版本号、DeleteMarker、TTL、行、Family、列等,为实现这些功能读路径已经较为复杂;另外一方面,Filter自己是一个高度抽象的框架,用户能够基于这个抽象的框架实现各类各样自定义的过滤器,实现须要考虑各类现实场景的适用性。对普通用户来讲,正确和高效的使用,有必定的小门槛,所以写了这篇文章,但愿对用户正确的使用和理解Filter以及FilterList有所帮助。

                                           Apache HBaseConAsia 2019峰会

                                                 将于【7月20日上午9:00】

                                                     北京金隅喜来登酒店

                                                             隆重召开!

                                     现场不只能和HBase行业内大牛面对面交流

                                               还能得到大会限量版记念T恤

                                                       最后50个开放名额

                                      

                         

相关文章
相关标签/搜索