1. 项目概述:为什么HBase过滤器是数据查询的“手术刀”?
在HBase的世界里,数据以海量、稀疏、多维度的形式存储在HDFS之上。我们最常使用的Get和Scan操作,默认行为是把整行数据或者一个扫描范围内的所有数据都拉取回来。想象一下,你有一个存储了千万级用户行为日志的表,你只想找出今天上午10点到11点之间,来自北京地区且操作类型为“登录失败”的记录。如果直接用Scan,客户端会收到这个时间段内所有的、可能包含几十个字段的原始数据行,然后你不得不在应用层写一堆if-else进行过滤。这就像用渔网捞鱼,不管大鱼小鱼、虾米水草,先一网打尽,再在甲板上慢慢挑,网络I/O和客户端内存的压力巨大,效率极低。
HBase过滤器(Filter)就是为了解决这个痛点而生的高级特性。它不是HBase之后在客户端进行的内存过滤,而是将过滤逻辑下推到RegionServer端执行。RegionServer在读取存储文件(HFile)和内存存储(MemStore)中的数据时,会逐行(甚至逐单元格)应用你定义的过滤器。只有通过过滤器检查的数据,才会被包装成Result返回给客户端。这把“手术刀”能精准地切除你不需要的数据组织,极大地减少了不必要的数据传输,提升了查询性能。尤其是在面对宽表(几十上百列)或者需要复杂条件组合查询的场景时,过滤器的价值无可替代。今天,我们就深入这把“手术刀”的内部,看看它有哪些种类、如何工作,以及怎么用好它。
2. 过滤器核心架构与工作原理拆解
要精通过滤器,不能只停留在API调用的层面,必须理解它的运行机制和设计哲学。这能帮助你在遇到性能问题或怪异行为时,快速定位根因。
2.1 过滤器的执行模型:一个两阶段的精细过滤流程
很多人以为过滤器只是在RegionServer从磁盘读数据时生效,其实它的执行分为两个关键阶段,理解这点对编写高效过滤器至关重要。
第一阶段:StoreFileScanner/MemStoreScanner的初步筛选。当RegionServer处理一个Scan请求时,会为每个涉及到的列族(Column Family)创建一个RegionScanner。RegionScanner会管理该列族下所有存储单元(即每个Store)对应的StoreFileScanner(用于HFile)和MemStoreScanner(用于MemStore)。在初始化这些Scanner时,如果Scan对象上设置了过滤器,并且该过滤器实现了Filter#filterRowKey或Filter#filterAllRemaining等方法,就会在这个阶段被调用。例如,PrefixFilter(前缀过滤器)就可以利用filterRowKey方法,直接跳过那些RowKey不匹配前缀的HFile数据块,这发生在真正读取单元格数据之前,效率最高。
第二阶段:KeyValue级别的逐条过滤。对于通过了初步筛选(或者没有初步筛选逻辑)的数据,Scanner会读取出一行中的一个个KeyValue(单元格)。每个KeyValue会依次传递给过滤器的Filter#filterKeyValue方法。这个方法决定当前这个KeyValue是被包含(Filter.ReturnCode.INCLUDE)、跳过(Filter.ReturnCode.SKIP)还是直接结束本行(Filter.ReturnCode.NEXT_ROW)甚至结束整个扫描(Filter.ReturnCode.NEXT_COL或结合filterAllRemaining)。这是最精细的过滤粒度。
注意:过滤器的
filterKeyValue方法可能会被调用很多次。一个包含多列的行,每个列都会触发一次调用。因此,这个方法内的逻辑必须尽可能高效,避免复杂的计算或对象创建。
2.2 过滤器的关键抽象:Filter接口与ReturnCode
所有过滤器的基类都是org.apache.hadoop.hbase.filter.Filter接口。它的核心方法决定了过滤器的行为:
boolean filterRowKey(byte[] buffer, int offset, int length):早期RowKey过滤。如果返回true,则跳过整行。ReturnCode filterKeyValue(Cell v):核心方法,处理每一个KeyValue。boolean filterRow():所有列处理完后,最终决定是否过滤掉整行。常用于需要汇总行内多列信息才能做决定的场景。void reset():在开始处理下一行数据前被调用,用于重置过滤器的内部状态。这是实现状态型过滤器(如PageFilter)的关键,也是最容易出错的地方。boolean filterAllRemaining():是否终止整个Scan操作。
其中,ReturnCode是一个枚举,它告诉RegionScanner当前KeyValue的命运:
INCLUDE:包含此KeyValue。INCLUDE_AND_NEXT_COL:包含此KeyValue,并跳过本行的后续列(跳到下一列族或下一行)。适用于“获取某行第一列”的场景。SKIP:跳过此KeyValue。NEXT_COL:跳过此列族当前列的后续版本,跳到下一列。NEXT_ROW:跳过本行剩余所有KeyValue,直接处理下一行。SEEK_NEXT_USING_HINT:跳过到下一个可能匹配的KeyValue。这是高级特性,被WhileMatchFilter等使用,可以优化扫描路径。
理解这些ReturnCode的语义,是组合使用过滤器和自定义过滤器的前提。例如,SingleColumnValueFilter在找到匹配的列并决定包含该行时,会返回INCLUDE_AND_NEXT_COL,因为它已经做出了判断,无需检查该行其他列。
2.3 过滤器与协处理器(Coprocessor)的边界
初学者容易混淆过滤器和协处理器(尤其是RegionObserver)。它们虽然都能在RegionServer端执行逻辑,但定位完全不同:
- 过滤器:核心功能是数据过滤。它关注“读什么数据”,作用于数据读取的流水线上,决定哪些数据能返回给客户端。它是声明式的,你告诉HBase过滤条件,HBase负责执行。
- 协处理器:核心功能是扩展计算。它可以实现复杂的业务逻辑,如聚合、二级索引、跨行事务等。它更像是在数据读写前后注入的钩子(Hook),可以访问和修改更广泛的状态。
一个简单的区分:如果你只是想减少网络传输的数据量,用过滤器;如果你想在服务器端完成求和、计数等计算,或者实现自定义的索引逻辑,用协处理器。在实践中,它们也可以结合使用,例如在协处理器的preGet或preScan钩子中,动态地为请求添加过滤器。
3. 内置过滤器详解与实战避坑指南
HBase提供了丰富的内置过滤器,我们可以将其分为几个大类来理解和应用。下面我会结合代码示例和避坑点来详细说明。
3.1 行键(RowKey)相关过滤器
这类过滤器作用于数据的“主键”,效率通常最高,因为RowKey是HBase数据排序和索引的第一依据。
1. PrefixFilter (前缀过滤器)这是最常用且高效的过滤器之一。它根据RowKey的前缀进行过滤。
Scan scan = new Scan(); Filter filter = new PrefixFilter(Bytes.toBytes("user_20240501_")); scan.setFilter(filter);- 原理:它主要利用
filterRowKey方法。RegionServer可以利用HFile中RowKey的排序信息,快速定位到可能包含该前缀的数据块,甚至跳过完全不相关的块。 - 避坑点:前缀的设计至关重要。如果你的RowKey设计是
[区域码]_[日期]_[用户ID],那么用PrefixFilter查某个日期的所有数据会很快。但如果你想查某个用户的所有数据(用户ID在末尾),前缀过滤器就无能为力了,会导致全表扫描。这反推了RowKey设计原则:将最常用的查询条件放在RowKey的前部。
2. RowFilter (行过滤器)这是一个更通用的行键过滤器,需要配合比较器(CompareOperator)和比较器(ByteArrayComparable)使用,功能强大但需谨慎。
// 查找RowKey大于等于 `startKey` 的行 Scan scan = new Scan(); BinaryComparator comp = new BinaryComparator(Bytes.toBytes("startKey")); Filter filter = new RowFilter(CompareOperator.GREATER_OR_EQUAL, comp); scan.setFilter(filter);- 原理:它对每个RowKey应用比较逻辑。由于比较是逐行进行的,虽然比客户端过滤快,但不如
PrefixFilter能利用排序跳跃,性能取决于比较器的复杂度和数据分布。 - 避坑点:避免使用过于复杂的比较器,或在海量数据上使用非等值查询(如
NOT_EQUAL),这可能导致RegionServer仍需遍历大量数据。RowFilter通常用于定义扫描的起始和结束边界(Scan的setStartRow/setStopRow已经能高效处理范围),或者与其他过滤器进行逻辑组合。
3. FirstKeyOnlyFilter (首键过滤器)这是一个非常特殊但有用的过滤器,它只返回每行的第一个KeyValue。
Scan scan = new Scan(); Filter filter = new FirstKeyOnlyFilter(); scan.setFilter(filter);- 原理:在
filterKeyValue方法中,对每行的第一个KeyValue返回INCLUDE,之后通过返回NEXT_ROW跳过该行所有剩余KeyValue。 - 应用场景:快速行数统计。如果你只关心有多少行数据,而不关心内容,使用这个过滤器比全表Scan快几个数量级,因为每个HFile数据块它可能只需要读取第一个KeyValue。可以结合
KeyOnlyFilter(只返回Key,不返回Value)进一步减少数据量。
3.2 列限定符(Qualifier)与值(Value)相关过滤器
这是最丰富的过滤器类别,用于对列名和列值进行筛选。
1. QualifierFilter & SingleColumnValueFilter (列名与单列值过滤器)
QualifierFilter:根据列名进行过滤。使用方式与RowFilter类似,需要比较器和比较器。// 过滤出列名以 `attr_` 开头的列 Filter filter = new QualifierFilter(CompareOperator.EQUAL, new BinaryPrefixComparator(Bytes.toBytes("attr_")));SingleColumnValueFilter:这是重中之重。它用于查询“某列的值满足某种条件”的行。这是实现类似SQL中WHERE column = value查询的关键。SingleColumnValueFilter filter = new SingleColumnValueFilter( Bytes.toBytes("info"), // 列族 Bytes.toBytes("age"), // 列限定符 CompareOperator.GREATER, Bytes.toBytes(18) // 比较值 ); filter.setFilterIfMissing(true); // 如果该列不存在,则过滤掉该行- 原理:
SingleColumnValueFilter会遍历行内的KeyValue,寻找指定列族和列限定符的列。找到后,用其值与设定值进行比较。根据比较结果和setFilterIfMissing的配置,决定整行的去留。 - 避坑点:
setFilterIfMissing(true/false):这个设置极易出错。true表示“如果该列不存在,则过滤掉整行”。false表示“如果该列不存在,则让该行通过”。你需要根据业务逻辑仔细选择。例如,找“年龄大于18的用户”,如果用户记录根本没有年龄列,通常应该过滤掉(true)。- 性能:如果表很宽,但目标列在行中靠后出现,过滤器仍然需要扫描该行前面的所有列,直到找到目标列或行尾。因此,将频繁查询的列放在列族靠前的位置(HBase内部按列限定符排序)有微优化作用,但主要优化还是依赖于RowKey设计。
- 版本:默认只检查最新版本的值。如果需要检查所有版本,需要调用
filter.setLatestVersionOnly(false),但这会显著增加开销。
2. ValueFilter & ColumnValueFilter (通用值过滤器)
ValueFilter:过滤任何列的值满足条件的单元格。它不关心是哪个列族或列限定符。// 查找值包含“error”字符串的任何单元格所在的行 Filter filter = new ValueFilter(CompareOperator.EQUAL, new SubstringComparator("error"));ColumnValueFilter(HBase 2.0+):可以看作是SingleColumnValueFilter的升级版,API更清晰,并且返回行为更易理解(默认包含匹配的列)。- 避坑点:
ValueFilter非常强大,但也非常危险。因为它需要检查每一行中的每一个单元格的值,相当于在RegionServer端做了一次全列扫描。在宽表上使用可能导致性能急剧下降,应尽量避免在大数据量扫描中使用。它的典型用途是在已知行数很少(例如通过RowKey精确Get到几行)后,再对这几行数据进行精细的列值筛选。
3. ColumnPrefixFilter & MultipleColumnPrefixFilter (列前缀与多列前缀过滤器)
ColumnPrefixFilter:只返回列名以指定前缀开头的列。// 只返回列名以 `name` 开头的列,如 `name`, `name_first`, `name_last` Filter filter = new ColumnPrefixFilter(Bytes.toBytes("name"));MultipleColumnPrefixFilter:可以指定多个前缀。byte[][] prefixes = new byte[][]{Bytes.toBytes("name"), Bytes.toBytes("email")}; Filter filter = new MultipleColumnPrefixFilter(prefixes);- 原理:它们在
filterKeyValue阶段工作,检查每个KeyValue的列限定符是否匹配前缀。由于列限定符在行内也是排序存储的,这种过滤器也能利用排序进行一定优化。 - 应用场景:非常适合“列族设计”下的查询。例如,在一个
attributes列族下,有attr_color,attr_size,attr_price等动态列,你想一次性取出所有attr_开头的列,用这个过滤器就非常高效。
3.3 结构控制与分页过滤器
这类过滤器不关注具体内容,而是控制返回数据的结构和数量。
1. PageFilter (分页过滤器)这是实现服务器端分页的核心,但也是最容易用错的过滤器之一。
Scan scan = new Scan(); PageFilter filter = new PageFilter(10); // 每页10行 scan.setFilter(filter); // 第一页 ResultScanner scanner = table.getScanner(scan); for (Result result : scanner) { // 处理结果 } // 获取最后一行的RowKey,作为下一页的startRow byte[] lastRowKey = ...; Scan nextPageScan = new Scan(); nextPageScan.setStartRow(Bytes.add(lastRowKey, new byte[0])); // 从上一页最后一行之后开始 nextPageScan.setFilter(new PageFilter(10));- 原理:
PageFilter内部有一个计数器。每通过一行(即该行至少有一个KeyValue被包含在最终结果中),计数器减1。当计数器为0时,filterAllRemaining()返回true,扫描终止。 - 避坑点(极其重要):
PageFilter不保证返回行数精确等于设定值。如果某一行被其他过滤器(如SingleColumnValueFilter)完全过滤掉,这行不会被计入PageFilter的计数器。所以,你请求10行,返回的可能只有8行,因为中间有2行没通过其他过滤条件。客户端需要处理这种情况。- 必须配合正确的
StartRow进行翻页。你不能简单地为每次查询都设置PageFilter(10)然后期望得到第2页。你必须记录上一页最后一个RowKey,并将其作为下一页扫描的StartRow(且需要在这个RowKey上追加一个空字节数组,以确保从它之后开始扫描,避免重复)。这是HBase分页的标准模式。 - 性能:翻页到很深时(比如第1000页),虽然
PageFilter只返回10行,但RegionServer仍然需要顺序扫描并跳过前面的9990行,开销很大。HBase不适合做深度随机分页,更适合基于时间戳或有序RowKey的“上一页/下一页”式导航。
2. ColumnPaginationFilter & ColumnCountGetFilter (列分页与列数限制过滤器)
ColumnPaginationFilter:限制返回的列数,可以指定偏移量。例如,跳过前2列,返回接下来的3列。// 从每行的第3列开始(offset=2),返回最多3列(limit=3) Filter filter = new ColumnPaginationFilter(3, 2);ColumnCountGetFilter:限制每行返回的列数(从第一列开始)。主要用于Get操作。- 应用场景:在宽表场景下,限制返回的列数,避免单次响应数据过大。例如,用户画像表有几百个标签列,列表页只需要展示其中5个核心标签。
3. KeyOnlyFilter & FirstKeyValueMatchingQualifiersFilter
KeyOnlyFilter:只返回Key(RowKey, Family, Qualifier, Timestamp),不返回Value。用于只需要检查键是否存在或结构的场景,能极大减少网络传输。FirstKeyValueMatchingQualifiersFilter:给定一组列限定符,返回第一个匹配到的列。用于“多列选一”的场景,比如优先返回手机号,没有手机号则返回邮箱。
4. 过滤器的组合、序列化与性能调优实战
单独使用过滤器往往不能满足复杂查询需求,我们需要组合它们,并关注其性能影响。
4.1 过滤器列表(FilterList)与逻辑组合
FilterList用于将多个过滤器组合起来,支持MUST_PASS_ALL(逻辑与)和MUST_PASS_ONE(逻辑或)两种关系。
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL); SingleColumnValueFilter filter1 = new SingleColumnValueFilter( Bytes.toBytes("cf"), Bytes.toBytes("status"), CompareOperator.EQUAL, Bytes.toBytes("ACTIVE")); filter1.setFilterIfMissing(true); PrefixFilter filter2 = new PrefixFilter(Bytes.toBytes("USER_")); filterList.addFilter(filter1); filterList.addFilter(filter2); scan.setFilter(filterList);上面的例子表示:查找RowKey以USER_开头,且cf:status列的值等于ACTIVE的行。
- 执行顺序:
FilterList中的过滤器按添加顺序依次执行。顺序会影响性能。通常,应该把能最快过滤掉数据的、成本低的过滤器放在前面。例如,把PrefixFilter放在ValueFilter前面,可以先通过RowKey快速缩小范围,避免对不匹配的行进行耗时的值比较。 - 避坑点:
MUST_PASS_ONE(逻辑或)要慎用。因为每个过滤器都可能需要扫描全部数据,逻辑或组合可能导致扫描次数倍增,性能很差。如果业务允许,尽量通过设计RowKey或使用其他方案(如二级索引)来避免复杂的逻辑或查询。
4.2 过滤器的序列化与传输
当你在客户端创建过滤器并附加到Scan对象后,这个Scan对象会被序列化(默认使用Protobuf)并发送到RegionServer。这意味着:
- 自定义过滤器:如果你实现了自定义过滤器,必须确保它实现了
Writable接口(老API)或可以被Protobuf序列化,并且RegionServer的类路径下必须有这个过滤器类的JAR包。否则会抛出UnknownFilterException。这是部署自定义过滤器最大的麻烦,通常需要将自定义代码打包到HBase的lib目录或通过动态协处理器加载。 - 过滤器状态:过滤器的成员变量会被序列化过去。例如,你给
PageFilter设置的pageSize,给SingleColumnValueFilter设置的比较值和比较操作符。但运行时的临时状态(如PageFilter内部的行计数器)不会被序列化,因为它是在RegionServer端执行时产生的。
4.3 性能调优要点与监控
- 尽可能利用RowKey:这是黄金法则。所有基于RowKey的过滤(
PrefixFilter,RowFilter配合范围)效率远高于基于列值或列名的过滤。好的数据模型设计是高性能查询的基础。 - 避免全表扫描:任何不指定
StartRow的Scan,或者使用无法利用RowKey排序特性的过滤器(如对RowKey后缀进行ValueFilter),都可能退化成全表扫描,对性能是灾难性的。 - 关注服务端负载:复杂的过滤器会增加RegionServer的CPU负担。使用
SingleColumnValueFilter或ValueFilter进行子字符串匹配、正则表达式匹配时,代价很高。监控RegionServer的CPU使用率和Scan操作的RPC耗时。 - 合理设置Scan属性:
setCaching(int):设置RPC缓存的行数。增大该值可以减少RPC次数,但会增加客户端内存消耗和一次响应的大小。需要根据单行数据大小和网络情况权衡。setBatch(int):设置每行返回的列数上限。对于宽表,如果你只需要几列,设置一个较小的batch值可以避免一次传输过多数据。setMaxResultSize(long):设置一次Scan返回的数据量字节上限。防止误操作扫描大量数据拖垮网络或客户端。
- 使用布隆过滤器(Bloom Filter):这不是客户端API的过滤器,而是HBase表的属性。在创建表时,对经常作为查询条件的列启用布隆过滤器(
CREATE 'table', {NAME => 'cf', BLOOMFILTER => 'ROWCOL'}),可以大幅提升Get和指定列的Scan性能,因为它能快速判断一个数据块中是否包含目标行或列,从而避免不必要的磁盘读取。
5. 常见问题排查与自定义过滤器入门
即使理解了原理,在实际使用中还是会遇到各种问题。
5.1 问题排查清单
问题:过滤器好像没生效,返回了所有数据。
- 检查1:是否误将过滤器设置到了
Get对象,但执行的是Scan?或者反之?Get和Scan的API是分开的。 - 检查2:对于
SingleColumnValueFilter,是否错误设置了setFilterIfMissing(false)?这会导致没有该列的行也被返回。 - 检查3:过滤器逻辑是否是“逻辑或”(
MUST_PASS_ONE)?可能其中一个条件非常宽泛。 - 检查4:检查比较器和比较值的数据类型是否匹配。用
BinaryComparator比较数字和字符串,结果可能出乎意料。
- 检查1:是否误将过滤器设置到了
问题:分页不准,数据重复或丢失。
- 检查1:翻页时,是否正确地设置了下一页的
StartRow为上一页最后一条记录的RowKey并追加了空字节?这是最常见原因。 - 检查2:是否在分页查询中使用了非确定性的过滤器(例如基于当前时间)?这会导致前后页数据边界不一致。
- 检查3:理解
PageFilter的计数逻辑,它只计数实际返回的行,被其他过滤器过滤掉的行不计数。
- 检查1:翻页时,是否正确地设置了下一页的
问题:扫描性能突然变慢。
- 检查1:是否在扫描过程中修改了过滤器?
Filter对象应该是无状态的(除了初始化参数),或者正确实现了reset()方法。状态混乱会导致扫描行为异常。 - 检查2:查看RegionServer日志,是否有大量的
Block被加载?可能是布隆过滤器未命中,或者扫描触发了大量随机读。 - 检查3:使用
hbase shell的debug命令或HBase Metrics监控,查看扫描涉及的Region数量、RPC次数、过滤掉的KeyValue数量等指标。
- 检查1:是否在扫描过程中修改了过滤器?
5.2 实现一个简单的自定义过滤器
当内置过滤器无法满足需求时,可以考虑自定义。这里实现一个“时间范围过滤器”,只返回指定时间戳范围内的数据。
import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.filter.FilterBase; import org.apache.hadoop.hbase.exceptions.DeserializationException; import org.apache.hadoop.hbase.shaded.protobuf.generated.FilterProtos; import org.apache.hadoop.hbase.util.Bytes; import java.io.IOException; public class TimestampRangeFilter extends FilterBase { private long minTimestamp; private long maxTimestamp; public TimestampRangeFilter(long minTimestamp, long maxTimestamp) { this.minTimestamp = minTimestamp; this.maxTimestamp = maxTimestamp; } @Override public ReturnCode filterKeyValue(Cell cell) throws IOException { long ts = cell.getTimestamp(); // 如果时间戳在范围内,则包含;否则跳过 if (ts >= minTimestamp && ts <= maxTimestamp) { return ReturnCode.INCLUDE; } else { return ReturnCode.SKIP; } } // 必须重写reset方法,清除任何行间状态(本例中无状态) @Override public void reset() throws IOException { // 本例无状态需要重置 } // 以下方法是为了序列化/反序列化(简化版,生产环境需实现完整) @Override public byte[] toByteArray() throws IOException { FilterProtos.TimestampRangeFilter.Builder builder = FilterProtos.TimestampRangeFilter.newBuilder(); builder.setMinTimestamp(minTimestamp).setMaxTimestamp(maxTimestamp); return builder.build().toByteArray(); } public static TimestampRangeFilter parseFrom(final byte[] pbBytes) throws DeserializationException { try { FilterProtos.TimestampRangeFilter proto = FilterProtos.TimestampRangeFilter.parseFrom(pbBytes); return new TimestampRangeFilter(proto.getMinTimestamp(), proto.getMaxTimestamp()); } catch (Exception e) { throw new DeserializationException(e); } } }使用方式:
Scan scan = new Scan(); long startTime = ...; long endTime = ...; TimestampRangeFilter filter = new TimestampRangeFilter(startTime, endTime); scan.setFilter(filter);自定义过滤器要点:
- 继承
FilterBase:它提供了Filter接口的默认实现,你只需覆盖需要的方法。 - 谨慎管理状态:如果过滤器需要记录行内状态(比如统计某列值之和),必须在
reset()方法中清除,否则会影响下一行的处理。 - 实现序列化:必须实现
toByteArray()和parseFrom()方法,否则过滤器无法发送到RegionServer。这是自定义过滤器最复杂的部分,通常需要借助Protobuf。 - 部署:将编译好的JAR包放到所有RegionServer的
HBASE_HOME/lib目录下,并重启RegionServer。或者研究使用协处理器的动态加载机制。
过滤器是HBase高效查询的基石。从简单的行键过滤到复杂的多条件组合,再到自定义过滤逻辑,它提供了一套强大而灵活的服务器端数据剪裁机制。掌握它的核心在于理解其“下推执行”的两阶段模型,清楚每个内置过滤器的适用场景和陷阱,并在设计数据模型时就预先考虑如何让查询最大限度地利用RowKey和过滤器。记住,最好的优化永远是避免不必要的扫描,而一个好的过滤器,就是实现这一目标最锋利的工具。在实际项目中,多结合hbase shell的scan 'table', {FILTER=>...}命令进行原型测试,观察过滤效果,是快速掌握过滤器的好方法。