有这样一个场景,在HBase中需要分页查询,同时根据某一列的值进行过滤。

不同于RDBMS天然支持分页查询,HBase要进行分页必须由自己实现。据我了解的,目前有两种方案, 一是《HBase权威指南》中提到的用PageFilter加循环动态设置startRow实现,详细见这里。但这种方法效率比较低,且有冗余查询。因此京东研发了一种用额外的一张表来保存行序号的方案。 该种方案效率较高,但实现麻烦些,需要维护一张额外的表。

不管是方案也好,人也好,没有最好的,只有最适合的。
在我司的使用场景中,对于性能的要求并不高,所以采取了第一种方案。本来使用的美滋滋,但有一天需要在分页查询的同时根据某一列的值进行过滤。根据列值过滤,自然是用SingleColumnValueFilter(下文简称SCVFilter)。代码大致如下,只列出了本文主题相关的逻辑,

  1. Scan scan = initScan(xxx);
  2. FilterList filterList=new FilterList();
  3. scan.setFilter(filterList);
  4. filterList.addFilter(new PageFilter(1));
  5. filterList.addFilter(new SingleColumnValueFilter(FAMILY,ISDELETED, CompareFilter.CompareOp.EQUAL, Bytes.toBytes(false)));

数据如下

  1. row1 column=f:content, timestamp=1513953705613, value=content1
  2. row1 column=f:isDel, timestamp=1513953705613, value=1
  3. row1 column=f:name, timestamp=1513953725029, value=name1
  4. row2 column=f:content, timestamp=1513953705613, value=content2
  5. row2 column=f:isDel, timestamp=1513953744613, value=0
  6. row2 column=f:name, timestamp=1513953730348, value=name2
  7. row3 column=f:content, timestamp=1513953705613, value=content3
  8. row3 column=f:isDel, timestamp=1513953751332, value=0
  9. row3 column=f:name, timestamp=1513953734698, value=name3

在上面的代码中。向scan添加了两个filter:首先添加了PageFilter,限制这次查询数量为1,然后添加了一个SCVFilter,限制了只返回isDeleted=false的行。

上面的代码,看上去无懈可击,但在运行时却没有查询到数据!

刚好最近在看HBase的代码,就在本地debug了下HBase服务端Filter相关的查询流程。

Filter流程

首先看下HBase Filter的流程,见图:

然后再看PageFilter的实现逻辑。

  1. public class PageFilter extends FilterBase {
  2. private long pageSize = Long.MAX_VALUE;
  3. private int rowsAccepted = 0;
  4.  
  5. /**
  6. * Constructor that takes a maximum page size.
  7. *
  8. * @param pageSize Maximum result size.
  9. */
  10. public PageFilter(final long pageSize) {
  11. Preconditions.checkArgument(pageSize >= 0, "must be positive %s", pageSize);
  12. this.pageSize = pageSize;
  13. }
  14.  
  15. public long getPageSize() {
  16. return pageSize;
  17. }
  18.  
  19. @Override
  20. public ReturnCode filterKeyValue(Cell ignored) throws IOException {
  21. return ReturnCode.INCLUDE;
  22. }
  23.  
  24. public boolean filterAllRemaining() {
  25. return this.rowsAccepted >= this.pageSize;
  26. }
  27.  
  28. public boolean filterRow() {
  29. this.rowsAccepted++;
  30. return this.rowsAccepted > this.pageSize;
  31. }
  32.  
  33. }

其实很简单,内部有一个计数器,每次调用filterRow的时候,计数器都会+1,如果计数器值大于pageSize,filterrow就会返回true,那之后的行就会被过滤掉。

