前置问题与边界

一次 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
hbase shell
1
create_namespace 'lab'
1
create 'lab:t04_write_path', 'cf'
1
put 'lab:t04_write_path', 'row-1', 'cf:q', 'v1'
1
get 'lab:t04_write_path', 'row-1'
1
flush 'lab:t04_write_path'
1
hdfs dfs -ls -R /hbase/data/lab/t04_write_path
1
put 'lab:t04_write_path', 'row-1', 'cf:q', 'v2'
1
get 'lab:t04_write_path', 'row-1'
1
flush 'lab:t04_write_path'
1
major_compact 'lab:t04_write_path'

实验观察目标:第一次 put 后即使尚未 flush,get 也应通过 MemStore 路径看到可见值;flush 后 HDFS 目录出现 HFile;第二次 put 产生新版本或覆盖语义,读路径需要合并 MemStore 与 StoreFile。文件数量和压缩结果取决于环境,不能写成固定输出。

清理命令:

1
disable 'lab:t04_write_path'
1
drop 'lab:t04_write_path'
1
drop_namespace 'lab'

Java Public API 示例

UNVERIFIED_RUNTIME:以下代码只使用 HBase 2.6 公共 client API,未连接真实集群执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.nio.charset.StandardCharsets;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Durability;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;

public final class WritePathExample {
public static void main(String[] args) throws Exception {
Configuration conf = HBaseConfiguration.create();
try (Connection connection = ConnectionFactory.createConnection(conf);
Table table = connection.getTable(TableName.valueOf("lab:t04_write_path"))) {
byte[] row = "row-1".getBytes(StandardCharsets.UTF_8);
byte[] family = "cf".getBytes(StandardCharsets.UTF_8);
byte[] qualifier = "q".getBytes(StandardCharsets.UTF_8);

Put put = new Put(row);
put.addColumn(family, qualifier, "v1".getBytes(StandardCharsets.UTF_8));
put.setDurability(Durability.USE_DEFAULT);
table.put(put);

Get get = new Get(row);
get.addColumn(family, qualifier);
Result result = table.get(get);
System.out.println(result.size());
}
}
}

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 取决于策略和状态。

练习

  1. 将一次 Put 成功前后的状态按 WAL、MemStore、MVCC、HFile 标出来。

  2. 解释为什么未 flush 的数据仍可能被 Get 读到。

  3. 比较默认 durability 和跳过 WAL 的崩溃恢复差异。

  4. 分析一个频繁 flush 的表会如何影响 StoreFile 数量和读路径。

  5. 说明客户端超时时为什么不能简单判断写入失败。

系列导航

篇号 主题 状态
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 与设计边界

参考资料