HBase 分布式数据库

本文合并自:04-分布式数据库HBase.md、05-HBase分布式数据库.md

本文从谷歌 BigTable 入手,介绍 HBase 的定位、访问接口、数据模型、实现原理与运行机制,对比 HBase 与传统关系数据库的差异,讲解安装部署、Shell 命令、Java API 编程、性能优化、二级索引方案、运维排错,并附练习题与答案。

主要内容:

  • 一、HBase 概述与数据模型
  • 二、架构与存储原理
  • 三、安装部署
  • 四、HBase Shell 与 API 操作
  • 五、运维、优化与排错
  • 六、练习

一、HBase 概述与数据模型

1.1 从 BigTable 说起

BigTable 是一个分布式存储系统,起初用于解决典型的互联网搜索问题。

建立互联网索引

  1. 爬虫持续不断地抓取新页面,这些页面每页一行地存储到 BigTable 里;
  2. MapReduce 计算作业运行在整张表上,生成索引,为网络搜索应用做准备。

搜索互联网

  1. 用户发起网络搜索请求;
  2. 网络搜索应用查询建立好的索引,从 BigTable 得到网页;
  3. 搜索结果提交给用户。

(原图:网页在 BigTable 中的存储样例)

BigTable 具备以下特点:

  • 是一个分布式存储系统;
  • 利用谷歌提出的 MapReduce 分布式并行计算模型处理海量数据;
  • 使用谷歌分布式文件系统 GFS 作为底层数据存储;
  • 采用 Chubby 提供协同服务管理;
  • 可以扩展到 PB 级别的数据和上千台机器,具备广泛应用性、可扩展性、高性能和高可用性等特点。

谷歌的许多项目都存储在 BigTable 中,包括搜索、地图、财经、打印、社交网站 Orkut、视频共享网站 YouTube 和博客网站 Blogger。

1.2 HBase 简介与定位

HBase 是一个高可靠、高性能、面向列(准确地说是面向列族)、可伸缩的分布式数据库,是谷歌 BigTable 的开源实现,主要用来存储非结构化和半结构化的松散数据。HBase 的目标是处理非常庞大的表,可以通过水平扩展的方式,利用廉价计算机集群处理由超过 10 亿行数据和数百万列元素组成的数据表。(原图:图4-1 Hadoop 生态系统中 HBase 与其他部分的关系)

HBase 的核心定位可概括为三句话:

  1. BigTable 的开源实现:数据模型、寻址机制、LSM 树存储思想均来自 BigTable 论文。
  2. 构建在 HDFS 之上的列族数据库:HBase 自身不管理磁盘块与副本,所有数据文件(HFile)与日志(WAL)都写在 HDFS 上,由 HDFS 提供三副本冗余与容错。
  3. 用随机实时读写弥补 HDFS 的短板:HDFS 面向大文件顺序批量读写、只追加不修改,无法支撑毫秒级的按 key 随机点查与更新;HBase 在其上叠加内存写缓冲(MemStore)+ 预写日志(WAL)+ 有序文件(HFile)+ 行键索引,从而提供高并发的随机实时读写能力。

HBase 与 BigTable 的底层技术对应关系如下表。

表 1-1 HBase 和 BigTable 的底层技术对应关系

BigTable HBase
文件存储系统 GFS HDFS
海量数据处理 MapReduce Hadoop MapReduce
协同服务管理 Chubby Zookeeper

为什么需要 HBase?

关系数据库已经流行很多年,并且 Hadoop 已经有了 HDFS 和 MapReduce,但是仍然需要 HBase,原因如下:

  • Hadoop 可以很好地解决大规模数据的离线批量处理问题,但受限于 Hadoop MapReduce 编程框架的高延迟数据处理机制,无法满足大规模数据实时处理应用的需求;
  • HDFS 面向批量访问模式,不是随机访问模式;
  • 传统通用关系型数据库无法应对数据规模剧增时导致的系统扩展性和性能问题(分库分表也不能很好解决);
  • 传统关系数据库在数据结构变化时一般需要停机维护,空列浪费存储空间;
  • 因此业界出现了一类面向半结构化数据存储和处理的高可扩展、低写入/查询延迟的系统,如键值数据库、文档数据库和列族数据库(BigTable 和 HBase 等)。

HBase 已成功应用于互联网服务领域和传统行业的众多在线式数据分析处理系统中。

1.3 HBase 与传统关系数据库的对比

HBase 与传统关系数据库的区别主要体现在以下几个方面:

  1. 数据类型:关系数据库采用关系模型,具有丰富的数据类型和存储方式;HBase 则采用更简单的数据模型,把数据存储为未经解释的字符串(字节数组)。
  2. 数据操作:关系数据库包含丰富的操作,会涉及复杂的多表连接;HBase 操作不存在复杂的表与表之间的关系,只有简单的插入、查询、删除、清空等,因为 HBase 在设计上就避免了复杂的表间关系。
  3. 存储模式:关系数据库基于行模式存储,一行记录的所有列连续存放;HBase 基于列(列族)存储,每个列族都由几个文件保存,不同列族的文件是分离的,因此只读取部分列时 IO 更小,同列数据类型相同也更利于压缩。
  4. 数据索引:关系数据库通常可针对不同列构建复杂的多个索引以提高访问性能;HBase 只有一个索引——行键,通过巧妙设计,HBase 中所有访问方法或者通过行键访问、或者通过行键扫描,从而使整个系统不会慢下来。
  5. 数据维护:关系数据库中更新操作用最新当前值替换记录中原旧值,旧值被覆盖后不复存在;HBase 中执行更新操作时并不会删除数据旧版本,而是生成一个新版本,旧版本仍然保留。
  6. 可伸缩性:关系数据库很难实现横向扩展,纵向扩展空间也较有限;HBase 和 BigTable 等分布式数据库就是为实现灵活水平扩展而开发的,能够轻易通过在集群中增减硬件数量实现性能伸缩。
  7. 事务:关系数据库支持跨行、跨表的 ACID 事务;HBase 只保证单行(单 RowKey)操作的原子性,不支持跨行事务、不支持多行/多表 ACID,也没有回滚。需要跨行事务能力时需借助 Phoenix(有限支持)或在应用层自行实现补偿。

表 1-2 对比速查

维度 关系型数据库(MySQL/Oracle) HBase
数据类型 丰富(int、varchar、date…) 只有字节数组 byte[]
数据操作 增删改查 + 多表 JOIN、聚合、子查询 put / get / scan / delete / incr,无 JOIN
存储模式 行式存储 列族式(列簇内列式)存储
索引 主键 + 任意二级索引 只有 RowKey 一个索引,二级索引需外挂
可伸缩性 纵向扩展为主,横向扩展困难 天生水平扩展,加机器即扩容
事务 完整 ACID,跨行跨表 仅单行原子性,无跨行事务
更新语义 覆盖旧值 追加新版本,旧版本保留
空值 占用存储空间 稀疏存储,空列不占空间
典型数据量 单表千万~亿级 单表十亿~千亿行、PB 级

1.4 与 HDFS、Hive 的关系与分工

  • HDFS 是 HBase 的底层存储:HBase 的 HFile、WAL 全部落在 HDFS 上。HDFS 提供高吞吐顺序读写与副本容错,但只支持追加、不支持随机改写,且没有按 key 的点查能力。HBase 在其上补齐了"低延迟随机读写 + 单行更新 + 按行键有序扫描"的能力。
  • Hive 是构建在 Hadoop 上的数据仓库,把 SQL(HQL)翻译成 MapReduce/Tez/Spark 作业,面向离线批量分析,延迟在分钟级,不支持行级随机更新。
  • HBase 面向在线随机点查与更新(毫秒级),不擅长复杂多表 JOIN 与全表聚合分析。

三者常见协作方式:数据由 Flume/Sqoop/Kafka 落到 HDFS,Hive 做离线加工与聚合,把需要在线点查的结果写入 HBase 提供对外查询;同时 Hive 可以通过 HBaseStorageHandler 建外部表直接映射 HBase 表,实现"用 SQL 分析 HBase 数据"。

详见 16-03-HDFS分布式文件系统.md

表 1-3 HDFS / Hive / HBase 分工

组件 定位 延迟 随机读写 典型场景
HDFS 分布式文件系统 高(批处理) 不支持随机写 海量文件存储、批量顺序扫描
Hive SQL 数据仓库 分钟级 不支持行级更新 离线报表、ETL、聚合分析
HBase 分布式 KV/列族数据库 毫秒级 支持 用户画像、订单查询、消息、时序

1.5 与其他 NoSQL 的对比

类型 代表产品 数据模型 查询能力 与 HBase 的差异
键值数据库 Redis、Memcached key → value 只能按 key 存取 Redis 内存为主、数据量受内存限制;HBase 磁盘为主,可存 PB 级
文档数据库 MongoDB、CouchDB key → JSON/BSON 文档 支持文档内字段查询、二级索引、聚合 MongoDB 有丰富二级索引与聚合管道;HBase 只有行键索引,但横向扩展与超大表能力更强
列族数据库 HBase、Cassandra、BigTable 行键 + 列族 行键点查、范围扫描 Cassandra 为无中心 P2P 架构、最终一致、无单点,HBase 为主从架构、强一致(单行)、依赖 HDFS 与 ZooKeeper
图数据库 Neo4j、JanusGraph 顶点 + 边 图遍历、最短路径 图库擅长关系遍历;JanusGraph 可用 HBase 作后端存储

详见 11-08-NoSQL与MongoDB.md

1.6 适用与不适用场景

适用场景

  • 数据量极大(上亿行、TB~PB 级),单机数据库或分库分表已难以支撑;
  • 主要按行键(或行键前缀范围)做随机读写,QPS 高、延迟要求毫秒级;
  • 写入量巨大且持续(日志、埋点、监控指标、消息、IoT 时序数据);
  • 表结构稀疏、列不固定、需要动态加列(用户画像标签、商品扩展属性);
  • 需要保留数据多版本或按 TTL 自动过期;
  • 需要与 Hadoop 生态(MapReduce/Hive/Spark)无缝配合做离线分析。

不适用场景

  • 数据量很小(几百万行以内),关系型数据库足够且更方便;
  • 需要复杂 SQL:多表 JOIN、复杂聚合、任意条件组合查询(应上 Hive/ClickHouse,或用 Phoenix + 二级索引缓解);
  • 需要跨行/跨表 ACID 事务与外键约束;
  • 需要按任意列高效检索(HBase 只有行键索引,任意列查询会退化为全表扫描,应外挂 Solr/ES 或 Phoenix 二级索引);
  • 对单条查询延迟要求达到亚毫秒且数据可全内存(应选 Redis)。

1.7 HBase 访问接口

表 1-4 HBase 访问接口

类型 特点 场景
Native Java API 最常规和高效的访问方式 适合 Hadoop MapReduce 作业并行批处理 HBase 表数据
HBase Shell HBase 的命令行工具,最简单的接口 适合 HBase 管理
Thrift Gateway 利用 Thrift 序列化技术,支持 C++、PHP、Python 等多种语言 适合其他异构系统在线访问 HBase 表数据
REST Gateway 解除了语言限制 支持 REST 风格的 Http API 访问 HBase
Pig 使用 Pig Latin 流式编程语言处理 HBase 中的数据 适合做数据统计
Hive 简单 当需要以类似 SQL 语言方式访问 HBase 的时候

1.8 数据模型详解

HBase 是一个稀疏、多维度、排序的映射表,这张表的索引是行键、列族、列限定符和时间戳。每个值是一个未经解释的字符串,没有数据类型。

用户在表中存储数据,每一行都有一个可排序的行键和任意多的列。表在水平方向由一个或多个列族组成,一个列族中可以包含任意多个列,同一个列族里面的数据存储在一起。

列族支持动态扩展,可以很轻松地添加一个列族或列,无需预先定义列的数量及类型,所有列均以字符串(字节数组)形式存储,用户需自行进行数据类型转换。

HBase 中执行更新操作时,并不会删除数据旧版本,而是生成一个新版本,旧版本仍然保留(这与 HDFS 只允许追加不允许修改的特性直接相关)。

1.8.1 核心概念

  • 表(Table):HBase 采用表来组织数据,表由行和列组成,列划分为若干个列族。表可以归属于某个命名空间(Namespace)。
  • 行(Row)与行键(RowKey):每个 HBase 表由若干行组成,每个行由行键来标识。行键是 HBase 唯一的索引,本质是一个字节数组,最大长度 64KB(实际建议 10~100 字节)。
    • RowKey 的作用:唯一标识一行;是数据在 Region 内与 Region 之间分布的依据;是唯一可以高效检索的字段。
    • 字典序排序:HBase 中的行按 RowKey 的字节字典序升序排列。这意味着 "row1" < "row10" < "row2",数字型行键若不补零(zero-padding)会导致排序与数值顺序不一致;例如 1, 2, 10, 20 应写为 0001, 0002, 0010, 0020。字典序排序是范围扫描(scan STARTROW/STOPROW)与前缀扫描能高效工作的根本原因,也是热点问题的根源。
  • 列族(Column Family):一个 HBase 表被分组成许多"列族"的集合,它是基本的访问控制单元,也是物理存储与配置(压缩、版本数、TTL、BlockCache、布隆过滤器)的单元。列族必须在建表时(或 alter 时)定义,列族名会随每个 KeyValue 一起存储,因此要短。
  • 列限定符(Column Qualifier,简称列):列族里的数据通过列限定符来定位。列限定符不需要预定义,写入时动态产生,同一列族下可以有百万级不同的限定符。完整列名写作 列族:列限定符,例如 course:math。
    • 列族与列限定符的关系:列族是"schema 的一部分",需要提前声明且数量要少;列限定符是"数据的一部分",可以任意动态增加。
    • 列族要少的原因:① 每个列族在每个 Region 内对应一个独立的 Store(一套 MemStore + 一组 HFile),列族越多,MemStore 越多,内存被切碎,flush 出的小文件越多;② flush 与 compaction 是以 Region 为单位触发的,一个列族达到阈值会连带其他列族一起 flush,数据量差异大的列族放在一起会产生大量小文件;③ Region 分裂以 Region 为单位,若列族数据量极不均衡,会导致小列族被切成很多极小的 Store;④ 检索一个列族的数据仍可能需要跨多个 Store 定位。经验值:列族数量控制在 1~3 个,最佳是 1 个。
  • 单元格(Cell):通过行、列族和列限定符确定一个"单元格",单元格中存储的数据没有数据类型,总被视为字节数组 byte[]。Cell 在物理上是一条 KeyValue 记录,包含 rowkey + family + qualifier + timestamp + type(Put/Delete) + value。
  • 时间戳(Timestamp)与多版本:每个单元格都保存同一份数据的多个版本,这些版本采用时间戳进行索引。写入时若不指定,HBase 自动使用当前毫秒时间戳;也可人工指定。默认查询返回时间戳最大(最新)的版本。每个列族可配置 VERSIONS 决定保留多少个版本(HBase 1.x 默认 1,早期 0.9x 默认 3);超出的旧版本在 major compaction 时被清除。
  • 数据类型:HBase 中一切都是字节数组,包括行键、列族名、列限定符和值。Java 端用 Bytes.toBytes() / Bytes.toString() / Bytes.toInt() 等做转换,类型语义由应用层负责。

1.8.2 数据的四维定位(数据坐标)

HBase 中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此可视为一个"四维坐标",即 [行键, 列族, 列限定符, 时间戳]。例如:

["201505003", "Info", "email", 1174184619081] = "xie@qq.com"
["201505003", "Info", "email", 1174184620720] = "you@163.com"

可以把 HBase 理解为一个四层嵌套的 Map:

Map<RowKey, Map<ColumnFamily, Map<Qualifier, Map<Timestamp, Value>>>>

因此完整定位一个值需要 4 个坐标;省略时间戳则返回最新版本;省略限定符则返回整个列族;省略列族则返回整行。

1.8.3 稀疏存储

HBase 的"稀疏"指:某一行没有某个列时,该列在物理上完全不存在,不占用任何存储空间,也不需要 NULL 占位。 这与关系型数据库中每行都要为所有列预留位置(即使为 NULL)截然不同。

示例:用户表有 500 个画像标签列,某用户只命中 3 个标签,则该行在 HFile 中只有 3 条 KeyValue 记录;另一个用户命中 200 个,则有 200 条。表结构不需要变更,新增标签只需写入新的列限定符。

代价是:每条 KeyValue 都要重复携带完整的 rowkey、列族名、列限定符与时间戳,因此列名与行键越短越省空间(详见 5.3 性能优化)。

1.8.4 概念视图与物理视图(理解 HBase 的关键)

概念视图:在概念视图中,一张表可以视为一个稀疏的多维映射,看起来像一张"有很多空格的宽表"。

表 1-5 HBase 数据的概念视图(示例)

行键 时间戳 列族 contents 列族 anchor
"com.cnn.www" t9 anchor:cnnsi.com = "CNN"
"com.cnn.www" t8 anchor:my.look.ca = "CNN.com"
"com.cnn.www" t6 contents:html = "\<html>..."
"com.cnn.www" t5 contents:html = "\<html>..."
"com.cnn.www" t3 contents:html = "\<html>..."

物理视图:在物理存储时,HBase 是按列族分别存储的,同一个列族的多个数据版本保存在一起,概念视图中"空"的单元格在物理视图中并不占用存储空间。

表 1-6 HBase 数据的物理视图(按列族存储)

列族 contents:

行键 时间戳 列族 contents
"com.cnn.www" t6 contents:html = "\<html>..."
"com.cnn.www" t5 contents:html = "\<html>..."
"com.cnn.www" t3 contents:html = "\<html>..."

列族 anchor:

行键 时间戳 列族 anchor
"com.cnn.www" t9 anchor:cnnsi.com = "CNN"
"com.cnn.www" t8 anchor:my.look.ca = "CNN.com"

对照理解要点(这是理解 HBase 的关键):

  1. 概念视图上的"一行",物理上被拆散到多个 Store(列族)× 多个 HFile 中;读取一整行可能要在多个文件中做归并。
  2. 概念视图上的"空单元格"物理上不存在,所以宽表设计(几百列)在 HBase 中代价极低。
  3. 概念视图上的"一个单元格多版本",物理上是多条独立的 KeyValue,只是排序上相邻(同 rowkey 同列,按 timestamp 倒序)。
  4. 因此 HBase 的存储代价与"实际写入的 Cell 数量"成正比,而与"表定义了多少列"无关。
  5. HFile 内部 KeyValue 的排序规则为:RowKey 升序 → ColumnFamily 升序 → Qualifier 升序 → Timestamp 降序 → KeyType。理解这个顺序就能理解为什么 scan 前缀高效、为什么最新版本总是先被读到。

1.8.5 面向列的存储

HBase 是面向列(列族)的存储。行式数据库把一行的所有字段连续存放,适合 OLTP 整行读写;列式数据库把同一列的数据连续存放,适合只读少数列的分析型查询,且同类型数据压缩率更高。HBase 处于两者之间:列族之间是列式分离的,列族内部按行聚合存放。(原图:图4-3 行式数据库和列式数据库示意图;图4-4 行式存储结构和列式存储结构)

1.9 RowKey 设计原则

RowKey 是 HBase 设计中最重要的一环,直接决定了数据分布是否均衡、查询能否高效。

1.9.1 唯一性

RowKey 必须唯一标识一行。相同 RowKey 的写入会被视为对同一行的更新(产生新版本或覆盖),而不是插入新行。业务上若无天然唯一键,可用"业务主键 + 时间戳"、"业务主键 + UUID 后缀"等方式组合。

1.9.2 长度控制

  • RowKey 最大 64KB,但建议 10~100 字节,越短越好,且最好是 8 字节的整数倍(利于操作系统内存对齐)。
  • 原因:① RowKey 会在每一条 KeyValue 中重复存储,HFile 中的冗余随列数成倍放大;② MemStore 与 BlockCache 中缓存的也是完整 KeyValue,RowKey 过长直接降低缓存命中率;③ HFile 的块索引常驻内存,RowKey 越长索引越大。
  • 做法:避免用可读性长字符串拼接,可用定长编码、MD5 取前若干字节、Long 的 8 字节表示等。

1.9.3 散列避免热点

热点(Hotspotting) 指大量读写请求集中到少数 Region / RegionServer 上,导致该节点成为瓶颈甚至宕机,而其他节点空闲。根因是 RowKey 单调递增(如自增 ID、时间戳前缀),写入永远落在最后一个 Region。三种常用手段:

1)加盐(Salting):在原 RowKey 前面拼接一个随机或散列前缀(通常是固定位数),把连续键打散到不同 Region。

原始: 20260822_1001
加盐: 03|20260822_1001      # 前缀 = hash(rowkey) % 预分区数,补零定长
  • 优点:分布均衡,实现简单。
  • 缺点:同一原始前缀的数据被打散,范围扫描需要向所有 salt 桶各发起一次 scan 再合并(Phoenix 的 SALT_BUCKETS 就是这个思路)。

2)哈希(Hashing):用 MD5/SHA1 对原 RowKey 计算散列,取前几位作为前缀,或直接整体哈希。

rowkey = md5(userId).substring(0,4) + "_" + userId
  • 优点:散列结果可复现,已知原 key 时能精确定位(相比随机加盐更适合点查)。
  • 缺点:完全丧失原有顺序,无法做业务范围扫描。

3)反转(Reversing):把固定长度且低位变化快的字段反转,例如手机号、时间戳。

手机号 13812345678 → 87654321831
时间戳 1724300000  → 0000034271
  • 优点:保留了可精确定位的能力(点查时同样反转即可),把高位固定的单调键变成高位随机。
  • 缺点:同样牺牲了自然顺序的范围扫描。

补充手段:Long.MAX_VALUE - timestamp 作为行键的一部分,可让最新数据排在最前,便于用 scan LIMIT 快速取最近记录(行键按字典序存储,设计行键时要充分利用该排序特点,将经常一起读取的数据存储到一块,将最近可能被访问的数据放在一块)。

1.9.4 按查询模式设计

HBase 只有行键索引,因此必须先明确查询需求,再反推 RowKey 结构:

  • 只按用户查明细:userId_timestamp,可 scan 前缀取某用户全部记录。
  • 按用户 + 时间倒序取最近 N 条:userId_(Long.MAX_VALUE - timestamp)。
  • 按多个维度查:把选择性高、且查询中一定会出现的字段放在最左(类似联合索引最左前缀原则);不同查询模式可以冗余一张"索引表"或用二级索引方案。
  • 需要多维任意组合查询:外挂 Solr/Elasticsearch 存索引,HBase 存明细,先查索引拿到 RowKey 再回查 HBase。

访问 HBase 表中的行只有三种方式:通过单个行键访问、通过一个行键区间访问(scan)、全表扫描。RowKey 设计的目标就是让业务查询尽可能落在前两种。

1.9.5 时间序列数据的设计陷阱与解决

陷阱:时序数据(监控指标、日志、IoT)最自然的 RowKey 是 timestamp 或 timestamp_metric。由于时间单调递增,所有写入都集中到 RowKey 最大的那个 Region,形成写热点:单个 RegionServer 被打满、频繁 split、其余节点闲置。

解决方案:

  1. 前缀打散:salt(metricId) + metricId + reverse_or_normal_timestamp,其中 salt 取 hash(metricId) % 预分区数。同一指标的数据仍然连续(可范围扫描该指标的时间段),不同指标分散到不同 Region。这是 OpenTSDB 的经典做法(metricUID + timestamp + tagUIDs)。
  2. 按维度分桶:deviceId_timestamp,天然按设备打散,适合"查某设备某时间段"的场景。
  3. 时间分片建表:按天/月建表(metrics_20260822),旧表整体归档或删除,避免单表无限增长;配合 TTL 自动过期。
  4. 时间粒度取模前置:(timestamp % N)_timestamp,写入轮转到 N 个桶,读取时并发扫 N 个桶。

1.9.6 预分区与 SPLITS

新建表默认只有 1 个 Region,所有写入集中在一台 RegionServer,直到数据量触发自动分裂,期间性能极差且伴随大量 split 与数据搬迁。预分区(Pre-splitting) 在建表时就切好多个 Region,配合散列的 RowKey 让写入从一开始就均衡。

# 方式一:显式指定分割点
create 'user', 'info', SPLITS => ['1000', '2000', '3000', '4000']

# 方式二:从文件读取分割点(每行一个)
create 'user', 'info', SPLITS_FILE => '/opt/splits.txt'

# 方式三:按 16 进制均匀切分(配合 md5/hash 前缀的 RowKey),切 15 刀得 16 个 Region
create 'user', 'info', {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}

# 方式四:按十进制均匀切分
create 'user', 'info', {NUMREGIONS => 20, SPLITALGO => 'DecimalStringSplit'}

# 方式五:按字节均匀切分(RowKey 为任意二进制时)
create 'user', 'info', {NUMREGIONS => 16, SPLITALGO => 'UniformSplit'}

预分区数量经验值:Region 数 ≈ RegionServer 台数 × 每台期望 Region 数(2~5),或按"预计一年数据总量 / 单 Region 期望大小(10GB)"估算,两者取较大值。分区键必须与 RowKey 前缀的取值分布一致,否则预分区形同虚设。


二、架构与存储原理

2.1 功能组件概览

HBase 的实现包括三个主要功能组件:

  1. 库函数:链接到每个客户端;
  2. 一个 Master 主服务器;
  3. 许多个 Region 服务器。
  • 主服务器 Master 负责管理和维护 HBase 表的分区信息,维护 Region 服务器列表,分配 Region,负载均衡。
  • Region 服务器负责存储和维护分配给自己的 Region,处理来自客户端的读写请求。
  • 客户端并不是直接从 Master 上读取数据,而是在获得 Region 的存储位置信息后,直接从 Region 服务器读取数据。
  • 客户端并不依赖 Master,而是通过 ZooKeeper 来获得 Region 位置信息,大多数客户端甚至从来不和 Master 通信,这种设计使 Master 负载很小。

2.2 架构组件详解

(原图:图4-9 HBase 的系统架构)

2.2.1 Client(客户端)与元数据缓存

客户端包含访问 HBase 的接口(Java API、Thrift、REST 等),同时在缓存中维护着已经访问过的 Region 位置信息,用来加快后续数据访问过程。

  • 首次访问某表时,客户端需要走完整的寻址流程(查 ZooKeeper → 查 hbase:meta),随后把 "RowKey 区间 → RegionServer" 的映射缓存在本地(MetaCache)。
  • 后续请求直接用缓存定位,一步 RPC 命中目标 RegionServer,完全不经过 Master,也不经过 ZooKeeper。
  • 当 Region 发生分裂、合并或迁移后,缓存会失效,客户端会收到 NotServingRegionException / RegionMovedException,此时清除缓存并重新寻址(这就是"缓存失效问题"的处理方式)。
  • 客户端还负责重试与批处理:hbase.client.retries.number、hbase.client.pause 控制重试次数与退避间隔;Table.put(List<Put>) 会按 RegionServer 分组批量发送。

2.2.2 ZooKeeper

ZooKeeper 可以帮助选举出一个 Master 作为集群总管,并保证任何时刻总有唯一一个 Master 在运行,避免了 Master 的"单点失效"问题。ZooKeeper 是很好的集群管理工具,被大量用于分布式计算,提供配置维护、域名服务、分布式同步、组服务等。

HBase 中 ZooKeeper 的三大职责:

  1. 存储 hbase:meta 表所在的 RegionServer 位置(znode:/hbase/meta-region-server)。这是客户端寻址的入口,也是"整个寻址过程客户端只需询问 ZooKeeper、不需要连接 Master"的原因。
  2. Master 选举与 Active Master 记录(znode:/hbase/master、/hbase/backup-masters)。多个 HMaster 竞争创建 /hbase/master 临时节点,成功者为 Active,其余作为 Backup 监听该节点;Active 宕机后临时节点消失,Backup 立即竞选接管。
  3. RegionServer 存活监控(znode:/hbase/rs/<server>)。每个 RegionServer 启动时注册一个临时节点并维持心跳(session)。session 超时(zookeeper.session.timeout,默认 90s)后节点消失,Master 感知到并启动故障恢复流程(WAL 拆分 + Region 重新分配)。

其他 znode:/hbase/table(表状态)、/hbase/region-in-transition(RIT 状态,2.x 起改由 Master 的 ProcedureV2 管理)、/hbase/replication(复制状态)。

详见 16-06-ZooKeeper集群.md

2.2.3 HMaster