再看SCVFilter的实现逻辑。

  1. public class SingleColumnValueFilter extends FilterBase {
  2. private static final Log LOG = LogFactory.getLog(SingleColumnValueFilter.class);
  3.  
  4. protected byte [] columnFamily;
  5. protected byte [] columnQualifier;
  6. protected CompareOp compareOp;
  7. protected ByteArrayComparable comparator;
  8. protected boolean foundColumn = false;
  9. protected boolean matchedColumn = false;
  10. protected boolean filterIfMissing = false;
  11. protected boolean latestVersionOnly = true;
  12.  
  13. /**
  14. * Constructor for binary compare of the value of a single column. If the
  15. * column is found and the condition passes, all columns of the row will be
  16. * emitted. If the condition fails, the row will not be emitted.
  17. * <p>
  18. * Use the filterIfColumnMissing flag to set whether the rest of the columns
  19. * in a row will be emitted if the specified column to check is not found in
  20. * the row.
  21. *
  22. * @param family name of column family
  23. * @param qualifier name of column qualifier
  24. * @param compareOp operator
  25. * @param comparator Comparator to use.
  26. */
  27. public SingleColumnValueFilter(final byte [] family, final byte [] qualifier,
  28. final CompareOp compareOp, final ByteArrayComparable comparator) {
  29. this.columnFamily = family;
  30. this.columnQualifier = qualifier;
  31. this.compareOp = compareOp;
  32. this.comparator = comparator;
  33. }
  34.  
  35. @Override
  36. public ReturnCode filterKeyValue(Cell c) {
  37. if (this.matchedColumn) {
  38. // We already found and matched the single column, all keys now pass
  39. return ReturnCode.INCLUDE;
  40. } else if (this.latestVersionOnly && this.foundColumn) {
  41. // We found but did not match the single column, skip to next row
  42. return ReturnCode.NEXT_ROW;
  43. }
  44. if (!CellUtil.matchingColumn(c, this.columnFamily, this.columnQualifier)) {
  45. return ReturnCode.INCLUDE;
  46. }
  47. foundColumn = true;
  48. if (filterColumnValue(c.getValueArray(), c.getValueOffset(), c.getValueLength())) {
  49. return this.latestVersionOnly? ReturnCode.NEXT_ROW: ReturnCode.INCLUDE;
  50. }
  51. this.matchedColumn = true;
  52. return ReturnCode.INCLUDE;
  53. }
  54.  
  55. private boolean filterColumnValue(final byte [] data, final int offset,
  56. final int length) {
  57. int compareResult = this.comparator.compareTo(data, offset, length);
  58. switch (this.compareOp) {
  59. case LESS:
  60. return compareResult <= 0;
  61. case LESS_OR_EQUAL:
  62. return compareResult < 0;
  63. case EQUAL:
  64. return compareResult != 0;
  65. case NOT_EQUAL:
  66. return compareResult == 0;
  67. case GREATER_OR_EQUAL:
  68. return compareResult > 0;
  69. case GREATER:
  70. return compareResult >= 0;
  71. default:
  72. throw new RuntimeException("Unknown Compare op " + compareOp.name());
  73. }
  74. }
  75.  
  76. public boolean filterRow() {
  77. // If column was found, return false if it was matched, true if it was not
  78. // If column not found, return true if we filter if missing, false if not
  79. return this.foundColumn? !this.matchedColumn: this.filterIfMissing;
  80. }
  81.  
  82. }

在HBase中,对于每一行的每一列都会调用到filterKeyValue,SCVFilter的该方法处理逻辑如下:

  1. 1. 如果已经匹配过对应的列并且对应列的值符合要求,则直接返回INCLUE,表示这一行的这一列要被加入到结果集
  2. 2. 否则如latestVersionOnlytrue(latestVersionOnly代表是否只查询最新的数据,一般为true),并且已经匹配过对应的列(但是对应的列的值不满足要求),则返回EXCLUDE,代表丢弃该行
  3. 3. 如果当前列不是要匹配的列。则返回INCLUDE,否则将matchedColumn置为true,代表以及找到了目标列
  4. 4. 如果当前列的值不满足要求,在latestVersionOnlytrue时,返回NEXT_ROW,代表忽略当前行还剩下的列,直接跳到下一行
  5. 5. 如果当前列的值满足要求,将matchedColumn置为true,代表已经找到了对应的列,并且对应的列值满足要求。这样,该行下一列再进入这个方法时,到第1步就会直接返回,提高匹配效率

再看filterRow方法,该方法调用时机在filterKeyValue之后,对每一行只会调用一次。
SCVFilter中该方法逻辑很简单:

  1. 1. 如果找到了对应的列,如其值满足要求,则返回false,代表将该行加入到结果集,如其值不满足要求,则返回true,代表过滤该行
  2. 2. 如果没找到对应的列,返回filterIfMissing的值。

猜想:

是不是因为将PageFilter添加到SCVFilter的前面,当判断第一行的时候,调用PageFilter的filterRow,导致PageFilter的计数器+1,但是进行到SCVFilter的filterRow的时候,该行又被过滤掉了,在检验下一行时,因为PageFilter计数器已经达到了我们设定的pageSize,所以接下来的行都会被过滤掉,返回结果没有数据。

验证:

在FilterList中,先加入SCVFilter,再加入PageFilter

  1. Scan scan = initScan(xxx);
  2. FilterList filterList=new FilterList();
  3. scan.setFilter(filterList);
  4. filterList.addFilter(new SingleColumnValueFilter(FAMILY,ISDELETED, CompareFilter.CompareOp.EQUAL, Bytes.toBytes(false)));
  5. filterList.addFilter(new PageFilter(1));

