深入 HBase 04 - 写入路径:WAL、MVCC、MemStore 与 flush
前置问题与边界
一次 Put 从客户端发出后,不会直接变成 HFile。HBase 写路径先把 mutation 交给目标 RegionServer,在 Region 内完成行锁、WAL、MVCC、MemStore 更新和返回协议;随后由 flush 把 MemStore snapshot 写成 HDFS 上的 HFile。HFile 之后还会被 compaction 合并。
本篇回答一个核心问题:Put 从客户端到 HFile 经历哪些状态,何时对读取可见。BlockCache 和 HFile 读路径放到第 05、06 篇;compaction 清理旧版本和 Delete 放到第 07 篇;RegionServer 崩溃恢复放到第 11 篇。
HBase 2.6 的实现细节需要谨慎表述。WAL 是否同步、何时返回,受 mutation durability 和表配置影响。flush policy 可以选择哪些 Store 需要 flush,不能笼统写成任何 flush 都必然把 Region 内所有 column family 同时刷盘。
写入路径图
sequenceDiagram
participant C as Client
participant RS as RegionServer
participant R as HRegion
participant W as WAL
participant M as MemStore
participant H as HFile/HDFS
C->>RS: Table.put(Put)
RS->>R: route to Region
R->>R: row lock and validation
R->>W: append WALEdit
R->>W: sync according to durability
R->>M: apply cells to MemStore
R->>R: complete MVCC write entry
R-->>C: success or exception
M->>H: flush snapshot later
这张图省略了大量锁和异常分支,但保留了关键顺序:成功返回前,mutation 必须满足当前 durability 设置和 Region 内可见性推进;HFile 是后续 flush 结果,不是每次 Put 的同步落点。
Put:单行 mutation
HBase 公共 API 中,Put 用于对单行执行写入。它携带 row key,并通过 addColumn(family, qualifier, value) 或带 timestamp 的重载方法增加 Cell。多个 family 和 qualifier 可以放进同一个 Put,只要 row key 相同。
这个边界很重要。Put 的单行原子性不能扩展为跨行事务。多个 Put 的 batch 可以提高提交效率,但不自动获得跨行 all-or-nothing 语义。第 10 篇会展开 CheckAndMutate、Increment、Append 和跨行限制。
WAL:先记录可恢复事实
WAL 是 Write-Ahead Log。RegionServer 在内存结构更新前记录可恢复的 edit,用于崩溃后 replay。WAL 通常写在 HDFS 上,由 HDFS 负责底层副本和持久化。
durability 决定 WAL 同步强度。默认强 durability 会让写入在返回前同步到 WAL;跳过 WAL 或降低同步强度可以减少延迟,但会扩大崩溃丢失窗口。文章不能把“写入返回”无条件等同于“WAL 已按最强模式 fsync”,也不能把“写入返回”写成“数据已进入 HFile”。
MVCC:读写可见性的分界
MVCC 在写路径里承担可见性排序。Region 为写入分配 write entry,读请求持有 read point;只有完成到相应阶段的写入才对读可见。这样,读路径在 MemStore 和 StoreFile 合并结果时,可以过滤掉对当前 read point 不可见的 Cell。
MVCC 不是关系数据库多语句事务。它在 HBase Region 内为读写提供有序可见性,配合单行原子性保证读者不会看到同一行 mutation 的半成品。跨行事务边界仍然不存在。
MemStore:内存中的有序增量
MemStore 保存尚未 flush 的有序 Cell。Put 写入 MemStore 后,新的读请求可以在 MVCC 允许的条件下读到它,即使它还没有进入 HFile。读取合并路径会同时考虑 MemStore 和 StoreFile,因此“未 flush”不等于“不可读”。
MemStore 变大后会触发 flush。flush 会先形成 snapshot,使新写入可以继续进入新的 active MemStore;snapshot 被写成 HFile 后,旧 snapshot 才能释放。这个过程把大量随机写转换成 HDFS 上的顺序文件写。
Flush:从内存视图到不可变文件
flush 的产物是 HFile。每个 column family 对应 Store,Store 下有 MemStore 和 StoreFile。HBase 2.6 的 FlushPolicy 用来决定 flush region 时哪些 Store 需要 flush,存在 FlushAllStoresPolicy 和 FlushLargeStoresPolicy 等实现。因此准确说法是:flush 发生在 Region/Store 管理范围内,具体刷哪些 Store 由策略和状态决定。
flush 后 StoreFile 数量上升,读路径可能需要检查更多 HFile。Compaction 会在后台把多个 StoreFile 合并,减少未来读放大,并在条件满足时清理旧版本和 tombstone。flush 解决“内存数据落成 HFile”,不负责独自完成空间回收。
关键对象和状态
| 状态 | 数据位置 | 对读取是否可见 | 崩溃恢复依据 |
|---|---|---|---|
| Client 构造 Put | 客户端内存 | 不可见 | 客户端重试 |
| 到达 RegionServer | RPC 请求 | 不可见或处理中 | 客户端超时/异常语义 |
| WAL append/sync | WAL | 取决于后续 MVCC 完成 | WAL replay |
| MemStore update | RegionServer 内存 | 完成 MVCC 后可见 | WAL replay + MemStore 现状 |
| flush snapshot | MemStore snapshot | 旧读点仍可读 | WAL + snapshot 写入状态 |
| HFile 写成 | HDFS HFile | 可由 StoreFileScanner 读取 | HDFS 副本 + Store 元数据 |
| compaction 后 | 新 HFile | 按版本和 tombstone 规则可见 | 新 StoreFile |
最小实验
UNVERIFIED_RUNTIME:当前任务环境无目标 HBase 集群。以下步骤只描述可复现实验,不提供伪造 metrics、日志或文件数量。
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
实验观察目标:第一次 put 后即使尚未 flush,get 也应通过 MemStore 路径看到可见值;flush 后 HDFS 目录出现 HFile;第二次 put 产生新版本或覆盖语义,读路径需要合并 MemStore 与 StoreFile。文件数量和压缩结果取决于环境,不能写成固定输出。
清理命令:
1 | |
1 | |
1 | |
Java Public API 示例
UNVERIFIED_RUNTIME:以下代码只使用 HBase 2.6 公共 client API,未连接真实集群执行。
1 | |
Durability.USE_DEFAULT 让 mutation 使用表或集群默认 durability。若改成跳过 WAL 的模式,崩溃恢复边界会变化;这类设置只能在明确接受数据丢失风险的场景使用。
失败恢复
故障分支需要按状态拆开。Put 尚未到达 RegionServer,客户端只拥有重试问题;WAL 未成功并且客户端收到异常,服务端不应承诺写入成功;WAL 已满足 durability 但 MemStore 尚未 flush,RegionServer 崩溃后依赖 WAL replay;HFile 已写成后,恢复依赖 HDFS 文件和 Store 元数据。
客户端超时是最麻烦的边界。超时不等于服务端一定失败,也不等于服务端一定成功。幂等性、重试和 CheckAndMutate 会在第 09、10 篇继续展开。
工程迁移
| HBase 写路径 | 通用问题 | 迁移提醒 |
|---|---|---|
| WAL | 返回前是否有可恢复日志 | durability 是延迟与丢失窗口的开关 |
| MVCC | 读者看到哪个写入序列 | 可见性不是跨行事务 |
| MemStore | 随机写如何进入内存有序结构 | 内存压力会变成 flush 压力 |
| Flush | 内存如何变成不可变文件 | flush 可能增加读放大 |
| Compaction | 文件数量如何收敛 | 后台 I/O 会影响前台延迟 |
[PATTERN] 写优化系统常用“WAL 保恢复、内存保低延迟、不可变文件保顺序写、后台合并保读成本”的组合。调优时不能只看写入返回速度,还要看恢复窗口和后续读放大。
常见误解
误解一:Put 成功就说明数据已经在 HFile。Put 成功只说明写入协议按 durability 和可见性要求完成;HFile 来自后续 flush。
误解二:MVCC 等于跨行事务。MVCC 管可见性序列,普通 HBase mutation 的强边界仍是单行。
误解三:跳过 WAL 只是性能优化。跳过 WAL 会改变崩溃恢复语义。
误解四:flush 会清理旧版本和 Delete。flush 只是把 MemStore snapshot 写成 HFile;旧版本和 tombstone 清理由 compaction 条件决定。
误解五:flush 必然刷 Region 内所有 Store。HBase 2.6 有 FlushPolicy,具体选择哪些 Store 取决于策略和状态。
练习
-
将一次
Put成功前后的状态按 WAL、MemStore、MVCC、HFile 标出来。 -
解释为什么未 flush 的数据仍可能被
Get读到。 -
比较默认 durability 和跳过 WAL 的崩溃恢复差异。
-
分析一个频繁 flush 的表会如何影响 StoreFile 数量和读路径。
-
说明客户端超时时为什么不能简单判断写入失败。
系列导航
| 篇号 | 主题 | 状态 |
|---|---|---|
| 00 | 导读:row key 决定数据位置 | |
| 01 | 数据模型:Cell、Column Family 与版本 | |
| 02 | 集群架构:HMaster、RegionServer、ZooKeeper 与 hbase:meta | |
| 03 | Schema 与 row key 设计 | 上一篇 |
| 04 | 写入路径:WAL、MVCC、MemStore 与 flush | 本篇 |
| 05 | 读取路径:BlockCache、Bloom Filter 与 HFile block | 下一篇 |
| 06 | HFile 内部结构 | |
| 07 | Compaction、TTL、版本与 Delete | |
| 08 | Region 生命周期:split、merge 与 assignment | |
| 09 | Client 路由与重试 | |
| 10 | 行级原子性与一致性边界 | |
| 11 | RegionServer 崩溃与 WAL 恢复 | |
| 12 | 跨集群 Replication | |
| 13 | Snapshot、Backup 与恢复 | |
| 14 | Get、Scan、Filter 与分页 | |
| 15 | BufferedMutator 与 Bulk Load | |
| 16 | Coprocessor、Endpoint 与 Phoenix 边界 | |
| 17 | Kerberos、RPC 保护与 ACL | |
| 18 | Metrics、hbtop、Compaction 与性能调优 | |
| 19 | HBase 3.0 与设计边界 |
参考资料
- Apache HBase 2.6 Client Package API:https://hbase.apache.org/2.6/apidocs/org/apache/hadoop/hbase/client/package-summary.html
- Apache HBase 2.6 Put API:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Put.html
- Apache HBase 2.6 Durability API:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Durability.html
- Apache HBase 2.6 FlushPolicy Developer API:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/regionserver/FlushPolicy.html
- Apache HBase RegionServer:https://hbase.apache.org/docs/architecture/regionserver/
- Apache HBase ACID Semantics:https://hbase.apache.org/acid-semantics/
- Apache HBase HFile Format:https://hbase.apache.org/docs/hfile-format/
