HBase 列式存储
什么是 HBase
HBase 是基于 Google Bigtable 论文实现的分布式列式 NoSQL 数据库,运行在 HDFS 之上。它提供对大规模数据的随机、实时读写访问能力,弥补了 HDFS 只支持顺序读写的不足。
核心特点:
- 面向列族(Column Family)的存储模型
- 支持随机读写,毫秒级点查
- 水平扩展,支持 PB 级数据
- 强一致性(同一行数据)
- 适合稀疏数据(空列不占存储)
数据模型
HBase 的数据模型与关系型数据库有本质区别:
Table: user_behavior
┌──────────────┬─────────────────────────┬──────────────────────────┐
│ RowKey │ CF: info │ CF: action │
│ ├──────────┬──────────────┼──────────┬───────────────┤
│ │ name │ age │ login │ purchase │
├──────────────┼──────────┼──────────────┼──────────┼───────────────┤
│ user_001 │ Alice │ 28 │ 2024-01 │ item_123 │
│ user_002 │ Bob │ 35 │ 2024-01 │ │
│ user_003 │ Charlie │ │ │ item_456 │
└──────────────┴──────────┴──────────────┴──────────┴───────────────┘核心概念:
| 概念 | 说明 |
|---|---|
| RowKey | 行键,唯一标识一行,按字典序排序,是唯一的索引 |
| Column Family(列族) | 列的分组,建表时定义,物理上存储在一起 |
| Column Qualifier(列限定符) | 列族内的具体列,可动态添加 |
| Cell | RowKey + CF + Qualifier + Timestamp 唯一确定一个 Cell |
| Timestamp | 每个 Cell 可以有多个版本,通过时间戳区分 |
架构设计
┌─────────────────────────────────────────────────────────────┐
│ HBase 集群 │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ HMaster │ │ ZooKeeper │ │
│ │ - 表管理 │ │ - 存储 HBase 元数据 │ │
│ │ - Region 分配│ │ - Master 选举 │ │
│ │ - 负载均衡 │ │ - RegionServer 监控 │ │
│ └──────────────┘ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ RegionServer 集群 │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ RegionServer1 │ │ RegionServer2 │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ │ │ Region A │ │ │ │ Region B │ │ │ │
│ │ │ │ MemStore │ │ │ │ MemStore │ │ │ │
│ │ │ │ HFile │ │ │ │ HFile │ │ │ │
│ │ │ └───────────┘ │ │ └───────────┘ │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ HDFS │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘Region
Region 是 HBase 的基本数据分片单位:
- 一张表按 RowKey 范围切分成多个 Region
- 每个 Region 由一个 RegionServer 负责
- Region 过大时自动分裂(Split)
MemStore & HFile
写入流程:
写入请求
│
▼
WAL(Write-Ahead Log)预写日志 ← 保证持久性
│
▼
MemStore(内存缓冲区)
│
│ 达到阈值(默认 128MB)
▼
HFile(HDFS 上的 SSTable 格式文件)
│
│ 定期 Compaction(合并)
▼
更少、更大的 HFileCompaction
- Minor Compaction:合并少量小 HFile,不删除过期数据
- Major Compaction:合并所有 HFile,删除过期版本和标记删除的数据(生产环境建议手动触发,避免影响业务)
RowKey 设计
RowKey 设计是 HBase 性能的关键,直接影响数据分布和查询效率。
设计原则
- 唯一性:RowKey 必须唯一标识一行
- 长度适中:建议 10-100 字节,过长浪费存储和内存
- 散列性:避免热点,数据均匀分布到各 Region
- 业务相关:根据查询模式设计,支持范围扫描
热点问题与解决方案
问题:如果 RowKey 是递增的时间戳,所有写入都集中在最后一个 Region。
时间戳 RowKey(有热点):
20240101000001
20240101000002
20240101000003 ← 所有写入都在这里解决方案 1:加盐(Salting)
在 RowKey 前加随机前缀:
a_20240101000001
b_20240101000002
c_20240101000003解决方案 2:哈希散列
java
// 取 MD5 前 4 位作为前缀
String rowKey = MD5(userId).substring(0, 4) + "_" + userId;
// 例:a3f2_user_001解决方案 3:反转
原始:20240101000001
反转:100000101042 ← 末尾变化大,分布均匀典型 RowKey 设计案例
用户行为日志(按用户查询所有行为):
userId_timestamp
user_001_20240101120000
user_001_20240101130000
user_002_20240101120000时序数据(按时间范围查询):
metricName_timestamp
cpu_usage_20240101120000
cpu_usage_20240101120001Java API 示例
java
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
public class HBaseDemo {
public static void main(String[] args) throws Exception {
// 创建连接
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "zk1,zk2,zk3");
Connection connection = ConnectionFactory.createConnection(conf);
// 获取表
Table table = connection.getTable(TableName.valueOf("user_behavior"));
// ===== 写入 =====
Put put = new Put(Bytes.toBytes("user_001"));
put.addColumn(
Bytes.toBytes("info"), // 列族
Bytes.toBytes("name"), // 列限定符
Bytes.toBytes("Alice") // 值
);
put.addColumn(
Bytes.toBytes("info"),
Bytes.toBytes("age"),
Bytes.toBytes("28")
);
table.put(put);
// ===== 读取(点查) =====
Get get = new Get(Bytes.toBytes("user_001"));
get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
Result result = table.get(get);
byte[] value = result.getValue(
Bytes.toBytes("info"),
Bytes.toBytes("name")
);
System.out.println("name: " + Bytes.toString(value));
// ===== 范围扫描 =====
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("user_001"));
scan.withStopRow(Bytes.toBytes("user_010"));
scan.addFamily(Bytes.toBytes("info"));
// 过滤器:只返回 name 列
scan.setFilter(new SingleColumnValueFilter(
Bytes.toBytes("info"),
Bytes.toBytes("age"),
CompareOperator.GREATER,
Bytes.toBytes("25")
));
ResultScanner scanner = table.getScanner(scan);
for (Result r : scanner) {
System.out.println(Bytes.toString(r.getRow()));
}
scanner.close();
// ===== 删除 =====
Delete delete = new Delete(Bytes.toBytes("user_001"));
table.delete(delete);
table.close();
connection.close();
}
}HBase Shell 常用命令
bash
# 进入 HBase Shell
hbase shell
# 创建表(指定列族)
create 'user_behavior', 'info', 'action'
# 创建表(带配置)
create 'user_behavior', {NAME => 'info', VERSIONS => 3, TTL => 86400}
# 查看表结构
describe 'user_behavior'
# 写入数据
put 'user_behavior', 'user_001', 'info:name', 'Alice'
put 'user_behavior', 'user_001', 'info:age', '28'
# 读取数据
get 'user_behavior', 'user_001'
get 'user_behavior', 'user_001', 'info:name'
# 扫描表
scan 'user_behavior'
scan 'user_behavior', {STARTROW => 'user_001', STOPROW => 'user_010'}
scan 'user_behavior', {LIMIT => 10}
# 删除数据
delete 'user_behavior', 'user_001', 'info:name'
deleteall 'user_behavior', 'user_001'
# 统计行数
count 'user_behavior'
# 删除表
disable 'user_behavior'
drop 'user_behavior'
# 查看 Region 分布
status 'detailed'性能调优
读优化
xml
<!-- hbase-site.xml -->
<!-- 增大 BlockCache(默认 40% 堆内存) -->
<property>
<name>hfile.block.cache.size</name>
<value>0.4</value>
</property>
<!-- 开启 Bloom Filter(减少不必要的磁盘读取) -->
<!-- 在建表时指定 -->bash
# 建表时开启 Bloom Filter
create 'user_behavior', {NAME => 'info', BLOOMFILTER => 'ROW'}写优化
java
// 批量写入(关闭自动 flush)
table.setAutoFlushTo(false);
table.setWriteBufferSize(8 * 1024 * 1024); // 8MB
List<Put> puts = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Put put = new Put(Bytes.toBytes("row_" + i));
put.addColumn(...);
puts.add(put);
}
table.put(puts);
table.flushCommits();预分区
避免初始只有一个 Region 导致热点:
bash
# 创建表时预分区(按 RowKey 范围)
create 'user_behavior', 'info', SPLITS => ['user_100', 'user_200', 'user_300']
# 或使用十六进制前缀预分区
create 'user_behavior', 'info', {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}