Skip to content

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(列限定符)列族内的具体列,可动态添加
CellRowKey + 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(合并)

更少、更大的 HFile

Compaction

  • Minor Compaction:合并少量小 HFile,不删除过期数据
  • Major Compaction:合并所有 HFile,删除过期版本和标记删除的数据(生产环境建议手动触发,避免影响业务)

RowKey 设计

RowKey 设计是 HBase 性能的关键,直接影响数据分布和查询效率。

设计原则

  1. 唯一性:RowKey 必须唯一标识一行
  2. 长度适中:建议 10-100 字节,过长浪费存储和内存
  3. 散列性:避免热点,数据均匀分布到各 Region
  4. 业务相关:根据查询模式设计,支持范围扫描

热点问题与解决方案

问题:如果 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_20240101120001

Java 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'}

本站内容由 褚成志 整理编写,仅供学习参考