大数据系列(八) HBase:海量数据的随机读写怎么破?
HBase:海量数据的"随机读写"怎么破?
大数据系列第 8 篇:HDFS 适合批量读,但想要像数据库那样随机查一条数据?HBase 来救场。
从一个矛盾说起
前面咱们聊了 HDFS,它是个很好的分布式文件系统,适合存大文件、批量读取。但有个场景它搞不定:
我想快速查一条数据。
比如:
- 用户打开 App,要查他的个人信息(根据 user_id 查)
- 电商系统要根据订单号查订单详情
- 物流系统要根据快递单号查物流轨迹
这些场景的特点是:数据量巨大(几亿、几十亿条),但每次只查其中一条或几条。
你用 HDFS 试试?HDFS 的设计是"批量顺序读",你要从几十亿条记录里找一条,得扫描整个文件,慢得要死。
用 MySQL?单机存不下这么多数据,分库分表后跨库查询又麻烦。
这时候就需要一个能支持海量数据随机读写的分布式数据库——HBase。
HBase 是什么?
HBase 是 Apache 的分布式列式数据库,基于 Google 的 Bigtable 论文实现。它的核心特点是:
- 海量数据存储:PB 级别不是问题
- 高并发随机读写:毫秒级延迟查单条数据
- 线性扩展:加机器就能扩容
- 强一致性:读到的数据一定是最新的
但注意:HBase 不是关系型数据库,不支持 SQL、不支持事务、不支持复杂查询。它只擅长一件事:根据主键(RowKey)快速读写数据。
HBase 的数据模型:跟 MySQL 完全不一样
如果你用 MySQL 的思维来理解 HBase,会完全懵掉。咱们从头来:
MySQL 的模型(行式存储)
┌─────────────────────────────────────────────────────────┐ │ MySQL 表(行式存储) │ ├─────────────────────────────────────────────────────────┤ │ │ │ id │ name │ age │ city │ phone │ │ │ ───┼───────┼─────┼─────────┼─────────────┤ │ │ 1 │ 张三 │ 25 │ 北京 │ 13800138000 │ │ │ 2 │ 李四 │ 30 │ 上海 │ 13900139000 │ │ │ 3 │ 王五 │ 28 │ 广州 │ 13700137000 │ │ │ │ │ 特点:按行存储,一行数据存在一起 │ │ 查询:SELECT * FROM users WHERE id = 1 │ │ → 找到 id=1 的那一行,读出所有列 │ │ │ └─────────────────────────────────────────────────────────┘HBase 的模型(列式存储)
┌─────────────────────────────────────────────────────────────────┐ │ HBase 数据模型(列式存储) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ HBase 的表结构: │ │ │ │ RowKey │ Column Family: info │ Column Family: contact │ │ │──────────────────────────────│────────────────────────│ │ │ name │ age │ city │ phone │ email │ │ ───────┼───────┼───────┼──────────────┼──────────┼─────────────┤ │ user1 │ 张三 │ 25 │ 北京 │ 138... │ zhang@qq.com│ │ user2 │ 李四 │ 30 │ 上海 │ 139... │ li@qq.com │ │ user3 │ 王五 │ 28 │ 广州 │ 137... │ wang@qq.com │ │ │ │ 关键概念: │ │ • RowKey:主键,唯一标识一行,按字典序排序存储 │ │ • Column Family:列族,列的集合,表定义时确定 │ │ • Column:列,属于某个列族,可以动态添加 │ │ • Cell:具体的数据单元,由 {rowkey, column, version} 唯一标识 │ │ │ │ 特点: │ │ • 按列族存储,不同列族的数据存在不同文件里 │ │ • 列可以动态添加,不需要预先定义 │ │ • 每个 Cell 可以有多个版本(时间戳) │ │ │ └─────────────────────────────────────────────────────────────────┘HBase 的核心概念
1. RowKey(行键)
RowKey 是 HBase 最重要的设计。所有数据按 RowKey 的字典序排序存储,查询时必须指定 RowKey(或 RowKey 范围)。
RowKey 设计得好不好,直接决定了 HBase 的性能。
2. Column Family(列族)
列的集合,表定义时就要确定。不同列族的数据存储在不同的 HFile 里,可以独立设置压缩、缓存等属性。
一个表的列族不要太多,通常 1-3 个。列族太多会影响性能。
3. Column Qualifier(列标识符)
列族下的具体列,可以动态添加。比如info:name、info:age、contact:phone。
4. Cell(数据单元)
由{rowkey, column family, column qualifier, timestamp}唯一标识。每个 Cell 可以存多个版本的数据(通过 timestamp 区分)。
5. Namespace(命名空间)
类似 MySQL 的 database,用于逻辑隔离不同的表。
HBase 的架构:怎么做到随机读写?
┌─────────────────────────────────────────────────────────────────┐ │ HBase 架构(简化版) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ HMaster("大管家") │ │ │ │ │ │ │ │ • 管理表的创建、删除、修改 │ │ │ │ • 分配 Region 给 RegionServer │ │ │ │ • 监控 RegionServer 健康状态 │ │ │ │ • Region 分裂/合并的决策 │ │ │ │ │ │ │ │ 注意:HMaster 不处理读写请求,只负责管理! │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ │ 管理指令 │ │ │ │ │ ┌───────────────────────────┼───────────────────────────┐ │ │ │ RegionServer 集群("干活的") │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ RS 1 │ │ RS 2 │ │ RS 3 │ │ RS 4 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Region A│ │ Region C│ │ Region E│ │ Region G│ │ │ │ │ │ Region B│ │ Region D│ │ Region F│ │ Region H│ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ 每个 RegionServer 管理多个 Region │ │ │ │ 每个 Region 是一段连续的 RowKey 范围 │ │ │ │ 客户端直接和 RegionServer 通信,不经过 HMaster │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ ZooKeeper:协调服务,存储元数据,选举 HMaster │ │ HDFS:底层存储,RegionServer 把数据文件存在 HDFS 上 │ │ │ └─────────────────────────────────────────────────────────────────┘Region:数据分片
HBase 表的数据按 RowKey 范围划分为多个Region,每个 Region 负责一段连续的 RowKey:
RowKey 范围: Region 1: ["", "user1000") → 存在 RegionServer 1 Region 2: ["user1000", "user2000") → 存在 RegionServer 2 Region 3: ["user2000", "user3000") → 存在 RegionServer 3 Region 4: ["user3000", "") → 存在 RegionServer 4 当某个 Region 数据太多时,会分裂成两个 Region 当 Region 太小时,会合并客户端读写时,先查 Meta 表(存在 ZooKeeper 里),找到 RowKey 属于哪个 Region,然后直接找对应的 RegionServer。不需要经过 HMaster,所以 HMaster 挂了不影响读写。
LSM-Tree:HBase 的存储引擎
HBase 能做到高吞吐写入,核心在于它的存储引擎——LSM-Tree(Log-Structured Merge-Tree)。
传统数据库的写入方式(B+树)
MySQL 的 InnoDB 用 B+树存储数据。写入时:
- 找到数据所在页
- 如果页有空位,直接插入
- 如果页满了,分裂页,调整树结构
- 更新索引
问题:随机写入时,磁盘寻道开销大,性能差。
LSM-Tree 的写入方式:先写内存,再批量刷盘
┌─────────────────────────────────────────────────────────────────┐ │ LSM-Tree 写入流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 写入请求 │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ MemStore(内存)│ ← 先写到内存里,速度飞快 │ │ │ │ │ │ │ 数据按 RowKey │ │ │ │ 排序存储 │ │ │ └────────┬────────┘ │ │ │ MemStore 满了(默认 128MB) │ │ ▼ │ │ ┌─────────────────┐ │ │ │ Flush 到磁盘 │ ← 变成 HFile(不可变的排序文件) │ │ │ │ │ │ │ HFile 1 │ │ │ │ HFile 2 │ │ │ │ HFile 3 │ │ │ └─────────────────┘ │ │ │ │ │ │ HFile 太多了,触发 Compaction(合并) │ │ ▼ │ │ ┌─────────────────┐ │ │ │ Compaction │ ← 多个小 HFile 合并成一个大 HFile │ │ │ (后台异步) │ 清理过期版本、删除标记 │ │ │ │ │ │ │ HFile_big │ ← 合并后的大文件 │ │ └─────────────────┘ │ │ │ │ 读取流程: │ │ 1. 先查 MemStore(内存) │ │ 2. 再查 BlockCache(读缓存) │ │ 3. 最后查 HFile(磁盘) │ │ │ └─────────────────────────────────────────────────────────────────┘LSM-Tree 的核心思想:
- 写入只追加,不修改:数据先写到内存(MemStore),满了刷成不可变的 HFile
- 批量顺序写:刷盘时是顺序写,磁盘性能高
- 后台合并:多个小 HFile 定期合并成大文件,减少读时需要扫描的文件数
读取时可能需要查多个 HFile + MemStore,所以 HBase 的读性能不如写性能。这也是 LSM-Tree 的 trade-off。
RowKey 设计:HBase 的灵魂
RowKey 设计是 HBase 使用中最重要的环节,设计不好,性能差到怀疑人生。
设计原则
1. 散列性:避免热点
如果 RowKey 设计得太集中,会导致某些 RegionServer 特别忙,其他的很闲。
反例:RowKey = 自增 ID(如 0001, 0002, 0003...) Region 1: ["", "1000") ← 新数据全写这里,热点! Region 2: ["1000", "2000") Region 3: ["2000", "3000") 新数据总是往最后一个 Region 写,那个 RegionServer 被打爆解决方案:
- 加盐(Salting):RowKey 前加随机前缀
原始 RowKey: user12345 加盐后: a_user12345, b_user67890, c_user11111... 前缀随机,数据分散到不同 Region - 哈希:对 RowKey 取哈希值作为前缀
RowKey = MD5(user_id)[0:4] + user_id - 反转:把时间戳反转
原始: 20240101120000(时间戳) 反转: 000002110104102 这样新数据不会集中在末尾
2. 唯一性:不能重复
RowKey 是主键,必须唯一。
3. 长度短:节省存储
RowKey 会重复存储在每条记录里,太长会浪费空间。建议控制在 16 字节以内。
实际案例
场景:存储用户订单数据,经常按用户 ID 查询
方案 1(简单):RowKey = user_id 问题:如果某些用户订单特别多,形成热点 方案 2(推荐):RowKey = hash(user_id)[0:4] + user_id + order_time 优点: • hash 前缀保证散列性,避免热点 • user_id 保证同一用户的数据相邻,范围查询快 • order_time 保证同一用户的订单按时间排序 示例: RowKey = "a3f2" + "user12345" + "20240101120000" = "a3f2user1234520240101120000"HBase 的适用场景
| 适合的场景 | 不适合的场景 |
|---|---|
| 海量数据的随机读写(PB 级) | 复杂查询(多表 Join、子查询) |
| 高并发低延迟查询(毫秒级) | 事务处理(不支持 ACID) |
| 写密集型应用(日志、时序数据) | 小数据量( overhead 太大) |
| 需要版本控制的数据 | 需要 SQL 接口(用 Phoenix) |
| 稀疏矩阵存储(列可以动态添加) | 频繁更新的数据(虽然有版本,但不如数据库灵活) |
典型应用场景:
- 用户画像:根据 user_id 查用户的标签、行为数据
- 消息/Feeds 流:存储用户的时间线数据
- 物联网时序数据:存储设备传感器数据
- 推荐系统:存储用户-物品的交互数据
HBase vs Cassandra:两个列式数据库怎么选?
| 维度 | HBase | Cassandra |
|---|---|---|
| 架构 | 主从(HMaster + RegionServer) | 无主(P2P,所有节点平等) |
| 一致性 | 强一致(CP) | 可调(AP 默认,可配成 CP) |
| 依赖 | 依赖 HDFS、ZooKeeper | 独立运行,无外部依赖 |
| 查询方式 | 只能通过 RowKey 查 | 支持二级索引、CQL(类 SQL) |
| 多数据中心 | 原生不支持 | 原生支持多数据中心复制 |
| 运维复杂度 | 较高(多个组件) | 较低 |
| 适用场景 | 与 Hadoop 生态集成 | 独立部署、多地域、高可用 |
简单选型:
- 已经在用 Hadoop 生态(HDFS、YARN)→HBase
- 需要独立部署、跨数据中心 →Cassandra
- 需要类 SQL 查询 → Cassandra 的 CQL 更友好
小结
今天咱们聊了 HBase:
- 定位:海量数据的分布式列式数据库,擅长随机读写
- 数据模型:RowKey + Column Family + Column,跟关系型数据库完全不同
- 架构:HMaster 管理元数据,RegionServer 处理读写,Region 是数据分片单位
- LSM-Tree:先写内存再批量刷盘,写性能高,读性能相对弱
- RowKey 设计:最关键的设计环节,要考虑散列性、唯一性、长度
- 适用场景:用户画像、时序数据、Feeds 流等海量随机读写场景
HBase 的设计哲学是:牺牲一部分功能(SQL、事务、复杂查询),换取海量数据下的高并发随机读写能力。在合适的场景下,它是无可替代的。
你用过 HBase 吗?RowKey 是怎么设计的?有没有遇到过热点问题?欢迎聊聊~
