前置问题与边界

HBase 的读请求不是“把表读出来再过滤”。Get 直接定位单行;Scan 按 row-key range 顺序推进;Filter 可以减少返回给客户端的 Cell,也可能在服务端继续扫描大量数据。cachingbatchlimitreversed 影响 RPC 粒度、结果分片和遍历方向,但不会改变 HFile 的物理排序。

本篇回答一个核心问题:scan range、cache、batch、limit、reversed scan 如何影响 RPC 和结果语义。BlockCache、Bloom Filter 和 HFile block 在第 05、06 篇;row key 设计在第 03 篇;Coprocessor 和 Phoenix 边界在第 16 篇。

读取路径图

flowchart TD
    C[Client] --> O{Operation}
    O -->|Get row| G[Single row lookup]
    O -->|Scan range| S[Region scanner]
    S --> R[Row-key ordered traversal]
    R --> F[Filter evaluation]
    F --> B[Batch splits cells]
    F --> K[Caching controls rows per RPC]
    K --> N[ResultScanner next]
    B --> P[Partial result if row is wide]

GetScan 都经过 Region 定位和 RegionServer 读路径,但访问形状不同。Get 适合单行或少量已知 row key;Scan 适合连续 row-key range。把 Filter 当成关系数据库 where 子句,会低估 row key 设计对扫描成本的影响。

关键对象和状态

对象 作用 语义边界
Get 单 row key 读取 不做范围遍历
Scan row-key range 遍历 stop row 通常是排他边界
ResultScanner 客户端迭代器 多次 next 触发 RPC
setCaching 每次 RPC 预取行数 不是 BlockCache 开关
setBatch 每个 Result 返回的 Cell 数上限 可能产生 partial result
setLimit scan 返回行数上限 不等于每 RPC 行数
Filter 服务端过滤结果 不能替代合理 row key range
setReversed 反向 scan 不改变底层 HFile 排序

caching 和 BlockCache 名字相似,但不是一件事。Scan#setCaching 控制一次 RPC 尽量拿多少行;BlockCache 缓存 HFile block,属于 RegionServer 存储读路径。

Scan range

row key 的字节排序决定了 scan 的自然顺序。withStartRowwithStopRow 用来限制范围,常见 stop row 是排他边界。范围设计越贴近查询谓词,RegionServer 需要扫描的无关行越少。

Prefix 查询的常见做法不是全表 scan 加 PrefixFilter,而是先把 start row 和 stop row 约束到同一前缀范围,再用 filter 做补充。MultiRowRangeFilter 适合把多个离散 row-key range 合并进一次 scan,但它仍然要按范围工作。

模式提炼:先缩小物理范围,再做逻辑过滤。公式是 scan(range(row_key)) -> filter(cells) -> return(results)。搜索引擎先按倒排缩小候选、数据库先走索引再回表、对象存储先按 prefix 列举,都属于同类顺序。

Caching、batch 和 partial result

setCaching(n) 控制客户端 scanner 一次 RPC 请求服务端返回多少行左右。值太小会增加 RPC 次数;值太大可能提高客户端内存占用和单次响应延迟。它不是“把数据放进缓存”,也不是服务器端 BlockCache 的开关。

setBatch(n) 控制每个 Result 最多包含多少个 Cell。宽行场景下,一个逻辑行可能被拆成多个 partial results。公共 API 中存在 Result#mayHaveMoreCellsInRow() 这一类信号,调用方必须识别 partial result,否则容易把半行误认为整行。

setLimit(n) 限制 scan 返回的行数。它与 caching 的区别在于:limit 是总结果行数边界,caching 是 RPC 批量获取粒度。两者常一起使用,但含义不同。

Filter 的职责

Filter 在 RegionServer 端参与结果过滤,可以减少返回给客户端的数据量。常见 filter 包括 PrefixFilterPageFilterWhileMatchFilterColumnPrefixFilterSingleColumnValueFilterMultiRowRangeFilter 等。Filter 的效果取决于它是否能提前停止扫描或跳过数据;有些 filter 只减少返回,不减少底层扫描量。

PageFilter 容易被误用为稳定分页方案。它限制的是 scan 返回数量,不保存全局游标。多页读取更可靠的做法通常是使用上一次返回的最后 row key 作为下一次 start row,并处理边界去重。并发写入和 Region 移动下,分页语义要按业务可接受的一致性设计。

Reversed scan

