深入剖析 HBase 数据写入的完整链路
深入剖析 HBase 数据写入的完整链路
HBase 作为 Hadoop 生态中的分布式列式存储系统,被广泛应用于海量数据存储场景。当我们执行一条简单的 put 操作时,背后经历了怎样的旅程?本文将带你深入 HBase 的内部,完整剖析一次数据写入的全链路过程。
写入流程概览
让我们先从宏观角度看一次完整的写入流程:
客户端 Put 请求
↓
1. 客户端路由(定位 RegionServer)
↓
2. 写入 WAL(Write-Ahead Log)
↓
3. 写入 MemStore(内存)
↓
4. 返回客户端(写入成功)
↓
5. MemStore 刷写到磁盘(HFile)
↓
6. HFile 合并(Compaction)
看起来很简单?让我们逐步深入每个环节的细节。
阶段一:客户端路由与定位
1.1 元数据查询
当客户端发起一次写入时,首先需要确定数据应该写到哪个 RegionServer:
// 客户端代码示例
Connection connection = ConnectionFactory.createConnection(config);
Table table = connection.getTable(TableName.valueOf("user_table"));
Put put = new Put(Bytes.toBytes("user001"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"),
Bytes.toBytes("张三"));
table.put(put);
路由流程:
1. 客户端查询 ZooKeeper
↓ 获取 hbase:meta 表所在的 RegionServer
2. 访问 hbase:meta 表
↓ 根据 RowKey 查找对应的 Region
↓ 获取 Region 所在的 RegionServer 地址
3. 缓存路由信息
↓ 客户端本地缓存(MetaCache)
↓ 避免每次都查询 meta 表
1.2 hbase:meta 表结构
meta 表存储了所有用户表的 Region 信息:
RowKey: [table_name],[start_key],[region_id]
示例: user_table,user001,1234567890
列族 info 包含:
- regioninfo: Region 的详细信息(序列化的 HRegionInfo)
- server: 当前服务这个 Region 的 RegionServer
- serverstartcode: RegionServer 启动时间戳
1.3 Region 分裂与路由更新
如果客户端缓存过期(Region 发生了分裂或迁移):
客户端请求 → RegionServer 返回 NotServingRegionException
↓
清空本地缓存
↓
重新查询 meta 表
↓
更新路由信息
↓
重试写入请求
阶段二:RegionServer 接收请求
2.1 请求处理入口
数据到达 RegionServer 后的处理流程:
// RegionServer 内部处理(简化)
public void put(Put put) {
// 1. 获取对应的 Region
HRegion region = getRegion(put.getRow());
// 2. 执行写入
region.put(put);
}
2.2 Region 层面的处理
1. 获取行锁(Row Lock)
↓ 保证同一行的写入串行化
2. 检查约束
↓ 版本数限制、TTL、列族是否存在等
3. 开始写入流程
阶段三:写入 WAL(关键!)
WAL(Write-Ahead Log)是 HBase 数据可靠性的核心保障。
3.1 WAL 的作用
为什么需要 WAL?
- MemStore 数据在内存中,断电会丢失
- WAL 先持久化到磁盘,保证数据不丢
- 崩溃恢复时可以重放 WAL
3.2 WAL 写入详细流程
// WAL 写入伪代码
public void appendToWAL(WALEdit edit) {
// 1. 构造 WAL Entry
WALKey key = new WALKey(encodedRegionName, tableName,
sequenceId, timestamp);
// 2. 写入 WAL 缓冲区
walWriter.append(new Entry(key, edit));
// 3. 根据配置决定同步策略
if (durability == Durability.SYNC_WAL) {
walWriter.sync(); // 强制刷盘(fsync)
} else if (durability == Durability.FSYNC_WAL) {
walWriter.hsync(); // 更强的同步保证
}
// ASYNC_WAL: 异步刷盘(性能最好,可能丢数据)
}
3.3 WAL 文件结构
WAL 以文件形式存储在 HDFS 上:
/hbase/WALs/regionserver1,16020,1234567890/
├── regionserver1%2C16020%2C1234567890.1234567890001
├── regionserver1%2C16020%2C1234567890.1234567890002
└── regionserver1%2C16020%2C1234567890.1234567890003.meta
每个 WAL 文件包含:
- Header: 版本信息、压缩算法
- WAL Entry: Key + Value
- Key: 表名、Region、SequenceId、时间戳
- Value: 具体的 Mutation(Put/Delete)
- Trailer: 文件尾部信息
3.4 WAL 的持久化级别
HBase 提供了多种持久化级别:
// 1. SKIP_WAL(不推荐,可能丢数据)
put.setDurability(Durability.SKIP_WAL);
// 2. ASYNC_WAL(异步,高性能)
put.setDurability(Durability.ASYNC_WAL);
// 3. SYNC_WAL(默认,平衡性能和可靠性)
put.setDurability(Durability.SYNC_WAL);
// 4. FSYNC_WAL(最高可靠性)
put.setDurability(Durability.FSYNC_WAL);
性能对比:
SKIP_WAL > ASYNC_WAL > SYNC_WAL > FSYNC_WAL
↑ 性能 ↓
↓ 可靠性 ↑
3.5 WAL 滚动与清理
WAL 文件滚动触发条件:
1. 文件大小达到阈值(默认 hbase.regionserver.logroll.period = 1小时)
2. Region 数量变化
3. 手动触发滚动
WAL 文件清理时机:
1. MemStore 刷写完成后,对应的 WAL 可以删除
2. WAL 文件过期(所有数据已持久化到 HFile)
3. 定期清理任务
阶段四:写入 MemStore
WAL 写入成功后,数据写入内存中的 MemStore。
4.1 MemStore 结构
HRegion
├── Store (列族1)
│ └── MemStore
│ ├── Active Segment(当前写入)
│ └── Snapshot Segments(待刷写)
└── Store (列族2)
└── MemStore
├── Active Segment
└── Snapshot Segments
4.2 MemStore 内部数据结构
核心数据结构:ConcurrentSkipListMap
// MemStore 的核心存储
ConcurrentSkipListMap<Cell, Cell> cellSet;
// Cell 的排序规则(字典序):
// RowKey → ColumnFamily → Qualifier → Timestamp(降序) → Type
为什么用跳表?
- 有序存储,支持高效范围查询
- 支持并发写入(ConcurrentSkipListMap)
- 时间复杂度 O(log n)
4.3 写入 MemStore 的过程
// 简化的写入逻辑
public void add(Cell cell) {
// 1. 检查 MemStore 大小
long size = getSize();
if (size > threshold) {
// 触发 flush,将 active segment 转为 snapshot
requestFlush();
}
// 2. 插入 Cell 到跳表
cellSet.put(cell, cell);
// 3. 更新大小统计
incrementSize(cell.getSerializedSize());
}
4.4 MemStore 的内存管理
全局内存限制:
- hbase.regionserver.global.memstore.size = 0.4
(RegionServer 堆内存的 40%)
单个 Region 的限制:
- hbase.hregion.memstore.flush.size = 128MB
(单个 MemStore 达到此大小触发 flush)
阻塞写入:
- hbase.regionserver.global.memstore.size.lower.limit = 0.95
(达到全局限制的 95% 时阻塞写入)
阶段五:返回客户端
此时,数据已经完成了:
- ✅ WAL 持久化(磁盘)
- ✅ MemStore 写入(内存)
可以安全地返回客户端成功响应:
// 释放行锁
rowLock.release();
// 返回成功
return Result.create(cells);
写入延迟分析:
总延迟 = WAL写入时间 + MemStore写入时间 + 网络时间
典型场景:
- WAL (SYNC_WAL): 5-10ms(取决于磁盘)
- MemStore: <1ms(内存操作)
- 网络: 1-5ms
总计: 约 10-20ms
阶段六:MemStore Flush(异步)
6.1 触发 Flush 的条件
1. MemStore 大小达到阈值
- hbase.hregion.memstore.flush.size = 128MB
2. RegionServer 全局内存压力
- 所有 MemStore 总大小超过限制
3. WAL 文件数量过多
- hbase.regionserver.maxlogs(防止恢复时间过长)
4. 定期 Flush
- hbase.regionserver.optionalcacheflushinterval = 1小时
5. Region 关闭或分裂前
6.2 Flush 的详细流程
1. 准备阶段
↓ 获取更新锁(阻止新的写入)
↓ 将 Active Segment 转为 Snapshot
↓ 创建新的 Active Segment
↓ 释放更新锁(恢复写入)
2. 写入阶段
↓ 遍历 Snapshot 中的所有 Cell
↓ 按序写入 HFile
↓ 构建 Bloom Filter
↓ 构建 Block Index
3. 完成阶段
↓ 关闭 HFile Writer
↓ 将 HFile 移动到最终位置
↓ 更新元数据
↓ 删除对应的 WAL
↓ 清空 Snapshot
6.3 HFile 文件格式
Flush 生成的 HFile 是 HBase 在 HDFS 上的存储格式:
HFile 结构(从头到尾):
┌─────────────────────────────────┐
│ Header (Magic Number, Version) │
├─────────────────────────────────┤
│ Data Block 1 │ ← 存储实际的 KeyValue
│ - KeyValue 1 │
│ - KeyValue 2 │
│ - ... │
├─────────────────────────────────┤
│ Data Block 2 │
├─────────────────────────────────┤
│ ... │
├─────────────────────────────────┤
│ Meta Block (Bloom Filter) │ ← 布隆过滤器
├─────────────────────────────────┤
│ Meta Block (TimeRange) │ ← 时间范围信息
├─────────────────────────────────┤
│ FileInfo │ ← 文件元信息
├─────────────────────────────────┤
│ Data Index │ ← Data Block 索引
├─────────────────────────────────┤
│ Meta Index │ ← Meta Block 索引
├─────────────────────────────────┤
│ Trailer (指向各个索引的指针) │
└─────────────────────────────────┘
6.4 HFile 的优化特性
1. Block 压缩
支持的压缩算法:
- NONE(无压缩)
- SNAPPY(速度快,压缩比中等)
- LZO(速度快)
- GZIP(压缩比高,CPU密集)
- LZ4(速度最快)
配置示例:
create 'table', {NAME => 'cf', COMPRESSION => 'SNAPPY'}
2. Bloom Filter
作用:快速判断某个 RowKey 是否在 HFile 中
- ROW: 基于行键过滤
- ROWCOL: 基于行键+列过滤
优势:避免读取不包含目标数据的 HFile
误判率:可配置(默认 1%)
3. Data Block Encoding
减少存储空间的编码方式:
- PREFIX: 前缀压缩(相邻 KeyValue 的公共前缀)
- DIFF: 差异编码
- FAST_DIFF: 快速差异编码(默认)
阶段七:Compaction(合并)
随着 Flush 的进行,HFile 数量不断增加,需要定期合并。
7.1 为什么需要 Compaction?
问题:
1. HFile 数量过多,查询需要扫描多个文件(性能下降)
2. 删除操作只是标记,需要真正清理
3. 过期数据需要清除(TTL)
4. 数据版本需要合并(保留最新的 N 个版本)
7.2 Compaction 类型
Minor Compaction(小合并)
特点:
- 选择少量 HFile 合并(2-10个)
- 不清理删除标记和过期数据
- 速度快,频率高
- 自动触发
触发条件:
- HFile 数量 > hbase.hstore.compaction.min (默认 3)
Major Compaction(大合并)
特点:
- 合并一个 Store 的所有 HFile
- 清理删除标记和过期数据
- 清理多余版本
- 耗时长,I/O密集
- 最终只生成一个 HFile
触发条件:
- 手动触发:hbase shell> major_compact 'table'
- 自动触发:hbase.hregion.majorcompaction (默认 7天)
7.3 Compaction 流程
1. 选择待合并的 HFile
↓ 根据策略选择(ExploringCompactionPolicy)
2. 创建 Scanner
↓ 对选中的 HFile 创建多路归并扫描器
3. 归并写入
↓ 按 Cell 顺序读取
↓ 应用删除标记
↓ 过滤过期数据
↓ 保留最新版本
↓ 写入新的 HFile
4. 原子替换
↓ 更新元数据,指向新 HFile
↓ 删除旧 HFile
7.4 Compaction 调优
// 1. 控制 Compaction 的并发度
hbase.regionserver.thread.compaction.small = 1
hbase.regionserver.thread.compaction.large = 1
// 2. 限流(避免影响读写)
hbase.regionserver.throughput.controller = PressureAwareCompactionThroughputController
// 3. 时间窗口(在业务低峰期执行)
hbase.offpeak.start.hour = 0
hbase.offpeak.end.hour = 6
// 4. 禁用自动 Major Compaction(手动控制)
hbase.hregion.majorcompaction = 0
完整时序图
让我们用一张完整的时序图串联整个流程:
客户端 ZK Meta RS 目标 RS HDFS
│ │ │ │ │
│──查询 meta 位置─→│ │ │ │
│←────返回────────│ │ │ │
│ │ │ │ │
│──查询 Region 位置──────────→│ │ │
│←────返回 RS 地址──────────────│ │ │
│ │ │ │ │
│──Put 请求────────────────────────────→│ │
│ │ │ 获取行锁 │
│ │ │ 写 WAL ─────────→│
│ │ │ │ ←─ack──────│
│ │ │ 写 MemStore │
│←────返回成功──────────────────────────│ │
│ │ │ │ │
│ │ │ (后台异步) │
│ │ │ Flush───────────→│
│ │ │ │ ←──完成────│
│ │ │ │ │
│ │ │ (定期执行) │
│ │ │ Compaction────────→│
│ │ │ │ ←──完成────│
性能优化实践
1. 写入性能优化
批量写入
// 不好的做法
for (Put put : puts) {
table.put(put); // 每次都是一次 RPC
}
// 好的做法
table.put(List<Put> puts); // 批量提交,减少 RPC 次数
禁用 WAL(特定场景)
// 适用于可以容忍数据丢失的场景(如日志、临时数据)
put.setDurability(Durability.SKIP_WAL);
使用批量加载(Bulk Load)
# 对于大规模数据导入,直接生成 HFile
hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles \
/path/to/hfiles tableName
2. Region 预分区
// 避免频繁的 Region 分裂
byte[][] splits = new byte[][] {
Bytes.toBytes("1000"),
Bytes.toBytes("2000"),
Bytes.toBytes("3000")
};
admin.createTable(tableDescriptor, splits);
3. MemStore 调优
<!-- 增大 MemStore 大小,减少 Flush 频率 -->
<property>
<name>hbase.hregion.memstore.flush.size</name>
<value>268435456</value> <!-- 256MB -->
</property>
4. Compaction 策略
生产环境建议:
1. 禁用自动 Major Compaction
2. 在业务低峰期手动触发
3. 使用 StripeCompaction(大表优化)
故障恢复机制
WAL 重放
当 RegionServer 崩溃时:
1. Master 检测到 RS 下线
↓
2. 将该 RS 上的 Region 分配给其他 RS
↓
3. 新 RS 读取对应的 WAL
↓
4. 重放 WAL 中未刷写的数据
↓
5. MemStore 恢复,Region 重新上线
HDFS 的高可用
HBase 依赖 HDFS 的高可用特性:
- 多副本机制(默认 3 副本)
- NameNode HA(主备切换)
- DataNode 故障自动恢复
写入链路的关键指标
监控这些指标有助于发现性能瓶颈:
1. 写入 QPS
- hbase.regionserver.writeRequestCount
2. WAL 同步延迟
- hbase.regionserver.wal.sync.time
3. MemStore 大小
- hbase.regionserver.memStoreSize
4. Flush 队列长度
- hbase.regionserver.flushQueueLength
5. Compaction 队列长度
- hbase.regionserver.compactionQueueLength
6. Block Cache 命中率
- hbase.regionserver.blockCacheHitRatio
总结
HBase 的写入链路看似简单,实则精妙:
- 客户端路由:通过 ZooKeeper 和 meta 表实现智能路由
- WAL 先行:保证数据可靠性,是崩溃恢复的基础
- MemStore 缓冲:内存写入,提供高吞吐
- 异步 Flush:后台刷写,不阻塞前台写入
- 定期 Compaction:优化读性能,清理冗余数据
- LSM-Tree 架构:写优化的经典设计
理解这些机制,不仅能帮助我们:
- 合理设计 RowKey 和表结构
- 选择合适的配置参数
- 定位和解决性能问题
- 应对各种故障场景
下次当你执行 table.put() 时,不妨想想这背后发生的精彩故事。
参考资料
希望这篇文章能帮助你深入理解 HBase 的写入机制。如有任何问题或建议,欢迎讨论!
浙公网安备 33010602011771号