HBase Java API 这个名字,只要碰过 HBase 的开发者基本都绕不开。我最早是在一个日志收集系统的改造项目里被它折腾的够呛,当时团队在 JDBC 方案和 HBase 之间反复对比,最后选定 HBase Java API 作为底层读写的唯一入口。这篇文章我想用实际开发中踩过的坑和验证过的做法,把 HBase Java API 从版本选型、连接管理、增删改查,到过滤器组合、性能调优、线上问题排查的完整链路讲清楚。适合三类人看:第一次接触 HBase 的初学者、写过一些读代码但总被超时和资源泄漏困扰的工程师、以及准备做技术选型想评估原生 API 成本的技术负责人。
1. 动手之前:HBase Java API 的版本与选型
1.1 版本差异:别让 pom 坐标坑了你
聊 HBase Java API 第一件事永远是版本。HBase 1.x、2.x、3.x 三个大版本的客户端 API 表面看起来差不多,实际上改动非常大。1.x 时代最经典的 HTable 和 HTablePool 在 2.x 被彻底废弃,换成了 Connection、Table、Admin 这套接口。3.x 虽然在 API 结构上继承了 2.x,但默认参数调了一遍,和 Hadoop 版本的兼容矩阵也完全变了。如果你在网上一搜就是老帖子的代码,把 HTable 直接复制到新项目里,编译那一步就会挂掉。
我的建议很简单:新项目直接选 2.4.x 或 2.5.x 的稳定维护版本,3.x 除非团队里有人已经吃透了它的新特性,否则别急着当小白鼠。原因非常现实:HBase 客户端要和服务端版本保持兼容,跨版本连接经常遇到 RPC 协议不匹配或者方法签名不存在的问题,这种问题的报错信息往往很隐晦,排查起来特别痛苦。所以 pom 里那行 hbase-client 坐标,第一原则是和生产集群版本一致,第二原则才是追求新版本。
我见过一次挺惨的事故,某团队把 client 从 1.2 升到 2.1,没同步检查服务端参数,结果 ZooKeeper 的 session 超时两边对不上,所有写入在晚高峰全部触发重试,下游报表延迟了整整一个晚上。最后定位到根子上就是版本那一行坐标。所以别小看版本管理,HBase 的兼容性不是靠“乐观”就能糊弄过去的。
1.2 为什么用原生 Java API 而不是查询引擎
现在 HBase 生态里有很多现成封装,Phoenix 能用 SQL 查,Spark 有自带连接器,Hive 也能映射外表。那为什么我还是推荐直接写 HBase Java API?答案就三个字:可控性。
用 Phoenix 写起来确实快,但框架会生成大量 Scan,很多底层细节被黑盒封装,真出了性能问题很难定位到具体是哪条查询在拖累集群。Java API 不一样,它让你直接操作 Put、Get、Scan 这些原生对象,每一个 RPC 边界都清清楚楚。尤其是做 RowKey 落地设计和列族规划的时候,你会发现用原生 API 写出来的代码和表结构是一一对应的,开发、运维、排障之间的沟通成本都低得多。
从性能角度说,原生 API 也是过滤器下推最彻底的途径。HBase 的 Filter 机制可以在 RegionServer 端做数据裁剪,只把命中的数据返回给客户端。这个能力不管是 Phoenix 还是其他框架,都只能间接使用。延迟敏感的高频查询场景下,原生 API 的优势是实打实的。
注意:如果团队里大多数人并不熟悉 Java,或者业务模型天然适合 SQL,那选 Phoenix 也情有可原。技术选型没有绝对正确,只有明确代价。原生 API 的代价就是学习曲线偏陡,需要有人真正搞懂内部机制。
1.3 客户端线程模型:Connection、Table、Admin 的生命周期管理
这一节很关键,哪怕你暂时不想研究版本选型,也建议把这段记下来。HBase 客户端里有三个核心类型,生命周期完全不一样:Connection 是重量级对象,一个进程只需要一个;Table 是轻量级操作入口,但不是无脑 new 的对象;Admin 只管表结构元操作,用完也要关。
先说 Connection。它负责维持到 ZooKeeper 的连接,内部维护 RPC 连接池和元数据缓存。创建成本高,销毁成本更高,所以正确做法是把它做成单例,整个 JVM 只初始化一次。我曾经在一个批处理任务里犯过这个错:循环里每次 new Connection,结果 ZooKeeper 连接数直接被打满,集群 watchdog 连续重启了好几个节点。那个问题定位倒是快,日志里全是 connection closed 和 zk session expired。从那以后我在团队里立了一条规矩:任何类都不允许直接 new Connection,必须从统一的管理器里拿。
Table 和 Admin 是操作入口,不适合长期持有,也不适合频繁创建。Table 实现内部复用了 Connection 的连接和线程,所以反复 getTable 的成本远低于新建 Connection,但也别在每次 Put 时都去 getTable。合理方式是在一个请求线程内部获取、用完即关,用 try-with-resources 最稳妥。
2. 环境准备与连接配置:先把地基打稳
2.1 客户端依赖的最小化为配置
从零搭一个 HBase 客户端项目,依赖不要贪多。核心只引入 hbase-client 它就够了,它会把 hbase-common、hbase-protocol、hbase-shaded-miscellaneous 等必要依赖拉进来。Hadoop 相关依赖要按集群版本来,别默认传递,避免 jar 包冲突。
一个能跑的基础 pom 坐标大致是这样的:
<dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-client</artifactId> <version>2.4.13</version> </dependency>如果你的运行环境还有其他 Hadoop 组件,特别注意 hbase-client 传递依赖里的 Hadoop 版本是不是和 runtime 环境一致。最典型的问题是运行时报 ClassNotFound 或者 NoSuchMethodError,十有八九是 jar 包被某条依赖链覆盖了。排查方式不复杂,用 mvn dependency:tree 看冲突,再在打包配置里把不需要的传递排除掉,但确实很耗费时间,所以一开始就把依赖锁干净比事后救火划算。
2.2 连接配置:ZooKeeper、Configuration 与常见参数
HBase 客户端连接不像普通数据库那样直接给一个 IP 和端口。它依赖 ZooKeeper 定位 meta 表,再找到真实的 RegionServer 地址。所以客户端配置里必须指定 ZooKeeper 的地址列表。有一点容易被忽略:较新的 HBase 版本里,客户端连接参数主要靠 hbase-site.xml 或 Configuration 对象注入,而不是像老版本那样在代码里 set 一串字符串就行。
基础连接代码通常长这样:
Configuration conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum", "zk1,zk2,zk3"); conf.set("hbase.zookeeper.property.clientPort", "2181"); Connection conn = ConnectionFactory.createConnection(conf);如果是 Kerberos 认证环境,还要额外配置 principal 和 keytab。测试环境里很多开发者习惯用 root 用户直接跑,这在生产环境千万别干,权限和审计都不合格。还有一个细节:客户端自定义 ZK session 超时时,必须保证在服务端配置的允许范围内,否则会出现客户端自己先判定会话超时,服务端却认为会话还活着,两边状态不一致,请求就会互相踩踏。
2.3 Admin 操作:建表、删表、修改列族的正确姿势
用 Java API 做表结构管理,核心对象是 Admin。建表代码很直接,但里面的坑不少。先看示例:
TableName tableName = TableName.valueOf("user_log"); TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName); ColumnFamilyDescriptorBuilder cfBuilder = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("info")); builder.setColumnFamily(cfBuilder.build()); try (Admin admin = connection.getAdmin()) { if (!admin.tableExists(tableName)) { admin.createTable(builder.build()); } }这里第一个坑:建表时如果没有预分区,默认只有一个 Region,海量并发写入时所有数据会堆在同一个 RegionServer 上,热点问题瞬间暴露。所以正式表几乎必然要做预分区。预分区可以手动指定 split keys,也可以让 HBase 按预设规则切分,但手工指定 split keys 可控性最强。比如按照业务 ID 的哈希范围拆成 16 个区间,每个区间一个 Region,写入压力就均匀了。
修改列族属性时也用 Admin。调整 TTL、压缩算法、版本数都可以在线执行,但有一些变更做不了,比如修改列族名称在 HBase 里不支持,只能重建表。这个设计坑了很多人,最佳实践是列族名称从一开始就规划好,后续别频繁改。表结构变更之前,最好先在预发环境用同样的数据量验证一次,避免在生产环境里临时发现 API 行为和你预期不一致。
3. 核心读写操作:Put、Get、Scan、Delete 的日常正确姿势
3.1 Put 写入:从单条到批量,关键属性不能乱调
Put 是使用频率最高的对象,但很多人在 Put 上犯的错比想象中多。最典型的就是不考虑 Durability。Durability 决定写入是否要等待 WAL(Write Ahead Log)落盘,默认是 SYNC_WAL,意味着每次写入都要同步写 WAL 才返回成功。
如果你的场景是日志上报、监控指标这类允许少量丢失的数据,可以设置成 ASYNC_WAL 或 SKIP_WAL,写入吞吐会明显提升。但是千万注意,一旦 RegionServer 宕机,SKIP_WAL 的数据直接丢。这个损失业务能不能接受,必须提前评估。我的原则是:非核心日志可以调,核心业务数据坚决不动。
还有一个容易踩的坑是老版本用 setWriteToWAL 控制 WAL,新版本这个接口已经废弃,改用 Durability 枚举。很多老代码升级后忘了改,导致行为不符合预期。批量写入建议用 BufferedMutator,而不是在循环里逐个 flush:
try (BufferedMutator mutator = connection.getBufferedMutator(TableName.valueOf("user_log"))) { for (byte[] rowKey : rowKeys) { Put put = new Put(rowKey); put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("level"), Bytes.toBytes("ERROR")); mutator.mutate(put); } mutator.flush(); }BufferedMutator 内部会按 writeBufferSize 批量提交,默认是 2MB,可以按写入量和网络带宽调整。这个对象也是重量级资源,要复用,别在循环里 new。一个常见的性能优化动作是把 writeBufferSize 调到 8MB,RPC 次数能减少一大截,但要注意单次批量失败后的重试成本也会变高。
3.2 Get 获取:精确读取、列裁剪和版本控制
Get 的关键在于怎么最小化网络开销。默认情况下,一次 Get 会返回该行全部列族的所有列,即使你只需要其中一列。所以显示指定列非常有必要:
Get get = new Get(Bytes.toBytes("row123")); get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("level")); get.readVersions(1); Result result = table.get(get); byte[] value = result.getValue(Bytes.toBytes("info"), Bytes.toBytes("level"));这里有个容易混淆的概念:readVersions 读取版本数,setMaxVersions 设置扫描时的版本上限,而真正的版本保留数量取决于列族配置里的 VERSIONS 属性。如果你在列族里只保留 1 个版本,代码里就算 readVersions(5) 也读不到多个版本,服务端根本没存。这个逻辑和传统数据库差异很大,新手上线前最好先验证一次。
另一个坑是很多人在 Result 拿到 null 之后不判断,直接 getValue,结果空指针。HBase 的 Get 查不到数据时返回的是空 Result,不是 null,这一点和 JDBC 的返回风格不一样,写代码时要把这个分支处理好。
3.3 Scan 扫描:范围、缓存、批量的性能权衡
Scan 是整个 API 里最需要小心的对象。一个没有限制的 Scan,会把整张表所有 RegionServer 全部扫一遍,对集群来说就是灾难。所以写 Scan 的第一原则永远是设定 StartRow 和 StopRow,并且尽量让 RowKey 前缀能过滤掉大部分无关数据。
Scan scan = new Scan(); scan.withStartRow(Bytes.toBytes("user_1000")); scan.withStopRow(Bytes.toBytes("user_2000")); scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age")); scan.setCaching(500);setCaching 决定了客户端每次 RPC 向 RegionServer 拉取的行数,不是总行数。设成 1,每取一行就产生一次 RPC,Scan 会慢到让人怀疑人生。设成 10000,内存又可能被打爆。常规经验是 500 到 1000,还要结合单行大小:单行 1KB,取 1000 行就是 1MB 左右,这个量级比较合理。
setBatch 是另一个参数,控制每次 RPC 返回的列数。如果行特别宽,列数非常多,batch 小于列数时,同一行会被分成多次返回,客户端要自己拼接。这个参数新手很少碰,但对宽表读性能的影响非常大。如果一张表有几百个列,只查其中几列时,setBatch 和 addColumn 结合使用能大幅减少网络传输量。Scan 的结果集用完一定要 close,因为底层 ResultScanner 持有 RPC 连接资源,不关就是连接泄漏。这一点我会在问题排查部分详细说。
3.4 Delete 删除:逻辑删除与物理清除的区别
Delete 在 HBase 里的表现和 MySQL 完全不同。HBase 的 Delete 本质是写入一条删除标记(Tombstone),而不是物理删除。被删除的数据仍然占用磁盘空间,要等下一次 Major Compaction 才会被真正清理。
这个特性带来的后果是:你可能删完数据后,表容量没有下降,某些版本还能查到旧数据,监控看起来也很怪异。所以做数据清理任务时,预期要放宽:删除生效是逻辑层面的,物理空间释放是滞后的。如果业务对物理空间释放有硬性要求,只能手动触发 major_compact,但这种方式对集群性能影响较大,最好在低峰期做。
删除代码本身简单:
Delete delete = new Delete(Bytes.toBytes("row123")); delete.addColumns(Bytes.toBytes("info"), Bytes.toBytes("level")); table.delete(delete);注意 addColumns 与 addColumn 的区别。addColumns 删除整列的所有版本,addColumn 只删除最新一个版本。这个 API 命名极容易混淆,我亲眼见过生产事故,把 addColumn 当成 addColumns 用,旧版本数据一直删不掉,用户反复看到过期配置。这种问题最难查,因为它不报错,只是结果不符合预期。
4. 批量操作与过滤器:复杂查询的基础构件
4.1 批量写:多线程与缓冲的正确组合
批量操作不只是把多个 Put 放进 list 里调用 table.put(list),那是同步提交,写入量大时依然会成为瓶颈。真正的高吞吐方案通常有两种:单线程 BufferedMutator 加调大缓冲区,或者多线程配合 BufferedMutator 分段写。
多线程写入时,线程数不是越多越好。RegionServer 的数量、每个 RegionServer 的 handler 数、客户端机器核心数共同决定最佳线程数。经验做法是从 8 个线程起步,用压测逐步往上加,观察吞吐和延迟的拐点。一上来就开 64 线程,RegionServer 的 handler 被占满之后,RPC 排队时间会急剧上升,吞吐反而下降。这个拐点每个集群都不一样,必须实际压测。
还有一个必须注意的问题:同一行的多个 Put 要保证顺序。HBase 对同一行的修改按提交顺序执行,如果多线程模式下对同一行并发写,最终结果不确定。所以设计 RowKey 时要有意识地让重要操作路由到同一个线程,或者干脆串行化。HBase 的 row 级原子性只保证单行内操作,跨行的多个写没有事务,需要多行一致性的业务必须在应用层自己处理。
4.2 过滤器:让 RegionServer 替你干活
Filter 是 HBase 查询的精髓。很多人在做查询时习惯把数据全部拉回客户端再过滤,这是最笨的办法。正确思路是能下推的过滤逻辑全部用 Filter 表达,让 RegionServer 在读磁盘阶段就裁掉无关数据。
常用过滤器里,RowFilter 按 RowKey 做比较运算,SingleColumnValueFilter 按列值过滤,PrefixFilter 做前缀匹配,PageFilter 做分页,KeyOnlyFilter 只返回 key 不返回 value,适合存在性判断。这些过滤器可以组合,FilterList 支持 MUST_PASS_ALL 和 MUST_PASS_ONE 两种逻辑,分别相当于 AND 和 OR。组合时要注意执行顺序,FilterList 是逐条作用的,后一个 Filter 在前一个结果上继续过滤,顺序直接影响性能。经验是把选择性最强的 Filter 放前面,越早裁剪越好。
分页查询的典型实现是 startRow 加 PageFilter。第一次从空 startRow 开始,拿到 count 行,第二页用上一页最后一条 RowKey 作为新的 startRow,并把它递增到下一行。这不是严格的 offset 分页,但对海量数据来说这是唯一高效的方案。我之前做过一个管理后台,列表要分页,最开始准备用客户端分页,数据量一上来就扛不住,改成 PageFilter 加分段扫描后,性能提升非常明显。
4.3 RowKey 设计:最高优先级的性能决策
RowKey 设计虽然不完全属于 API 代码问题,但在 HBase Java API 实战中绕不开。RowKey 直接决定了你的数据能不能被快速定位。几个关键原则:
第一,散列优先。直接用自增 ID 做 RowKey,会把热点打到最后一个 Region。推荐对 ID 取哈希后再拼业务字段,让数据分布均匀。比如 user_123 可以改成 user_9bc5f2。
第二,控制长度。RowKey 不是越短越好,但要避免过长。太长会放大索引和内存占用,一般控制在 16 到 64 字节够用。
第三,时间戳别放在前缀。如果 RowKey 以写入时间开头,所有新数据都会写进同一个 Region,形成写入热点。正确做法是时间戳做后缀,散列值做前缀。
第四,精确查询和范围查询往往不可兼得。如果既想用业务 ID 精确定位,又想按时间范围扫描,可以考虑二级索引,或者把 RowKey 设计成散列值加固定时间桶的两级结构。这个问题没有银弹,必须根据业务访问模式做取舍。
5. 性能调优与问题排查实录
5.1 客户端参数调优的几个关键旋钮
HBase 客户端性能不是靠一个参数就能提升的,而是一组参数配合。我挑几个最常用也最值得关注的:
- hbase.client.write.buffer:BufferedMutator 写缓冲大小,默认 2MB,调大到 8MB 可减少 RPC 次数,但也要考虑失败重试的放大成本。
- hbase.client.pause:重试间隔基准值,默认 100 毫秒。不要调太大,慢 Region 会让所有请求排队。
- hbase.client.retries.number:重试次数,默认 15 次。在线业务建议压到 3 到 5 次,快速失败比无限重试更友好。
- hbase.rpc.timeout:单次 RPC 超时,默认 60 秒。大批量 Scan 时容易触发,可以根据业务调大到 120 秒。
- hbase.client.scanner.timeout.period:Scanner 空闲超时,默认 60000 毫秒。分页扫描时,如果每一页之间间隔太久,这里会先触发超时,要特别留意。
这些参数既可以在代码里设置,也可以写进 hbase-site.xml 放在 classpath。我更推荐后者,因为运维和开发看到的是同一份配置,省得代码里到处都是散落的配置项,排查时不知道哪个值覆盖了哪个值。
参数调优前后一定要有对比数据作为依据,不要凭感觉调。我倾向于先记录调优前的 P99 延迟和吞吐,改一个参数再压测一轮,逐个验证。这样才能知道每个参数在这个集群上到底起了多大作用。
5.2 连接泄漏、会话过期与 RPC 超时
写 HBase 客户端常见的问题有三类,按实际排查频率排序。
连接泄漏是最普遍的。症状是跑一段时间后日志里频繁出现 Connection closed 或 Call timeout,监控里活跃连接数只增不减。主要原因就是 ResultScanner 没 close、Table 对象没 close,或者 Connection 每次都新建。解决办法不是加内存,而是把资源关闭全部收口到 finally 或者 try-with-resources。我做代码评审时看到这种问题,第一反应就是查资源关闭路径。
ZooKeeper 会话过期是另一种典型。通常发生在客户端长时间 GC 或网络闪断后,客户端和 ZK 的 session 断开,meta 缓存失效。此时即使表还在,所有请求都会报 NotServingRegionException 或 ZooKeeper session expired。解决方向是让客户端快速重建连接,错误处理里可以捕获可重试异常,但别在 DoNotRetryIOException 上继续重试,那是白费力气。
RPC 超时更隐蔽。同一个查询,有时 20 毫秒,有时 60 秒超时,这种波动多半是 RegionServer 在做 Compaction 或 GC。排查时要看 RegionServer 的 GC 日志和 Compaction 队列状态。如果确认是 Compaction 引起的,可以考虑调整 compaction 的阻塞因子,或者把表的大合并时间窗口分散到业务低峰期。超时问题最难的一点是它不报明确错误,只表现为延迟毛刺,需要监控指标配合才能定位。
5.3 大结果集 Scan 的内存保护
Scan 是内存杀手。如果查询返回的数据量超过客户端 JVM 堆的承受能力,最直接的结果是 Full GC 频繁,甚至 OOM。除了合理设置 setCaching 和 setBatch 之外,还有一个容易被忽视的点:scan.next() 虽然每次返回一个 Result,但 ResultScanner 内部会把 caching 数量的行一次性拉回内存。所以大结果集一定要分批处理,不能一把梭。
正确的处理模式是用 try-with-resources 包住 Scanner,边读边处理,不在内存里堆积:
try (ResultScanner scanner = table.getScanner(scan)) { for (Result result : scanner) { process(result); } }这个模式看着普通,核心在于 try-with-resources 保证 Scanner 在任何异常路径下都被关闭,process 里不要堆积数据。如果要全量导出,建议不要在一个进程里跑完,而是按 StartRow 和 StopRow 分段,用多个任务并行处理。这样每个任务内存可控,失败重试的粒度也小得多。
我曾经优化过一个数据导出任务,原来是一个大 Scan 全表跑,堆内存 8GB 都撑不住,改成按 Region 边界分段后,每个分片几百 MB 内存就搞定了,时间还缩短了一半。核心逻辑就是避免把所有数据同时放进客户端内存。
6. 从代码到线上:经验沉淀与上线前清单
6.1 一个最小可用 Demo:从建表到读写
很多新人问我要模板,我直接给一个能跑通的最小例子,覆盖连接、建表、写入、读取全流程。本地起一个 standalone HBase 就可以验证,不用依赖生产环境。
public class HBaseQuickStart { public static void main(String[] args) throws Exception { Configuration conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum", "localhost"); try (Connection conn = ConnectionFactory.createConnection(conf)) { TableName tableName = TableName.valueOf("demo_user"); try (Admin admin = conn.getAdmin()) { if (!admin.tableExists(tableName)) { TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableName); builder.setColumnFamily( ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("info")).build()); admin.createTable(builder.build()); } } try (Table table = conn.getTable(tableName)) { Put put = new Put(Bytes.toBytes("row1")); put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("zhangsan")); table.put(put); Get get = new Get(Bytes.toBytes("row1")); get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name")); Result result = table.get(get); byte[] value = result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")); System.out.println(Bytes.toString(value)); } } } }这个例子虽然简单,但它体现了正确的资源管理思维:Connection 单例级复用,Admin 和 Table 都在 try-with-resources 里。初学者可以从这里起步,往里面加 Scan、Filter、BufferedMutator,逐步熟悉 API 的行为。
6.2 上线前必查清单
这里整理一份我每次发布 HBase 相关应用前都会检查的清单,照着过一遍能减少大部分低级故障:
- Connection 是否单例复用,有没有在循环里创建?
- Table 和 ResultScanner 是否一定被关闭,有没有裸奔的资源?
- 客户端版本和生产集群版本是否完全一致?
- RowKey 设计是否包含散列,能不能抗住写入热点?
- 建表时是否做了预分区,region 数量是否匹配数据规模?
- 写缓冲、重试次数、超时时间是否符合业务延迟预期?
- 是否有监控埋点,能区分读延迟和写延迟?
- 是否压测过最大并发,P99 延迟在不在接受范围内?
这份清单看着基础,但每条背后都有真实事故支撑。我见过线上故障里很大一部分,回溯源就是清单里的某一项没做到位。特别是资源关闭和版本一致这两条,最简单但出问题最多。
6.3 最后聊一个实际动作:客户端监控
最后分享一个个人的习惯:给 HBase 客户端加监控。很多人把监控重点放在 RegionServer 端,忽略了客户端视角。其实同一个查询从客户端发起和从服务端处理,两个视角看到的延迟差异非常大,因为中间还隔着网络、序列化和连接排队。
我在客户端里加了三个维度的小埋点:RPC 调用次数、平均延迟、异常分类计数。配合服务端按 Region 维度的指标,基本能快速判断问题出在哪个环节。这个工作看起来很琐碎,但排查线上问题的时候价值极高。有一次报表系统慢查询,服务端指标全绿,客户端却显示某个 RegionServer 的连接排队时间一直在涨,最后定位是那个节点在做大合并,请求全部挤在了一个 Region 上。如果没有客户端监控,这个问题不知道要查多久。
HBase Java API 的上手难度不在语法,而在思维方式的转变。它的事物质性、版本控制、过滤器下推、RowKey 设计,每一条都需要在实际数据量下验证过才真正有体感。这篇内容就是我把这几年踩过的坑整理出来的结果,希望你能少走一些弯路。