主服务器 Master 主要负责表和 Region 的管理工作:

  • 管理用户对表的增加、删除、修改、查询等操作(即 DDL:create / alter / drop / enable / disable / truncate、命名空间管理);
  • Region 分配:为新建表、分裂产生的 Region、故障恢复的 Region 指派 RegionServer;
  • 实现不同 Region 服务器之间的负载均衡(内置 balancer 周期运行,默认 5 分钟);
  • 在 Region 分裂或合并后,负责重新调整 Region 的分布;
  • 对发生故障失效的 Region 服务器上的 Region 进行迁移(含 WAL 拆分);
  • 管理 ACL 权限、快照、复制 peer 等元操作;
  • 运行后台线程:CatalogJanitor(清理已分裂的父 Region 与 meta 中的残留)、LogCleaner、HFileCleaner、Balancer。

关键点:HMaster 不参与读写路径。 客户端的 put/get/scan 请求直接发往 RegionServer,从不经过 Master。因此:

  • Master 短时间宕机不影响已有表的读写(只影响 DDL、Region 分配、负载均衡、故障恢复);
  • 但 Master 长期不可用会导致 Region 分裂后无法上线、RegionServer 宕机后 Region 无人接管,集群最终不可用,所以生产必须配置 Backup Master;
  • Master 因此负载很轻,可与 NameNode 共机部署(小集群),大集群建议独立。

2.2.4 RegionServer

Region 服务器是 HBase 中最核心的模块,负责维护分配给自己的 Region,并响应用户的读写请求。具体职责:

  • 承载若干个 Region,处理这些 Region 上的 get / put / scan / delete / increment 请求;
  • 管理 MemStore(写缓冲)与 BlockCache(读缓存);
  • 执行 flush(MemStore → HFile)、compaction(HFile 合并)、split(Region 分裂);
  • 维护本机唯一的 WAL(HLog),保证故障时数据不丢;
  • 与 HDFS 交互读写 HFile 与 WAL;
  • 通过心跳向 Master 汇报状态与负载。

RegionServer 通常与 DataNode 同机部署,以获得数据本地性(local read),compaction 后新 HFile 的一个副本会写在本地。

2.2.5 Region

(原图:图4-5 一个 HBase 表被划分成多个 Region;图4-6 一个 Region 会分裂成多个新的 Region;图4-7 不同的 Region 可以分布在不同的 Region 服务器上)

Region 是 HBase 分布式与负载均衡的最小单位,一个表按 RowKey 区间水平切分成多个 Region,每个 Region 保存 [startKey, endKey) 区间的数据。

  • 开始只有一个 Region,后来不断分裂;
  • Region 拆分操作非常快,接近瞬间,因为拆分后的 Region 读取的仍是原存储文件(通过 Reference 文件指向父文件的上半/下半),直到"合并"(compaction)过程把存储文件异步写到独立文件之后,才会读取新文件;
  • 每个 Region 默认大小是 100MB 到 200MB(2006 年以前的硬件配置);
  • 每个 Region 的最佳大小取决于单台服务器的有效处理能力,目前每个 Region 最佳大小建议 1GB~2GB(2013 年以后的硬件配置),现代生产环境常设 10GB~20GB(hbase.hregion.max.filesize);
  • 同一个 Region 不会被分拆到多个 Region 服务器;
  • 每个 Region 服务器存储 10~1000 个 Region(现代经验值:单 RegionServer 100~200 个 Region 为宜,过多会导致 MemStore 内存碎片与 flush 风暴)。

2.2.6 HDFS 作为底层存储

HBase 自身不做磁盘管理,所有持久化数据都写在 HDFS:

  • HFile(数据文件)与 WAL(日志)均为 HDFS 文件,天然拥有 3 副本容错;
  • RegionServer 宕机后数据仍在 HDFS 上,只需把 Region 分配给其他 RegionServer 并回放 WAL,无需拷贝数据,这是 HBase 快速故障恢复的基础;
  • HDFS 只支持追加不支持随机改写,这正是 HBase 采用 LSM 树(写内存 + 顺序落盘 + 后台合并)而非 B+ 树的原因,也是"更新即写新版本、删除即写墓碑"的根本原因。

2.3 Region 的定位与寻址流程

2.3.1 旧版三层结构(-ROOT- / .META.)

(原图:图4-8 HBase 的三层结构)

HBase 0.96 之前通过三层结构定位 Region:

  • 元数据表(又名 .META. 表):存储 Region 和 Region 服务器的映射关系;当 HBase 表很大时,.META. 表也会被分裂成多个 Region。
  • 根数据表(又名 -ROOT- 表):记录所有元数据的具体位置;-ROOT- 表只有唯一一个 Region,名字在程序中被写死。
  • ZooKeeper 文件:记录了 -ROOT- 表的位置。

表 2-1 HBase 三层结构中各层次的名称和作用

层次 名称 作用
第一层 ZooKeeper 文件 记录了 -ROOT- 表的位置信息
第二层 -ROOT- 表 记录了 .META. 表的 Region 位置信息;-ROOT- 表只能有一个 Region,通过 -ROOT- 表可以访问 .META. 表中的数据
第三层 .META. 表 记录了用户数据表的 Region 位置信息;.META. 表可以有多个 Region,保存了 HBase 中所有用户数据表的 Region 位置信息

为加快访问速度,.META. 表的全部 Region 都会被保存在内存中。假设 .META. 表每行(一个映射条目)在内存中约占用 1KB,每个 Region 限制为 128MB,则三层结构可保存的用户数据表 Region 数目计算方法为:(-ROOT- 表能够寻址的 .META. 表 Region 个数)×(每个 .META. 表 Region 可寻址的用户数据表 Region 个数)。

  • 一个 -ROOT- 表最多只能有一个 Region(128MB),每行占用 1KB,可容纳 128MB/1KB = 2^17 行,即一个 -ROOT- 表可寻址 2^17 个 .META. 表 Region;
  • 每个 .META. 表 Region 可寻址 128MB/1KB = 2^17 个用户数据表 Region;
  • 最终三层结构可保存的 Region 数目是 (128MB/1KB) × (128MB/1KB) = 2^34 个 Region。

客户端访问数据时的"三级寻址":客户端先询问 ZooKeeper 获得 -ROOT- 表位置,再依次访问 -ROOT- 表和 .META. 表,最终定位到目标 Region。为加速寻址,客户端会缓存位置信息,同时需要解决缓存失效问题。整个寻址过程客户端只需询问 ZooKeeper 服务器,不需要连接 Master 服务器。

2.3.2 现代两层结构(hbase:meta)

从 HBase 0.96 起,-ROOT- 表被取消,.META. 表改名为 hbase:meta(位于 hbase 系统命名空间),并且规定 hbase:meta 永远只有一个 Region、永不分裂,其位置直接记录在 ZooKeeper 的 /hbase/meta-region-server 节点上。寻址由三层简化为两层:

① 客户端连接 ZooKeeper,读取 /hbase/meta-region-server
   → 得到 hbase:meta 所在的 RegionServer 地址
② 客户端向该 RegionServer 发起 scan/get,查询 hbase:meta 表
   → 用目标表名 + RowKey 定位到负责该 RowKey 的 Region 及其 RegionServer
③ 客户端直连目标 RegionServer 执行真正的 put / get / scan
④ 客户端把 ①②的结果缓存起来,后续请求直接跳到 ③(一次 RPC)
⑤ 若目标 Region 已迁移/分裂,RegionServer 返回异常,客户端清缓存回到 ①或②重试

hbase:meta 表结构(可直接 scan 'hbase:meta' 查看):

  • RowKey:表名,startKey,时间戳.Region编码名.
  • 列族 info,常见列:info:regioninfo(Region 的 startKey/endKey 等)、info:server(所在 RegionServer host:port)、info:serverstartcode(RS 启动时间)、info:seqnumDuringOpen、info:sn、info:state。

演进的意义:单个 hbase:meta Region 已足以支撑数十万级 Region 的元数据,去掉 -ROOT- 减少了一次网络往返,简化了实现与故障场景。

2.4 RegionServer 内部结构

(原图:图4-10 Region 服务器向 HDFS 文件系统中读写数据)

层级关系自上而下为:

RegionServer
 ├── HLog / WAL              (每个 RegionServer 一个,所有 Region 共用)
 └── Region(多个,按 RowKey 区间划分)
      └── Store(每个列族对应一个 Store)
           ├── MemStore      (1 个,内存中的有序写缓冲,默认 128MB)
           └── StoreFile     (多个,即 HFile,持久化在 HDFS 上)

要点:

  • 一个 Region = 一个表在某 RowKey 区间的分片;一个 Region 内部按列族切成多个 Store。
  • 每个列族对应一个 Store,这是"列族是物理存储单元"的直接体现,也是"列族要少"的原因。
  • MemStore 是跳表(ConcurrentSkipListMap)实现的有序内存结构,按 KeyValue 排序规则组织,写入先进 MemStore,因此新写入的数据立即可读。
  • StoreFile 就是 HFile,是 MemStore flush 出来的不可变有序文件,存放在 HDFS 上。一个 Store 可以有多个 HFile。
  • HLog 即 WAL(Write Ahead Log,预写式日志):每个 RegionServer 只有一个(1.x 起可配多个 WAL 提升并发),该 RegionServer 上所有 Region 的写入都顺序追加到同一个 WAL 文件,因此写入是顺序写,吞吐高。
    • 共用日志的优点:提高对表的写操作性能(避免多文件随机写);缺点:恢复时需要按 Region 分拆日志(WAL split)。

2.5 写流程完整说明

HBase 写入(put)的完整链路:

1. 客户端定位
   查本地 MetaCache;未命中则 ZooKeeper → hbase:meta → 得到目标 RegionServer
2. 客户端把 Put 发送到目标 RegionServer 的对应 Region
3. RegionServer 先写 WAL(HLog)
   - 追加到 HDFS 上的 WAL 文件并 sync(hbase.wal.provider / hflush)
   - 只有当操作写入 HLog 之后,commit() 调用才会将其返回给客户端
4. 再写 MemStore
   - 按列族写入对应 Store 的 MemStore,内存中保持有序
5. 向客户端返回成功(此时数据只在内存 + WAL 中,尚未落 HFile)
6. 当 MemStore 大小达到阈值(hbase.hregion.memstore.flush.size,默认 128MB)
   或全局内存压力触发 / 手工 flush / WAL 数量过多
   → flush:MemStore 内容顺序写成一个新的 HFile,清空 MemStore,
     并在 HLog 里写入一个标记(记录已持久化的序列号)
7. 旧的、其对应数据已全部 flush 的 WAL 文件被归档删除

补充说明:

  • 写 WAL 与写 MemStore 的顺序不可颠倒:必须先写日志才能写内存,且直到 MemStore 缓存内容对应的日志已经写入磁盘,该缓存内容才能被刷写到磁盘。这样任何时刻宕机都能靠 WAL 回放恢复。
  • 每次刷写都生成一个新的 StoreFile,因此每个 Store 包含多个 StoreFile 文件;文件数增多会触发 compaction。
  • flush 是以 Region 为单位触发的:某个列族的 MemStore 超阈值,该 Region 下所有列族的 MemStore 都会 flush。
  • 更新(同 RowKey 同列再 put)不会去修改已有数据,只是写入一条时间戳更大的新 KeyValue;查询时返回最新版本,旧版本在 major compaction 时按 VERSIONS 配置清理。
  • HBase 的写入之所以快:顺序写 WAL + 内存写 MemStore,完全没有磁盘随机写(LSM 树的核心思想)。

2.6 读流程完整说明

当用户读取数据时,Region 服务器会首先访问 MemStore 缓存,如果找不到,再去磁盘上面的 StoreFile 中寻找。完整的查找顺序为:

1. 客户端定位到目标 RegionServer(同写流程)
2. BlockCache(读缓存,LRU/BucketCache)
   - 命中则直接返回数据块中的结果
3. MemStore(写缓冲,尚未落盘的最新数据)
4. StoreFile / HFile(磁盘,可能多个文件)
   - 对每个候选 HFile:先看时间范围与 RowKey 范围是否可能包含目标
   - 再查布隆过滤器判断"这个 HFile 里是否可能有该 row/rowcol"
   - 再用 HFile 的多级块索引定位到具体 Data Block,读入并在 BlockCache 中缓存
5. 归并:把来自 MemStore 与多个 HFile 的同 RowKey 结果按
   (Row → Family → Qualifier → Timestamp 降序)归并,
   过滤掉墓碑标记与超出 VERSIONS/TTL 的版本,返回最终结果

加速机制:

  • BlockCache:以 HFile Data Block(默认 64KB)为粒度缓存,热点数据反复读时几乎无 IO。可选 LruBlockCache(堆内)、BucketCache(堆外/SSD)、CombinedBlockCache(索引在堆内、数据在堆外)。
  • 布隆过滤器(Bloom Filter):按列族配置 BLOOMFILTER => 'ROW'(默认,按行)或 'ROWCOL'(按行+列)。作用是快速排除"肯定不含该 key 的 HFile",避免无谓的磁盘读。存在极小误判率(说有可能没有),但绝不漏判(说没有就一定没有)。随机点查场景收益极大。
  • HFile 索引:HFile 内含多级索引(Root/Intermediate/Leaf Index Block)与 Data Block 起始 key,索引常驻内存,可二分定位到目标块,无需扫全文件。

读为什么比写复杂:

  1. 写只需追加到一个 WAL 和一个 MemStore;读则必须查遍 MemStore + 所有可能相关的 HFile(同一 RowKey 的不同版本、不同列可能散落在多个文件中)。
  2. 需要做多路归并排序,并在归并过程中处理多版本、TTL 过期、墓碑删除标记的语义。
  3. HFile 数量越多(compaction 不及时),读放大越严重——这正是需要 compaction 的原因。
  4. 存在删除但未清理的数据:墓碑标记必须参与归并才能正确"隐藏"旧版本。

2.7 Compaction 合并

背景:每次刷写都生成一个新的 StoreFile,数量太多会影响查找速度;调用 Store.compact() 把多个 StoreFile 合并成一个。合并操作比较耗费资源,只有数量达到一个阈值才启动合并。(原图:图4-11 StoreFile 的合并和分裂过程)

表 2-2 Minor 与 Major Compaction 对比

维度 Minor Compaction(小合并) Major Compaction(大合并)
选取文件 部分小的、相邻的 HFile 一个 Store 下全部 HFile
触发条件 HFile 数 ≥ hbase.hstore.compactionThreshold(默认 3);memstore flush 后检查 周期性(hbase.hregion.majorcompaction,默认 7 天);手工 major_compact;Store 只剩一个文件且满足条件时 minor 会升级为 major
是否清理删除数据 不会彻底删除,墓碑与被覆盖的旧版本仍可能保留(跨文件无法确定) 会真正物理删除被墓碑标记的数据、超出 VERSIONS 的旧版本、TTL 过期数据
资源开销 小,秒级到分钟级 大,读写全部数据,产生大量磁盘与网络 IO
结果文件数 减少若干个 每个 Store 只剩 1 个 HFile
附带收益 降低读放大 恢复数据本地性(新文件本地写一份副本)

为什么生产环境要关闭自动 major compaction:

  • major compaction 会把该 Store 的所有数据完整读一遍再写一遍,产生极高的磁盘 IO 与网络流量,同时占用 RegionServer 的 CPU 与 handler 线程;
  • 自动触发时间不可控,很可能撞上业务高峰,导致读写延迟飙升、超时、雪崩;
  • 大集群上多个 Region 同时 major compaction 会把 HDFS 打满。

做法:关闭自动,改为在业务低峰(如凌晨)通过脚本手工分批执行。

<!-- hbase-site.xml:0 表示关闭自动 major compaction -->
<property>
  <name>hbase.hregion.majorcompaction</name>
  <value>0</value>
</property>
<!-- 自动 major compaction 的抖动系数,关闭时一并置 0 -->
<property>
  <name>hbase.hregion.majorcompaction.jitter</name>
  <value>0</value>
</property>
<!-- minor compaction 触发的文件数阈值 -->
<property>
  <name>hbase.hstore.compactionThreshold</name>
  <value>3</value>
</property>
<!-- 单次 compaction 最多合并的文件数 -->
<property>
  <name>hbase.hstore.compaction.max</name>
  <value>10</value>
</property>
<!-- HFile 数超过此值则阻塞写入(防止无限积压) -->
<property>
  <name>hbase.hstore.blockingStoreFiles</name>
  <value>16</value>
</property>
# 低峰期手工执行(可按表或按 Region 分批)
major_compact 'student'
major_compact 'student', 'course'          # 只合并指定列族
major_compact 'ns1:user,1000,1690000000.abc123def.'   # 只合并指定 Region

# 查看 compaction 是否完成
compaction_state 'student'

2.8 Region Split 分裂

Store 是 Region 服务器的核心。多个 StoreFile 合并成一个;当单个 StoreFile 过大时,又会触发分裂操作,1 个父 Region 被分裂成两个子 Region。

分裂策略的演进:

版本 默认策略 行为
0.94 之前 ConstantSizeRegionSplitPolicy Store 中最大 HFile 超过 hbase.hregion.max.filesize(默认 10GB)即分裂。小表长期不分裂,大表分裂均匀
0.94 ~ 1.x IncreasingToUpperBoundRegionSplitPolicy 阈值 = min(该表在本 RS 上的 Region 数^3 × memstore.flush.size × 2, max.filesize)。新表小阈值快速分裂,成熟后趋近 10GB
2.x 起 SteppingSplitPolicy 若该表在本 RS 只有 1 个 Region,阈值 = 2 × flush.size(快速分裂消除热点);否则阈值 = max.filesize
可选 KeyPrefixRegionSplitPolicy / DelimitedKeyPrefixRegionSplitPolicy 保证相同前缀的行不被分到不同 Region
可选 DisabledRegionSplitPolicy 完全禁止自动分裂,全靠预分区与手工 split(对分布可控的大表推荐)
<!-- 单 Region 最大大小,超过则触发分裂 -->
<property>
  <name>hbase.hregion.max.filesize</name>
  <value>10737418240</value>   <!-- 10GB -->
</property>
<!-- 指定分裂策略 -->
<property>
  <name>hbase.regionserver.region.split.policy</name>
  <value>org.apache.hadoop.hbase.regionserver.SteppingSplitPolicy</value>
</property>

预分区的必要性(回顾 1.9.6):自动分裂过程中 Region 短暂下线、伴随 Reference 文件与后续 compaction 的数据搬迁,会造成写入抖动;且从 1 个 Region 开始意味着初期必然热点。因此生产建表一律预分区,并尽量把自动分裂关掉或调大阈值,改为按需手工 split。

# 手工分裂
split 'student'                          # 让 HBase 自选切点分裂表中所有 Region
split 'student', 'row5000'               # 按指定 RowKey 分裂表
split 'REGION_ENCODED_NAME', 'splitKey'  # 分裂指定 Region

# 手工合并(用于清理过多的小 Region / 空 Region)
merge_region 'ENCODED_REGION_1', 'ENCODED_REGION_2'
merge_region 'ENCODED_REGION_1', 'ENCODED_REGION_2', true   # true=强制合并非相邻 Region

2.9 数据删除机制(墓碑标记)

HBase 的 delete 不会立即物理删除数据,而是写入一条特殊的 KeyValue —— 墓碑标记(Tombstone),其 KeyType 为 Delete/DeleteColumn/DeleteFamily。

墓碑的三种粒度:

类型 由什么命令产生 含义
Delete delete 't','r','cf:c', ts 指定时间戳 只删除该单元格指定时间戳的那一个版本
DeleteColumn delete 't','r','cf:c' 不指定时间戳 删除该列所有版本(≤ 当前时间戳)
DeleteFamily delete 't','r','cf' / deleteall 't','r' 删除整个列族 / 整行所有列的所有版本

流程:delete → 写 WAL + MemStore(写入墓碑)→ flush 成 HFile → 只有 major compaction 才会把墓碑及其覆盖的数据真正物理清除。

为什么"删除后立即查还能查到旧版本"(常见困惑):

  1. 正常 get/scan 会在归并时应用墓碑,看不到被删数据 —— 这是正确行为。
  2. 但如果用 RAW => true 的 scan,或指定了早于墓碑时间戳的 TIMESTAMP/TIMERANGE,就会绕过或错开墓碑,从而看到仍然存在于 HFile 中的旧版本。
  3. 若删除时指定了时间戳(delete 't','r','cf:c', 1000),只会删掉 ts=1000 这一个版本,比它更早或更晚的其他版本仍然可见。
  4. 还有一个经典陷阱:删除(墓碑 ts = T)之后,再写入一条 时间戳小于 T 的数据,该数据会被墓碑"遮住"看不见,直到 major compaction 清掉墓碑后它又"复活"。因此不要手工指定过去的时间戳写入。
# 查看原始数据(含墓碑与所有版本),排查"删了还能查到"时非常有用
scan 'student', {RAW => true, VERSIONS => 10}

# 想立刻彻底删除,需手工 major compaction
major_compact 'student'

2.10 HLog 工作原理与故障恢复

分布式环境必须考虑系统出错,HBase 采用 HLog 保证系统恢复。HBase 系统为每个 Region 服务器配置一个 HLog 文件,它是一种预写式日志(Write Ahead Log)。用户更新数据必须首先写入日志,才能写入 MemStore 缓存;并且直到 MemStore 缓存内容对应的日志已经写入磁盘,该缓存内容才能被刷写到磁盘。

每个 Region 服务器都有一个自己的 HLog 文件,每次启动都检查该文件,确认最近一次执行缓存刷新操作之后是否发生新的写入;如果发现更新,则先写入 MemStore,再刷写到 StoreFile,最后删除旧的 HLog 文件,开始为用户提供服务。

当 Region 服务器发生故障时的恢复过程:

  1. ZooKeeper 实时监测每个 Region 服务器状态,当某个 Region 服务器发生故障时通知 Master;
  2. Master 首先处理该故障 Region 服务器遗留的 HLog 文件,该文件中包含来自多个 Region 对象的日志记录;
  3. 系统根据每条日志记录所属的 Region 对象对 HLog 数据进行拆分(WAL split),分别放到相应 Region 对象的目录下(recovered.edits),再将失效的 Region 重新分配到可用的 Region 服务器,并把相关 HLog 日志记录也发送给相应 Region 服务器;
  4. Region 服务器领取到分配给自己的 Region 对象及相关 HLog 日志记录后,重新做一遍日志记录中的各种操作(回放),把日志记录中的数据写入 MemStore 缓存,再刷新到磁盘的 StoreFile 文件中,完成数据恢复。

现代版本使用 Distributed Log Splitting(由多个 RegionServer 并行拆分)或 Distributed Log Replay 加速这一过程,把恢复时间从分钟级压缩到数十秒。

2.11 HDFS 上的目录布局与文件结构

# 查看 HBase 在 HDFS 上的根目录
hdfs dfs -ls /hbase
/hbase
├── data/                                 数据主目录
│   ├── default/                          默认命名空间
│   │   └── student/                      表
│   │       ├── .tabledesc/               表描述文件(TableDescriptor)
│   │       ├── .tmp/                     临时目录(flush/compaction 中间文件)
│   │       └── 1a2b3c4d5e6f.../          Region(目录名 = Region 编码名)
│   │           ├── .regioninfo           Region 元信息(startKey/endKey)
│   │           ├── recovered.edits/      WAL 拆分后待回放的日志
│   │           ├── course/               Store = 列族目录
│   │           │   ├── 8f7e6d5c...       HFile(StoreFile)
│   │           │   └── 9a8b7c6d...       HFile
│   │           └── info/                 另一个列族
│   └── hbase/                            系统命名空间
│       ├── meta/                         hbase:meta 表
│       └── namespace/                    命名空间表
├── WALs/                                 活跃 WAL
│   └── rs1.example.com,16020,169.../     每个 RegionServer 一个目录
│       └── rs1...169....default.169...   WAL 文件(按时间滚动)
├── oldWALs/                              已归档待清理的 WAL
├── archive/                              被删除/合并后的 HFile 归档(快照引用它)
├── .hbase-snapshot/                      快照元数据
├── .tmp/                                 全局临时目录
├── corrupt/                              损坏的 WAL
├── hbase.id                              集群唯一 ID
└── hbase.version                         文件格式版本

HFile 内部结构(HFile v2/v3):

┌──────────────────────────┐
│ Scanned Block Section    │  Data Block(KeyValue,默认 64KB,可压缩)
│                          │  Leaf Index Block、Bloom Block
├──────────────────────────┤
│ Non-Scanned Block Section│  Meta Block(如布隆元数据)
├──────────────────────────┤
│ Load-on-open Section     │  Root Data Index、Meta Index、
│                          │  File Info、Bloom Filter Metadata
├──────────────────────────┤
│ Trailer                  │  各段偏移量、版本、压缩算法、比较器
└──────────────────────────┘
  • Data Block 内是按序排列的 KeyValue:keyLen | valueLen | rowLen | row | cfLen | cf | qualifier | timestamp | keyType | value;
  • HFile 不可变,任何"修改"都通过写新文件 + compaction 实现;
  • Trailer 在文件末尾,打开文件时先读 Trailer 再加载索引与布隆过滤器(load-on-open 部分常驻内存)。

WAL 文件结构:由一系列 WALKey + WALEdit 组成的 SequenceFile 风格记录。WALKey 含 Region 名、表名、序列号(sequenceId)、写入时间;WALEdit 含该次写入的一组 Cell。WAL 按大小/时间滚动(hbase.regionserver.logroll.period,默认 1 小时;大小达 HDFS 块的 0.95 倍),数据全部 flush 后移入 oldWALs,由 LogCleaner 延时删除(复制场景需保留给 peer 消费)。

HBase 1.x 与 2.x 的架构差异要点:

项目 HBase 1.x HBase 2.x
Master Web UI 端口 60010(1.0 起改为 16010,0.9x 为 60010) 16010
RegionServer Web UI 60030(1.0 起改为 16030) 16030
Region 状态管理 ZooKeeper 中的 region-in-transition Master 内的 ProcedureV2(procedure WAL),更可靠,RIT 卡死显著减少
MemStore 实现 单一 skiplist,flush 后即落盘 In-Memory Compaction(CompactingMemStore),内存内先合并,减少 flush 与写放大
读写路径 —— 支持 Read Replica / Region Replica 提供 timeline-consistent 读
Assignment AssignmentManager(依赖 ZK) AssignmentManagerV2(不依赖 ZK)
API HTableDescriptor / HColumnDescriptor / HBaseAdmin(1.x 可用,2.x 已废弃) TableDescriptorBuilder / ColumnFamilyDescriptorBuilder / Admin 接口
hbck hbase hbck -fix 可直接修复 原 hbck 修复功能移除,改用 HBCK2(独立 jar,hbase hbck -j hbase-hbck2.jar)
依赖 JDK 7/8,Hadoop 2.x JDK 8+(2.3+ 支持 JDK 11 运行),Hadoop 2.8+/3.x
过滤器/协处理器 老接口 Coprocessor 接口重构(RegionObserver 改为 default 方法)

三、安装部署

3.1 版本选择与兼容性矩阵

备注:安装 Hadoop 时只包含 HDFS 和 MapReduce 等核心组件,并不包含 HBase,因此 HBase 需要单独安装。

表 3-1 HBase 与 Hadoop 版本兼容性(S=支持,X=不支持,NT=未测试)

HBase 版本 Hadoop 2.7.x Hadoop 2.8.x Hadoop 2.9.x Hadoop 2.10.x Hadoop 3.1.1+ Hadoop 3.2.x Hadoop 3.3.x
HBase 1.2.x S S NT X X X X
HBase 1.3.x S S NT X X X X
HBase 1.4.x X S S S X X X
HBase 1.6.x/1.7.x X S S S S NT NT
HBase 2.0.x X S S X X X X
HBase 2.1.x X S S S S X X
HBase 2.2.x X X S S S S NT
HBase 2.3.x / 2.4.x X X X S S S S
HBase 2.5.x X X X S S S S

表 3-2 HBase 与 JDK 兼容性

HBase 版本 JDK 7 JDK 8 JDK 11 JDK 17
HBase 1.2.x ~ 1.4.x 支持 支持 不支持 不支持
HBase 1.6.x/1.7.x 不支持 支持 不支持 不支持
HBase 2.0.x ~ 2.2.x 不支持 支持 不支持 不支持
HBase 2.3.x ~ 2.5.x 不支持 支持 运行支持(不用于编译,实验性) 不支持

表 3-3 HBase 与 ZooKeeper / Hive / Phoenix

组件 要求
ZooKeeper HBase 1.x 要求 3.4.x;HBase 2.x 要求 3.4.10+,推荐 3.5.7+/3.6.x(2.3+ 支持)
Hive Hive 1.x 配 HBase 1.x;Hive 3.1.x 配 HBase 2.x。Hive-HBase 整合从 Hive 0.6.0 开始出现,两者版本必须匹配
Phoenix Phoenix 4.x 配 HBase 1.x;Phoenix 5.x 配 HBase 2.x(大版本严格对应,不可混用)