结果是我们期望的第2行的值。

结论

当要将PageFilter和其他Filter使用时,最好将PageFilter加入到FilterList的末尾,否则可能会出现结果个数小于你期望的数量。
(其实正常情况PageFilter返回的结果数量可能大于设定的值,因为服务器集群的PageFilter是隔离的。)

彩蛋

其实,在排查问题的过程中,并没有这样顺利,因为问题出在线上,所以我在本地查问题时自己造了一些测试数据,令人惊讶的是,就算我先加入SCVFilter,再加入PageFilter,返回的结果也是符合预期的。
测试数据如下:

  1. row1 column=f:isDel, timestamp=1513953705613, value=1
  2. row1 column=f:name, timestamp=1513953725029, value=name1
  3. row2 column=f:isDel, timestamp=1513953744613, value=0
  4. row2 column=f:name, timestamp=1513953730348, value=name2
  5. row3 column=f:isDel, timestamp=1513953751332, value=0
  6. row3 column=f:name, timestamp=1513953734698, value=name3

当时在本地一直不能复现问题。很是苦恼,最后竟然发现使用SCVFilter查询的结果还和数据的列的顺序有关。

在服务端,HBase会对客户端传递过来的filter封装成FilterWrapper。

  1. class RegionScannerImpl implements RegionScanner {
  2.  
  3. RegionScannerImpl(Scan scan, List<KeyValueScanner> additionalScanners, HRegion region)
  4. throws IOException {
  5. this.region = region;
  6. this.maxResultSize = scan.getMaxResultSize();
  7. if (scan.hasFilter()) {
  8. this.filter = new FilterWrapper(scan.getFilter());
  9. } else {
  10. this.filter = null;
  11. }
  12. }
  13. ....
  14. }

在查询数据时,在HRegion的nextInternal方法中,会调用FilterWrapper的filterRowCellsWithRet方法

FilterWrapper相关代码如下:

  1. /**
  2. * This is a Filter wrapper class which is used in the server side. Some filter
  3. * related hooks can be defined in this wrapper. The only way to create a
  4. * FilterWrapper instance is passing a client side Filter instance through
  5. * {@link org.apache.hadoop.hbase.client.Scan#getFilter()}.
  6. *
  7. */
  8.  
  9. final public class FilterWrapper extends Filter {
  10. Filter filter = null;
  11.  
  12. public FilterWrapper( Filter filter ) {
  13. if (null == filter) {
  14. // ensure the filter instance is not null
  15. throw new NullPointerException("Cannot create FilterWrapper with null Filter");
  16. }
  17. this.filter = filter;
  18. }
  19.  
  20. public enum FilterRowRetCode {
  21. NOT_CALLED,
  22. INCLUDE, // corresponds to filter.filterRow() returning false
  23. EXCLUDE // corresponds to filter.filterRow() returning true
  24. }
  25.  
  26. public FilterRowRetCode filterRowCellsWithRet(List<Cell> kvs) throws IOException {
  27. this.filter.filterRowCells(kvs);
  28. if (!kvs.isEmpty()) {
  29. if (this.filter.filterRow()) {
  30. kvs.clear();
  31. return FilterRowRetCode.EXCLUDE;
  32. }
  33. return FilterRowRetCode.INCLUDE;
  34. }
  35. return FilterRowRetCode.NOT_CALLED;
  36. }
  37.  
  38. }

这里的kvs就是一行数据经过filterKeyValue后没被过滤的列。

可以看到当kvs不为empty时,filterRowCellsWithRet方法中会调用指定filter的filterRow方法,上面已经说过了,PageFilter的计数器就是在其filterRow方法中增加的。

而当kvs为empty时,PageFilter的计数器就不会增加了。再看我们的测试数据,因为行的第一列就是SCVFilter的目标列isDeleted。回顾上面SCVFilter的讲解我们知道,当一行的目标列的值不满足要求时,该行剩下的列都会直接被过滤掉!

对于测试数据第一行,走到filterRowCellsWithRet时kvs是empty的。导致PageFilter的计数器没有+1。还会继续遍历剩下的行。从而使得返回的结果看上去是正常的。

而出问题的数据,因为在列isDeleted之前还有列content,所以当一行的isDeleted不满足要求时,kvs也不会为empty。因为列content的值已经加入到kvs中了(这些数据要调用到SCVFilter的filterrow的时间会被过滤掉)。

感想