setReversed(true) 让 scan 反向遍历 row key range。它适合“取某个前缀下最新 N 条”这类 row key 已经按时间可逆表达的场景。反向 scan 不会把 HFile 物理排序改成倒序,也不会修复不适合范围查询的 row key 设计。

如果 row key 使用正向时间戳,反向 scan 可能方便拿最近数据;如果 row key 用反转时间戳或补码时间戳,正向 scan 就可能已经满足最新优先。选择哪一种,应回到第 03 篇的 row key 排序设计。

Java public API 示例

UNVERIFIED_RUNTIME:以下示例使用 HBase 2.x public API 形态,当前任务未在目标版本编译运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.PageFilter;
import org.apache.hadoop.hbase.util.Bytes;

public final class ScanWindowExample {
public static void main(String[] args) throws IOException {
Configuration conf = HBaseConfiguration.create();
try (Connection connection = ConnectionFactory.createConnection(conf);
Table table = connection.getTable(TableName.valueOf("lab:t14_scan"))) {
Scan scan = new Scan()
.withStartRow(Bytes.toBytes("user#1000#"))
.withStopRow(Bytes.toBytes("user#1001#"))
.setCaching(100)
.setBatch(20)
.setLimit(200)
.setFilter(new PageFilter(200));

try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
if (result.mayHaveMoreCellsInRow()) {
// 宽行被 batch 拆分时,调用方不能把当前 Result 当成完整行。
}
}
}
}
}
}

最小实验

UNVERIFIED_RUNTIME:以下命令用于 HBase 2.6.6 环境复现,当前任务未采集输出。

1
hbase shell
1
create_namespace 'lab'
1
create 'lab:t14_scan', 'cf'
1
put 'lab:t14_scan', 'user#1000#202608290001', 'cf:a', 'a1'
1
put 'lab:t14_scan', 'user#1000#202608290002', 'cf:a', 'a2'
1
put 'lab:t14_scan', 'user#1001#202608290001', 'cf:a', 'b1'
1
scan 'lab:t14_scan', {STARTROW => 'user#1000#', STOPROW => 'user#1001#'}
1
scan 'lab:t14_scan', {STARTROW => 'user#1000#', STOPROW => 'user#1001#', LIMIT => 1}
1
scan 'lab:t14_scan', {STARTROW => 'user#1000#', STOPROW => 'user#1001#', REVERSED => true}

实验只观察范围、limit 和 reversed 的结果边界,不记录耗时。Filter 相关实验应配合服务端扫描指标,避免把“返回少”误判为“扫描少”。

失败恢复

读路径的失败通常不是 WAL replay 级别的恢复,而是客户端 scanner 重试、Region 重新定位和应用侧分页状态恢复。一次 scan 跨多个 RPC;Region 移动、scanner lease 过期或客户端超时后,应用需要能从最后确认处理的 row key 继续。

宽行和 partial result 会放大恢复复杂度。若 batch 导致一行拆成多个 Result,应用只记录最后 row key 可能不够,还要记录列进度或避免使用会拆行的批量设置。分页任务如果要求严格不重不漏,需要把 row key、最后处理的 qualifier、版本和业务去重键一起设计。

工程迁移

访问需求 推荐读法 风险
单实体读取 Get row key 必须已知
用户时间线 prefix range scan row key 要把用户和时间排在一起
后台导出 大 range scan 加合理 caching scanner lease 和 Region 移动
宽行读取 控制 batch 或避免超宽行 partial result 处理
翻页展示 last row key 游标 并发写入下结果可能变化

模式速查:听到“分页、列表、最近 N 条”,先把 row-key range 画出来,再决定 caching、batch、limit 和 filter。没有 range 的 scan,通常是建模问题。

常见误解

  • “Filter 就像 SQL where 一样省扫描”不准确;很多 filter 只减少返回结果。
  • “caching 是 BlockCache 配置”不准确;它控制 scanner RPC 预取行数。
  • “batch 是分页行数”不准确;它限制的是 Cell 数,可能拆开一行。
  • “reversed scan 会改变文件排序”不准确;它只改变遍历方向。

练习

  • user#{userId}#{ts} 设计最近 20 条读取方案,分别给出正向时间戳和反向时间戳写法。
  • 解释 setCaching(1)setLimit(1) 的差异。
  • 写出一个 scanner 失败后从最后 row key 恢复的处理流程。

系列导航

参考资料