版本选择建议:

  • 教学/实验:HBase 1.1.x ~ 1.2.x + Hadoop 2.7.x + JDK 8(本文示例沿用 hbase-1.1.2 / hbase-1.1.5);
  • 生产新建:HBase 2.4.x / 2.5.x + Hadoop 3.3.x + JDK 8 + ZooKeeper 3.6.x;
  • 不要混用:HBase 发行包中自带的 hadoop jar 版本必须与集群 Hadoop 版本一致,否则会报 NoSuchMethodError、ProtobufException、ClassNotFoundException 等;不一致时需用集群的 hadoop jar 替换 $HBASE_HOME/lib 下的同名 jar,或使用 hbase-x.y.z-bin.tar.gz 中与 Hadoop 3 对应的构建版本。

3.2 三种运行模式

HBase 有三种运行模式:

模式 说明 数据存放 ZooKeeper 适用
单机模式(Standalone) 所有进程(Master、RegionServer、ZooKeeper)在一个 JVM 内 本地文件系统 file:/// HBase 内置 学习、单元测试
伪分布式(Pseudo-distributed) 各守护进程独立 JVM,但都在同一台机器 HDFS(单机 HDFS) 内置或独立 单机验证完整流程
完全分布式(Fully-distributed) 多台机器,多 RegionServer + 主备 Master HDFS 集群 独立 ZooKeeper 集群 生产

以下先决条件很重要,例如没有配置 JAVA_HOME 环境变量就会报错:

  • JDK(必需);
  • Hadoop(单机模式不需要,伪分布式模式和分布式模式需要);
  • SSH(必需,用于脚本远程启停)。

HBASE_MANAGES_ZK 的取值含义:

  • HBASE_MANAGES_ZK=true:由 HBase 自己管理 ZooKeeper(随 HBase 启停);
  • HBASE_MANAGES_ZK=false:使用外部独立的 ZooKeeper 集群。

建议:单机版 HBase 使用自带 ZooKeeper;集群安装 HBase 则采用单独 ZooKeeper 集群。

启动关闭 Hadoop 和 HBase 的顺序一定是:启动 Hadoop → 启动 HBase → 关闭 HBase → 关闭 Hadoop。

3.3 前置条件准备

3.3.1 Hadoop 集群可用

# 确认 HDFS 正常,NameNode 已退出安全模式
start-dfs.sh
hdfs dfsadmin -report
hdfs dfsadmin -safemode get      # 应为 Safe mode is OFF
hdfs dfs -mkdir -p /tmp && hdfs dfs -ls /

3.3.2 ZooKeeper 集群

# 每个节点检查 ZK 状态,应有一个 leader、其余 follower
zkServer.sh status
echo ruok | nc zk1 2181         # 返回 imok
zkCli.sh -server zk1:2181 ls /

详见 16-06-ZooKeeper集群.md

3.3.3 JDK

java -version
echo $JAVA_HOME
# 若未设置,安装后写入 /etc/profile

3.3.4 SSH 免密

# 在 master 上生成密钥并分发到所有节点(含自己)
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
for h in master slave1 slave2 slave3; do ssh-copy-id -i ~/.ssh/id_rsa.pub $h; done
# 验证:应无需输入密码
for h in master slave1 slave2 slave3; do ssh $h hostname; done

3.3.5 时间同步(对 HBase 尤其重要)

HBase 对节点间时间偏差极其敏感:HBase 用时间戳作为数据版本,且 Master 与 RegionServer 之间会校验时钟。若某 RegionServer 与 Master 的时间差超过 hbase.master.maxclockskew(默认 30000 毫秒 = 30 秒),该 RegionServer 会直接启动失败并抛出 ClockOutOfSyncException;即使偏差不到 30 秒,也可能造成版本顺序错乱、数据"被墓碑遮住"等诡异现象。

# 方案一:chrony(CentOS 7+/RHEL 8+ 推荐)
yum install -y chrony
vim /etc/chrony.conf
#   server ntp.aliyun.com iburst
#   (内网可指定 master 为 server,并在 master 上 allow 内网段)
systemctl enable --now chronyd
chronyc sources -v
chronyc tracking          # 关注 System time 偏差

# 方案二:ntpd
yum install -y ntp
systemctl enable --now ntpd
ntpq -p

# 方案三:临时强制同步(所有节点执行)
ntpdate -u ntp.aliyun.com
hwclock -w                # 写入硬件时钟

# 统一时区
timedatectl set-timezone Asia/Shanghai
# 快速核对所有节点时间
for h in master slave1 slave2 slave3; do echo -n "$h: "; ssh $h date; done

必要时可放宽阈值(不推荐,只作为应急):

<property>
  <name>hbase.master.maxclockskew</name>
  <value>180000</value>
</property>

3.3.6 hosts 解析

HBase 客户端拿到的是 RegionServer 的主机名,因此客户端机器也必须能解析集群所有节点主机名,否则出现"能连上 ZooKeeper 但读写超时"。

vim /etc/hosts
192.168.1.10  master
192.168.1.11  slave1
192.168.1.12  slave2
192.168.1.13  slave3

注意:

  • 删除或注释掉 127.0.0.1 <hostname> 这类会让服务注册成 localhost 的条目;
  • 主机名不要包含下划线;
  • hostname 输出必须与 hosts 中一致,并写入 /etc/hostname;
  • 关闭防火墙或放通端口 16000/16010/16020/16030/2181/8020/50010。
systemctl stop firewalld && systemctl disable firewalld
setenforce 0 && sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config

3.3.7 ulimit(nofile 与 nproc)

HBase 会同时打开大量文件(每个 Store 的多个 HFile + WAL + socket),默认 1024 的句柄数远远不够,典型报错为 java.io.IOException: Too many open files、Could not obtain block。

vim /etc/security/limits.conf
# 建议 nofile ≥ 32768(生产常设 65536/100000),nproc ≥ 32000
hadoop  -  nofile  65536
hadoop  -  nproc   32000
hbase   -  nofile  65536
hbase   -  nproc   32000
*       soft nofile 65536
*       hard nofile 65536
*       soft nproc  32000
*       hard nproc  32000
# CentOS 6 需同时修改(否则 nproc 被覆盖)
vim /etc/security/limits.d/90-nproc.conf     # 或 20-nproc.conf
# 内容: *  soft  nproc  32000

# 重新登录后验证
ulimit -n
ulimit -u
# 确认某个已启动进程的实际限制
cat /proc/$(pgrep -f HRegionServer)/limits | grep -E 'files|processes'

同时建议关闭 swap、调整内核参数:

sysctl -w vm.swappiness=0
echo 'vm.swappiness = 0' >> /etc/sysctl.conf
echo 'net.core.somaxconn = 32768' >> /etc/sysctl.conf
sysctl -p

并确认 HDFS 侧的 dfs.datanode.max.transfer.threads(旧名 dfs.datanode.max.xcievers)足够大:

<!-- hdfs-site.xml -->
<property>
  <name>dfs.datanode.max.transfer.threads</name>
  <value>8192</value>
</property>

3.4 完整部署步骤

3.4.1 下载解压与目录说明

# 下载安装包(教学示例使用 hbase-1.1.2 / hbase-1.1.5;生产建议 2.4.x/2.5.x)
wget https://archive.apache.org/dist/hbase/1.1.5/hbase-1.1.5-bin.tar.gz

# 解压安装包至路径 /usr/local
tar -zxvf hbase-1.1.5-bin.tar.gz -C /usr/local/
mv /usr/local/hbase-1.1.5/ /usr/local/hbase

# 授权(假设使用 hadoop 用户运行)
chown -R hadoop:hadoop /usr/local/hbase

# 验证版本
/usr/local/hbase/bin/hbase version

目录说明:

目录 内容
bin/ 启停脚本与命令:start-hbase.sh、stop-hbase.sh、hbase、hbase-daemon.sh、hbase-daemons.sh、hbase-cleanup.sh
conf/ 配置文件:hbase-env.sh、hbase-site.xml、hbase-default.xml(只读参考)、regionservers、backup-masters、log4j.properties、hadoop-metrics2-hbase.properties
lib/ 依赖 jar(Java API 开发时可直接导入本目录全部 jar)
logs/ 运行日志(hbase-<user>-master-<host>.log 等),排错第一现场
docs/、hbase-webapps/ 文档与 Web UI 静态资源
bin/hbase-config.sh 供脚本引用的配置加载逻辑

3.4.2 hbase-env.sh

vim /usr/local/hbase/conf/hbase-env.sh
# ① JDK 路径(必须显式设置,否则报 JAVA_HOME is not set)
export JAVA_HOME=/usr/local/jdk

# ② 使用独立 ZooKeeper 集群,不由 HBase 启停 ZK
export HBASE_MANAGES_ZK=false

# ③ 让 HBase 找到 Hadoop 配置(可选,等价于软链 core-site/hdfs-site)
export HADOOP_HOME=/usr/local/hadoop
export HBASE_CLASSPATH=$HADOOP_HOME/etc/hadoop

# ④ 堆内存设置。经验:RegionServer 20~32GB(超过 32GB 失去指针压缩,
#    且 CMS 下 GC 停顿难控;需要更大缓存时用堆外 BucketCache)
export HBASE_HEAPSIZE=4G                      # 统一设置(简单场景)
export HBASE_MASTER_OPTS="-Xms2g -Xmx2g"      # Master 轻量,2~4G 足够
export HBASE_REGIONSERVER_OPTS="-Xms16g -Xmx16g \
  -XX:+UseG1GC -XX:MaxGCPauseMillis=100 \
  -XX:+ParallelRefProcEnabled -XX:-ResizePLAB \
  -XX:MaxDirectMemorySize=8g \
  -Xloggc:/usr/local/hbase/logs/gc-regionserver.log \
  -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/usr/local/hbase/logs"

# ⑤ 日志与 pid 目录
export HBASE_LOG_DIR=/usr/local/hbase/logs
export HBASE_PID_DIR=/var/run/hbase

# ⑥ JDK 8 需注释掉这两行(它们是 JDK7 的 PermSize 参数,JDK8 会告警)
# export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -XX:PermSize=128m -XX:MaxPermSize=128m"
# export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -XX:PermSize=128m -XX:MaxPermSize=128m"

3.4.3 hbase-site.xml 逐参数详解