从实现上来看HBase的Filter的实现还是比较粗糙的。效率也比较感人,不考虑网络传输和客户端内存的消耗,基本上和你在客户端过滤差不多。

本人免费整理了Java高级资料,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G,需要自己领取。
传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

避免HBase PageFilter踩坑,这几点你必须要清楚的更多相关文章

  1. Spark踩坑记——数据库(Hbase+Mysql)

    [TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...

  2. [转]Spark 踩坑记:数据库(Hbase+Mysql)

    https://cloud.tencent.com/developer/article/1004820 Spark 踩坑记:数据库(Hbase+Mysql) 前言 在使用Spark Streaming ...

  3. Spark踩坑记——数据库(Hbase+Mysql)转

    转自:http://www.cnblogs.com/xlturing/p/spark.html 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库 ...

  4. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

  5. Spark踩坑记——共享变量

    [TOC] 前言 Spark踩坑记--初试 Spark踩坑记--数据库(Hbase+Mysql) Spark踩坑记--Spark Streaming+kafka应用及调优 在前面总结的几篇spark踩 ...

  6. Spark踩坑记——从RDD看集群调度

    [TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...

  7. Spark踩坑记:Spark Streaming+kafka应用及调优

    前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark streaming从k ...

  8. 一次flume exec source采集日志到kafka因为单条日志数据非常大同步失败的踩坑带来的思考

    本次遇到的问题描述,日志采集同步时,当单条日志(日志文件中一行日志)超过2M大小,数据无法采集同步到kafka,分析后,共踩到如下几个坑.1.flume采集时,通过shell+EXEC(tail -F ...

  9. 一次shardingjdbc踩坑引起的胡思乱想

    项目里面的一个分表用到了sharding-jdbc 当时纠结过是用mycat还是用sharding-jdbc的, 但是最终还是用了sharding-jdbc, 原因如下: 1. mycat比较重, 相 ...

随机推荐

  1. Taro聊天室|react+taro仿微信聊天App界面|taro聊天实例

    一.项目简述 taro-chatroom是基于Taro多端实例聊天项目,运用Taro+react+react-redux+taroPop+react-native等技术开发的仿微信App界面聊天室,实 ...

  2. 如何将HTML页面中的文本设置首行缩进

    text-indent属性介绍 属性值单位 描述 em 比如:1em 就代表缩进1个字,2em缩进2个字...... 由于简单我就不过多的介绍了直接上代码了哦,注意:text-indent属性的值支持 ...

  3. C# Dictionary增加的方法

    1.简单的函数,实现Dictionary如果有就替换,没有就增加的功能. /// <summary>        /// Dictionary增加的方法        /// </ ...

  4. 【XML】XML基本结构以及XML-Schema约束

    XML 简介 1998年2月,W3C正式批准了可扩展标记语言的标准定义,可扩展标记语言可以对文档和数据进行结构化处理,从而能够在部门.客户和供应商之间进行交换,实现动态内容生成,企业集成和应用开发.可 ...

  5. 访问rabbitmq-server失败

    测试项目正常运行突然访问不了,各项目启动失败,查看日志发现是RabbitMQ拒绝连接. 重启后依然失败,看var/log/rabbitmq/startup_err 发现什么错误信息也没有,后查看磁盘空 ...

  6. 10.JavaCC官方入门指南-例5

    例5:计算器--添加乘除法运算 1.calculator2.jj 根据上一个例子,可知要添加乘法和除法运算是很简单的,我们只需在词法描述部分添加如下两个token: TOKEN : { < TI ...

  7. Centos7安装配置----1配置网络

    1.下载镜像安装,选择的是最小安装,设置root用户密码 (此处省略其中步骤,直到安装成功) 2.安装完成后重启,输入用户名密码进入系统 由于此时未配置网络,所以网卡什么的均未获取ip联网 输入ip ...

  8. C++ 基础语法 快速复习笔记---面对对象编程(2)

    1.C++面对对象编程: a.定义: 类定义是以关键字 class 开头,后跟类的名称.类的主体是包含在一对花括号中.类定义后必须跟着一个分号或一个声明列表. 关键字 public 确定了类成员的访问 ...

  9. nginx配置中root和alias的区别

    例:访问http://127.0.0.1/download/*这个目录时候让他去/opt/app/code这个目录找. 方法一(使用root关键字): location / { root /usr/s ...

  10. 修改Tooltip 文字提示 的背景色 箭头颜色

    3==>vue 鼠标右击<div @contextmenu.prevent="mouseRightClick">prevent是阻止鼠标的默认事件 4==> ...