先交代一下背景。我之前维护过一个业务系统,单表数据量到千万级之后,MySQL 的慢查询基本就按不住了,几张大表的关联统计跑一次要几十秒,业务方抱怨不断。当时也考虑过分库分表,但团队评估下来改造链路太长,后来选了 HBase 作为历史明细数据的存储层,把线上的时序数据和订单流水逐步迁过去,问题才算真正解决。HBase 是一个构建在 HDFS 之上的分布式、面向列的 NoSQL 数据库,擅长海量数据的写入和按 RowKey 的毫秒级随机读,适合几亿甚至几十亿级别的结构化、半结构化数据场景。
这篇博文把我从零开始安装、配置、使用 HBase 的完整过程整理出来,包括版本选型、依赖环境搭建、核心配置逐项解释、Shell 和 Java API 的基本操作,以及我在部署和使用过程中真实踩过、排查过、修复过的几个坑。无论你是第一次接触 HBase 的新手,还是想搭一套测试环境做技术验证的开发同学,这篇文章应该能帮你少走不少弯路。
1. 先搞清楚 HBase 适合干什么,再决定要不要装
1.1 一张表把 HBase 和 MySQL 的定位差异说清楚
很多人第一次听到 HBase,下意识会把它当成“分布式的 MySQL”。这个理解不能说全错,但会误导你朝错误的方向使用它。它确实支持类似主键的 RowKey 随机读写,也支持多版本数据,但它没有真正意义上的跨行事务,也不支持 SQL 里的各种 JOIN 和复杂索引。它与 MySQL 在使用定位上有本质区别,我用一张表说明:
| 对比项 | MySQL | HBase |
|---|---|---|
| 数据规模 | 单表千万级后开始吃力 | 百亿级也能扛,取决于集群资源 |
| 存储方式 | 行式存储,所有列一起落盘 | 面向列族存储,空列不占空间 |
| 读写模式 | 强事务,适合频繁更新、删改 | 适合海量写入,更新本质是追加新版本 |
| 查询方式 | SQL + 索引,支持 JOIN | 按 RowKey get、按范围 Scan,不支持 JOIN |
| 扩展方式 | 分库分表,业务侵入大 | 横向扩展 RegionServer,自动拆分 Region |
| 事务能力 | ACID 完整事务 | 仅单行级原子性,跨行需要额外设计 |
| 数据排序 | 按主键聚簇索引,或二级索引 | 按 RowKey 字典序自动排序 |
| 适用场景 | 在线业务、交易系统、台账 | 日志、流水、历史明细、时序数据 |
我自己实际使用下来的感受是:MySQL 负责“现在”的数据,HBase 负责“历史”的数据。比如订单表,近三个月的订单留在 MySQL 做实时业务,三个月前的订单归档到 HBase 做查询和统计分析,这样两边压力都小,各用所长。
1.2 存储层面的四个核心概念,懂了它你才看得懂日志
安装 HBase 之前,建议至少把下面四个概念理解透,因为后面所有的配置、日志、报错都和它们有关。
- Region:HBase 数据水平分片的基本单位。一张表被按 RowKey 区间拆成多个 Region,每个 Region 交给一个 RegionServer 管理。数据量大了之后 Region 还会继续分裂,从 1 个变 2 个,2 个变 4 个。
- WAL(Write-Ahead Log):写数据时的预写日志。数据到达 RegionServer,会先写 WAL 再写 MemStore,这样即使节点宕机,也能通过 WAL 恢复未落盘的数据。它对应的就是 hbase 的 wal 目录。
- MemStore:每个 Region 内部的内存缓冲区,默认 128MB。写入的数据先在这里积攒,达到阈值后一次性刷写成磁盘上的 HFile 文件。它相当于一个“攒批”机制,把大量随机写变成顺序写。
- HFile:数据最终落地的物理文件,存储在 HDFS 上。HFile 内部的数据是按 RowKey 有序排列的,底层还带布隆过滤器和时间范围索引,这是它能做快速查找的基础。
一句话总结它们的链路:客户端写数据 -> WAL -> MemStore -> HFile -> 定期 Compaction 合并小文件成大数据文件。整个读写链路里的很多问题排查,最后都会落到这四个概念上。
2. 安装前的版本选择和依赖搭建:这一关最容易半途而废
2.1 我实测过的一套稳定版本组合
HBase 安装最让人头疼的不是安装本身,而是版本兼容。JDK、Hadoop、Zookeeper、HBase 四个组件任何两个版本不匹配,都可能出现各种诡异报错。网上很多教程直接用“当前最新版”,这在实际操作中是大忌。我这边实际跑通、验证过的组合是:
- JDK 1.8(1.8.0_202 及以上小版本都可以,不要用 JDK 11 去跑 HBase 2.x,兼容性会有额外问题)
- Hadoop 3.3.4
- Zookeeper 3.6.3(独立部署,三个节点)
- HBase 2.4.17
HBase 目前主流的稳定分支是 2.x。HBase 3.x 虽然已经发布,但周边生态、客户端兼容性还不如 2.x 成熟,多数公司生产环境还在 2.4 系列。选 JDK 8 也是因为 HBase 2.x 对 JDK 8 的支持最成熟,CDH 等大数据套件的适配基准也基本是 JDK 8。如果你只是为了本地学习,JDK 8 + Hadoop 3.3.x + HBase 2.4.x 这一套组合非常稳,别自己凭空混搭版本。
需要特别提醒:安装前一定要去官网查 HBase 官方兼容矩阵,确认 Hadoop 版本匹配。否则后期会在 RPC 层报一堆奇怪的编解码错误,排查起来非常痛苦。
2.2 三台机器的基础环境操作清单
我这里用三台服务器做演示,主机名规划为 node01、node02、node03。node01 跑 HMaster 和 RegionServer,node02、node03 跑 RegionServer。如果是单机学习,你可以把下面的 hostname 都替换成自己的服务器 IP,也能跑起来。
在装 Hadoop 和 HBase 之前,有几个基础环境操作必须先做好,我自己在这上面踩过多次坑,这里原样列出来:
- 修改
/etc/hosts,把三台机器的 IP 和主机名对应关系写上,并且把注释掉的 127.0.0.1 对应行处理好。否则节点之间通过主机名互相访问时会解析到 no such host。 - 配置 SSH 免密登录,至少保证 node01 可以免密登录到所有节点。因为 HBase 启动脚本会通过 SSH 去各节点执行远程启动命令,没有免密就会卡在输入密码那里。
- 三台机器的时间必须同步,时间差超过 30 秒以上,HBase 的 RegionServer 启动会有各种超时问题。我是直接配置了统一的 NTP 时间同步服务,简单场景你手动 date 命令校准也行。
- 关闭防火墙或放行端口。最简单的方式是先关闭防火墙再说,HBase 涉及 16010、16030、2181、8020 等一系列端口,生产环境再按需放行。
这些操作做完了,再去装 Hadoop。如果你只是测试 HBase,Hadoop 的最低要求是 HDFS 能启动。这里我直接用了 Hadoop 自带的分布式文件系统作为底层存储。
2.3 Zookeeper:用内置的还是单独搭
HBase 强依赖 Zookeeper 做分布式协调,比如 HMaster 选举、RegionServer 的注册与心跳、系统表 meta 的寻址。Zookeeper 挂了,整个 HBase 集群对外基本就不可用了。
HBase 二进制包自带了 Zookeeper,用最简单的方式启动时可以直接使用内置 ZK,也就是把hbase-env.sh里的HBASE_MANAGES_ZK保持为 true。这适合单机测试和快速体验。
但生产环境我强烈建议用独立部署的 Zookeeper 集群,原因有两个:一是 HBase 自带 ZK 和 HBase 进程捆绑,如果 RegionServer 负载很高,会拖累 ZK 心跳;二是 HBase 升级或重启时,如果连带 ZK 一起重启,会对整个集群稳定性造成额外影响。独立部署 ZK 之后,只需要在配置里把 quorum 指向它,然后把HBASE_MANAGES_ZK=false就行。
3. 下载配置启动:完整走一遍 HBase 部署流程
3.1 下载解压和目录规划
版本确定之后,下载就简单了。我习惯把大数据组件统一放在/opt/bigdata目录下,这样环境变量配置和维护都集中。下载命令以 HBase 2.4.17 为例:
cd /opt/bigdata wget https://archive.apache.org/dist/hbase/2.4.17/hbase-2.4.17-bin.tar.gz tar -zxvf hbase-2.4.17-bin.tar.gz mv hbase-2.4.17 hbase解压后目录里有几个关键子目录和文件,新手要记一下位置:
conf/:所有配置文件所在目录logs/:日志目录,排查问题第一个要找的地方就是这里bin/:启动、停止脚本目录lib/:HBase 依赖的 jar 包目录
3.2 hbase-site.xml 和 hbase-env.sh 配置逐项说明
所有核心配置都在conf/hbase-site.xml里。我先给出一个三节点生产环境的完整配置示例,然后逐项解释为什么这么配:
<configuration> <property> <name>hbase.rootdir</name> <value>hdfs://node01:8020/hbase</value> </property> <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> <property> <name>hbase.zookeeper.quorum</name> <value>node01,node02,node03</value> </property> <property> <name>hbase.zookeeper.property.clientPort</name> <value>2181</value> </property> <property> <name>hbase.regionserver.handler.count</name> <value>50</value> </property> <property> <name>hbase.master.info.port</name> <value>16010</value> </property> </configuration>hbase.rootdir:HBase 数据在 HDFS 上的根目录,必须是 HDFS 路径。注意目录要提前存在,而且权限要够,否则 Master 启动时会报错无法创建目录。hbase.cluster.distributed设为true,就是告诉 HBase 以分布式模式运行,走 HDFS 和独立 ZK。如果设成 false,它会以 standalone 模式运行,数据落在本地文件系统,不是我们想要的。hbase.zookeeper.quorum:Zookeeper 集群主机列表。
这里不需要填端口,只需要主机名,客户端连接时默认读 2181。
hbase.zookeeper.property.clientPort:指定 ZK 客户端端口,默认就是 2181,显式写出来方便自己以后检查。hbase.regionserver.handler.count:每个 RegionServer 可以同时处理的 RPC 请求数量。默认值偏保守,读写并发比较高时可以加大到 50 到 100。但不是越大越好,太大反而堆积线程,要根据机器核数来看。hbase.master.info.port:HBase Master 的 Web UI 端口。HBase 2.x 默认是 16010,通过浏览器访问http://node01:16010可以看到集群状态。
另外在conf/hbase-env.sh里必须设置两样东西:
export JAVA_HOME=/usr/local/java/jdk1.8.0_202 export HBASE_MANAGES_ZK=falseJAVA_HOME一定要显式写绝对路径。HBase 的启动脚本虽然会自动检测,但检测逻辑经常会在多版本 JDK 的环境下找错。HBASE_MANAGES_ZK=false表示使用独立 ZK,如果你用了内置 ZK,就不要这一行配置。
HBase 默认分配的堆内存偏保守,对于负责存储计算的进程来说不够。如果你的服务器内存足够(比如 32GB),建议把 Master 和 RegionServer 的堆内存调大:
export HBASE_HEAPSIZE=8G这个方法会同时作用在 Master 和 RegionServer 上。对于大集群更精细的调法是把它们的堆内存拆开设置,测试环境可以先用这个统一配置。
3.3 启动集群并验证结果
所有配置完成后,启动顺序有讲究:先启动 HDFS,再启动 Zookeeper,最后启动 HBase。顺序颠倒容易出现 HBase 找不到 HDFS 元数据的情况。
按顺序执行:
# 1. 在 node01 上启动 HDFS start-dfs.sh # 2. 在三台节点上分别启动 Zookeeper(或使用 zkServer.sh start) zkServer.sh start # 3. 在 node01 上启动 HBase start-hbase.sh启动完成后,验证集群是否正常的标准动作有三个:
# 1. 查看 Java 进程,确认 HMaster 和 HRegionServer 都已经启动 jps在 node01 上应该看到 HMaster 和 HRegionServer 进程,在 node02、node03 上应该看到 HRegionServer 进程。如果只看到 HMaster 没有 HRegionServer,大概率配置有问题,常见原因:hbase.rootdir权限不足或 hosts 解析异常。
# 2. 进入 HBase Shell,检查集群状态 hbase shell status正常情况下会显示类似如下的输出,说明三个 RegionServer 都正常:
1 active master, 0 backup masters, 3 servers, 0 dead, 33.0000 average load# 3. 浏览器访问 http://node01:16010,能看到集群整体状态,以及每个 RegionServer 的 Region 数量、请求量、堆内存使用情况。到这里,HBase 就装好了。看起来步骤不多,但这里每一步我几乎都踩过坑,后面单独拿出来讲。
4. Shell 命令和 Java API 是两套基本功,都得会
4.1 Shell 里最常用的建表、读写、删除命令
HBase Shell 是排查问题、快速操作数据最直接的工具。下面这些命令我几乎每天都在用,按操作场景分类给出来:
# 创建一张表,名为 student,包含两个列族 info 和 score create 'student', 'info', 'score' # 查看表结构 describe 'student' # 写入一条数据,RowKey 为 1001,向 info 列族写入 name 列的值 put 'student', '1001', 'info:name', 'zhangsan' # 读取指定 RowKey 的全部数据 get 'student', '1001' # 读取指定 RowKey 的单个单元格数据 get 'student', '1001', {COLUMN => 'info:name'} # 读取指定 RowKey 的多个版本(默认保留1个版本,修改配置后可保留多个) get 'student', '1001', {COLUMN => 'score:math', VERSIONS => 3} # 全表扫描(生产环境慎用,数据量大时很危险) scan 'student' # 带限制条件的扫描,只取前5条 scan 'student', {LIMIT => 5} # 按 RowKey 范围扫描 scan 'student', {STARTROW => '1001', STOPROW => '1005'} # 删除某个单元格的数据 delete 'student', '1001', 'info:name' # 删除整行的数据 deleteall 'student', '1001' # 清空表但不删除表结构 truncate 'student' # 禁用表和重新启用表(修改表结构前必须先 disable) disable 'student' enable 'student' # 删除表(必须先 disable) drop 'student'刚上手时最容易犯的错误是直接对一张正在被读写的表执行drop。正确顺序一定是先disable,再drop。另外,scan全表在生产上要极其克制,因为它会扫描所有 Region,压力非常大。最常见的做法是给scan加STARTROW、STOPROW和LIMIT条件,把扫描范围框住。
4.2 Java API 的正确打开方式
程序访问 HBase 主要通过 Java API。下面这段代码是我实际项目里抽出来的模板,可以做最基本的建表、写入、查询和扫描:
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.*; import org.apache.hadoop.hbase.client.*; import org.apache.hadoop.hbase.util.Bytes; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class HBaseDemo { private static Connection connection; public static void main(String[] args) throws IOException { Configuration conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum", "node01,node02,node03"); conf.set("hbase.zookeeper.property.clientPort", "2181"); // Connection 是重量级对象,一个进程只创建一个,全程复用 connection = ConnectionFactory.createConnection(conf); createTable("student", "info", "score"); putData("student", "1001", "info", "name", "zhangsan"); getData("student", "1001"); connection.close(); } public static void createTable(String tableName, String... columnFamilies) throws IOException { Admin admin = connection.getAdmin(); TableName tn = TableName.valueOf(tableName); if (admin.tableExists(tn)) { System.out.println("Table already exists"); return; } TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tn); for (String cf : columnFamilies) { builder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf)).build()); } admin.createTable(builder.build()); admin.close(); } public static void putData(String tableName, String rowKey, String cf, String column, String value) throws IOException { Table table = connection.getTable(TableName.valueOf(tableName)); Put put = new Put(Bytes.toBytes(rowKey)); put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(column), Bytes.toBytes(value)); table.put(put); table.close(); } public static void getData(String tableName, String rowKey) throws IOException { Table table = connection.getTable(TableName.valueOf(tableName)); Get get = new Get(Bytes.toBytes(rowKey)); Result result = table.get(get); Cell[] cells = result.rawCells(); for (Cell cell : cells) { System.out.println( "RowKey: " + Bytes.toString(CellUtil.cloneRow(cell)) + ", Column: " + Bytes.toString(CellUtil.cloneQualifier(cell)) + ", Value: " + Bytes.toString(CellUtil.cloneValue(cell)) ); } table.close(); } }这段代码里有几个细节挺关键的。ConnectionFactory.createConnection()创建的是重量级对象,内部包含 RPC 连接池、Zookeeper 连接等资源,一个进程只需要一个,多个线程共享即可。千万不要在每次操作时 new 一个 Connection,否则连接数会爆掉,性能差到让你怀疑人生。Table对象相对轻量,但也不要极频繁地创建和销毁,一般每个线程保持一个。
4.3 批量写入和 Scan 的缓存设置
批量写入场景下,不要一条一条 put,要么用table.put(List<Put>),要么用BufferedMutator。我实测下来的经验是,批量 put 的性能相比单条 put 有几十倍的提升,因为减少了 RPC 次数。
BufferedMutator mutator = connection.getBufferedMutator(TableName.valueOf("student")); List<Put> puts = new ArrayList<>(); for (int i = 0; i < 10000; i++) { Put put = new Put(Bytes.toBytes("user_" + i)); put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("user_" + i)); puts.add(put); if (puts.size() >= 1000) { mutator.mutate(puts); puts.clear(); } } if (puts.size() > 0) { mutator.mutate(puts); } mutator.flush(); mutator.close();BufferedMutator会在客户端本地攒一批数据再提交,类似一个缓冲区。默认缓存大小可以配置,写入量大时建议调大到 16MB 以上。
Scan 方面,ResultScanner是分页从服务器端拉取数据的。默认下一批数据一次最多返回 100 行,如果是大批量导出场景,这个值太小会导致 RPC 次数过多。可以设置scan.setCaching(1000)把一次取回的行数调大,但也要注意 RegionServer 端的内存消耗,取值不要超过 5000,不然很容易把 RegionServer 的堆内存打满。
5. RowKey、预分区、列族数量:这三个设计点决定你后面用着爽不爽
5.1 RowKey 三条原则,反着做的都吃过亏
HBase 的查询效率几乎完全取决于 RowKey 的设计,因为只有 RowKey 才能走索引。它的排序规则是按字典序从小到大排列,这一点既是优点也是陷阱。我在日常运维中总结出三条必须遵守的原则:
第一,散列性。RowKey 不能是单调递增的连续值。比如用自增 ID、时间戳直接当 RowKey,物理上新数据永远写入最后一个 Region,热点就集中在一个 RegionServer 上,其他节点闲着。解决方案可以加盐(salt),比如user_id_hash % 100拼在 RowKey 前面;或者对原始主键做反向、哈希处理后作为前缀。
第二,长度越短越好。RowKey 是每条数据的索引,也是数据的一部分。HFile 无论是内存还是磁盘里,都会对 RowKey 做大量存储和比较。一个 100 字节的 RowKey 和一个 20 字节的 RowKey,在几十亿数据量下消耗的存储和 IO 差距是巨大的。建议 RowKey 设计在 10 到 50 字节以内,不要在 RowKey 里塞过长的业务描述字段。
第三,唯一性。RowKey 在同一张表里必须唯一,后者写入会覆盖前者数据。如果你需要同一个主键下保留多条历史版本,可以把时间戳、版本号拼进 RowKey,或者依赖 HBase 的 VERSIONS 机制,不过版本数一般不建议开太大。
5.2 默认建表只开一个 Region,写入热点几乎是必然的
新建一张表默认只有一个 Region,所有写入压力都会集中在这一个 Region 的 RegionServer 上。数据量变大后 Region 会分裂,但分裂过程中会有短暂的性能波动,而且分裂出的 Region 边界往往不均匀。生产环境的正确做法是预分区,在建表时直接指定 RowKey 的切分节点。
# 在 Shell 中按多个分割点建表,表会自动分成多个 Region create 'student', 'info', 'score', SPLITS => ['100000', '200000', '300000', '400000']这样建出来的表会按 RowKey 100000、200000、300000、400000 自动划分成 5 个 Region,数据写入时就能分散到多个 RegionServer 上。
如果你不清楚具体切分点,也可以用十六进制字符串切分算法,它会均匀地把整个 RowKey 空间切开:
# 创建 15 个 Region,适合 RowKey 为十六进制字符串的场景 create 'student', 'info', 'score', {NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}预分区到底要建多少 Region,没有一个固定值。可以根据集群节点数和预计数据量估一个范围,每个 Region 的理想数据量在 10GB 到 20GB 左右,这样可以减少 Region 后续分裂带来的开销。
5.3 列族和布隆过滤器怎么选
列族的数量直接影响读写性能。很多人习惯把一个业务模块一个列族,一张表建了三四个列族,这在 HBase 里是低效的。原因是每个列族的数据独立落在不同 HFile 里,刷写 MemStore 时如果一个列族数据量小、另一个数据量大,就会出现小文件较多的场景,最终 Compaction 和 Region 分裂时的 IO 都被放大。
实际经验是:一张表一个列族是最理想的状态,最多不要超过两到三个。列族的作用更多是区分不同生命周期或访问频率的数据,比如一个放详情数据,一个放统计汇总数据,它们的 TTL、压缩策略往往不同。
布隆过滤器则是偏读优化的重要手段。默认情况下,读取时会扫描这个 Region 涉及的所有 HFile 文件。开启布隆过滤器之后,它在一开始就能快速判断“这个 RowKey 不存在于该文件”,从而跳过大量无效文件的打开和数据块加载。对于随机读场景,开布隆过滤器的收益非常明显。
# 建表时指定布隆过滤器类型 create 'student', {NAME => 'info', BLOOMFILTER => 'ROW'}如果你的查询模式是针对整个 RowKey,用ROW;如果经常查询某个 RowKey 下的特定列,用ROWCOL。用ROWCOL的开销会比ROW大一些,但过滤粒度更细。写入多、读取少的场景,你可以不开布隆过滤器,省一点写入时的内存和 CPU 开销。
6. 部署和日常使用中我踩过的三个坑
6.1 坑一:RegionServer 反复退出,日志全是连接超时
第一次部署时,我在 node01 执行完start-hbase.sh后,jps 明明看到了 HRegionServer 进程,但是过了几分钟再回头看,进程不见了。去看logs/hbase-hbase-regionserver-node01.log,里面大量出现TimeoutException和Connection refused,看起来像是 RegionServer 连不上其他节点。
我的排查链路是这样走的:
- 先检查 Zookeeper 是否正常。用
echo srvr | nc node01 2181确认 ZK 能响应,没问题。 - 再检查 HDFS。执行
hdfs dfsadmin -report,发现 DataNode 节点只有 node01 在线,node02、node03 的 DataNode 根本没通过检查。 - 去 node02 看 DataNode 日志,发现它一直报
java.net.UnknownHostException: node02。排查 hosts 文件才发现,node02 的/etc/hosts里同时保留了127.0.0.1 node02和实际 IP 对应的 node02,导致它把自己解析到了回环地址。 - 把每个节点的 hosts 文件统一改成“实际 IP + 主机名”,去掉回环映射后,DataNode 正常注册,HBase 也随之稳定。
这个坑的本质不是 HBase 配置问题,而是 Hadoop 层没有跑通。遇到 RegionServer 反复退出,先检查底层 HDFS 节点健康状态,再看 ZK 和 hosts,不要一上来就去调 HBase 参数。
6.2 坑二:单条查询慢到离谱,从头排查一遍
另一个印象深刻的坑是:线上表用 get 按 RowKey 查询时,明明索引路径是准的,但查询响应平均 300 到 500 毫秒,个别请求甚至到秒级。这个表现完全不像分布式的随机读。
我的排查链路是这样的:
- 先看 HBase UI 上每个 RegionServer 的读请求量和延迟,发现所有请求几乎都打在一个 RegionServer 上。再往细看,那台机器的 Region 数量很少,但每个 Region 的 StoreFile 数量很多。
- 用
hbase hfile -m -f hdfs://node01:8020/hbase/data/default/student/xxx/HFile 文件路径看 HFile 的索引和布隆过滤器情况,确认表在创建时没有开启布隆过滤器。 - 再去看表结构,确认表创建时才 2 个 Region(Share 大量分裂导致的),导致 RowKey 前缀的写入全部落在同一个 Region 区间。
- 最终处理方案:重设表结构,按业务 RowKey 散列后缀做预分区,开启
BLOOMFILTER => 'ROW',然后对旧表执行一次 major_compact,把大量小的 StoreFile 合并成少量大文件。处理完再测,单条 get 拉回 1 到 5 毫秒区间。
这里是两个问题的叠加:RowKey 热点导致数据都在一个 Region,加上没有布隆过滤器,读取时要扫描的全文件就很多。如果当初建表就做了预分区并开启布隆过滤器,后面基本不会走到这一步。
6.3 坑三:数据写成功后立刻查不到
还有一次排查了很久的诡异问题:程序里 Put 返回成功,但立刻用 get 查同一 RowKey 却查不到。当时第一反应是数据一致性出问题了,一度怀疑 MemStore 还没刷盘导致读取链路有 bug。实际上问题出在客户端配置。
我的排查链路是这样的:
- 先在 HBase Shell 里手动 put 同一条数据,然后立刻 get,能查到。说明服务端没问题。
- 再去检查 Java 客户端代码,发现它连接的 ZK quorum 指向了另一个测试集群,虽然表名相同,但写入的是另一张表。
- 确认客户端
hbase.zookeeper.quorum配置和服务端集群一致后,问题消失。
这类“数据丢失”问题其实大概率是客户端连错了集群或连错了表。排查顺序建议:Shell 验证服务端 -> 检查客户端配置 -> 检查表名和 RowKey 是否写错。不要一上来就怀疑 HBase 的写入链路,HBase 的 Put 返回成功代表 WAL 和 MemStore 都处理完了,在单行 PUT 场景下不会出现丢数据的现象。
最后分享一个小经验:在学习环境里跑 HBase 伪分布式时,HDFS 默认副本数是 3,三台虚拟机加起来可能磁盘没多大就满了。做个单机测试环境,可以把 HDFS 副本数改成 1,在hdfs-site.xml里设置dfs.replication=1,磁盘占用会少很多,性能反而更好。等到真正生产部署时,再按数据可靠性要求把副本数设定为 3。HBase 安装并不难,难点全在版本匹配、环境准备和设计习惯上。希望这篇记录能帮你把第一张表顺利建起来,少掉几次头发。