vim /usr/local/hbase/conf/hbase-site.xml
<configuration>

  <!-- ① HBase 数据在 HDFS 上的根目录。
       完全分布式必须写 HDFS 地址;若 Hadoop 配置了 HA,
       这里写 nameservice 名(如 hdfs://mycluster/hbase)。
       单机模式可写 file:///usr/local/hbase/data
       伪分布式可写 hdfs://localhost:9000/hbase -->
  <property>
    <name>hbase.rootdir</name>
    <value>hdfs://master:9000/hbase</value>
  </property>

  <!-- ② 是否分布式模式。false=单机(Master/RS/ZK 同一 JVM);
       true=伪分布式或完全分布式 -->
  <property>
    <name>hbase.cluster.distributed</name>
    <value>true</value>
  </property>

  <!-- ③ ZooKeeper 集群地址列表,逗号分隔,不要写端口(用下面的 clientPort)。
       客户端也必须配置相同的值才能连上 HBase -->
  <property>
    <name>hbase.zookeeper.quorum</name>
    <value>master,slave1,slave2</value>
  </property>

  <!-- ZooKeeper 客户端端口,默认 2181 -->
  <property>
    <name>hbase.zookeeper.property.clientPort</name>
    <value>2181</value>
  </property>

  <!-- ④ ZooKeeper 数据目录。HBASE_MANAGES_ZK=false 时此项仅作参考,
       应与独立 ZK 的 dataDir 保持一致;=true 时 HBase 用它存 ZK 数据 -->
  <property>
    <name>hbase.zookeeper.property.dataDir</name>
    <value>/usr/local/zookeeper/data</value>
  </property>

  <!-- HBase 在 ZooKeeper 中的根 znode,多套 HBase 共用一个 ZK 时必须区分 -->
  <property>
    <name>hbase.zookeeper.znode.parent</name>
    <value>/hbase</value>
  </property>

  <!-- ⑤ Master Web UI 端口。1.0 及以后默认 16010;0.9x 为 60010。
       设为 -1 可关闭 Web UI -->
  <property>
    <name>hbase.master.info.port</name>
    <value>16010</value>
  </property>
  <!-- Master RPC 端口,默认 16000 -->
  <property>
    <name>hbase.master.port</name>
    <value>16000</value>
  </property>
  <!-- RegionServer RPC 端口 16020 / Web UI 端口 16030(1.x 之前为 60030) -->
  <property>
    <name>hbase.regionserver.port</name>
    <value>16020</value>
  </property>
  <property>
    <name>hbase.regionserver.info.port</name>
    <value>16030</value>
  </property>

  <!-- ⑥ RegionServer 处理客户端 RPC 的线程数,默认 30。
       写多读少可调到 100~150;调太大会放大内存占用与 GC 压力,
       且并发过高时反而因排队与上下文切换变慢。经验:CPU 核数 × 2~10 -->
  <property>
    <name>hbase.regionserver.handler.count</name>
    <value>100</value>
  </property>

  <!-- ⑦ 关闭对底层文件系统 hflush/hsync 能力的强制校验。
       HBase 2.x + 本地文件系统(file:///)或某些 HDFS 版本上,
       若不设为 false 会报
       "java.lang.IllegalStateException: The procedure WAL relies on
        the ability to hsync for proper operation"
       从而 Master 启动即退出。使用标准 HDFS 的生产集群应保持 true(默认) -->
  <property>
    <name>hbase.unsafe.stream.capability.enforce</name>
    <value>false</value>
  </property>

  <!-- ===== 以下为常用调优项 ===== -->
  <!-- 单个 MemStore flush 阈值,默认 128MB -->
  <property>
    <name>hbase.hregion.memstore.flush.size</name>
    <value>134217728</value>
  </property>
  <!-- MemStore 达到 flush.size × 该倍数时阻塞写入,默认 4 -->
  <property>
    <name>hbase.hregion.memstore.block.multiplier</name>
    <value>4</value>
  </property>
  <!-- 所有 MemStore 占堆上限,默认 0.4 -->
  <property>
    <name>hbase.regionserver.global.memstore.size</name>
    <value>0.4</value>
  </property>
  <!-- BlockCache 占堆比例,默认 0.4。与上一项之和建议 ≤ 0.8 -->
  <property>
    <name>hfile.block.cache.size</name>
    <value>0.4</value>
  </property>
  <!-- 单 Region 最大大小,超过触发分裂 -->
  <property>
    <name>hbase.hregion.max.filesize</name>
    <value>10737418240</value>
  </property>
  <!-- 关闭自动 major compaction,改为低峰手工执行 -->
  <property>
    <name>hbase.hregion.majorcompaction</name>
    <value>0</value>
  </property>
  <!-- scan 默认一次取回的行数(客户端 caching 缺省) -->
  <property>
    <name>hbase.client.scanner.caching</name>
    <value>1000</value>
  </property>
  <!-- ZooKeeper session 超时,决定 RS 宕机被发现的速度,默认 90000 -->
  <property>
    <name>zookeeper.session.timeout</name>
    <value>90000</value>
  </property>
  <!-- Master 与 RS 允许的最大时钟偏差(毫秒),默认 30000 -->
  <property>
    <name>hbase.master.maxclockskew</name>
    <value>30000</value>
  </property>
</configuration>

单机/伪分布式最小配置(对应 Java API 示例中的 hbase.rootdir):

<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://localhost:9000/hbase</value>
</property>
</configuration>

备注:单机版可使用 file:///DIRECTORY/hbase。

3.4.4 regionservers 文件

列出所有运行 RegionServer 的主机名,每行一个。start-hbase.sh 会据此通过 SSH 逐台启动。

vim /usr/local/hbase/conf/regionservers
slave1
slave2
slave3

注意:默认内容为 localhost,完全分布式部署时必须删除该行。

3.4.5 backup-masters 文件(配置备用 Master)

列出备用 HMaster 主机名。这些节点上会启动处于 standby 状态的 Master,Active Master 宕机后由 ZooKeeper 选举其中之一接管,实现 Master 高可用。

vim /usr/local/hbase/conf/backup-masters
slave1

3.4.6 软链接 Hadoop 配置文件

ln -s /usr/local/hadoop/etc/hadoop/core-site.xml /usr/local/hbase/conf/core-site.xml
ln -s /usr/local/hadoop/etc/hadoop/hdfs-site.xml /usr/local/hbase/conf/hdfs-site.xml

为什么必须这样做:

  • HBase 需要读取 HDFS 的客户端配置才能正确访问 hbase.rootdir。若缺少 core-site.xml,HBase 不知道 fs.defaultFS;若 Hadoop 启用了 NameNode HA,hbase.rootdir 写的是 nameservice 名(如 hdfs://mycluster/hbase),必须靠 hdfs-site.xml 中的 dfs.nameservices、dfs.ha.namenodes.*、dfs.client.failover.proxy.provider.* 才能解析,否则报 UnknownHostException: mycluster。
  • 还会继承 HDFS 的副本数(dfs.replication)、短路读取(dfs.client.read.shortcircuit)等设置。
  • 用软链接而非拷贝,是为了 Hadoop 配置变更后 HBase 自动跟随,避免两份配置不一致。

3.4.7 环境变量

vim /etc/profile
export JAVA_HOME=/usr/local/jdk
export HADOOP_HOME=/usr/local/hadoop
export HBASE_HOME=/usr/local/hbase
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$HBASE_HOME/bin:

使配置生效(也可配置系统环境,将 hbase 下的 bin 目录添加到系统 path 中):

source /etc/profile
hbase version

3.4.8 分发到所有节点

for h in slave1 slave2 slave3; do
  scp -r /usr/local/hbase $h:/usr/local/
  scp /etc/profile $h:/etc/profile
  ssh $h "source /etc/profile; chown -R hadoop:hadoop /usr/local/hbase"
done

3.5 启动与验证

3.5.1 启动顺序

# ① 先启动 ZooKeeper(每个 ZK 节点执行;若用 HBase 自带 ZK 则跳过)
zkServer.sh start

# ② 启动 HDFS(HBase 底层依赖 HDFS,需先启动 HDFS,再启动 HBase)
start-dfs.sh
# 如需 YARN(跑 MapReduce 导入时)
start-yarn.sh

# ③ 启动 HBase
start-hbase.sh

停止顺序严格相反:

stop-hbase.sh
stop-yarn.sh
stop-dfs.sh
zkServer.sh stop

3.5.2 单独启停守护进程

# 本机单独启停(排错时最常用)
hbase-daemon.sh start master
hbase-daemon.sh start regionserver
hbase-daemon.sh stop  master
hbase-daemon.sh stop  regionserver

# 启动一个 backup master(前台不接管,等待选举)
hbase-daemon.sh start master --backup

# 通过 regionservers 文件批量启停所有 RS
hbase-daemons.sh start regionserver
hbase-daemons.sh stop  regionserver

# 启动 Thrift / REST 网关
hbase-daemon.sh start thrift
hbase-daemon.sh start rest -p 8080

# 新增一台 RegionServer 上线(无需重启集群)
# 1. 在新节点安装并同步配置 2. 在 regionservers 中加入主机名
# 3. 在新节点执行:
hbase-daemon.sh start regionserver
# 4. 触发均衡
echo "balancer" | hbase shell

# 优雅下线一台 RegionServer(先迁走 Region 再停进程)
graceful_stop.sh slave3

3.5.3 jps 检查进程

使用 jps 查看 Java 进程,确认相关进程已启动:

jps

伪分布式(单机)示例输出:

4258 Jps
3780 SecondaryNameNode
4022 HMaster
3594 DataNode
3452 NameNode

其中 HMaster 是 HBase 的主进程。完全分布式应看到:

节点 应有进程
master HMaster、NameNode、QuorumPeerMain(若该节点跑 ZK)、ResourceManager
slave1(备 Master) HMaster(backup)、HRegionServer、DataNode、QuorumPeerMain、NodeManager
slave2 / slave3 HRegionServer、DataNode、NodeManager、QuorumPeerMain(若跑 ZK)
  • HMaster:HBase 主进程;
  • HRegionServer:Region 服务器进程;
  • QuorumPeerMain:独立 ZooKeeper 进程(若 HBASE_MANAGES_ZK=true,HBase 启动的 ZK 进程名为 HQuorumPeer)。
# 批量查看所有节点进程
for h in master slave1 slave2 slave3; do echo "===== $h ====="; ssh $h jps; done

3.5.4 Web UI

组件 HBase 1.0+ / 2.x 端口 HBase 0.9x(旧版)端口
HMaster Web UI 16010 60010
RegionServer Web UI 16030 60030
HMaster RPC 16000 60000
RegionServer RPC 16020 60020
http://master:16010        # Master 状态、表列表、RegionServer 列表、Region 分布
http://slave1:16030        # 单个 RegionServer 的 Region、MemStore、compaction 队列
http://master:16010/jmx    # JMX 指标(JSON)
http://master:16010/logs/  # 日志目录

若 Master 运行在主机 master.foo.com 上,主页地址为 http://master.foo.com:16010(旧版为 http://master.foo.com:60010),可通过浏览器查看 HBase 集群当前状态。这个自带 UI 即 Master-status 监控工具。

3.5.5 hbase shell 连接验证

进入 HBase 交互式命令行:

hbase shell
# 集群状态:几台活跃 RS、几台死亡、平均负载
status
status 'simple'        # 各 RS 的请求数、Region 数、存储量
status 'summary'
status 'detailed'      # 每个 Region 的详细信息
status 'replication'   # 复制状态

# 版本信息
version

# 当前用户与权限组
whoami

# 建一张表做端到端验证,然后清理
create 'test', 'cf'
put 'test', 'row1', 'cf:a', 'value1'
scan 'test'
get 'test', 'row1'
disable 'test'
drop 'test'
exit

正常的 status 输出形如:

1 active master, 1 backup masters, 3 servers, 0 dead, 1.6667 average load

3.5.6 检查 HDFS 上的 /hbase 目录

hdfs dfs -ls /hbase
hdfs dfs -ls /hbase/data/default          # 用户表目录
hdfs dfs -ls /hbase/data/hbase/meta       # meta 表
hdfs dfs -ls /hbase/WALs                  # 每个 RS 一个 WAL 目录
hdfs dfs -cat /hbase/hbase.id
hdfs dfs -du -h /hbase                    # 各目录占用

若 /hbase 未生成,说明 Master 启动失败,应立即查看日志:

tail -200f /usr/local/hbase/logs/hbase-hadoop-master-master.log
tail -200f /usr/local/hbase/logs/hbase-hadoop-regionserver-slave1.log
# 也可通过 Web UI 的 /logs/ 页面查看

四、HBase Shell 与 API 操作

4.1 进入、退出与帮助

# 进入 shell(本质是一个 JRuby IRB 环境)
hbase shell

# 退出
exit
# 或
quit

# 非交互模式执行单条命令(脚本化运维常用)
echo "list" | hbase shell
echo "status" | hbase shell -n            # -n 非交互,命令失败返回非 0 退出码
hbase shell /opt/scripts/init_tables.txt  # 执行脚本文件

# 帮助
help                       # 总帮助,列出所有命令分组
help 'create'              # 单命令详细帮助与示例
help 'general'             # 分组帮助:general / ddl / dml / tools /
                           # replication / snapshots / namespace / security /
                           # visibility labels / quotas / configuration / procedures
table_help                 # 表对象(table reference)的用法帮助

table_help 提到的表引用用法:

t = get_table 'student'    # 取得表引用
t.put '95001', 'Sname', 'LiYing'
t.get '95001'
t.scan
t.describe
t.disable
t.drop

4.2 通用命令

status                     # 集群状态
status 'simple'
status 'summary'
status 'detailed'
version                    # HBase 版本
whoami                     # 当前登录用户与所属组
processlist                # 查看正在执行的 RPC 任务(2.x)
list_procedures            # 查看正在执行的 Procedure(2.x)
list_locks                 # 查看锁(2.x)

4.3 DDL 命令(表管理)

4.3.1 create 创建表

# 最简写法:表名 + 列族列表
create 'student', 'Sname', 'Ssex', 'Sage', 'Sdept', 'course'
create 'tempTable', 'f1', 'f2', 'f3'
create 'student', 'score'

# 字典写法(可为每个列族指定属性)
create 'student', {NAME => 'info'}, {NAME => 'course'}

# 指定最大版本数
create 'student', {NAME => 'info', VERSIONS => 5}

# 指定 TTL(秒),过期数据在 major compaction 时清除;86400 = 1 天
create 'logs', {NAME => 'cf', TTL => 604800}          # 保留 7 天

# 指定压缩算法:NONE / GZ / SNAPPY / LZO / LZ4 / ZSTD
create 'logs', {NAME => 'cf', COMPRESSION => 'SNAPPY'}

# 指定布隆过滤器:NONE / ROW(默认)/ ROWCOL
create 'user', {NAME => 'info', BLOOMFILTER => 'ROWCOL'}

# 多属性组合 + 常驻内存 + 块大小 + 缓存策略
create 'user',
  {NAME => 'info', VERSIONS => 3, TTL => 2592000,
   COMPRESSION => 'SNAPPY', BLOOMFILTER => 'ROW',
   IN_MEMORY => true, BLOCKSIZE => 65536,
   BLOCKCACHE => true, DATA_BLOCK_ENCODING => 'FAST_DIFF',
   MIN_VERSIONS => 1, KEEP_DELETED_CELLS => 'FALSE'}

# 预分区:显式分割点
create 'user', 'info', SPLITS => ['1000', '2000', '3000', '4000']
# 预分区:分割点来自文件
create 'user', 'info', SPLITS_FILE => '/opt/splits.txt'
# 预分区:按算法均分
create 'user', 'info', {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}
create 'user', 'info', {NUMREGIONS => 20, SPLITALGO => 'DecimalStringSplit'}
create 'user', 'info', {NUMREGIONS => 16, SPLITALGO => 'UniformSplit'}
# 预分区 + 列族属性同时指定
create 'user', {NAME => 'info', COMPRESSION => 'SNAPPY'},
  {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}

# 在命名空间下建表
create 'ns1:user', 'info'

常用列族属性一览:

属性 含义 默认
NAME 列族名 必填
VERSIONS 保留最大版本数 1(1.x)
MIN_VERSIONS 即使 TTL 过期也至少保留的版本数 0
TTL 存活秒数,FOREVER 为永久 FOREVER
COMPRESSION HFile 压缩算法 NONE
DATA_BLOCK_ENCODING 块内前缀编码:NONE/PREFIX/DIFF/FAST_DIFF/PREFIX_TREE NONE
BLOOMFILTER 布隆过滤器粒度 ROW
BLOCKSIZE Data Block 大小(字节) 65536
BLOCKCACHE 是否使用读缓存 true
IN_MEMORY 优先常驻 BlockCache false
KEEP_DELETED_CELLS 是否保留被删除的 Cell(用于时间点查询) FALSE
REPLICATION_SCOPE 0=不复制,1=参与 Replication 0
CACHE_DATA_ON_WRITE / CACHE_BLOOMS_ON_WRITE / CACHE_INDEX_ON_WRITE 写入时即缓存 false

对应的 Java 端优化 API(旧接口):

  • InMemory:创建表时,可以通过 HColumnDescriptor.setInMemory(true) 将表放到 Region 服务器的缓存中,保证读取时被 cache 命中。
  • Max Versions:创建表时,可以通过 HColumnDescriptor.setMaxVersions(int maxVersions) 设置表中数据的最大版本;如果只需要保存最新版本数据,可以设置 setMaxVersions(1)。
  • Time To Live:创建表时,可以通过 HColumnDescriptor.setTimeToLive(int timeToLive) 设置表中数据的存储生命期,过期数据将自动被删除。例如只存储最近两天数据,可设置 setTimeToLive(2 * 24 * 60 * 60)。

4.3.2 list / describe

list                          # 列出 HBase 中所有的表信息
list 'stu.*'                  # 支持正则过滤
list_namespace_tables 'ns1'   # 列出某命名空间下的表

describe 'student'            # 查看表的基本信息(列族及其属性、Region 数等)
desc 'student'                # describe 的简写
describe 'ns1:user'

4.3.3 alter 修改表

# 新增列族
alter 'student', NAME => 'address'
alter 'student', {NAME => 'address', VERSIONS => 3}

# 修改已有列族属性
alter 'student', {NAME => 'course', VERSIONS => 5, TTL => 2592000}
alter 'student', {NAME => 'course', COMPRESSION => 'SNAPPY'}

# 删除列族(两种写法)
alter 'student', NAME => 'address', METHOD => 'delete'
alter 'student', 'delete' => 'address'

# 一次修改多个列族
alter 'student', {NAME => 'f1', VERSIONS => 2}, {NAME => 'f2', TTL => 86400}

# 修改表级属性
alter 'student', MAX_FILESIZE => '21474836480'      # 20GB
alter 'student', MEMSTORE_FLUSHSIZE => '268435456'  # 256MB
alter 'student', READONLY => 'true'
alter 'student', 'coprocessor' => 'hdfs:///lib/myCoprocessor.jar|com.demo.MyObserver|1001'

# 删除表级配置项
alter 'student', METHOD => 'table_att_unset', NAME => 'MAX_FILESIZE'

# 查看 alter 是否完成
alter_status 'student'

注意:HBase 1.x 之前修改列族需要先 disable 表;1.x/2.x 支持在线 alter,但仍建议在低峰执行。

4.3.4 disable / enable / drop

disable 'student'              # 使表无效(下线所有 Region,禁止读写)
enable  'student'              # 使表有效
is_disabled 'student'          # 判断是否已 disable
is_enabled  'student'          # 判断是否已 enable
disable_all 'test.*'           # 按正则批量 disable
enable_all  'test.*'

drop 'student'                 # 删除表(必须先 disable,否则报 TableNotDisabledException)
drop_all 'test.*'              # 批量删除

exists 'student'               # 判断表是否存在

删除表的标准两步(例子4:使表 tempTable 无效并删除该表):

disable 'tempTable'
drop 'tempTable'

4.3.5 truncate 清空表

truncate 'student'
# 等价于内部执行:disable → drop → 按原表结构重建(Region 会退化为 1 个)

truncate_preserve 'student'
# 清空数据但**保留原有的 Region 分区**,预分区过的大表应用这个

4.4 DML 命令(数据操作)

4.4.1 put 写入与更新

put:向表、行、列指定的单元格添加数据,一次只能为一个表的一行数据的一个列添加一个数据。

# 语法:put '表名','行键','列族[:列限定符]','值' [,时间戳]

# 例子2:向表 tempTable 中的第 r1 行、第 f1:c1 列添加数据值 hello,dblab
put 'tempTable', 'r1', 'f1:c1', 'hello,dblab'

# 讲义示例:为行键 95001 添加 Sname 列,值为 LiYing(该行的学号即为 95001)
put 'student', '95001', 'Sname', 'LiYing'
# 为行键 95001 下的 course 列族中的 math 列添加数据 80
put 'student', '95001', 'course:math', '80'

# 学生成绩示例
put 'student', 'zhangsan', 'score:English',  '69'
put 'student', 'zhangsan', 'score:Math',     '86'
put 'student', 'zhangsan', 'score:Computer', '77'

# 添加数据时 HBase 会自动添加时间戳,也可人工指定时间戳
put 'student', '95001', 'course:math', '85', 1724300000000

# 覆盖即产生多版本:同一 RowKey + 同一列再次 put,
# 不会修改原数据,而是写入时间戳更大的新版本
put 'student', '95001', 'course:math', '90'
get 'student', '95001', {COLUMN => 'course:math', VERSIONS => 3}

4.4.2 get 读取单行

get:通过表名、行、列、时间戳、时间范围和版本号获得相应单元格的值。

# 读取整行
get 'student', '95001'

# 例子3:(1)从 tempTable 中获取第 r1 行、第 f1:c1 列的值
get 'tempTable', 'r1', 'f1:c1'
#     (2)获取第 r1 行、第 f1:c3 列的值(f1 是列族,c1 和 c3 都是列限定符)
get 'tempTable', 'r1', 'f1:c3'
# 结果可看出 f1:c3 列的值当前不存在(返回 0 row)

# 指定列族
get 'student', '95001', 'course'
get 'student', '95001', {COLUMN => 'course'}
get 'student', '95001', COLUMNS => ['Sname', 'course']

# 指定列
get 'student', '95001', 'course:math'
get 'student', 'zhangsan', {COLUMN => 'score:English'}

# 指定版本数(返回该列最近 N 个版本)
get 'student', '95001', {COLUMN => 'course:math', VERSIONS => 3}
get 'student', '95001', {VERSIONS => 5}

# 指定精确时间戳(只返回该时间戳的版本)
get 'student', '95001', {COLUMN => 'course:math', TIMESTAMP => 1724300000000}

# 指定时间范围 [min, max)
get 'student', '95001', {TIMERANGE => [1724300000000, 1724400000000]}
get 'student', '95001', {COLUMN => 'course:math',
                         TIMERANGE => [1724300000000, 1724400000000],
                         VERSIONS => 3}

# 带过滤器
get 'student', '95001', {FILTER => "ValueFilter(=, 'binary:80')"}

# 格式化输出(值为二进制数字时用 toLong / toInt 展示)
get 'counter', 'row1', {COLUMN => 'cf:c', FORMATTER => 'toString'}
get 'student', '95001', {ATTRIBUTES => {'mykey' => 'myvalue'}}

4.4.3 scan 扫描

scan:浏览表的相关信息。

# 全表扫描(生产环境慎用,大表会打满 RegionServer)
scan 'student'
scan 'tempTable'

# 限制返回行数
scan 'student', {LIMIT => 10}

# 指定起止行键:STARTROW 包含,STOPROW 不包含
scan 'student', {STARTROW => '95001', STOPROW => '95010'}
scan 'student', {STARTROW => '95001'}                      # 从此行到表尾
scan 'student', {STOPROW  => '95010'}                      # 从表头到此行前

# 指定列族 / 列
scan 'student', {COLUMNS => 'course'}
scan 'student', {COLUMNS => ['Sname', 'course:math']}
scan 'student', {COLUMN  => 'course:math'}

# 版本与时间
scan 'student', {VERSIONS => 3}
scan 'student', {TIMERANGE => [1724300000000, 1724400000000]}
scan 'student', {TIMESTAMP => 1724300000000}

# 反向扫描(从大到小,用于取"最后几条")
scan 'student', {REVERSED => true, LIMIT => 5}
scan 'student', {STARTROW => '95010', STOPROW => '95001', REVERSED => true}

# 客户端一次 RPC 取回的行数(提高吞吐;过大易 OOM 与超时)
scan 'student', {CACHING => 500}
# 一次返回的最大列数(宽行必须设置,防止单行过大 OOM)
scan 'student', {BATCH => 100}
# 单次 RPC 返回的最大字节数
scan 'student', {MAX_RESULT_SIZE => 2097152}

# 只扫描 MemStore/HFile 中的原始数据,包含墓碑与所有版本(排错用)
scan 'student', {RAW => true, VERSIONS => 10}

# 只扫描指定 Region
scan 'student', {REGION => 'student,95001,1690000000.abcdef.'}

# 带过滤器
scan 'student', {FILTER => "PrefixFilter('9500')"}
scan 'student', {FILTER => "ColumnPrefixFilter('ma')"}

# 组合示例:按前缀扫描、只取两列、限制 20 行、每次取回 100 行
scan 'student', {STARTROW => '95001', STOPROW => '95999',
                 COLUMNS => ['Sname', 'course:math'],
                 LIMIT => 20, CACHING => 100}

# 扫描系统表
scan 'hbase:meta'
scan 'hbase:meta', {LIMIT => 5}
scan 'hbase:namespace'

4.4.4 delete / deleteall 删除

# delete:删除指定列(该时间戳及更早的所有版本)
delete 'student', '95001', 'Sname'
delete 'student', '95001', 'course:math'
# 只删除指定时间戳的那一个版本
delete 'student', '95001', 'course:math', 1724300000000

# 讲义示例:删除 student 表中 95001 行下 Sname 列的所有版本数据
delete 'student', '95001', 'Sname'

# deleteall:删除整行
deleteall 'student', '95001'
# 删除整行中某列族/某列(等价于 delete 的更宽形式)
deleteall 'student', '95001', 'course'
# 按行键前缀批量删除(2.x 支持)
deleteall 'student', {ROWPREFIXFILTER => '9500'}

delete 与 deleteall 的区别

命令 作用 说明
delete 删除一个数据 是 put 的反向操作,删除指定列的数据
deleteall 删除一整行数据 删除指定行键对应的整行数据

提醒:delete 只是写入墓碑标记,物理删除发生在 major compaction(详见 2.9)。

4.4.5 count 统计行数

count 'student'
# 每 1000 行打印一次进度(默认 1000)
count 'student', INTERVAL => 100000
# 设置扫描缓存以加速(默认 10,调大显著提速)
count 'student', CACHE => 1000
# 组合
count 'student', INTERVAL => 100000, CACHE => 5000
# 带过滤器统计
count 'student', FILTER => "PrefixFilter('9500')"

大表用 count 会做全表扫描,极慢。生产应使用 MapReduce 作业统计: hbase org.apache.hadoop.hbase.mapreduce.RowCounter 'student'

4.4.6 incr 计数器与 append

# incr:原子自增(值以 8 字节 Long 存储)
incr 'counters', 'page1', 'cf:pv'            # 默认 +1
incr 'counters', 'page1', 'cf:pv', 10        # +10
incr 'counters', 'page1', 'cf:pv', -1        # 支持减

# get_counter:以数字形式读取计数器(直接 get 会显示为二进制乱码)
get_counter 'counters', 'page1', 'cf:pv'

# 建表时无需特殊声明,但读取需注意格式化
get 'counters', 'page1', {COLUMN => 'cf:pv', FORMATTER => 'toLong'}

# append:在原值末尾追加字节(原子操作)
append 'student', '95001', 'Sname', '_suffix'

4.5 过滤器(Filter)

过滤器在 RegionServer 端执行,可显著减少网络传输。可通过 show_filters 查看 shell 支持的全部过滤器。

show_filters

比较运算符:= 、!= 、> 、>= 、< 、<=

比较器(comparator)写法:

写法 含义
binary:值 完整字节比较(等值/大小)
binaryprefix:值 前缀字节比较
substring:值 包含子串(只支持 = 与 !=,不区分大小写)
regexstring:正则 正则匹配(只支持 = 与 !=)

4.5.1 常用过滤器

# RowFilter:按行键过滤
scan 'student', {FILTER => "RowFilter(=, 'binary:95001')"}
scan 'student', {FILTER => "RowFilter(>=, 'binary:95001')"}
scan 'student', {FILTER => "RowFilter(=, 'substring:9500')"}
scan 'student', {FILTER => "RowFilter(=, 'regexstring:^9500[1-5]$')"}

# PrefixFilter:行键前缀(比 RowFilter 高效,可跳过整个 Region)
scan 'student', {FILTER => "PrefixFilter('9500')"}

# ValueFilter:按单元格值过滤
scan 'student', {FILTER => "ValueFilter(=, 'binary:80')"}
scan 'student', {FILTER => "ValueFilter(=, 'substring:Li')"}
scan 'student', {FILTER => "ValueFilter(>=, 'binary:60')"}

# SingleColumnValueFilter:按某一列的值筛选整行(最常用)
# 参数:列族, 列, 比较符, 比较器
scan 'student', {FILTER =>
  "SingleColumnValueFilter('course','math',>=,'binary:80')"}
# 第 5、6 参数:filterIfMissing(该列不存在时是否过滤掉该行)、latestVersionOnly
scan 'student', {FILTER =>
  "SingleColumnValueFilter('course','math',>=,'binary:80',true,true)"}

# ColumnPrefixFilter:列限定符前缀
scan 'student', {FILTER => "ColumnPrefixFilter('ma')"}
# MultipleColumnPrefixFilter:多个列前缀
scan 'student', {FILTER => "MultipleColumnPrefixFilter('ma','en')"}

# QualifierFilter:按列限定符过滤
scan 'student', {FILTER => "QualifierFilter(=, 'binary:math')"}
# FamilyFilter:按列族过滤
scan 'student', {FILTER => "FamilyFilter(=, 'binary:course')"}

# 其他常用
scan 'student', {FILTER => "KeyOnlyFilter()"}          # 只返回 key 不返回 value
scan 'student', {FILTER => "FirstKeyOnlyFilter()"}     # 每行只返回第一个 cell(统计行数用)
scan 'student', {FILTER => "PageFilter(10)"}           # 分页,每个 Region 返回 10 行
scan 'student', {FILTER => "ColumnCountGetFilter(3)"}  # 每行最多返回 3 列
scan 'student', {FILTER => "InclusiveStopFilter('95010')"}  # 包含结束行
scan 'student', {FILTER => "ColumnRangeFilter('a',true,'m',false)"}  # 列区间
scan 'student', {FILTER => "TimestampsFilter(1724300000000, 1724300000001)"}
scan 'student', {FILTER => "DependentColumnFilter('course','math')"}

4.5.2 FilterList 组合

# AND 组合(shell 中用 AND 连接)
scan 'student', {FILTER =>
  "PrefixFilter('9500') AND ValueFilter(>=,'binary:80')"}

# OR 组合
scan 'student', {FILTER =>
  "ValueFilter(=,'binary:80') OR ValueFilter(=,'binary:90')"}

# 括号分组 + 取反 SKIP / WHILE
scan 'student', {FILTER =>
  "(PrefixFilter('9500') AND ColumnPrefixFilter('ma')) OR PrefixFilter('9600')"}
scan 'student', {FILTER => "SKIP ValueFilter(=,'binary:0')"}
scan 'student', {FILTER => "WHILE PrefixFilter('9500')"}

# 与 STARTROW/STOPROW 联用(先用行键区间缩小范围,再过滤,效率最高)
scan 'student', {STARTROW => '95001', STOPROW => '96000',
                 FILTER => "SingleColumnValueFilter('course','math',>=,'binary:80')"}

性能提示:RowFilter、ValueFilter 等本质仍是扫描后过滤,不会减少扫描量;只有 STARTROW/STOPROW、PrefixFilter 与布隆过滤器能真正减少 IO。设计时优先靠 RowKey 缩小范围。

4.6 命名空间(Namespace)

命名空间相当于关系型数据库中的 database,用于表的逻辑分组、配额与权限隔离。系统内置两个命名空间:hbase(系统表,如 hbase:meta、hbase:namespace)与 default(未指定命名空间时的默认库)。

create_namespace 'ns1'
create_namespace 'ns1', {'author' => 'zhangsan', 'date' => '2026-08-22'}

list_namespace
list_namespace 'ns.*'
describe_namespace 'ns1'
list_namespace_tables 'ns1'

alter_namespace 'ns1', {METHOD => 'set', 'author' => 'lisi'}
alter_namespace 'ns1', {METHOD => 'unset', NAME => 'author'}

# 删除命名空间(必须先删除其中所有表)
drop_namespace 'ns1'

# 全限定表名:命名空间:表名
create 'ns1:user', 'info'
put   'ns1:user', 'r1', 'info:name', 'tom'
get   'ns1:user', 'r1'
scan  'ns1:user'
disable 'ns1:user'
drop    'ns1:user'
# default 命名空间下的表可省略前缀,'student' 等价于 'default:student'

4.7 运维命令

# ---- flush:把 MemStore 强制刷写成 HFile ----
flush 'student'                                  # 按表
flush 'student,95001,1690000000.abcdef.'         # 按 Region(编码名)
flush 'slave1,16020,1690000000'                  # 按 RegionServer(2.x)

# ---- compaction ----
compact 'student'                                # minor compaction
compact 'student', 'course'                      # 只合并指定列族
compact 'REGION_ENCODED_NAME'
major_compact 'student'                          # major compaction
major_compact 'student', 'course'
major_compact 'REGION_ENCODED_NAME'
compaction_state 'student'                       # 查看 compaction 是否进行中
compact_rs 'slave1,16020,1690000000'             # 合并某 RS 上所有 Region
compact_rs 'slave1,16020,1690000000', true       # true = major

# ---- split / merge ----
split 'student'
split 'student', 'row5000'
split 'REGION_ENCODED_NAME', 'splitKey'
merge_region 'ENCODED_REGION_1', 'ENCODED_REGION_2'
merge_region 'ENCODED_REGION_1', 'ENCODED_REGION_2', true

# ---- Region 分配与迁移 ----
move 'REGION_ENCODED_NAME', 'slave2,16020,1690000000'   # 迁移到指定 RS
move 'REGION_ENCODED_NAME'                              # 由 Master 自选目标
assign   'REGION_ENCODED_NAME'                          # 上线(分配)Region
unassign 'REGION_ENCODED_NAME'                          # 下线 Region
unassign 'REGION_ENCODED_NAME', true                    # true = 强制
close_region 'REGION_ENCODED_NAME'                      # 关闭 Region(1.x)
list_regions 'student'                                  # 列出表的所有 Region(2.x)

# ---- 负载均衡 ----
balancer                        # 立即触发一次均衡,返回 true 表示已启动
balance_switch true             # 打开自动 balancer(默认打开)
balance_switch false            # 关闭(做批量运维/手工 move 前必须关闭,
                                #  否则 balancer 会把 Region 搬回去)
balancer_enabled                # 查看开关状态

# ---- 目录清理 ----
catalogjanitor_run              # 立即运行 CatalogJanitor:
                                # 清理已完成分裂的父 Region 及 meta 中的残留条目
catalogjanitor_switch true      # 开关
catalogjanitor_enabled
cleaner_chore_run               # 立即清理 archive/oldWALs(2.x)
normalize                       # 触发 Region 规整(合并过小、分裂过大)
normalizer_switch true

# ---- 一致性检查与修复 ----
# HBase 1.x:hbck 可检查并修复
hbase hbck                                   # 只检查,输出 inconsistencies
hbase hbck -details
hbase hbck -fixAssignments                   # 修复 Region 分配问题
hbase hbck -fixMeta                          # 修复 meta 表
hbase hbck -fixHdfsHoles                     # 修复 HDFS 上的 Region 空洞
hbase hbck -repair                           # 全量修复(危险,务必先备份)
hbase hbck -fix                              # 旧版本的通用修复

# HBase 2.x:原 hbck 的修复能力被移除,只保留检查;修复用 HBCK2
hbase hbck                                   # 2.x 仅做检查报告
hbase hbck -j /opt/hbase-hbck2-1.2.0.jar assigns  REGION_ENCODED_NAME
hbase hbck -j /opt/hbase-hbck2-1.2.0.jar unassigns REGION_ENCODED_NAME
hbase hbck -j /opt/hbase-hbck2-1.2.0.jar bypass -o PROCEDURE_ID
hbase hbck -j /opt/hbase-hbck2-1.2.0.jar setTableState student ENABLED
hbase hbck -j /opt/hbase-hbck2-1.2.0.jar fixMeta
hbase hbck -j /opt/hbase-hbck2-1.2.0.jar reportMissingRegionsInMeta

# ---- 其他实用工具 ----
hbase org.apache.hadoop.hbase.mapreduce.RowCounter 'student'   # MR 统计行数
hbase hfile -f /hbase/data/default/student/xxx/cf/yyy -p -m    # 查看 HFile 内容
hbase wal /hbase/WALs/rs1.../rs1...log -p                      # 解析 WAL
hbase zkcli                                                    # 内置 ZK 客户端
hbase clean --cleanZk                                          # 清理 ZK 中的 HBase 数据(危险)
hbase canary                                                   # 健康巡检
hbase pe --nomapred --rows=100000 randomWrite 5                # 性能压测(PerformanceEvaluation)
hbase ltt -write 10:1024:10 -num_keys 1000000                  # LoadTestTool 压测

4.8 快照(Snapshot)与跨集群迁移

快照只记录当前 HFile 的引用元数据,不复制数据,因此创建几乎瞬间完成、几乎不占空间(原文件被 compaction 淘汰时才会被移入 archive/ 保留)。需确认已启用(1.x 之后默认开启):

<property>
  <name>hbase.snapshot.enabled</name>
  <value>true</value>
</property>
# 创建快照(表可在线,不影响读写)
snapshot 'student', 'student_snap_20260822'

# 列出快照
list_snapshots
list_snapshots 'student.*'
list_table_snapshots 'student'

# 从快照恢复到原表(会覆盖原表,必须先 disable)
disable 'student'
restore_snapshot 'student_snap_20260822'
enable 'student'

# 从快照克隆出一张新表(不影响原表,最安全的"恢复"方式)
clone_snapshot 'student_snap_20260822', 'student_recovered'

# 删除快照
delete_snapshot 'student_snap_20260822'
delete_all_snapshot 'student.*'

# 快照信息(在 shell 外执行)
hbase snapshot info -snapshot student_snap_20260822 -files -stats

ExportSnapshot 跨集群迁移(推荐的整表迁移方式,直接搬 HFile,不经过 RegionServer,对源集群影响最小):

# 在源集群执行,把快照导出到目标集群的 HDFS
hbase org.apache.hadoop.hbase.snapshot.ExportSnapshot \
  -snapshot student_snap_20260822 \
  -copy-to hdfs://target-nn:9000/hbase \
  -mappers 16 \
  -bandwidth 100          # 限速,单位 MB/s,避免打满网络

# 也可从目标集群拉取(-copy-from)
hbase org.apache.hadoop.hbase.snapshot.ExportSnapshot \
  -snapshot student_snap_20260822 \
  -copy-from hdfs://source-nn:9000/hbase \
  -copy-to   hdfs://target-nn:9000/hbase

# 在目标集群恢复
hbase shell
list_snapshots
clone_snapshot 'student_snap_20260822', 'student'

4.9 Shell 操作命令速查

操作 命令格式 示例
创建表 create '表名','列族1','列族2',... create 'student','Sname','Ssex','Sage','Sdept','course'
列出所有表 list list
查看表信息 describe '表名' describe 'student'
修改表 alter '表名', {NAME=>'列族', ...} alter 'student', NAME=>'address'
添加/更新数据 put '表名','行键','列族[:列]','值' put 'student','95001','course:math','80'
查看单行 get '表名','行键' get 'student','95001'
扫描全表 scan '表名' scan 'student'
范围扫描 scan '表名',{STARTROW=>..,STOPROW=>..} scan 'student',{STARTROW=>'95001',LIMIT=>10}
删除某个数据 delete '表名','行键','列族[:列]' delete 'student','95001','Sname'
删除整行 deleteall '表名','行键' deleteall 'student','95001'
统计行数 count '表名' count 'student', CACHE=>1000
计数器自增 incr '表名','行键','列族:列',步长 incr 'counters','page1','cf:pv',1
使表无效 disable '表名' disable 'student'
使表有效 enable '表名' enable 'student'
删除表 先 disable 再 drop disable 'student'; drop 'student'
清空表 truncate '表名' truncate 'student'
判断表存在 exists '表名' exists 'student'
刷写 MemStore flush '表名' flush 'student'
大合并 major_compact '表名' major_compact 'student'
创建快照 snapshot '表名','快照名' snapshot 'student','snap1'
集群状态 status status 'simple'
退出 Shell exit exit

4.10 Java API 编程

HBase 是 Java 编写的,原生 API 也是 Java 开发的,但可以使用 Java 或其他语言调用 API 访问 HBase。首先要在工程中导入所需 jar 包:只需导入 hbase 安装目录中 lib 文件夹下的所有 jar 包,此处不用再导入 Hadoop 中的 jar 包,避免由于 Hadoop 和 HBase 版本冲突引起错误(本示例使用的 HBase 版本号为 1.1.2)。

4.10.1 Maven 依赖

<!-- HBase 1.x -->
<dependencies>
  <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>1.1.2</version>
  </dependency>
  <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-common</artifactId>
    <version>1.1.2</version>
  </dependency>
  <!-- 需要跑 MapReduce 读写 HBase 时引入 -->
  <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.1.2</version>
  </dependency>
</dependencies>
<!-- HBase 2.x:推荐使用 shaded 客户端,避免与 Hadoop 的 guava/protobuf 冲突 -->
<dependencies>
  <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-shaded-client</artifactId>
    <version>2.4.17</version>
  </dependency>
  <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-shaded-mapreduce</artifactId>
    <version>2.4.17</version>
  </dependency>
</dependencies>

同时把 hbase-site.xml(或至少 hbase.zookeeper.quorum)放到 classpath(src/main/resources)下。

4.10.2 Configuration / ConnectionFactory / Connection

Configuration conf = HBaseConfiguration.create();
// 方式一:靠 classpath 中的 hbase-site.xml 自动加载(推荐)
// 方式二:代码中显式指定
conf.set("hbase.zookeeper.quorum", "master,slave1,slave2");
conf.set("hbase.zookeeper.property.clientPort", "2181");
conf.set("hbase.rootdir", "hdfs://master:9000/hbase");
conf.set("hbase.client.retries.number", "3");
conf.set("hbase.rpc.timeout", "60000");
conf.set("hbase.client.operation.timeout", "120000");
conf.set("hbase.client.scanner.timeout.period", "120000");

Connection connection = ConnectionFactory.createConnection(conf);

线程安全与生命周期(非常重要):

对象 线程安全 生命周期 说明
Configuration 只读后安全 长期 创建 Connection 后不要再改
Connection 线程安全 整个应用只创建一个,重量级 内部持有 ZK 连接、meta 缓存、RPC 连接池与线程池,创建代价高,切勿每次请求 new
Table 不安全 轻量,用完即弃 从 Connection 获取,每个线程各自获取,try-with-resources 关闭
Admin 不安全 轻量 同上,只做 DDL
BufferedMutator 线程安全 可复用 异步批量写
// 推荐写法:Connection 单例 + Table/Admin 用完即关
public class HBaseUtil {
    private static final Connection CONN;
    static {
        try {
            Configuration conf = HBaseConfiguration.create();
            CONN = ConnectionFactory.createConnection(conf);
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try { CONN.close(); } catch (IOException ignored) {}
            }));
        } catch (IOException e) {
            throw new RuntimeException("init HBase connection failed", e);
        }
    }
    public static Connection getConnection() { return CONN; }
}

4.10.3 完整示例(HBase 1.x,学生信息表)

任务实例:学生信息表

创建一个学生信息表,存储学生姓名(姓名作为行键,假设姓名不重复)以及考试成绩,其中考试成绩是一个列族,分别存储各科考试成绩。表结构如下表。

表 4-1 学生信息表的表结构

name(行键) score
English Math Computer

需要添加的数据如下表。

表 4-2 需要添加的数据

name English Math Computer
zhangsan 69 86 77

主程序示例:

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;

public class Chapter4{
    public static Configuration configuration;
    public static Connection connection;
    public static Admin admin;

    public static void main(String[] args) throws IOException{
        createTable("student", new String[]{"score"});
        insertData("student","zhangsan","score","English","69");
        insertData("student","zhangsan","score","Math","86");
        insertData("student","zhangsan","score","Computer","77");
        getData("student", "zhangsan", "score", "English");
    }
    // public static void init(){……}      // 建立连接
    // public static void close(){……}     // 关闭连接
    // public static void createTable(){……}  // 创建表
    // public static void insertData(){……}   // 插入数据
    // public static void getData(){……}      // 浏览数据
}

建立连接:

// 建立连接
public static void init(){
    configuration = HBaseConfiguration.create();
    configuration.set("hbase.rootdir","hdfs://localhost:9000/hbase");
    try{
        connection = ConnectionFactory.createConnection(configuration);
        admin = connection.getAdmin();
    }catch (IOException e){
        e.printStackTrace();
    }
}

对应的 hbase-site.xml 配置:

<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://localhost:9000/hbase</value>
</property>
</configuration>

备注:单机版可使用 file:///DIRECTORY/hbase。

关闭连接:

// 关闭连接
public static void close(){
    try{
        if(admin != null){
            admin.close();
        }
        if(null != connection){
            connection.close();
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

① 创建表:

/* 创建表 */
/**
 * @param myTableName 表名
 * @param colFamily 列族数组
 * @throws Exception
 */
public static void createTable(String myTableName, String[] colFamily) throws IOException {
    TableName tableName = TableName.valueOf(myTableName);
    if(admin.tableExists(tableName)){
        System.out.println("table exists!");
    }else {
        HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);
        for(String str: colFamily){
            HColumnDescriptor hColumnDescriptor = new HColumnDescriptor(str);
            hTableDescriptor.addFamily(hColumnDescriptor);
        }
        admin.createTable(hTableDescriptor);
    }
}

运行时指定参数 myTableName 为 "student",colFamily 为 {"score"}。程序运行效果与如下 Shell 命令等效:

create 'student', 'score'

② 添加数据:

/* 添加数据 */
/**
 * @param tableName 表名
 * @param rowKey 行键
 * @param colFamily 列族
 * @param col 列限定符
 * @param val 数据
 * @throws Exception
 */
public static void insertData(String tableName, String rowKey, String colFamily, String col, String val) throws IOException {
    Table table = connection.getTable(TableName.valueOf(tableName));
    Put put = new Put(Bytes.toBytes(rowKey));
    put.addColumn(Bytes.toBytes(colFamily), Bytes.toBytes(col), Bytes.toBytes(val));
    table.put(put);
    table.close();
}

添加表 4-2 第一行数据时,运行如下 3 行代码:

insertData("student","zhangsan","score","English","69");
insertData("student","zhangsan","score","Math","86");
insertData("student","zhangsan","score","Computer","77");

等效于 HBase Shell 命令:

put 'student','zhangsan','score:English','69'
put 'student','zhangsan','score:Math','86'
put 'student','zhangsan','score:Computer','77'

③ 浏览数据:

/* 获取某单元格数据 */
/**
 * @param tableName 表名
 * @param rowKey 行键
 * @param colFamily 列族
 * @param col 列限定符
 * @throws IOException
 */
public static void getData(String tableName, String rowKey, String colFamily, String col) throws IOException {
    Table table = connection.getTable(TableName.valueOf(tableName));
    Get get = new Get(Bytes.toBytes(rowKey));
    get.addColumn(Bytes.toBytes(colFamily), Bytes.toBytes(col));
    // 获取的 result 数据是结果集,还需要格式化输出想要的数据
    Result result = table.get(get);
    System.out.println(new String(result.getValue(colFamily.getBytes(), col==null?null:col.getBytes())));
    table.close();
}

获取 "zhangsan" 在 "English" 上的数据时,指定参数 tableName 为 "student"、rowKey 为 "zhangsan"、colFamily 为 "score"、col 为 "English":

getData("student", "zhangsan", "score", "English");

等效于 HBase Shell 命令:

get 'student','zhangsan',{COLUMN=>'score:English'}

4.10.4 HBase 2.x 新 API:Admin 做 DDL

HBase 2.x 中 HTableDescriptor / HColumnDescriptor 已废弃,改用 Builder 模式。

import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.io.compress.Compression;
import org.apache.hadoop.hbase.regionserver.BloomType;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;

public class HBaseDDL {

    /** 建表:含多列族、版本数、TTL、压缩、布隆过滤器与预分区 */
    public static void createTable(Connection conn, String tableNameStr,
                                   String... families) throws IOException {
        try (Admin admin = conn.getAdmin()) {
            TableName tableName = TableName.valueOf(tableNameStr);
            if (admin.tableExists(tableName)) {
                System.out.println("table exists!");
                return;
            }
            TableDescriptorBuilder tableBuilder =
                    TableDescriptorBuilder.newBuilder(tableName);
            for (String cf : families) {
                ColumnFamilyDescriptor cfd = ColumnFamilyDescriptorBuilder
                        .newBuilder(Bytes.toBytes(cf))
                        .setMaxVersions(3)
                        .setMinVersions(1)
                        .setTimeToLive(30 * 24 * 3600)                 // TTL 30 天
                        .setCompressionType(Compression.Algorithm.SNAPPY)
                        .setBloomFilterType(BloomType.ROW)
                        .setBlocksize(64 * 1024)
                        .setBlockCacheEnabled(true)
                        .setInMemory(false)
                        .build();
                tableBuilder.setColumnFamily(cfd);
            }
            // 表级属性
            tableBuilder.setMaxFileSize(10L * 1024 * 1024 * 1024);     // 10GB
            tableBuilder.setMemStoreFlushSize(128L * 1024 * 1024);

            // 预分区:4 个分割点 → 5 个 Region
            byte[][] splitKeys = new byte[][]{
                    Bytes.toBytes("1000"), Bytes.toBytes("2000"),
                    Bytes.toBytes("3000"), Bytes.toBytes("4000")
            };
            admin.createTable(tableBuilder.build(), splitKeys);
        }
    }

    /** 列出所有表 */
    public static void listTables(Connection conn) throws IOException {
        try (Admin admin = conn.getAdmin()) {
            for (TableDescriptor td : admin.listTableDescriptors()) {
                System.out.println(td.getTableName().getNameAsString());
            }
        }
    }

    /** 新增列族 */
    public static void addFamily(Connection conn, String table, String cf)
            throws IOException {
        try (Admin admin = conn.getAdmin()) {
            admin.addColumnFamily(TableName.valueOf(table),
                    ColumnFamilyDescriptorBuilder.of(cf));
        }
    }

    /** 删除列族 */
    public static void deleteFamily(Connection conn, String table, String cf)
            throws IOException {
        try (Admin admin = conn.getAdmin()) {
            admin.deleteColumnFamily(TableName.valueOf(table), Bytes.toBytes(cf));
        }
    }

    /** 删除表:必须先 disable */
    public static void dropTable(Connection conn, String table) throws IOException {
        try (Admin admin = conn.getAdmin()) {
            TableName tn = TableName.valueOf(table);
            if (!admin.tableExists(tn)) { return; }
            if (admin.isTableEnabled(tn)) { admin.disableTable(tn); }
            admin.deleteTable(tn);
        }
    }

    /** 运维操作 */
    public static void ops(Connection conn, String table) throws IOException {
        try (Admin admin = conn.getAdmin()) {
            TableName tn = TableName.valueOf(table);
            admin.flush(tn);
            admin.compact(tn);
            admin.majorCompact(tn);
            admin.split(tn, Bytes.toBytes("5000"));
            admin.snapshot("snap_" + System.currentTimeMillis(), tn);
            admin.balancerSwitch(false, false);
        }
    }

    /** 命名空间 */
    public static void namespace(Connection conn) throws IOException {
        try (Admin admin = conn.getAdmin()) {
            admin.createNamespace(NamespaceDescriptor.create("ns1").build());
            for (NamespaceDescriptor nd : admin.listNamespaceDescriptors()) {
                System.out.println(nd.getName());
            }
        }
    }
}

4.10.5 Table 做 DML:Put / Get / Delete / Scan

import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.CellUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class HBaseDML {

    /** 单条写入 */
    public static void put(Connection conn, String tableName, String rowKey,
                           String cf, String col, String value) throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            Put put = new Put(Bytes.toBytes(rowKey));
            put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(col), Bytes.toBytes(value));
            // 指定时间戳(可选)
            // put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(col),
            //               System.currentTimeMillis(), Bytes.toBytes(value));
            // 关闭 WAL(提升写吞吐但宕机会丢数据,仅可容忍丢失的场景使用)
            // put.setDurability(Durability.SKIP_WAL);
            table.put(put);
        }
    }

    /** 批量写入:一次 RPC 提交多条,客户端会按 RegionServer 自动分组 */
    public static void batchPut(Connection conn, String tableName)
            throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            List<Put> puts = new ArrayList<>();
            for (int i = 0; i < 10000; i++) {
                Put put = new Put(Bytes.toBytes(String.format("row%08d", i)));
                put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"),
                              Bytes.toBytes("user" + i));
                put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"),
                              Bytes.toBytes(String.valueOf(20 + i % 40)));
                puts.add(put);
                if (puts.size() >= 1000) {      // 每 1000 条提交一次
                    table.put(puts);
                    puts.clear();
                }
            }
            if (!puts.isEmpty()) { table.put(puts); }
        }
    }

    /** 读取单行并解析 Result */
    public static void get(Connection conn, String tableName, String rowKey)
            throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            Get get = new Get(Bytes.toBytes(rowKey));
            get.addFamily(Bytes.toBytes("info"));                       // 指定列族
            get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name")); // 指定列
            get.readVersions(3);                    // 1.x:get.setMaxVersions(3)
            get.setTimeRange(0L, Long.MAX_VALUE);
            // get.setTimestamp(1724300000000L);

            Result result = table.get(get);
            if (result.isEmpty()) {
                System.out.println("row not found");
                return;
            }
            // 方式一:直接取某列最新值
            byte[] v = result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name"));
            System.out.println("name = " + Bytes.toString(v));

            // 方式二:遍历所有 Cell(用 CellUtil 解析,兼容 1.x/2.x)
            for (Cell cell : result.rawCells()) {
                System.out.printf("row=%s, family=%s, qualifier=%s, ts=%d, value=%s%n",
                        Bytes.toString(CellUtil.cloneRow(cell)),
                        Bytes.toString(CellUtil.cloneFamily(cell)),
                        Bytes.toString(CellUtil.cloneQualifier(cell)),
                        cell.getTimestamp(),
                        Bytes.toString(CellUtil.cloneValue(cell)));
            }
        }
    }

    /** 批量读取 */
    public static void batchGet(Connection conn, String tableName,
                                List<String> rowKeys) throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            List<Get> gets = new ArrayList<>();
            for (String rk : rowKeys) { gets.add(new Get(Bytes.toBytes(rk))); }
            Result[] results = table.get(gets);
            for (Result r : results) {
                if (!r.isEmpty()) {
                    System.out.println(Bytes.toString(r.getRow()));
                }
            }
        }
    }

    /** 删除 */
    public static void delete(Connection conn, String tableName, String rowKey)
            throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            // ① 删整行(DeleteFamily)
            Delete deleteRow = new Delete(Bytes.toBytes(rowKey));
            // ② 删整个列族
            // deleteRow.addFamily(Bytes.toBytes("info"));
            // ③ 删某列所有版本(DeleteColumn)—— 推荐
            // deleteRow.addColumns(Bytes.toBytes("info"), Bytes.toBytes("name"));
            // ④ 删某列最新一个版本(Delete)—— 注意与 addColumns 的区别
            // deleteRow.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
            table.delete(deleteRow);
        }
    }

    /** Scan + ResultScanner */
    public static void scan(Connection conn, String tableName) throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            Scan scan = new Scan();
            scan.withStartRow(Bytes.toBytes("row00000000"));       // 1.x: setStartRow
            scan.withStopRow(Bytes.toBytes("row00001000"));        // 1.x: setStopRow
            scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
            scan.setCaching(1000);      // 每次 RPC 取回 1000 行,减少往返
            scan.setBatch(100);         // 每次返回最多 100 列,宽行必设
            scan.setMaxResultSize(2 * 1024 * 1024);
            scan.setCacheBlocks(false); // 全表扫描时关闭 BlockCache,避免冲刷热数据
            scan.readVersions(1);
            scan.setReversed(false);

            try (ResultScanner scanner = table.getScanner(scan)) {
                for (Result result : scanner) {   // 迭代过程中才真正拉数据
                    System.out.println(Bytes.toString(result.getRow()) + " -> " +
                        Bytes.toString(result.getValue(
                            Bytes.toBytes("info"), Bytes.toBytes("name"))));
                }
            }   // ResultScanner 必须关闭,否则 RegionServer 端 scanner 泄漏
        }
    }

    /** 计数器(原子自增) */
    public static void increment(Connection conn, String tableName)
            throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            // 简写
            long v = table.incrementColumnValue(Bytes.toBytes("page1"),
                    Bytes.toBytes("cf"), Bytes.toBytes("pv"), 1L);
            System.out.println("pv = " + v);
            // 完整写法,可一次自增多列
            Increment inc = new Increment(Bytes.toBytes("page1"));
            inc.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("pv"), 1L);
            inc.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("uv"), 1L);
            table.increment(inc);
        }
    }

    /** CheckAndPut:乐观并发控制(单行原子) */
    public static void checkAndPut(Connection conn, String tableName)
            throws IOException {
        try (Table table = conn.getTable(TableName.valueOf(tableName))) {
            Put put = new Put(Bytes.toBytes("row1"));
            put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("status"),
                          Bytes.toBytes("PAID"));
            boolean ok = table.checkAndMutate(Bytes.toBytes("row1"),
                        Bytes.toBytes("info"))
                    .qualifier(Bytes.toBytes("status"))
                    .ifEquals(Bytes.toBytes("UNPAID"))
                    .thenPut(put);
            System.out.println("cas result = " + ok);
        }
    }
}

4.10.6 BufferedMutator 异步批量写

BufferedMutator 在客户端维护写缓冲区,攒够一定大小才发送,是高吞吐写入的首选(取代已废弃的 HTable.setAutoFlush(false))。

import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

public class HBaseBufferedWrite {
    public static void write(Connection conn, String tableName) throws Exception {
        BufferedMutatorParams params =
                new BufferedMutatorParams(TableName.valueOf(tableName))
                .writeBufferSize(8 * 1024 * 1024)            // 8MB 缓冲
                .listener((e, mutator) -> {                   // 异步失败回调
                    for (int i = 0; i < e.getNumExceptions(); i++) {
                        System.err.println("failed row: " +
                            Bytes.toString(e.getRow(i).getRow()));
                    }
                });

        try (BufferedMutator mutator = conn.getBufferedMutator(params)) {
            for (int i = 0; i < 1_000_000; i++) {
                Put put = new Put(Bytes.toBytes(String.format("row%08d", i)));
                put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("v"),
                              Bytes.toBytes("value" + i));
                mutator.mutate(put);                         // 只入缓冲,不立即发送
                if (i % 100000 == 0) { mutator.flush(); }     // 手工强制发送
            }
            mutator.flush();                                 // close() 也会自动 flush
        }
    }
}

4.10.7 过滤器的 Java 写法

import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.filter.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.CompareOperator;   // 2.x;1.x 用 CompareFilter.CompareOp

public class HBaseFilterDemo {
    public static Scan buildScan() {
        Scan scan = new Scan();

        // 行键前缀
        Filter prefix = new PrefixFilter(Bytes.toBytes("9500"));

        // 行键比较(2.x)
        Filter rowFilter = new RowFilter(CompareOperator.GREATER_OR_EQUAL,
                new BinaryComparator(Bytes.toBytes("95001")));
        // 1.x 写法:
        // Filter rowFilter = new RowFilter(CompareFilter.CompareOp.GREATER_OR_EQUAL,
        //         new BinaryComparator(Bytes.toBytes("95001")));

        // 行键正则
        Filter regexRow = new RowFilter(CompareOperator.EQUAL,
                new RegexStringComparator("^9500[1-5]$"));

        // 值过滤
        Filter valueFilter = new ValueFilter(CompareOperator.EQUAL,
                new SubstringComparator("LiYing"));

        // 单列值过滤整行(最常用)
        SingleColumnValueFilter scvf = new SingleColumnValueFilter(
                Bytes.toBytes("course"), Bytes.toBytes("math"),
                CompareOperator.GREATER_OR_EQUAL, Bytes.toBytes("80"));
        scvf.setFilterIfMissing(true);      // 没有该列的行直接过滤掉
        scvf.setLatestVersionOnly(true);

        // 列限定符前缀
        Filter colPrefix = new ColumnPrefixFilter(Bytes.toBytes("ma"));
        Filter multiColPrefix = new MultipleColumnPrefixFilter(
                new byte[][]{Bytes.toBytes("ma"), Bytes.toBytes("en")});

        // 其他
        Filter keyOnly   = new KeyOnlyFilter();
        Filter firstKey  = new FirstKeyOnlyFilter();
        Filter page      = new PageFilter(100);
        Filter colCount  = new ColumnCountGetFilter(5);
        Filter qualifier = new QualifierFilter(CompareOperator.EQUAL,
                new BinaryComparator(Bytes.toBytes("math")));
        Filter family    = new FamilyFilter(CompareOperator.EQUAL,
                new BinaryComparator(Bytes.toBytes("course")));

        // FilterList 组合:MUST_PASS_ALL = AND,MUST_PASS_ONE = OR
        FilterList andList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
        andList.addFilter(prefix);
        andList.addFilter(scvf);

        FilterList orList = new FilterList(FilterList.Operator.MUST_PASS_ONE);
        orList.addFilter(valueFilter);
        orList.addFilter(colPrefix);

        // 嵌套组合
        FilterList root = new FilterList(FilterList.Operator.MUST_PASS_ALL);
        root.addFilter(andList);
        root.addFilter(orList);

        scan.setFilter(root);
        return scan;
    }
}

4.10.8 完整可运行示例(HBase 2.x 版)

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;

public class HBaseDemo {

    private static Connection connection;

    public static void init() throws IOException {
        Configuration conf = HBaseConfiguration.create();
        conf.set("hbase.zookeeper.quorum", "master,slave1,slave2");
        conf.set("hbase.zookeeper.property.clientPort", "2181");
        connection = ConnectionFactory.createConnection(conf);
    }

    public static void close() throws IOException {
        if (connection != null && !connection.isClosed()) {
            connection.close();
        }
    }

    public static void main(String[] args) throws IOException {
        init();
        try {
            TableName tn = TableName.valueOf("student");
            // 1. 建表
            try (Admin admin = connection.getAdmin()) {
                if (!admin.tableExists(tn)) {
                    admin.createTable(TableDescriptorBuilder.newBuilder(tn)
                        .setColumnFamily(ColumnFamilyDescriptorBuilder
                            .newBuilder(Bytes.toBytes("score"))
                            .setMaxVersions(3).build())
                        .build());
                    System.out.println("table created");
                }
            }
            // 2. 写入
            try (Table table = connection.getTable(tn)) {
                Put p = new Put(Bytes.toBytes("zhangsan"));
                p.addColumn(Bytes.toBytes("score"), Bytes.toBytes("English"),
                            Bytes.toBytes("69"));
                p.addColumn(Bytes.toBytes("score"), Bytes.toBytes("Math"),
                            Bytes.toBytes("86"));
                p.addColumn(Bytes.toBytes("score"), Bytes.toBytes("Computer"),
                            Bytes.toBytes("77"));
                table.put(p);

                // 3. 读取
                Result r = table.get(new Get(Bytes.toBytes("zhangsan")));
                for (Cell c : r.rawCells()) {
                    System.out.printf("%s:%s = %s (ts=%d)%n",
                        Bytes.toString(CellUtil.cloneFamily(c)),
                        Bytes.toString(CellUtil.cloneQualifier(c)),
                        Bytes.toString(CellUtil.cloneValue(c)),
                        c.getTimestamp());
                }

                // 4. 扫描
                try (ResultScanner scanner =
                         table.getScanner(new Scan().setCaching(100))) {
                    for (Result res : scanner) {
                        System.out.println("row: " + Bytes.toString(res.getRow()));
                    }
                }

                // 5. 删除
                table.delete(new Delete(Bytes.toBytes("zhangsan")));
            }
        } finally {
            close();     // Connection 必须关闭,否则 JVM 不退出
        }
    }
}

资源关闭要点:Table、Admin、ResultScanner、BufferedMutator 都实现了 Closeable,务必用 try-with-resources;Connection 在应用退出时关闭一次即可。忘记关闭 ResultScanner 会在 RegionServer 端遗留 scanner,导致 ScannerTimeoutException 与内存泄漏。

4.11 在 HBase 之上构建 SQL 引擎

NoSQL 区别于关系型数据库的一点是不使用 SQL 作为查询语言。在 HBase 上提供 SQL 接口的原因:

  1. 易使用:使用 SQL 这样易于理解的语言,使人们更轻松地使用 HBase;
  2. 减少编码:使用 SQL 这样更高层次的语言编写,减少代码量。

方案:

1. Hive 整合 HBase

Hive 与 HBase 的整合功能从 Hive 0.6.0 版本开始出现,利用两者对外的 API 接口互相通信,通信主要依靠 hive_hbase-handler.jar 工具包(Hive Storage Handlers)。由于 HBase 有过一次较大的版本变动,并非每个版本的 Hive 都能与现有 HBase 版本整合,使用时需特别注意两者版本的一致性。

-- Hive 中创建映射到 HBase 的外部表
CREATE EXTERNAL TABLE hive_student (
  rowkey   STRING,
  name     STRING,
  math     INT,
  english  INT
)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES (
  "hbase.columns.mapping" = ":key,info:name,course:math,course:english"
)
TBLPROPERTIES ("hbase.table.name" = "student");

-- 之后即可用 HQL 查询/关联 HBase 数据
SELECT rowkey, name, math FROM hive_student WHERE math >= 80;

-- Hive 内部表(由 Hive 创建 HBase 表,drop 时 HBase 表一起被删)
CREATE TABLE hive_managed (key INT, value STRING)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,cf:val");

启动 Hive 时需带上 HBase 依赖与 ZK 地址:

hive --auxpath $HIVE_HOME/lib/hive-hbase-handler-x.y.z.jar,$HBASE_HOME/lib/hbase-client-x.y.z.jar \
     -hiveconf hbase.zookeeper.quorum=master,slave1,slave2

2. Phoenix

Phoenix 由 Salesforce.com 开源,是构建在 Apache HBase 之上的一个 SQL 中间层,可以让开发者在 HBase 上执行 SQL 查询。它把 SQL 编译成一系列 HBase scan,并利用协处理器把聚合下推到 RegionServer,还提供二级索引、JDBC 驱动、盐化预分区、UPSERT 等能力。

# 安装:把 phoenix-server jar 放入每个 RegionServer 的 $HBASE_HOME/lib 并重启
cp phoenix-server-hbase-2.4-5.1.3.jar $HBASE_HOME/lib/

# 命令行客户端
sqlline.py master:2181
-- 建表(自动映射为 HBase 表,SALT_BUCKETS 即加盐预分区)
CREATE TABLE student (
  id      VARCHAR PRIMARY KEY,
  name    VARCHAR,
  math    INTEGER,
  english INTEGER
) SALT_BUCKETS = 16;

UPSERT INTO student VALUES ('95001', 'LiYing', 80, 69);
SELECT * FROM student WHERE math > 70 ORDER BY math DESC;
SELECT COUNT(*) FROM student;

-- 二级索引
CREATE INDEX idx_math ON student(math) INCLUDE(name);
-- 映射已有 HBase 表
CREATE VIEW "student" (pk VARCHAR PRIMARY KEY, "info"."name" VARCHAR);

版本要求:Phoenix 4.x 对应 HBase 1.x,Phoenix 5.x 对应 HBase 2.x,不可跨大版本混用。

4.12 构建 HBase 二级索引

HBase 只有一个针对行键的索引。访问 HBase 表中的行只有三种方式:通过单个行键访问、通过一个行键区间访问、全表扫描。

可使用其他产品为 HBase 行键提供索引功能:Hindex 二级索引、HBase+Redis、HBase+Solr。其原理是采用 HBase 0.92 版本之后引入的 Coprocessor 特性。

Coprocessor 构建二级索引

Coprocessor 提供两个实现:endpoint 和 observer。endpoint 相当于关系型数据库的存储过程,observer 则相当于触发器。observer 允许在记录 put 前后做一些处理,因此可在插入数据时同步写入索引。

  • 缺点:每插入一条数据需要向索引表插入数据,耗时双倍,对 HBase 集群压力也是双倍;
  • 优点:非侵入性,引擎构建在 HBase 之上,既没有对 HBase 进行任何改动,也不需要上层应用做任何妥协。

Hindex 二级索引

Hindex 是华为公司开发的纯 Java 编写的 HBase 二级索引,兼容 Apache HBase 0.94.8,特性包括:多个表索引、多个列索引、基于部分列值的索引。

HBase+Redis

Redis+HBase 方案:采用 Coprocessor 构建二级索引,Redis 做客户端缓存,将索引实时更新到 Redis 等 KV 系统中,定时从 KV 更新索引到 HBase 的索引表。

Solr+HBase

Solr 是一个高性能、采用 Java5 开发、基于 Lucene 的全文搜索服务器,同时进行了扩展,提供了比 Lucene 更丰富的查询语言,实现了可配置、可扩展并对查询性能进行了优化,提供了完善的功能管理界面,是一款优秀的全文搜索引擎。Solr 保存索引,根据 RowKey 查询数据。

现代等价方案:HBase + Elasticsearch(ES 存索引字段与 RowKey,明细存 HBase)、Phoenix 全局/本地索引、Lily HBase Indexer / Cloudera Search(通过复制机制把变更同步到 Solr)。

4.13 与 MapReduce / Spark / Sqoop 集成

4.13.1 MapReduce 读写 HBase

# 让 MapReduce 作业能找到 HBase 依赖
export HADOOP_CLASSPATH=$(hbase classpath)
hadoop jar myjob.jar com.demo.HBaseMRJob
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.Job;
import java.io.IOException;

public class HBaseMRJob {

    /** TableMapper:从 HBase 表读,输入即 (RowKey, Result) */
    public static class ReadMapper
            extends TableMapper<Text, IntWritable> {
        @Override
        protected void map(ImmutableBytesWritable key, Result value, Context ctx)
                throws IOException, InterruptedException {
            byte[] v = value.getValue(Bytes.toBytes("course"), Bytes.toBytes("math"));
            if (v != null) {
                ctx.write(new Text("math"),
                          new IntWritable(Integer.parseInt(Bytes.toString(v))));
            }
        }
    }

    /** TableReducer:把结果写回 HBase 表 */
    public static class WriteReducer
            extends TableReducer<Text, IntWritable, ImmutableBytesWritable> {
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context ctx)
                throws IOException, InterruptedException {
            int sum = 0, cnt = 0;
            for (IntWritable v : values) { sum += v.get(); cnt++; }
            Put put = new Put(Bytes.toBytes(key.toString()));
            put.addColumn(Bytes.toBytes("stat"), Bytes.toBytes("avg"),
                    Bytes.toBytes(String.valueOf(cnt == 0 ? 0 : sum / cnt)));
            ctx.write(null, put);
        }
    }

    public static void main(String[] args) throws Exception {
        Configuration conf = HBaseConfiguration.create();
        Job job = Job.getInstance(conf, "hbase-mr-demo");
        job.setJarByClass(HBaseMRJob.class);

        Scan scan = new Scan();
        scan.setCaching(500);
        scan.setCacheBlocks(false);     // MR 全表扫描必须关闭 BlockCache
        scan.addFamily(Bytes.toBytes("course"));

        // 输入:HBase 表(每个 Region 对应一个 map task)
        TableMapReduceUtil.initTableMapperJob(
                "student", scan, ReadMapper.class,
                Text.class, IntWritable.class, job);
        // 输出:HBase 表
        TableMapReduceUtil.initTableReducerJob(
                "student_stat", WriteReducer.class, job);

        job.setNumReduceTasks(1);
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}

内置常用 MR 工具:

# 统计行数
hbase org.apache.hadoop.hbase.mapreduce.RowCounter 'student'
# 复制表(同集群或跨集群)
hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=student_bak 'student'

4.13.2 Spark 读写 HBase

// 方式一:newAPIHadoopRDD(通用,适用各版本)
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.client.Result
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.util.Bytes

val conf = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum", "master,slave1,slave2")
conf.set(TableInputFormat.INPUT_TABLE, "student")

val rdd = sc.newAPIHadoopRDD(conf, classOf[TableInputFormat],
  classOf[ImmutableBytesWritable], classOf[Result])

rdd.map { case (_, result) =>
  (Bytes.toString(result.getRow),
   Bytes.toString(result.getValue(Bytes.toBytes("course"), Bytes.toBytes("math"))))
}.collect().foreach(println)

// 方式二:hbase-spark connector(DataFrame API)
// spark.read.format("org.apache.hadoop.hbase.spark")
//   .option("hbase.columns.mapping", "rk STRING :key, math STRING course:math")
//   .option("hbase.table", "student").load()

// 写入:foreachPartition 内建立 Connection,避免序列化问题与连接风暴
rdd.foreachPartition { part =>
  val conn = ConnectionFactory.createConnection(HBaseConfiguration.create())
  val table = conn.getTable(TableName.valueOf("student_out"))
  part.foreach { r => /* table.put(...) */ }
  table.close(); conn.close()
}

详见 16-08-Spark与流计算.md

4.13.3 Sqoop 导入

# MySQL → HBase
sqoop import \
  --connect jdbc:mysql://192.168.1.100:3306/testdb \
  --username root --password 123456 \
  --table student \
  --hbase-table student \
  --column-family info \
  --hbase-row-key id \
  --hbase-create-table \
  --split-by id -m 4

注意:Sqoop 1.4.x 官方只支持 HBase 1.x;HBase 2.x 环境建议改用"Sqoop 导到 HDFS → importtsv/BulkLoad 入 HBase"两步法。

4.14 批量导入:importtsv 与 BulkLoad

为什么 BulkLoad 比 put 快:普通 put 走完整写路径(客户端 RPC → RegionServer → 写 WAL → 写 MemStore → flush → compaction),每条数据要经过多次内存拷贝、日志落盘与后台合并,RegionServer 的 CPU、内存、IO 与 GC 都成为瓶颈。BulkLoad 用 MapReduce 直接在 HDFS 上生成好 HFile 文件,然后只需把文件"挪"(HDFS rename/move)到表的 Store 目录并通知 RegionServer 加载:

  • 完全绕过 WAL 与 MemStore,没有日志落盘、没有 flush;
  • 不产生 compaction 风暴,也几乎不占用 RegionServer 的 handler 线程;
  • 导入过程对在线读写影响极小,速度通常是 put 的 数倍到数十倍;
  • 代价:需要经过 MR 排序阶段,只适合离线批量导入(TB 级历史数据初始化、每日全量刷新);且不参与 Replication(HBase 1.3+ 可配 hbase.replication.bulkload.enabled 支持)。

步骤一:importtsv 生成 HFile

# 准备数据文件(默认分隔符为 \t,第一列为 rowkey)
cat > /tmp/student.tsv <<'EOF'
95001   LiYing  F   22  CS  80
95002   ZhangSan    M   21  MA  90
EOF
hdfs dfs -mkdir -p /input/student
hdfs dfs -put /tmp/student.tsv /input/student/

# 建目标表(务必预分区,Region 数决定 reduce 数与并行度)
echo "create 'student_bulk', 'info', 'course'" | hbase shell

# 生成 HFile(-Dimporttsv.bulk.output 指定输出目录)
hbase org.apache.hadoop.hbase.mapreduce.ImportTsv \
  -Dimporttsv.separator='\t' \
  -Dimporttsv.columns='HBASE_ROW_KEY,info:name,info:sex,info:age,info:dept,course:math' \
  -Dimporttsv.bulk.output=/output/student_hfile \
  -Dimporttsv.skip.bad.lines=true \
  student_bulk /input/student

# 说明:若不指定 -Dimporttsv.bulk.output,importtsv 会退化为直接 put 写入
#      (走完整写路径,慢,但省一步 completebulkload)

步骤二:completebulkload 加载

# HBase 1.x
hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles \
  /output/student_hfile student_bulk

# HBase 2.x(新类名,旧名仍兼容)
hbase org.apache.hadoop.hbase.tool.LoadIncrementalHFiles \
  /output/student_hfile student_bulk
# 或使用别名
hbase completebulkload /output/student_hfile student_bulk

# 加载后验证
echo "count 'student_bulk'" | hbase shell

注意事项:

  • 生成 HFile 的 MR 作业的 reduce 数 = 目标表的 Region 数,所以必须先预分区,否则单 reduce 极慢;
  • HFile 输出目录必须与 HBase 在同一个 HDFS,且运行用户对 /hbase 有写权限(否则 completebulkload 会退化为逐条复制);
  • 加载后原输出目录被移空;
  • 自定义格式的数据可自行写 MR,用 HFileOutputFormat2.configureIncrementalLoad(job, table, regionLocator) 生成 HFile。
// 自定义 MR 生成 HFile 的关键配置
Job job = Job.getInstance(conf, "bulkload");
job.setMapOutputKeyClass(ImmutableBytesWritable.class);
job.setMapOutputValueClass(KeyValue.class);   // 或 Put
HFileOutputFormat2.configureIncrementalLoad(job,
        conn.getTable(tableName), conn.getRegionLocator(tableName));
FileOutputFormat.setOutputPath(job, new Path("/output/hfile"));
job.waitForCompletion(true);
// 随后调用 LoadIncrementalHFiles 加载

五、运维、优化与排错

5.1 备份与迁移

5.1.1 Export / Import

基于 MapReduce,把表数据导成 SequenceFile 存到 HDFS,再导回。支持按时间范围做增量。

# 全量导出
hbase org.apache.hadoop.hbase.mapreduce.Export \
  student /backup/student_20260822

# 导出并指定版本数、时间范围(增量备份)、压缩
hbase org.apache.hadoop.hbase.mapreduce.Export \
  -D mapreduce.output.fileoutputformat.compress=true \
  -D mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec \
  student /backup/student_inc 1 1724300000000 1724400000000
#         表名  输出路径          版本数  起始时间戳     结束时间戳

# 导入(目标表必须已存在且结构一致)
hbase org.apache.hadoop.hbase.mapreduce.Import \
  student /backup/student_20260822

# 导入时改表名 / 改列族映射
hbase org.apache.hadoop.hbase.mapreduce.Import \
  -Dhbase.mapreduce.inputtable=student \
  student_new /backup/student_20260822

# Import 改为生成 HFile 再 BulkLoad(大数据量更快)
hbase org.apache.hadoop.hbase.mapreduce.Import \
  -Dimport.bulk.output=/output/hfile student /backup/student_20260822
hbase completebulkload /output/hfile student

5.1.2 CopyTable

在线把一张表的数据复制到同集群的另一张表或另一个集群的同名表,走正常 put 路径。

# 同集群改名复制
hbase org.apache.hadoop.hbase.mapreduce.CopyTable \
  --new.name=student_bak student

# 跨集群复制(目标集群的 ZK 地址)
hbase org.apache.hadoop.hbase.mapreduce.CopyTable \
  --peer.adr=target-zk1,target-zk2:2181:/hbase \
  --new.name=student student

# 按时间范围与列族增量复制
hbase org.apache.hadoop.hbase.mapreduce.CopyTable \
  --starttime=1724300000000 --endtime=1724400000000 \
  --families=info,course \
  --peer.adr=target-zk1:2181:/hbase student

5.1.3 快照方式

见 4.8 节。创建瞬间完成、几乎零开销,配合 ExportSnapshot 是整表迁移的首选。

snapshot 'student', 'snap_20260822'
# 跨集群
hbase org.apache.hadoop.hbase.snapshot.ExportSnapshot \
  -snapshot snap_20260822 -copy-to hdfs://target-nn:9000/hbase -mappers 16
# 目标集群
clone_snapshot 'snap_20260822', 'student'

5.1.4 distcp 拷贝 HDFS 目录

直接在 HDFS 层面拷贝 /hbase 目录(必须停机或至少 flush + disable 表,否则文件不一致)。

# 先 flush 并 disable,保证 MemStore 数据落盘
echo "flush 'student'; disable 'student'" | hbase shell

hadoop distcp -m 20 -bandwidth 100 \
  hdfs://source-nn:9000/hbase/data/default/student \
  hdfs://target-nn:9000/hbase/data/default/student

# 目标集群修复 meta,让新目录被识别
hbase hbck -fixMeta -fixAssignments               # 1.x
hbase hbck -j hbase-hbck2.jar addFsRegionsMissingInMeta default:student   # 2.x
echo "enable 'student'" | hbase shell

5.1.5 各方式对比与选择

方式 是否停机 是否走 RS 速度 支持增量 跨集群 适用场景
Snapshot + ExportSnapshot 否 否(直搬 HFile) 最快 否(整表) 是 整表备份/迁移的首选
Export / Import 否 是 慢 是(时间范围) 是(分两步) 增量备份、跨版本迁移
CopyTable 否 是 慢 是(时间范围) 是 表改名、小表同步
distcp 需 disable/停机 否 快 否 是 整集群迁移、灾备同步
Replication 否 是 准实时 持续 是 异地灾备、双活
BulkLoad 否 否 最快 是(按批) 是 离线批量导入

选择建议:日常定期备份用 Snapshot(保留最近 N 份)+ 每日 Export 增量;机房搬迁用 ExportSnapshot 或 distcp;灾备用 Replication;误操作恢复用 clone_snapshot。

5.2 监控

5.2.1 Web UI 页面解读

HBase 性能监视可使用以下工具:Master-status(自带)、Ganglia、OpenTSDB、Ambari。

  • Master-status:HBase Master 默认基于 Web 的 UI 服务端口为 16010(HBase 0.9x 为 60010),Region 服务器默认端口为 16030(旧版 60030)。若 Master 运行在主机 master.foo.com 上,主页地址为 http://master.foo.com:16010,可通过浏览器查看 HBase 集群当前状态。
  • Ganglia:UC Berkeley 发起的开源集群监视项目,用于监控系统性能。
  • OpenTSDB:可从大规模集群(包括网络设备、操作系统、应用程序)中获取相应 metrics 并进行存储、索引和服务,使数据更易理解(如 Web 化、图形化)。它本身就是构建在 HBase 之上的时序数据库。
  • Ambari:作用是创建、管理、监视 Hadoop 集群。
  • 现代常用:Prometheus(jmx_exporter 抓取)+ Grafana 看板、Cloudera Manager。

Master UI(:16010)各页面:

页面/区块 关键信息
Home / Region Servers 每台 RS 的 Region 数、请求数(Requests/s)、堆使用、MemStore 大小、StoreFile 数与大小、compaction 队列 —— 判断负载是否均衡的第一入口
Backup Masters 备用 Master 是否在线(HA 是否有效)
Dead Region Servers 已宕机的 RS 列表
Tables 表列表、每表 Region 数、Online/Offline Region 数、hbase:meta 状态
Table Details 该表所有 Region 的 startKey/endKey、所在 RS、大小、请求数 —— 定位热点 Region 的关键页面
Regions in Transition (RIT) 正在迁移/分裂/上线的 Region。长时间不消失即为故障
Tasks / Procedures 正在执行的 Procedure(2.x),排查卡住的 DDL
Software Attributes HBase 版本、Hadoop 版本、ZK quorum、rootdir、编译信息
Log Level / Logs 在线调整日志级别、查看日志

RegionServer UI(:16030)各页面:

区块 关键信息
Server Metrics → Base Stats 请求数、Region 数、平均负载
Memory 堆使用、MemStore 大小、BlockCache 大小与命中率(Hit Ratio 应 > 90%)
Requests 读/写请求速率、RPC 队列长度
Block Cache 缓存块数、命中率、驱逐数(Evicted)—— 驱逐多说明缓存不足
Regions 每个 Region 的读写请求数、StoreFile 数、MemStore 大小、compaction 进度
Compactions compaction 队列长度与进度

5.2.2 JMX 指标

# 全部指标(JSON)
curl -s http://master:16010/jmx | less
curl -s http://slave1:16030/jmx | less
# 按 bean 过滤
curl -s 'http://slave1:16030/jmx?qry=Hadoop:service=HBase,name=RegionServer,sub=Server'
curl -s 'http://slave1:16030/jmx?qry=java.lang:type=Memory'
curl -s 'http://slave1:16030/jmx?qry=java.lang:type=GarbageCollector,name=*'

开启 metrics 输出到文件/Ganglia:

vim $HBASE_HOME/conf/hadoop-metrics2-hbase.properties
# 输出到文件
hbase.sink.file.class=org.apache.hadoop.metrics2.sink.FileSink
hbase.sink.file.filename=hbase-metrics.out
*.period=10
# 输出到 Ganglia
# hbase.sink.ganglia.class=org.apache.hadoop.metrics2.sink.ganglia.GangliaSink31
# hbase.sink.ganglia.servers=ganglia-host:8649

5.2.3 关键监控项与告警阈值

指标 含义 建议阈值 / 关注点
单 RS Region 数 numRegions 100~200 正常;> 300 需扩容或合并 Region;单 RS 与其他 RS 相差 > 20% 说明不均衡
MemStore 总大小 memStoreSize 接近 global.memstore.size × 堆 时会触发强制 flush 甚至阻塞写入
compaction 队列长度 compactionQueueLength 持续 > 10 说明合并跟不上写入,写放大严重,需调参或扩容
flush 队列长度 flushQueueLength 持续 > 0 说明 flush 积压
StoreFile 数 storeFileCount 单 Store > blockingStoreFiles(16) 会阻塞写入
RPC 延迟 Get_99th_percentile、Mutate_99th_percentile、Scan_next_99th_percentile P99 读 < 100ms、写 < 50ms 为健康;突增即报警
RPC 队列 numCallsInGeneralQueue、numActiveHandler 队列持续非空说明 handler 不足或有慢查询
BlockCache 命中率 blockCacheHitRatio > 90% 良好;< 70% 需增大缓存或优化访问模式
BlockCache 驱逐数 blockCacheEvictionCount 快速增长说明缓存不足
GC GcTimeMillis、Full GC 次数与耗时 单次 Young GC < 100ms;Full GC 应为 0;Full GC > ZK session timeout 会导致 RS 被判死
堆使用率 memHeapUsedM / memHeapMaxM 持续 > 85% 危险
WAL 数量 hlogFileCount 过多说明 flush 落后,会触发强制 flush
慢查询日志 日志中的 responseTooSlow / (operationTooSlow) 定位具体慢 RPC 与业务 SQL
HDFS 剩余空间 dfsadmin -report 使用率 > 80% 告警,> 90% 严重(HBase 会不可写)
死亡 RS 数 status 中的 dead servers 应为 0
# 常用巡检脚本片段
echo "status 'simple'" | hbase shell 2>/dev/null | grep -E 'requestsPerSecond|numberOfOnlineRegions'
hdfs dfsadmin -report | head -20
hbase canary -t 60000                       # 遍历每个 Region 做读探测
jstat -gcutil $(pgrep -f HRegionServer) 1000 10

5.3 性能优化

5.3.1 表设计层面

优化点 做法与经验值
列族数量 1 个最佳,不超过 2~3 个。原因见 1.8.1:flush/compaction 以 Region 为单位,多列族导致小文件与内存碎片
列名与列族名长度 每条 KeyValue 都重复存储列族名与列限定符,务必极短(如 cf、i、n)。100 亿 cell 上,列名从 10 字节缩到 2 字节可省近 100GB
RowKey 设计 见 1.9:唯一、短(10~100 字节)、散列避免热点、按查询模式设计、数字补零
预分区 建表即预分区(见 1.9.6),分区数 = RS 数 × 2~5 或按容量估算
TTL 有生命周期的数据一律设 TTL,让过期数据自动清理,避免手工删除产生大量墓碑
版本数 不需要多版本时设 VERSIONS => 1,可显著减少存储与读放大
压缩算法 SNAPPY(默认推荐,压缩比 ~2-3x,CPU 开销低);ZSTD/GZ(压缩比更高,CPU 更贵,适合冷数据);LZ4(最快);NONE 只在 CPU 极紧张时用。压缩节省的 IO 通常远大于 CPU 开销
DATA_BLOCK_ENCODING RowKey 相似度高时用 FAST_DIFF,可在块内做前缀差分编码,进一步省 30%+ 空间且加速缓存
宽表 vs 高表 HBase 中"高表"(行多列少)通常优于"宽表":单行不宜过大(建议 < 10MB),过大的行无法被 split,且 get 一行会读入巨量数据
# 典型的生产建表语句
create 'ns1:user_event',
  {NAME => 'd', VERSIONS => 1, TTL => 7776000,
   COMPRESSION => 'SNAPPY', DATA_BLOCK_ENCODING => 'FAST_DIFF',
   BLOOMFILTER => 'ROW', BLOCKSIZE => 65536},
  {NUMREGIONS => 32, SPLITALGO => 'HexStringSplit'},
  {MAX_FILESIZE => '21474836480'}

5.3.2 写入优化

优化点 做法 风险/说明
批量 put table.put(List<Put>),每批 500~2000 条 一次 RPC 承载多条,客户端自动按 RS 分组,吞吐提升数倍
BufferedMutator 见 4.10.6,writeBufferSize 设 4~8MB 异步、失败通过 listener 感知,可能丢最后一批(进程崩溃)
关闭 WAL put.setDurability(Durability.SKIP_WAL) 高风险:RegionServer 宕机时 MemStore 中未 flush 的数据永久丢失。仅适用于可重跑的离线导入、可容忍丢失的埋点。折中方案:ASYNC_WAL(异步刷盘,丢失窗口小)或 Durability.SYNC_WAL(默认)
BulkLoad 见 4.14 离线大批量导入的最优解,绕过 WAL 与 MemStore
MemStore 大小 hbase.hregion.memstore.flush.size 128MB → 256MB 减少 flush 次数与小文件;但单 RS Region 多时会占用过多内存
全局 MemStore 上限 hbase.regionserver.global.memstore.size 0.4(写多可到 0.5) 与 hfile.block.cache.size 之和 ≤ 0.8
阻塞倍数 hbase.hregion.memstore.block.multiplier 4 → 8 允许 MemStore 涨到 flush.size × 8 才阻塞写,缓冲突发写
handler 数量 hbase.regionserver.handler.count 30 → 100~150 见 5.3.4
关闭自动 major compaction hbase.hregion.majorcompaction=0 改为低峰手工执行
提高 compaction 吞吐 hbase.hstore.compaction.max=10、hbase.regionserver.thread.compaction.large/small 调大 让合并跟上写入,避免 StoreFile 堆积阻塞写
避免热点 散列 RowKey + 预分区 根本性措施
客户端重试与超时 hbase.client.retries.number=3、hbase.rpc.timeout=60000 快速失败优于长时间挂住

5.3.3 读取优化

优化点 做法 说明
BlockCache hfile.block.cache.size 0.4;读多写少可到 0.5~0.6 命中率目标 > 90%
堆外缓存 hbase.bucketcache.ioengine=offheap、hbase.bucketcache.size=16384(MB) 大缓存不进堆,规避 GC 停顿;也可用 file:/ssd/cache 走 SSD
IN_MEMORY 列族 IN_MEMORY => true 小而热的字典表优先常驻缓存
scan caching scan.setCaching(500~2000) 一次 RPC 返回多行,最有效的 scan 优化;过大会导致单次响应过大、ScannerTimeoutException
scan batch scan.setBatch(100) 限制单次返回的列数,宽行必设,防止客户端 OOM
关闭扫描缓存污染 scan.setCacheBlocks(false) 全表扫描/MR 作业必设,避免把热点数据从 BlockCache 中挤出
布隆过滤器 BLOOMFILTER => 'ROW'(点查行)或 'ROWCOL'(点查具体列) 随机点查场景可跳过大量 HFile,收益极大;ROWCOL 占用更多内存
只查需要的列 get.addColumn() / scan.addColumn(),不要裸 get(row) 精确指定列族与列可让 HBase 跳过其他 Store 与不相关的 HFile,IO 大幅下降
缩小扫描范围 优先 STARTROW/STOPROW、PrefixFilter,而不是靠 ValueFilter Filter 是"扫描后过滤",不减少 IO
批量 get table.get(List<Get>) 减少 RPC 往返
控制 HFile 数量 定期 compaction HFile 越少读放大越小
及时关闭 ResultScanner try-with-resources 否则 RS 端 scanner 泄漏

5.3.4 RegionServer 优化

堆内存与 GC

# 经验值:RegionServer 堆 16~32GB。
# 不要超过 32GB —— 超过后 JVM 关闭指针压缩(Compressed Oops),
# 实际可用内存反而下降,且 GC 停顿难以控制。
# 需要更大缓存时用堆外 BucketCache。
export HBASE_REGIONSERVER_OPTS="-Xms24g -Xmx24g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=100 \
  -XX:InitiatingHeapOccupancyPercent=65 \
  -XX:G1HeapRegionSize=32m \
  -XX:+ParallelRefProcEnabled \
  -XX:-ResizePLAB \
  -XX:MaxDirectMemorySize=16g \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/usr/local/hbase/logs \
  -Xloggc:/usr/local/hbase/logs/gc-rs.log \
  -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
  -XX:+PrintGCApplicationStoppedTime \
  -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=64m"
  • 堆 < 24GB 时也可用 CMS:-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly;
  • G1 是大堆(> 24GB)的推荐选择,MaxGCPauseMillis 设 80~150;
  • 开启 MSLAB(默认开启)减少 MemStore 内存碎片:hbase.hregion.memstore.mslab.enabled=true;
  • 关键约束:单次 Full GC 停顿若超过 zookeeper.session.timeout(默认 90s),该 RS 的 ZK session 过期,会被 Master 判定宕机并强制下线(日志中出现 regionserver.HRegionServer: STOPPED: Unhandled: org.apache.hadoop.hbase.YouAreDeadException)。这是"RegionServer 无故重启"最常见的原因。

handler 数量

<property>
  <name>hbase.regionserver.handler.count</name>
  <value>100</value>
</property>
<!-- 读写队列分离:60% 给读,其中 20% 给 scan(读多场景) -->
<property>
  <name>hbase.ioengine.handler.read.ratio</name>
  <value>0.6</value>
</property>
<property>
  <name>hbase.ipc.server.callqueue.read.ratio</name>
  <value>0.6</value>
</property>
<property>
  <name>hbase.ipc.server.callqueue.scan.ratio</name>
  <value>0.2</value>
</property>
<!-- 单个请求队列长度上限,防止内存被排队请求打满 -->
<property>
  <name>hbase.ipc.server.max.callqueue.size</name>
  <value>1073741824</value>
</property>
  • 经验值:CPU 核数 × 2 ~ 10,一般 50~150;
  • 不能盲目调大:每个 handler 处理请求时都会占用内存(尤其大 scan、大 put),handler 太多会导致内存被打满、GC 压力剧增,且上下文切换成本上升,吞吐反而下降;
  • 单请求数据量大(大 put/scan)→ handler 设小(30~50);单请求小而并发高(点查)→ handler 设大(100~200)。

Region 数量控制经验值

单 RS 建议 Region 数 = (RS 堆大小 × global.memstore.size)
                     / (memstore.flush.size × 列族数)
例:24GB 堆 × 0.4 / (128MB × 1) ≈ 76 个 Region(写满时才不触发强制 flush)

实践经验值:
- 单 RS 100~200 个 Region 为宜
- 单 Region 大小 10~20GB
- Region 过多的后果:MemStore 内存被切碎、flush 出大量小文件、
  compaction 压力大、RS 启动/恢复变慢、meta 表变大
- Region 过少的后果:并行度不足、单 Region 成为热点、split 频繁

超出时的处理:merge_region 合并小/空 Region、开启 normalizer、或增加 RegionServer 节点。

HDFS 层面:短路读取(Short-Circuit Read)

RegionServer 与 DataNode 同机时,正常读取仍要经过 DataNode 进程的 TCP socket 转发。开启短路读取后,客户端可直接读取本地磁盘文件(通过 Unix Domain Socket 传递文件描述符),减少一次数据拷贝与进程间通信,本地读性能可提升 20%~50%。

<!-- hdfs-site.xml(DataNode 与 HBase 客户端两侧都要配) -->
<property>
  <name>dfs.client.read.shortcircuit</name>
  <value>true</value>
</property>
<property>
  <name>dfs.domain.socket.path</name>
  <value>/var/lib/hadoop-hdfs/dn_socket</value>
</property>
<property>
  <name>dfs.client.read.shortcircuit.buffer.size</name>
  <value>131072</value>
</property>
<property>
  <name>dfs.client.read.shortcircuit.streams.cache.size</name>
  <value>4096</value>
</property>
<!-- 允许读取正在写入的块(HBase 需要) -->
<property>
  <name>dfs.client.read.shortcircuit.skip.checksum</name>
  <value>false</value>
</property>
<!-- Hedged Read:慢节点时并发向副本发请求,取最快返回 -->
<property>
  <name>dfs.client.hedged.read.threadpool.size</name>
  <value>20</value>
</property>
<property>
  <name>dfs.client.hedged.read.threshold.millis</name>
  <value>50</value>
</property>
# 创建 socket 目录并授权
mkdir -p /var/lib/hadoop-hdfs && chown hdfs:hadoop /var/lib/hadoop-hdfs
chmod 750 /var/lib/hadoop-hdfs
# 重启 DataNode 与 RegionServer 生效

其他 HDFS 侧优化:dfs.datanode.max.transfer.threads=8192、副本数保持 3、避免 DataNode 磁盘写满、定期 hdfs balancer 平衡数据、开启 HDFS 机架感知以保证 HBase 数据本地性。

数据本地性(Locality)

# Web UI 的 Region 列表中有 Locality 列,理想值为 1.0
# 本地性下降的原因:Region 迁移、RS 重启、balancer 搬迁
# 恢复手段:执行 major compaction(会把数据重写到本地节点)
major_compact 'student'

5.4 安全简述

5.4.1 Kerberos 认证

HBase 默认无认证,任何能连上 ZooKeeper 的客户端都可读写。生产环境应启用 Kerberos。

<!-- hbase-site.xml -->
<property>
  <name>hbase.security.authentication</name>
  <value>kerberos</value>
</property>
<property>
  <name>hbase.security.authorization</name>
  <value>true</value>
</property>
<property>
  <name>hbase.master.kerberos.principal</name>
  <value>hbase/_HOST@EXAMPLE.COM</value>
</property>
<property>
  <name>hbase.master.keytab.file</name>
  <value>/etc/security/keytabs/hbase.service.keytab</value>
</property>
<property>
  <name>hbase.regionserver.kerberos.principal</name>
  <value>hbase/_HOST@EXAMPLE.COM</value>
</property>
<property>
  <name>hbase.regionserver.keytab.file</name>
  <value>/etc/security/keytabs/hbase.service.keytab</value>
</property>
<property>
  <name>hbase.rpc.protection</name>
  <value>privacy</value>   <!-- authentication / integrity / privacy -->
</property>
<!-- 启用 ACL 协处理器 -->
<property>
  <name>hbase.coprocessor.master.classes</name>
  <value>org.apache.hadoop.hbase.security.access.AccessController</value>
</property>
<property>
  <name>hbase.coprocessor.region.classes</name>
  <value>org.apache.hadoop.hbase.security.access.AccessController,org.apache.hadoop.hbase.security.token.TokenProvider</value>
</property>
<property>
  <name>hbase.coprocessor.regionserver.classes</name>
  <value>org.apache.hadoop.hbase.security.access.AccessController</value>
</property>
# 客户端使用前先获取票据
kinit -kt /etc/security/keytabs/hbase.headless.keytab hbase@EXAMPLE.COM
klist
hbase shell
// Java 客户端 Kerberos 登录
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.security.authentication", "kerberos");
conf.set("hadoop.security.authentication", "kerberos");
UserGroupInformation.setConfiguration(conf);
UserGroupInformation.loginUserFromKeytab("app@EXAMPLE.COM", "/etc/app.keytab");
Connection conn = ConnectionFactory.createConnection(conf);

5.4.2 ACL 授权

权限类型:R(Read 读)、W(Write 写)、X(Execute 执行协处理器/endpoint)、C(Create 建删表)、A(Admin 管理,含 split/compact/balance/grant)。作用范围可为:全局、命名空间、表、列族、列。

# 授权:grant '用户/@组', '权限', ['表' [,'列族' [,'列']]]
grant 'zhangsan', 'RW', 'student'                    # 表级读写
grant 'zhangsan', 'R',  'student', 'course'          # 列族级只读
grant 'zhangsan', 'R',  'student', 'course', 'math'  # 列级只读
grant 'lisi', 'RWXCA'                                # 全局所有权限(超级用户)
grant 'lisi', 'RW', '@ns1'                           # 命名空间级(@ 前缀)
grant '@devgroup', 'R', 'student'                    # 授权给组(@ 前缀)

# 回收
revoke 'zhangsan', 'student'
revoke 'zhangsan', 'student', 'course'
revoke '@devgroup', 'student'

# 查看权限
user_permission                        # 所有权限
user_permission 'student'              # 某表的权限
user_permission '.*'                   # 正则匹配表
user_permission '@ns1'                 # 命名空间权限

超级用户配置:

<property>
  <name>hbase.superuser</name>
  <value>hbase,hadoop,@admingroup</value>
</property>

5.4.3 可见性标签(Visibility Labels,单元格级安全)

在 Cell 上打标签,只有被授予对应标签的用户才能看到该 Cell,实现单元格级别的行级/字段级权限。

<property>
  <name>hbase.coprocessor.region.classes</name>
  <value>org.apache.hadoop.hbase.security.visibility.VisibilityController</value>
</property>
<property>
  <name>hbase.coprocessor.master.classes</name>
  <value>org.apache.hadoop.hbase.security.visibility.VisibilityController</value>
</property>
# 定义标签
add_labels ['SECRET', 'TOPSECRET', 'PUBLIC']
list_labels

# 给用户授予标签
set_auths 'zhangsan', ['SECRET', 'PUBLIC']
set_auths '@devgroup', ['PUBLIC']
get_auths 'zhangsan'
clear_auths 'zhangsan', ['SECRET']

# 写入时打标签(支持 &(与)、|(或)、!(非)表达式)
put 'student', '95001', 'info:salary', '10000', {VISIBILITY => 'SECRET'}
put 'student', '95001', 'info:name', 'LiYing', {VISIBILITY => 'PUBLIC|SECRET'}

# 读取时指定授权(只能用自己被授予的标签)
scan 'student', {AUTHORIZATIONS => ['SECRET']}
get  'student', '95001', {AUTHORIZATIONS => ['PUBLIC']}

其他安全特性:透明加密(HFile/WAL 静态加密,hbase.crypto.keyprovider、列族 ENCRYPTION => 'AES')、配额限流(set_quota TYPE => THROTTLE, USER => 'u1', LIMIT => '10req/sec')。

5.5 高可用

5.5.1 多 Master 与自动切换

# 配置备用 Master
vim $HBASE_HOME/conf/backup-masters
slave1
slave2

切换机制:所有 HMaster 启动后竞争在 ZooKeeper 上创建临时节点 /hbase/master,成功者成为 Active,其余在 /hbase/backup-masters 下注册并 watch /hbase/master。Active Master 进程退出或 ZK session 超时 → 临时节点自动删除 → Backup 收到通知立即竞选 → 新 Active 在数秒内接管(读取 meta、恢复 Procedure 状态、继续未完成的 DDL)。

# 验证
echo "status" | hbase shell        # 应显示 "1 active master, 2 backup masters"
# 手工切换(在 Active 上)
hbase-daemon.sh stop master
# 观察 Backup 是否接管
curl -s http://slave1:16010/ | grep -i 'Master'

关键点重申:Master 不在读写路径上,短暂无 Master 不影响现有表的 get/put/scan,只影响 DDL、Region 分配、balancer 与故障恢复。

5.5.2 RegionServer 故障恢复流程

1. RS 进程退出 / 长时间 Full GC / 网络分区
   → ZooKeeper 临时节点 /hbase/rs/<server> 因 session 超时被删除
     (耗时 ≈ zookeeper.session.timeout,默认 90s,可调至 30~60s 加快发现)
2. Master 监听到节点删除,将该 RS 标记为 dead,
   并把它承载的所有 Region 标记为需要重新分配
3. WAL 拆分(Log Splitting)
   - Master 将该 RS 的 WAL 目录重命名为 <server>-splitting
   - 按 Region 把 WAL 中的 edits 拆分到各 Region 目录下的 recovered.edits/
   - 现代版本使用 Distributed Log Splitting:Master 把拆分任务发布到 ZK,
     多个存活 RS 并行领取执行,大幅缩短时间
4. Master 通过 AssignmentManager 把这些 Region 分配给存活的 RS
5. 接管的 RS 打开 Region 时,先回放 recovered.edits 中的记录写入 MemStore,
   flush 成 HFile 后删除 recovered.edits,Region 才对外提供服务
6. Region 上线,客户端缓存失效后重新寻址,读写恢复

恢复时间 ≈ ZK 发现时间(30~90s) + WAL 拆分时间(数秒~数分钟,取决于 WAL 量与并行度)
           + Region 上线与回放时间
典型值:1~3 分钟。WAL 越大、Region 越多,恢复越慢。

缩短恢复时间的手段:

<!-- 更快发现宕机(过小会因 GC 误判,建议 30000~60000) -->
<property>
  <name>zookeeper.session.timeout</name>
  <value>60000</value>
</property>
<!-- 开启分布式日志拆分(默认开启) -->
<property>
  <name>hbase.master.distributed.log.splitting</name>
  <value>true</value>
</property>
<!-- 每个 RS 参与拆分的线程数 -->
<property>
  <name>hbase.regionserver.wal.max.splitters</name>
  <value>4</value>
</property>
<!-- 控制单个 WAL 大小与数量,减少单次拆分量 -->
<property>
  <name>hbase.regionserver.maxlogs</name>
  <value>32</value>
</property>
<!-- 2.x:Region Replica 提供读高可用(timeline consistent 读) -->
<property>
  <name>hbase.regionserver.storefile.refresh.period</name>
  <value>30000</value>
</property>
<property>
  <name>hbase.region.replica.replication.enabled</name>
  <value>true</value>
</property>
# 建表时指定 3 个副本 Region,读请求可路由到副本(可能读到稍旧数据)
create 'student', 'info', {REGION_REPLICATION => 3}
alter  'student', {REGION_REPLICATION => 3}
# 客户端指定一致性
# scan.setConsistency(Consistency.TIMELINE)

5.5.3 跨集群复制(Replication)

基于 WAL 的异步主从复制,用于异地灾备、集群双活、把线上数据实时同步到分析集群。

<!-- 两个集群都要开启(HBase 1.x 需显式配置,2.x 默认开启) -->
<property>
  <name>hbase.replication</name>
  <value>true</value>
</property>
<!-- 复制 BulkLoad 的数据(1.3+) -->
<property>
  <name>hbase.replication.bulkload.enabled</name>
  <value>true</value>
</property>
<property>
  <name>hbase.replication.cluster.id</name>
  <value>source-cluster</value>
</property>
# ---- 在源集群 shell 中操作 ----

# 1. 添加 peer(目标集群)
add_peer '1', CLUSTER_KEY => 'target-zk1,target-zk2,target-zk3:2181:/hbase'
# 带表/列族过滤的 peer(只复制指定表)
add_peer '2', CLUSTER_KEY => 'zk1:2181:/hbase',
  TABLE_CFS => {"student" => ["info", "course"], "user" => []}
# 带命名空间过滤(2.x)
add_peer '3', CLUSTER_KEY => 'zk1:2181:/hbase',
  NAMESPACES => ["ns1"], SERIAL => true

# 2. 查看 peer
list_peers
show_peer_tableCFs '1'

# 3. 启用表复制(会自动把列族的 REPLICATION_SCOPE 置为 1,
#    并在目标集群创建同名同结构的表)
enable_table_replication 'student'
disable_table_replication 'student'

# 手工方式:直接修改列族的 REPLICATION_SCOPE
alter 'student', {NAME => 'info', REPLICATION_SCOPE => '1'}
# 目标集群需预先手工建好同名表

# 4. peer 控制
enable_peer  '1'
disable_peer '1'          # 暂停复制(WAL 会在源端 oldWALs 堆积,注意磁盘)
set_peer_tableCFs '1', {"student" => []}
set_peer_bandwidth '1', 10485760      # 限速 10MB/s
remove_peer '1'

# 5. 监控复制状态
status 'replication'
status 'replication', 'source'
status 'replication', 'sink'
# 关键指标:AgeOfLastShippedOp(复制延迟)、SizeOfLogQueue(积压的 WAL 数)

# 6. 校验两端数据一致性
hbase org.apache.hadoop.hbase.mapreduce.replication.VerifyReplication \
  --starttime=1724300000000 --endtime=1724400000000 1 student

复制原理:源集群 RegionServer 的 ReplicationSource 线程读取本机 WAL 中 REPLICATION_SCOPE=1 的 edits,通过 RPC 批量推送到目标集群随机选中的若干 RegionServer(ReplicationSink),由其以普通 put 方式写入。因此是异步、最终一致的,存在秒级延迟;目标集群的 WAL 不会再次向外复制(避免环路),双向复制需两边互相 add_peer 并依赖 clusterId 防环。

注意事项:

  • peer 被 disable 或目标集群不可达时,源端 oldWALs 会持续堆积占满 HDFS,必须监控 SizeOfLogQueue;
  • 复制不保证顺序(除非用 SERIAL => true);
  • 目标表结构必须与源一致;
  • BulkLoad 默认不复制,需显式开启。

5.6 排错清单

排错第一原则:先看日志。

tail -500f $HBASE_HOME/logs/hbase-*-master-*.log
tail -500f $HBASE_HOME/logs/hbase-*-regionserver-*.log
grep -iE 'error|exception|fatal|WARN' $HBASE_HOME/logs/hbase-*-master-*.log | tail -50
# Web UI 日志页:http://master:16010/logs/
# 在线调日志级别:http://master:16010/logLevel

5.6.1 HMaster 启动后自动退出

现象/日志关键字 原因 处理
JAVA_HOME is not set hbase-env.sh 未设 JAVA_HOME 在 hbase-env.sh 中显式 export JAVA_HOME=/usr/local/jdk
The procedure WAL relies on the ability to hsync HBase 2.x 在不支持 hsync 的文件系统(本地 file:/// 或旧 HDFS)上启动 hbase-site.xml 中设 hbase.unsafe.stream.capability.enforce=false
ConnectionLoss / KeeperErrorCode = ConnectionLoss for /hbase ZooKeeper 未启动或 quorum 配置错 先启 ZK,核对 hbase.zookeeper.quorum
Failed to become active master + hbase.version file 相关 前一次异常退出遗留脏数据,或 HDFS 中 /hbase 与 ZK 中 /hbase 元数据不一致(常见于重装、换 HDFS) 停 HBase → hdfs dfs -rm -r /hbase(会丢数据,仅测试环境)→ hbase zkcli 中 rmr /hbase → 重启
NoNode for /hbase/meta-region-server 或 meta 一直无法上线 meta 表损坏 / RS 全挂 见 5.6.11
ClassNotFoundException / NoSuchMethodError / IncompatibleClassChangeError HBase 自带的 hadoop jar 与集群 Hadoop 版本不一致 用集群版本的 hadoop jar 替换 $HBASE_HOME/lib 下同名 jar;或换用匹配 Hadoop 版本的 HBase 发行包
Server is not running yet 长期不消失 HDFS 处于安全模式,或 NameNode 不可用 hdfs dfsadmin -safemode get / -safemode leave;确认 HDFS 健康
UnknownHostException: mycluster HDFS 开启了 HA,但 HBase conf 下缺 hdfs-site.xml 软链 core-site.xml、hdfs-site.xml 到 $HBASE_HOME/conf
Permission denied: user=hbase, access=WRITE, inode="/hbase" HDFS 上 /hbase 权限不足 hdfs dfs -chown -R hbase:hbase /hbase 或以正确用户启动
Master 起来但 0 个 RegionServer regionservers 文件仍是 localhost,或 SSH 免密未配 修正 regionservers,配置免密,hbase-daemons.sh start regionserver

5.6.2 RegionServer 起不来:时间不同步(ClockOutOfSyncException)

org.apache.hadoop.hbase.ClockOutOfSyncException:
  Server slave2,16020,1724300000000 has been rejected;
  Reported time is too far out of sync with master.
  Time difference of 45231ms > max allowed of 30000ms

原因:该 RegionServer 与 HMaster 的系统时间差超过 hbase.master.maxclockskew(默认 30000ms = 30 秒),Master 直接拒绝其注册,RS 随即退出。

处理:

# 1. 确认偏差
for h in master slave1 slave2 slave3; do echo -n "$h: "; ssh $h date +%s; done

# 2. 立即同步(所有节点)
systemctl stop  chronyd     # 或 ntpd
ntpdate -u ntp.aliyun.com
hwclock -w
systemctl start chronyd
chronyc tracking            # 关注 System time 偏差应 < 1s

# 3. 配置长期自动同步(见 3.3.5),并统一时区
timedatectl set-timezone Asia/Shanghai

# 4. 重启该 RegionServer
hbase-daemon.sh start regionserver

应急放宽(不推荐,掩盖问题且可能导致版本错乱):

<property>
  <name>hbase.master.maxclockskew</name>
  <value>180000</value>
</property>

5.6.3 连不上 ZooKeeper / quorum 配置错误

org.apache.zookeeper.KeeperException$ConnectionLossException:
  KeeperErrorCode = ConnectionLoss for /hbase/hbaseid
ZooKeeper exists failed after 4 attempts
Opening socket connection to server localhost/127.0.0.1:2181  ← 明显是配置没生效

排查步骤:

# 1. ZK 是否活着、是否有 leader
for h in master slave1 slave2; do echo -n "$h: "; ssh $h "zkServer.sh status 2>&1 | grep Mode"; done
echo ruok | nc master 2181           # 期望 imok
echo stat | nc master 2181

# 2. 配置是否正确(不要在 quorum 里写端口)
grep -A2 'hbase.zookeeper.quorum' $HBASE_HOME/conf/hbase-site.xml
# 错误:<value>master:2181,slave1:2181</value>
# 正确:<value>master,slave1,slave2</value>  + 单独配 clientPort

# 3. znode 是否存在
hbase zkcli
> ls /hbase
> get /hbase/meta-region-server
> ls /hbase/rs

# 4. znode.parent 是否一致(多套 HBase 共用 ZK 时必须区分)
grep -A2 'znode.parent' $HBASE_HOME/conf/hbase-site.xml

# 5. 端口连通性与防火墙
telnet master 2181
systemctl status firewalld

# 6. HBASE_MANAGES_ZK 混用:既起了独立 ZK 又让 HBase 管 ZK,
#    会出现两个 ZK 抢 2181 端口
grep HBASE_MANAGES_ZK $HBASE_HOME/conf/hbase-env.sh   # 用独立 ZK 时必须为 false

# 7. ZK 连接数打满
echo cons | nc master 2181 | wc -l
# ZK 侧调大 maxClientCnxns(zoo.cfg)

5.6.4 客户端连接超时与 hosts 解析问题

org.apache.hadoop.hbase.client.RetriesExhaustedException:
  Can't get the location for replica 0
java.net.UnknownHostException: slave1
Call exception, tries=10, retries=35, ... on slave1,16020,1724300000000
(能连上 ZooKeeper、能读到 meta,但连接 RegionServer 超时)

根因:客户端从 ZooKeeper/meta 拿到的是 RegionServer 的主机名(不是 IP),如果客户端机器无法解析这些主机名,就会卡在连接阶段直到超时。

# 1. 客户端机器(含 Windows 开发机)必须能解析集群所有节点主机名
# Linux: /etc/hosts ;Windows: C:\Windows\System32\drivers\etc\hosts
192.168.1.10  master
192.168.1.11  slave1
192.168.1.12  slave2
192.168.1.13  slave3

# 2. 验证
ping slave1
nslookup slave1
telnet slave1 16020

# 3. 服务端检查:RS 注册的主机名是否是 localhost
echo "status 'simple'" | hbase shell | grep -i server
hbase zkcli -c "ls /hbase/rs"
# 若注册成 localhost,删除 /etc/hosts 中的 "127.0.0.1 <hostname>" 行

# 4. 客户端配置是否与服务端一致
#    hbase.zookeeper.quorum、clientPort、hbase.zookeeper.znode.parent

# 5. 超时参数(快速失败优于挂死)
#    hbase.rpc.timeout=60000
#    hbase.client.operation.timeout=120000
#    hbase.client.scanner.timeout.period=120000
#    hbase.client.retries.number=3
#    zookeeper.recovery.retry=3

# 6. 防火墙放通 2181/16000/16010/16020/16030

5.6.5 Region 一直处于 in transition(RIT)状态

现象:Master UI 的 "Regions in Transition" 区块中某 Region 长时间(数分钟以上)处于 OPENING、CLOSING、FAILED_OPEN、PENDING_OPEN 状态;读写该 Region 报 NotServingRegionException 或 RegionOfflineException。

常见原因:RS 宕机后分配失败、HDFS 上 HFile 损坏或缺失、meta 与实际状态不一致、Procedure 卡死、split 中途失败留下父/子 Region 不一致。

# 1. 定位(Web UI 或命令)
echo "status 'detailed'" | hbase shell | grep -i transition
hbase hbck -details | grep -iE 'inconsistenc|not deployed|multiply assigned|hole'

# 2. 温和处理:重新分配
echo "unassign 'REGION_ENCODED_NAME', true" | hbase shell
echo "assign   'REGION_ENCODED_NAME'"       | hbase shell

# 3. 清理已分裂父 Region 的残留
echo "catalogjanitor_run" | hbase shell

# 4. HBase 1.x 修复
hbase hbck -fixAssignments
hbase hbck -fixMeta
hbase hbck -fixHdfsHoles -fixHdfsOverlaps
hbase hbck -repair                 # 全量修复,危险,先做快照/备份

# 5. HBase 2.x 用 HBCK2
hbase hbck                                                    # 只检查
hbase hbck -j hbase-hbck2.jar assigns REGION_ENCODED_NAME
hbase hbck -j hbase-hbck2.jar unassigns REGION_ENCODED_NAME
hbase hbck -j hbase-hbck2.jar bypass -o PID                   # 跳过卡死的 Procedure
hbase hbck -j hbase-hbck2.jar fixMeta
hbase hbck -j hbase-hbck2.jar reportMissingRegionsInMeta
hbase hbck -j hbase-hbck2.jar addFsRegionsMissingInMeta default:student
hbase hbck -j hbase-hbck2.jar setTableState student ENABLED
hbase hbck -j hbase-hbck2.jar scheduleRecoveries SERVERNAME

# 6. 查看卡住的 Procedure
echo "list_procedures" | hbase shell
echo "list_locks"      | hbase shell

# 7. 最后手段:重启 Master(会重新走一遍分配)
hbase-daemon.sh stop master; hbase-daemon.sh start master

做任何 hbck 修复前,务必先 snapshot 或备份 /hbase。

5.6.6 热点问题:现象、定位与 RowKey 改造

现象:

  • 单台 RegionServer 的 Requests/s 远高于其他节点(Master UI 一目了然),其余节点近乎空闲;
  • 该节点 CPU/网卡打满、GC 频繁、写延迟飙升、频繁 flush 与 split;
  • 客户端出现 RegionTooBusyException、CallQueueTooBigException、写超时。

定位:

# 1. Master UI → Region Servers,按 Requests 排序找出异常节点
# 2. 进入该 RS UI(:16030)→ Regions,按 Read/Write Request Count 排序,
#    找出请求集中的那个 Region
# 3. 在 Master UI → Table Details 中查看该 Region 的 startKey/endKey,
#    反推是哪一段 RowKey 造成热点
echo "list_regions 'student'" | hbase shell
# 4. 确认 Region 分布是否均衡
echo "status 'simple'" | hbase shell
# 5. 查看慢查询
grep -i 'responseTooSlow' $HBASE_HOME/logs/hbase-*-regionserver-*.log | tail -20

RowKey 改造(根本解决):

场景 原 RowKey 改造后
自增 ID 写热点 1000001 md5(id).substring(0,4) + "_" + id(哈希前缀)
时间序列写热点 20260822153000 salt(metric) + metric + timestamp(加盐 + 维度前缀)
手机号 13812345678 87654321831(反转)
取最近 N 条 userId_timestamp userId_(Long.MAX_VALUE - timestamp)
单一大 Region 读热点 —— 手工 split 该 Region + 数据打散

应急缓解(不改 RowKey):

balance_switch true
balancer
split 'REGION_ENCODED_NAME', 'MID_KEY'                 # 手工切开热点 Region
move  'REGION_ENCODED_NAME', 'idle_rs,16020,169...'    # 迁到空闲节点
# 客户端侧:加本地缓存/限流,把读打散到副本(REGION_REPLICATION)

改造 RowKey 通常需要重建表 + 数据迁移(新表预分区 → MR/Spark 读旧表按新 RowKey 写新表或 BulkLoad → 切流量 → 删旧表)。

5.6.7 RegionServer 频繁 GC 与 OOM

java.lang.OutOfMemoryError: Java heap space
org.apache.hadoop.hbase.YouAreDeadException: Server REPORT rejected;
  currently processing slave2,16020,169... as dead server
(RS 无故"重启":实际是 Full GC 超过 ZK session timeout 被判死)

排查与处理:

# 1. 看 GC 日志与实时 GC 状况
jstat -gcutil $(pgrep -f HRegionServer) 1000 20
grep -E 'Full GC|Pause' $HBASE_HOME/logs/gc-rs.log | tail -30
# 关注:Full GC 频率与单次停顿;停顿 > 90s 必被判死

# 2. 看堆是否真的不够
curl -s 'http://slave1:16030/jmx?qry=java.lang:type=Memory'

# 3. dump 分析大对象
jmap -histo:live $(pgrep -f HRegionServer) | head -30
jmap -dump:live,format=b,file=/tmp/rs.hprof $(pgrep -f HRegionServer)

常见根因与对策:

根因 对策
堆太小 调大 HBASE_REGIONSERVER_OPTS 的 -Xmx(16~32GB,不超 32GB)
Region 数过多,MemStore 占满 合并 Region(merge_region)、扩容节点、调小 flush.size
global.memstore.size + block.cache.size > 0.8 两者之和降到 0.8 以内
大 scan 一次拉回海量数据 客户端 setCaching 调小、setBatch 限制列数、setMaxResultSize 限制字节
宽行(单行几十 MB) 拆分数据模型,改"高表";必须设 setBatch
CMS 长停顿 换 G1GC + MaxGCPauseMillis=100
BlockCache 在堆内过大 改用堆外 BucketCache:hbase.bucketcache.ioengine=offheap
MemStore 内存碎片 确认 MSLAB 开启(hbase.hregion.memstore.mslab.enabled=true)
ZK 超时太短,GC 稍长即被判死 zookeeper.session.timeout 调到 60000~120000(同时也延长故障发现时间,需权衡)

5.6.8 Compaction 队列积压

现象:RS UI 的 compactionQueueLength 持续 > 10 且不下降;StoreFile 数持续增长;读延迟上升;最终出现写阻塞。

Region xxx has too many store files; delaying flush up to 90000ms
org.apache.hadoop.hbase.RegionTooBusyException: Above memstore limit
# 1. 观察
curl -s 'http://slave1:16030/jmx' | grep -iE 'compactionQueueLength|storeFileCount|flushQueueLength'
echo "compaction_state 'student'" | hbase shell

# 2. 立即缓解:手工触发合并(低峰)
major_compact 'student'
compact_rs 'slave1,16020,169...'

调参:

<!-- 提高合并并行度 -->
<property><name>hbase.regionserver.thread.compaction.small</name><value>5</value></property>
<property><name>hbase.regionserver.thread.compaction.large</name><value>3</value></property>
<!-- 单次合并更多文件 -->
<property><name>hbase.hstore.compaction.max</name><value>15</value></property>
<property><name>hbase.hstore.compaction.min</name><value>3</value></property>
<!-- 提高写阻塞阈值(缓冲突发,不解决根本) -->
<property><name>hbase.hstore.blockingStoreFiles</name><value>32</value></property>
<property><name>hbase.hregion.memstore.block.multiplier</name><value>8</value></property>
<!-- 减少 flush 产生的小文件:加大 MemStore -->
<property><name>hbase.hregion.memstore.flush.size</name><value>268435456</value></property>
<!-- 限制 compaction 吞吐,避免抢占业务 IO -->
<property>
  <name>hbase.regionserver.throughput.controller</name>
  <value>org.apache.hadoop.hbase.regionserver.throttle.PressureAwareCompactionThroughputController</value>
</property>
<property><name>hbase.hstore.compaction.throughput.higher.bound</name><value>52428800</value></property>
<property><name>hbase.hstore.compaction.throughput.lower.bound</name><value>20971520</value></property>

根本原因往往是写入速率超过集群处理能力或Region 数过多,需扩容或减慢写入(BulkLoad 替代 put)。

5.6.9 HDFS 空间不足导致 HBase 不可写

org.apache.hadoop.ipc.RemoteException(java.io.IOException):
  File /hbase/WALs/... could only be replicated to 0 nodes instead of minReplication (=1)
java.io.IOException: No space left on device
Master 日志:NameNode is in safe mode / Resources are low on NN
# 1. 确认空间
hdfs dfsadmin -report | grep -E 'DFS Used%|Remaining'
hdfs dfs -du -h /hbase | sort -h
df -h                                   # 各 DataNode 本地磁盘

# 2. 找出占用大户
hdfs dfs -du -h /hbase/data/default
hdfs dfs -du -h /hbase/archive          # 快照引用的旧 HFile,常被忽视
hdfs dfs -du -h /hbase/oldWALs          # peer disable 会导致堆积

# 3. 释放空间(按安全顺序)
delete_all_snapshot 'old_snap.*'        # 删除过期快照(会释放 archive)
echo "cleaner_chore_run" | hbase shell  # 立即清理 archive / oldWALs
hdfs dfs -rm -r -skipTrash /user/xxx/tmp   # 清理无关数据
hdfs dfs -expunge                       # 清空回收站
# 检查 peer 是否 disable 导致 oldWALs 堆积
echo "list_peers" | hbase shell
echo "status 'replication'" | hbase shell

# 4. 对有 TTL 的表执行 major compaction,物理清除过期数据
major_compact 'logs'

# 5. 退出安全模式(空间恢复后)
hdfs dfsadmin -safemode leave

# 6. 长期:设置 TTL、增加 DataNode、降低副本数(谨慎)、监控 80% 告警

5.6.10 误删表的快照恢复

# 事前:建立定期快照机制(这是唯一能救命的准备)
# crontab: 0 2 * * * echo "snapshot 'student','snap_$(date +\%Y\%m\%d)'" | hbase shell -n

# 事后恢复(表被 drop 后)
hbase shell
list_snapshots                                    # 找到最近的快照
clone_snapshot 'snap_20260822', 'student'         # 克隆成原表名(推荐,无损)
# 或恢复到已存在的同名表(会覆盖,需先 disable)
disable 'student'
restore_snapshot 'snap_20260822'
enable 'student'
count 'student'                                   # 校验

# 无快照时的最后希望:数据可能还在 archive 目录
hdfs dfs -ls /hbase/archive/data/default/student
# 可尝试把 HFile 复制出来用 BulkLoad 重新导入到新建的同结构表
hdfs dfs -cp /hbase/archive/data/default/student/*/cf/* /tmp/recover/cf/
echo "create 'student_recover','cf'" | hbase shell
hbase completebulkload /tmp/recover student_recover
# 注意:archive 会被 HFileCleaner 定时清理(hbase.master.hfilecleaner.ttl,默认 5 分钟),
#      发现误删后应第一时间停掉 cleaner 或直接备份 archive 目录

预防措施:

# 打开 HDFS 回收站(core-site.xml)
# fs.trash.interval = 1440   (分钟)
# 延长 archive 保留时间(hbase-site.xml)
# hbase.master.hfilecleaner.ttl = 604800000   (7 天,单位毫秒)
# 关键表设为只读,需要写时临时打开
alter 'student', READONLY => 'true'
# 用 ACL 限制 C(Create)权限,禁止业务账号 drop 表

5.6.11 hbase:meta 损坏的修复思路

NotServingRegionException: hbase:meta,,1 is not online
NoNode for /hbase/meta-region-server
Master 卡在 "Waiting for meta to be assigned"
list 报错 / 所有表都不可访问

修复顺序(从最温和到最激进,每一步前先备份):

# 0. 立即备份现场(极其重要)
hdfs dfs -cp /hbase/data/hbase/meta /backup/meta_$(date +%s)
hdfs dfs -get /hbase/data/hbase/meta /root/meta_backup

# 1. 先确认 meta 的 Region 是否只是没上线
hbase zkcli
> get /hbase/meta-region-server
> ls /hbase/rs
echo "scan 'hbase:meta', {LIMIT => 5}" | hbase shell

# 2. 重启 Master,让它重新分配 meta
hbase-daemon.sh stop master; hbase-daemon.sh start master

# 3. 强制重新分配 meta(1.x)
hbase hbck -fixAssignments
hbase hbck -fixMeta
# 2.x
hbase hbck -j hbase-hbck2.jar assigns 1588230740      # meta 的固定编码名
hbase hbck -j hbase-hbck2.jar fixMeta

# 4. meta 中缺少某些表的 Region 条目(HDFS 上有目录但 meta 里没有)
hbase hbck -j hbase-hbck2.jar reportMissingRegionsInMeta
hbase hbck -j hbase-hbck2.jar addFsRegionsMissingInMeta default:student
hbase hbck -j hbase-hbck2.jar assigns -i /tmp/regions_to_assign.txt
# 之后必须重启 Master 使其重读 meta

# 5. meta 严重损坏:用 OfflineMetaRepair 从 HDFS 目录结构重建 meta(1.x)
#    必须先停掉整个 HBase 集群
stop-hbase.sh
hbase org.apache.hadoop.hbase.util.hbck.OfflineMetaRepair -fix
start-hbase.sh
# 2.x 无此工具,等价流程:
#   hbase hbck -j hbase-hbck2.jar reportMissingRegionsInMeta
#   hbase hbck -j hbase-hbck2.jar addFsRegionsMissingInMeta <ns:table>
#   重启 Master → assigns

# 6. 表状态不一致(enable/disable 卡住)
hbase hbck -j hbase-hbck2.jar setTableState student ENABLED
hbase zkcli
> ls /hbase/table
> get /hbase/table/student

# 7. 终极方案:ZK 元数据重建(不动 HDFS 数据)
stop-hbase.sh
hbase zkcli
> rmr /hbase                 # 只删 ZK 中的元数据,HDFS 上的数据仍在
> quit
start-hbase.sh
# 启动后 Master 会从 HDFS 重新扫描并重建 ZK 状态;
# 若 meta 表本身文件也损坏,需回到第 5 步重建 meta

# 8. 最后手段:快照/备份恢复整集群(见 5.1)

修复原则:

  1. 任何修复前先 hdfs dfs -cp 备份 /hbase(至少备份 /hbase/data/hbase/meta);
  2. hbase hbck 先只读检查(不带修复参数),看清问题再动手;
  3. 优先用最小粒度的修复(assigns/fixMeta),避免直接 -repair;
  4. HDFS 上的实际数据(HFile)是最终依据,meta 与 ZK 都可以重建;
  5. 修复后用 hbase hbck 复查至 0 inconsistencies,并抽样 count/scan 校验数据完整性。

5.6.12 其他常见错误速查

报错 原因 处理
TableNotDisabledException drop/alter 前未 disable 先 disable '表名'
TableExistsException 表已存在 list 确认,或 drop 后重建
NoSuchColumnFamilyException 写入了未定义的列族 列族必须先 alter 添加;检查拼写与大小写
RegionTooBusyException 该 Region MemStore 超限或写入过快 见热点与 compaction 章节;客户端降速重试
CallQueueTooBigException RPC 队列满 调大 handler.count / 队列上限;排查慢查询与热点
ScannerTimeoutException scan 迭代间隔超过 hbase.client.scanner.timeout.period 减小 setCaching;调大超时;及时关闭 scanner
OutOfOrderScannerNextException 客户端与 RS 的 scanner 序号错乱(常伴随超时重试) 减小 caching、调大 scanner timeout
Too many open files ulimit 太小 见 3.3.7,nofile 调到 65536
Could not obtain block / All datanodes are bad DataNode 句柄/线程不足或磁盘故障 调大 dfs.datanode.max.transfer.threads;检查磁盘
KeeperErrorCode = NodeExists for /hbase/master 上次 Master 异常退出遗留 通常会自动过期;必要时 hbase zkcli 删除该 znode
WrongRegionException BulkLoad 的 HFile 与目标 Region 边界不匹配 重新用 configureIncrementalLoad 生成,或让 LoadIncrementalHFiles 自动拆分
shell 中中文/数值显示为 \x00\x00 值是二进制 get ... {FORMATTER => 'toString'} 或 toLong
count 结果与预期不符 有墓碑未清理、或多版本 major_compact 后再统计;用 scan {RAW=>true} 观察

小结

HBase 是 BigTable 的开源实现,和 BigTable 一样支持大规模海量数据,分布式并发数据处理效率极高,易于扩展且支持动态伸缩,适用于廉价设备。

HBase 支持 Native Java API、HBase Shell、Thrift Gateway、REST Gateway、Pig、Hive 等多种访问接口,可以根据具体应用场合选择相应访问方式。

HBase 实际上就是一个稀疏、多维、持久化存储的映射表,采用行键、列键和时间戳进行索引,每个值都是未经解释的字符串。要特别注意 HBase 数据在概念视图和物理视图中的差别。

HBase 采用分区存储,一个大的表会被分拆成许多个 Region,这些 Region 会被分发到不同的服务器上实现分布式存储。

HBase 的系统架构包括客户端、ZooKeeper 服务器、Master 主服务器、Region 服务器。客户端包含访问 HBase 的接口;ZooKeeper 服务器负责提供稳定可靠的协同服务(存储 meta 位置、Master 选举、RegionServer 存活监控);Master 主服务器主要负责表和 Region 的管理工作且不参与读写路径;Region 服务器负责维护分配给自己的 Region,并响应用户的读写请求。

实践中最需要下功夫的是 RowKey 设计与预分区,它决定了数据分布是否均衡、查询能否高效;其次是列族数量控制、compaction 策略与 GC 调优。


练习

一、选择题

1. HBase 是下列哪个系统的开源实现? A. GFS B. MapReduce C. BigTable D. Chubby

答案:C。HBase 是谷歌 BigTable 的开源实现。GFS 对应 HDFS,MapReduce 对应 Hadoop MapReduce,Chubby 对应 ZooKeeper。

2. HBase 中,下列哪一项不是数据的定位坐标? A. 行键 B. 列族 C. 列限定符 D. Region 名

答案:D。HBase 的四维坐标是 [行键, 列族, 列限定符, 时间戳],Region 名不是数据坐标。

3. HBase 三层结构(旧版)中,-ROOT- 表的作用是? A. 记录用户数据表的 Region 位置 B. 记录 .META. 表的 Region 位置 C. 记录 ZooKeeper 的位置 D. 存储实际数据

答案:B。第一层 ZooKeeper 文件记录 -ROOT- 表位置,第二层 -ROOT- 表记录 .META. 表的 Region 位置,第三层 .META. 表记录用户数据表的 Region 位置。

4. 关于 HMaster,下列说法错误的是? A. 负责 Region 的分配与负载均衡 B. 负责表的创建与删除 C. 客户端的每次读写都要经过 HMaster D. 可以配置多个实现高可用

答案:C。客户端通过 ZooKeeper 与 hbase:meta 定位 Region 后直接访问 RegionServer,不经过 HMaster,因此 Master 负载很小。

5. 每个列族在一个 Region 内对应几个 Store? A. 0 个 B. 1 个 C. 2 个 D. 不确定

答案:B。每个列族对应一个 Store,一个 Store 包含一个 MemStore 和多个 StoreFile(HFile)。

6. 关于 major compaction,下列说法正确的是? A. 只合并部分小文件 B. 不会真正删除被标记删除的数据 C. 会物理删除墓碑标记的数据与过期版本 D. 资源开销很小,可随时执行

答案:C。major compaction 合并 Store 下全部 HFile,并真正物理删除墓碑数据、超出 VERSIONS 的旧版本与 TTL 过期数据;开销很大,生产应关闭自动触发、改为低峰手工执行。

7. RegionServer 启动时报 ClockOutOfSyncException,最可能的原因是? A. ZooKeeper 未启动 B. 与 Master 的系统时间偏差超过 30 秒 C. 端口被占用 D. 堆内存不足

答案:B。时间差超过 hbase.master.maxclockskew(默认 30000ms)会被 Master 拒绝注册,必须用 NTP/chrony 做时间同步。

8. HBase 1.0 及之后版本,HMaster 的 Web UI 默认端口是? A. 50070 B. 60010 C. 16010 D. 16030

答案:C。HBase 1.0 起 Master UI 为 16010、RegionServer UI 为 16030;此前旧版为 60010 与 60030。

二、简答题

1. 为什么已经有了 HDFS 和 MapReduce,还需要 HBase?

答:① Hadoop MapReduce 是高延迟的离线批处理框架,无法满足大规模数据的实时处理需求;② HDFS 面向批量顺序访问,不支持按 key 的随机读写,也不支持随机修改;③ 传统关系型数据库在数据规模剧增时存在扩展性与性能瓶颈(分库分表也难以很好解决),数据结构变化时通常需要停机维护,空列还浪费存储空间。因此需要 HBase 这类面向半结构化数据、高可扩展、低写入/查询延迟的列族数据库。

2. 简述 HBase 与传统关系数据库的六点区别。

答:① 数据类型:关系库有丰富数据类型,HBase 只有未经解释的字节数组;② 数据操作:关系库有复杂多表连接,HBase 只有简单的插入、查询、删除、清空,无表间关系;③ 存储模式:关系库行式存储,HBase 按列族存储且不同列族文件分离;④ 数据索引:关系库可建多个复杂索引,HBase 只有行键一个索引;⑤ 数据维护:关系库更新是覆盖旧值,HBase 更新是生成新版本、旧版本保留;⑥ 可伸缩性:关系库难以横向扩展,HBase 天生支持通过增减机器水平伸缩。此外还有事务差异:关系库支持跨行跨表 ACID,HBase 只保证单行原子性。

3. 什么是 HBase 的概念视图与物理视图?两者的区别说明了什么?

答:概念视图把表看作一个稀疏的多维映射,形如一张有很多空格的宽表,行键 + 时间戳 + 各列族的列构成一个大表格。物理视图则是按列族分别存储的,同一列族的多个数据版本保存在一起,不同列族的文件相互分离;概念视图中"空"的单元格在物理视图中完全不存在、不占用存储空间。

两者的区别说明:① 列族是物理存储单元,是 HBase 配置与访问控制的基本单位,因此列族要少;② HBase 的稀疏存储让宽表设计(几百列)代价极低,存储开销只与实际写入的 Cell 数成正比;③ 读取一整行可能需要跨多个 Store 与多个 HFile 做归并,这是读比写复杂的根源;④ 多版本在物理上是多条独立的 KeyValue。

4. 简述客户端定位一行数据的完整寻址流程(现代两层结构)。

答:① 客户端先查本地 MetaCache,命中则直接跳到第 ④ 步;② 未命中则连接 ZooKeeper 读取 /hbase/meta-region-server,得到 hbase:meta 表所在的 RegionServer;③ 向该 RegionServer 查询 hbase:meta,用"表名 + RowKey"定位到负责该 RowKey 的 Region 及其所在 RegionServer;④ 客户端直连目标 RegionServer 执行 put/get/scan,并把位置信息缓存起来;⑤ 若 Region 已分裂或迁移,RegionServer 抛出 NotServingRegionException/RegionMovedException,客户端清除缓存后重新寻址重试。整个过程不经过 HMaster。

5. 描述 HBase 的写流程与读流程,并说明读为什么比写复杂。

答:写流程:客户端定位 Region → RegionServer 先顺序追加写 WAL(HLog) 并 sync → 再写对应列族的 MemStore(内存有序结构)→ 向客户端返回成功 → MemStore 达到阈值(默认 128MB)或全局内存压力/手工触发时 flush 成一个新的 HFile,清空 MemStore 并在 HLog 中写入标记 → 已全部落盘的 WAL 被归档删除。写 WAL 与写 MemStore 的顺序不可颠倒。

读流程:定位 Region → 查 BlockCache → 查 MemStore → 查 StoreFile/HFile(先用时间范围与 RowKey 范围筛选文件,再用布隆过滤器排除不可能包含目标的文件,再用 HFile 多级索引定位 Data Block)→ 把来自 MemStore 与多个 HFile 的结果按 Row→Family→Qualifier→Timestamp 降序归并,过滤墓碑、超版本数与 TTL 过期数据后返回。

读更复杂的原因:① 写只需追加一个 WAL 与一个 MemStore,读必须查遍 MemStore 与所有可能相关的 HFile;② 同一行的不同版本、不同列可能散落在多个文件中,需要多路归并排序;③ 归并时还要正确处理多版本、TTL 与墓碑删除语义;④ HFile 越多读放大越严重,所以需要 compaction。

6. HLog(WAL)的作用是什么?RegionServer 宕机后的恢复过程是怎样的?

答:HLog 是预写式日志,每个 RegionServer 一个,该机上所有 Region 的写入都顺序追加到同一个 WAL(顺序写、吞吐高)。用户更新必须先写日志才能写 MemStore,且 MemStore 内容对应的日志已落盘后该内容才能刷写到磁盘,从而保证任何时刻宕机都能靠 WAL 回放恢复内存中未落盘的数据。

恢复过程:① ZooKeeper 实时监测 RegionServer 状态(临时节点 + session),发现某 RS 故障后通知 Master;② Master 处理该 RS 遗留的 HLog(其中含多个 Region 的日志记录);③ 按每条日志所属 Region 对 HLog 进行拆分,放到各 Region 目录下的 recovered.edits,再把失效 Region 重新分配到可用的 RegionServer,并把相关日志一并交付;④ 接管的 RegionServer 回放日志记录,把数据写入 MemStore 再刷写为 StoreFile,完成恢复后 Region 才对外服务。共用日志的优点是提高写性能,缺点是恢复时需要分拆日志。

7. 为什么 HBase 表的列族数量要少?

答:① 每个列族在每个 Region 内对应一个独立 Store(一套 MemStore + 一组 HFile),列族越多 MemStore 越多,内存被切碎;② flush 与 compaction 以 Region 为单位触发,一个列族达阈值会连带其他列族一起 flush,数据量差异大的列族放在一起会产生大量小文件;③ Region 分裂以 Region 为单位,列族数据量极不均衡时小列族会被切成很多极小的 Store;④ 读取一整行可能要跨多个 Store 归并。经验值:列族控制在 1~3 个,最佳 1 个。此外列族名会随每个 KeyValue 存储,因此名字要短。

8. 什么是热点问题?如何避免?

答:热点指大量读写请求集中到少数 Region / RegionServer,导致该节点被打满而其他节点空闲,表现为单节点 Requests/s 异常高、CPU 与网卡打满、频繁 flush/split、客户端出现 RegionTooBusyException 与超时。根因通常是 RowKey 单调递增(自增 ID、时间戳前缀),写入永远落在最后一个 Region。

避免手段:① 加盐(前缀拼随机/散列定长前缀,打散均匀但范围扫描需并发扫所有桶);② 哈希(MD5/SHA1 取前几位作前缀,可复现、便于点查,但失去顺序);③ 反转(把低位变化快的字段如手机号、时间戳反转,保留点查能力);④ 预分区(建表即 SPLITS/NUMREGIONS 切好多个 Region,配合散列前缀让写入从一开始就均衡);⑤ 按业务维度前置(如 deviceId_timestamp);⑥ 时间分片建表 + TTL。

9. 简述 minor compaction 与 major compaction 的区别,以及生产环境为什么要关闭自动 major compaction。

答:minor compaction 只选取部分小的相邻 HFile 合并,触发条件是文件数 ≥ hbase.hstore.compactionThreshold(默认 3),开销小,不会彻底删除墓碑数据与被覆盖的旧版本(跨文件无法确定)。major compaction 合并一个 Store 下的全部 HFile,结果只剩 1 个文件,会真正物理删除墓碑标记的数据、超出 VERSIONS 的旧版本与 TTL 过期数据,还能恢复数据本地性,但开销极大。

关闭自动 major compaction 的原因:它要把该 Store 全部数据完整读一遍写一遍,产生极高的磁盘 IO 与网络流量,占用 CPU 与 handler 线程;自动触发时间(默认 7 天)不可控,很可能撞上业务高峰,导致读写延迟飙升、超时甚至雪崩;大集群多个 Region 同时执行会打满 HDFS。做法是设 hbase.hregion.majorcompaction=0,改为脚本在业务低峰分批 major_compact。

10. 为什么"删除数据后立即查询还能查到旧版本"?

答:HBase 的 delete 不会立即物理删除,而是写入墓碑标记(Tombstone),物理清除只发生在 major compaction。正常 get/scan 在归并时会应用墓碑因而看不到被删数据;但以下情况会"看到"旧数据:① 使用 scan {RAW => true} 会绕过墓碑,直接返回 HFile/MemStore 中的原始 Cell;② 指定了早于墓碑时间戳的 TIMESTAMP/TIMERANGE,与墓碑错开;③ 删除时指定了时间戳(delete 't','r','cf:c',1000)只删掉该单个版本,其他版本仍可见;④ 经典陷阱:删除(墓碑 ts=T)后写入时间戳小于 T 的数据,会被墓碑遮住,等 major compaction 清掉墓碑后又"复活",所以不要手工指定过去的时间戳写入。想立刻彻底删除需手工 major_compact。

11. BulkLoad 为什么比 put 快?有哪些限制?

答:普通 put 走完整写路径(客户端 RPC → RegionServer → 写 WAL → 写 MemStore → flush → compaction),每条数据经历多次内存拷贝、日志落盘与后台合并,RegionServer 的 CPU、内存、IO 与 GC 都是瓶颈。BulkLoad 用 MapReduce 在 HDFS 上直接生成排好序的 HFile,然后只需把文件 rename/move 到表的 Store 目录并通知 RegionServer 加载,完全绕过 WAL 与 MemStore,不产生 compaction 风暴,几乎不占用 RegionServer 资源,速度通常是 put 的数倍到数十倍,对在线业务影响极小。

限制:① 需经过 MR 排序阶段,只适合离线批量导入;② 生成 HFile 的 reduce 数等于目标表 Region 数,必须先预分区;③ HFile 输出目录必须与 HBase 在同一个 HDFS 且用户有 /hbase 写权限;④ 默认不参与 Replication(需开启 hbase.replication.bulkload.enabled)。

12. 列出 HBase 备份迁移的几种方式并比较。

答:① Snapshot + ExportSnapshot:不停机、不走 RegionServer、直接搬 HFile,最快,是整表备份/迁移首选,但不支持增量;② Export / Import:基于 MR,不停机、走 RS,慢,但支持按时间范围增量,也可跨版本;③ CopyTable:在线复制到另一张表或另一集群,走 RS,慢,适合表改名与小表同步,支持时间范围;④ distcp:直接拷 HDFS 目录,快,但必须 flush + disable 或停机,且目标端需 hbck 修复 meta;⑤ Replication:准实时异步复制,适合异地灾备与双活;⑥ BulkLoad:离线批量导入的最快方式。实践中:定期 Snapshot + 每日 Export 增量做备份,机房搬迁用 ExportSnapshot/distcp,灾备用 Replication,误删恢复用 clone_snapshot。

13. HBase 1.x 与 2.x 有哪些重要差异?部署时需注意什么版本兼容性?

答:主要差异:① Region 状态管理从依赖 ZooKeeper 的 region-in-transition 改为 Master 内的 ProcedureV2(AssignmentManagerV2),RIT 卡死显著减少;② 新增 In-Memory Compaction(CompactingMemStore),减少 flush 与写放大;③ 新增 Region Replica 提供 timeline-consistent 读高可用;④ API 变更:HTableDescriptor/HColumnDescriptor/HBaseAdmin 废弃,改用 TableDescriptorBuilder/ColumnFamilyDescriptorBuilder/Admin;⑤ hbck 的修复能力被移除,改用独立的 HBCK2 jar;⑥ 协处理器接口重构;⑦ 推荐使用 hbase-shaded-client 避免依赖冲突;⑧ 2.x 在不支持 hsync 的文件系统上需设 hbase.unsafe.stream.capability.enforce=false。

版本兼容性注意:HBase 与 Hadoop 大版本必须匹配(1.2/1.3 配 Hadoop 2.7/2.8;2.3+ 配 Hadoop 2.10/3.1+/3.2/3.3;HBase 1.x 与 Hadoop 3.x 基本不兼容);HBase 2.x 要求 JDK 8+(1.2~1.4 还支持 JDK 7);ZooKeeper 要求 3.4.10+;Hive、Phoenix 都需与 HBase 大版本严格对应(Phoenix 4.x↔HBase 1.x,5.x↔2.x)。发行包自带的 hadoop jar 必须与集群一致,否则出现 NoSuchMethodError/ClassNotFoundException。

14. 单 RegionServer 上的 Region 数量应该控制在多少?过多与过少各有什么问题?

答:经验值 100~200 个 Region,单 Region 大小 10~20GB。理论估算:Region 数 ≈ (RS 堆 × global.memstore.size) / (memstore.flush.size × 列族数),例如 24GB 堆 × 0.4 / 128MB ≈ 76 个。

Region 过多的问题:MemStore 内存被切碎、每个 Region 的 MemStore 都很小导致 flush 出大量小文件、compaction 压力大、RS 启动与故障恢复变慢、hbase:meta 变大、GC 压力上升。Region 过少的问题:并行度不足、单 Region 容易成为热点、频繁触发 split。处理手段:过多则 merge_region 合并小/空 Region、开启 normalizer 或扩容节点;过少则手工 split 或重建表做更细的预分区。

15. 说明 HBase 的 Master 高可用与 RegionServer 故障恢复机制。

答:Master HA:在 backup-masters 文件中列出备用节点,所有 HMaster 启动后竞争创建 ZooKeeper 临时节点 /hbase/master,成功者为 Active,其余注册到 /hbase/backup-masters 并 watch。Active 退出或 session 超时 → 临时节点消失 → Backup 立即竞选,数秒内接管(读 meta、恢复 Procedure 状态)。由于 Master 不在读写路径上,短暂无 Master 不影响现有表的读写,只影响 DDL、Region 分配、balancer 与故障恢复。

RegionServer 故障恢复:ZK 临时节点因 session 超时(默认 90s)被删除 → Master 标记该 RS 为 dead 并把其 Region 标记为待分配 → WAL 拆分(把 WAL 目录改名为 -splitting,按 Region 把 edits 拆到各自的 recovered.edits;现代版本用 Distributed Log Splitting 多 RS 并行执行)→ Master 把 Region 分配给存活 RS → 接管 RS 打开 Region 时回放 recovered.edits 写入 MemStore、flush 后删除日志 → Region 上线服务。恢复时间 ≈ ZK 发现时间 + WAL 拆分 + 回放,典型 1~3 分钟。可通过调小 zookeeper.session.timeout(30~60s)、增大 hbase.regionserver.wal.max.splitters、控制 maxlogs 来缩短。

三、Shell 操作题

1. 创建一个 student 表,包含列族 Sname、Ssex、Sage、Sdept、course,然后查看表的基本信息。

create 'student','Sname','Ssex','Sage','Sdept','course'
describe 'student'

2. 创建一个表 tempTable,包含 3 个列族 f1、f2、f3;向第 r1 行、第 f1:c1 列添加数据 hello,dblab;再分别取出 f1:c1 与 f1:c3 的值;最后使表无效并删除。

create 'tempTable','f1','f2','f3'
put 'tempTable','r1','f1:c1','hello,dblab'
get 'tempTable','r1','f1:c1'
get 'tempTable','r1','f1:c3'        # f1:c3 不存在,返回 0 row
disable 'tempTable'
drop 'tempTable'

3. 为 student 表行键 95001 添加 Sname 为 LiYing、course:math 为 80;查看该行;扫描全表;删除 Sname 列;删除整行。

put 'student','95001','Sname','LiYing'
put 'student','95001','course:math','80'
get 'student','95001'
scan 'student'
delete 'student','95001','Sname'
deleteall 'student','95001'

4. 建一张 user 表,列族 info,要求:保留 3 个版本、TTL 30 天、SNAPPY 压缩、ROW 布隆过滤器,并按 16 进制预分 16 个 Region。

create 'user',
  {NAME => 'info', VERSIONS => 3, TTL => 2592000,
   COMPRESSION => 'SNAPPY', BLOOMFILTER => 'ROW'},
  {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}
describe 'user'
list_regions 'user'

5. 写出扫描 student 表中行键在 95001(含)到 95010(不含)之间、只返回 Sname 与 course:math 两列、最多 20 行、每次 RPC 取回 100 行的命令。

scan 'student', {STARTROW => '95001', STOPROW => '95010',
                 COLUMNS => ['Sname','course:math'],
                 LIMIT => 20, CACHING => 100}

6. 查询 student 表中 course:math 大于等于 80 的所有行(该列不存在的行要过滤掉)。

scan 'student', {FILTER =>
  "SingleColumnValueFilter('course','math',>=,'binary:80',true,true)"}

7. 查询 student 表中行键以 9500 开头且 course:math 值为 80 的行。

scan 'student', {FILTER =>
  "PrefixFilter('9500') AND SingleColumnValueFilter('course','math',=,'binary:80')"}

8. 查看 student 表 95001 行 course:math 列的最近 3 个版本;再查看包含墓碑的全部原始数据。

get 'student','95001',{COLUMN => 'course:math', VERSIONS => 3}
scan 'student',{RAW => true, VERSIONS => 10}

9. 为 student 表新增列族 address,把 course 列族的版本数改为 5,然后删除 address 列族。

alter 'student', NAME => 'address'
alter 'student', {NAME => 'course', VERSIONS => 5}
alter 'student', NAME => 'address', METHOD => 'delete'
describe 'student'

10. 统计 student 表行数(要求加速);用页面浏览计数器 counters 表中 page1 的 pv 并自增 1。

count 'student', INTERVAL => 100000, CACHE => 5000
incr 'counters','page1','cf:pv',1
get_counter 'counters','page1','cf:pv'

11. 创建命名空间 ns1,在其中建表 ns1:order(列族 info),写入并查询一条数据,最后清理。

create_namespace 'ns1'
list_namespace
create 'ns1:order','info'
put 'ns1:order','20260822001','info:amount','199'
get 'ns1:order','20260822001'
list_namespace_tables 'ns1'
disable 'ns1:order'
drop 'ns1:order'
drop_namespace 'ns1'

12. 对 student 表做快照,模拟误删表后用快照恢复。

snapshot 'student','student_snap_20260822'
list_snapshots
disable 'student'
drop 'student'                              # 误删
clone_snapshot 'student_snap_20260822','student'   # 恢复
count 'student'
delete_snapshot 'student_snap_20260822'

13. 关闭 balancer,把某个 Region 迁移到指定 RegionServer,然后恢复 balancer。

balancer_enabled
balance_switch false
list_regions 'student'
move 'student,95001,1690000000.abc123def.', 'slave2,16020,1724300000000'
balance_switch true
balancer

14. 在低峰期对 student 表执行大合并,并确认合并完成。

flush 'student'
major_compact 'student'
compaction_state 'student'

15. 清空一张已预分区的大表,要求保留原有 Region 分区。

truncate_preserve 'user'
list_regions 'user'
# 若用 truncate 'user',Region 会退化为 1 个,预分区丢失

四、设计题

1. 设计一个"用户订单查询"表的 RowKey:需求是按用户查询其订单列表(按下单时间倒序,取最近 N 条),订单量 10 亿级,集群 10 台 RegionServer。

答:

RowKey = md5(userId).substring(0,2) + userId + (Long.MAX_VALUE - orderTime) + orderId

设计理由:

  • md5(userId).substring(0,2):2 位 16 进制哈希前缀(256 种),把不同用户打散到各 Region 避免热点,且可复现(已知 userId 即可算出前缀,不影响点查与前缀扫描);
  • userId:保证同一用户的订单物理连续,可用 scan STARTROW=前缀+userId 做前缀扫描;
  • Long.MAX_VALUE - orderTime:让最新订单排在最前,配合 LIMIT N 即可高效取最近 N 条,无需反向扫描;
  • orderId:保证唯一性(同一毫秒可能有多单)。
  • 列族只用 1 个(如 d),列名极短(u、a、s)。
  • 预分区:10 台 RS × 3~5 = 32~64 个 Region,按 HexStringSplit 切 32 个(与 2 位 16 进制前缀分布一致): create 'order', {NAME=>'d', COMPRESSION=>'SNAPPY', BLOOMFILTER=>'ROW'}, {NUMREGIONS=>32, SPLITALGO=>'HexStringSplit'}
  • 查询:scan 'order', {STARTROW => prefix+userId, STOPROW => prefix+userId+'~', LIMIT => 20, CACHING => 20}
  • 若还需按订单号直接查,可额外建一张索引表 order_idx(RowKey=orderId,value=主表 RowKey),或用 Phoenix 二级索引。

2. 设计一个 IoT 设备监控指标表:10 万台设备,每台每 10 秒上报 20 个指标,需要查询"某设备某时间段的某指标曲线",数据保留 90 天。

答:

RowKey = salt + deviceId + metricId + timestamp
       salt = hash(deviceId) % 32,2 位定长(如 "07")
       timestamp 用定长 10 位或 8 字节 Long(必须补零,保证字典序=时间序)

设计理由:

  • 时序数据若直接用 timestamp 开头会造成严重写热点(所有写入落在最后一个 Region),因此必须把高基数维度前置;
  • salt + deviceId 保证写入均匀分布到 32 个分区,同时同一设备数据连续;
  • metricId 紧跟其后,使"某设备某指标"的数据物理连续,时间段查询退化为一次高效的范围扫描;
  • timestamp 定长补零放最后,支持 STARTROW/STOPROW 精确框定时间区间。
  • 表设计:1 个列族 d,列名 v(值);VERSIONS => 1(时序数据不需要多版本);TTL => 7776000(90 天)自动过期;COMPRESSION => 'SNAPPY' + DATA_BLOCK_ENCODING => 'FAST_DIFF'(RowKey 高度相似,前缀编码可省大量空间)。
  • 预分区 32 个(与 salt 桶数一致)。
  • 写入用 BufferedMutator 批量异步写,或每 10 秒攒批 table.put(List<Put>)。
  • 优化:可把同一分钟/小时的多个采样点合并成一行的多列(OpenTSDB 做法:RowKey 到小时,列名为秒偏移),把 10 亿行压成千万行,显著减少 KeyValue 数量与存储。
  • 查询:scan {STARTROW => salt+dev+metric+startTs, STOPROW => salt+dev+metric+endTs, CACHING => 1000}。
  • 若需跨设备聚合分析,另建 Hive 外部表映射或用 Spark 读 HBase 做离线计算。

3. 某表写入时单台 RegionServer 的请求量是其他节点的 10 倍,请给出完整的定位与解决方案。

答:

定位步骤:

  1. Master UI(:16010)→ Region Servers,按 Requests 排序确认异常节点;
  2. 进入该节点 RS UI(:16030)→ Regions,按 Write Request Count 排序,找出请求集中的 Region;
  3. Master UI → Table Details 查看该 Region 的 startKey/endKey,或 list_regions '表名',反推热点 RowKey 段;
  4. status 'simple' 确认 Region 数量分布是否也不均衡;
  5. 检查日志中的 responseTooSlow、RegionTooBusyException;
  6. 结合业务确认 RowKey 结构 —— 若 startKey/endKey 是最大的那一段且 RowKey 含自增 ID 或时间戳前缀,即可确诊为单调递增导致的写热点。

应急缓解:

balance_switch true; balancer                    # 先确认不是分布不均
split 'REGION_ENCODED_NAME', 'MID_KEY'           # 手工切开热点 Region
move  'REGION_ENCODED_NAME', 'idle_rs,16020,...' # 迁到空闲节点
# 客户端侧限流、加重试退避;调大该表的 handler 与 block.multiplier

根本解决(RowKey 改造 + 重建表):

  1. 新 RowKey 设计:加哈希前缀(md5(id).substring(0,4) + id)或反转,消除单调性;
  2. 新建表并预分区(Region 数 = RS 数 × 3~5,SPLITALGO 与前缀分布一致),单列族、短列名、SNAPPY 压缩;
  3. 用 MapReduce/Spark 读旧表按新 RowKey 生成 HFile,BulkLoad 导入新表(避免用 put 造成二次冲击);
  4. 双写或校验数据一致(VerifyReplication/count/抽样比对)后切换应用读写到新表;
  5. 观察一段时间确认负载均衡,再 snapshot 旧表后删除;
  6. 后续:关闭自动 major compaction 改低峰执行、监控各 RS 的 Requests/s 与 Region 分布并设置告警阈值(单节点偏离均值 20% 报警)。

出处:厦门大学计算机科学系 · 林子雨《大数据技术原理与应用(第2版)》第 4 章,及课程实验讲义《HBase 分布式数据库》

Department of Computer Science, Xiamen University

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