2.1 WAL:先记日志再写内存


文档摘要

2.1 WAL:先记日志再写内存 本节摘要:WAL(Write-Ahead Log,预写日志)是 HBase 写入路径的第一站:数据在进 MemStore 之前先顺序追加到 WAL,保证 RegionServer 宕机后可通过回放恢复未落盘数据。本节讲 WAL 的结构、sync 策略、滚动与切分,以及"异步 WAL 换持久性"的取舍。 为什么必须先记日志 回到写入路径的起点。客户端把 发到某台 RegionServer(怎么找到这台机器,见 1.3 节的寻址链路)。此时数据即将写进 MemStore——但 MemStore 是纯内存结构,机器一断电就没了;而 Flush 成 HFile 是若干秒之后的事。这中间存在一个"已确认写入但还没落盘"的窗口。

2.1 WAL:先记日志再写内存

本节摘要:WAL(Write-Ahead Log,预写日志)是 HBase 写入路径的第一站:数据在进 MemStore 之前先顺序追加到 WAL,保证 RegionServer 宕机后可通过回放恢复未落盘数据。本节讲 WAL 的结构、sync 策略、滚动与切分,以及"异步 WAL 换持久性"的取舍。

为什么必须先记日志

回到写入路径的起点。客户端把 put 'orders', 'u1001-...', 'cf:status', 'PAID' 发到某台 RegionServer(怎么找到这台机器,见 1.3 节的寻址链路)。此时数据即将写进 MemStore——但 MemStore 是纯内存结构,机器一断电就没了;而 Flush 成 HFile 是若干秒之后的事。这中间存在一个"已确认写入但还没落盘"的窗口。

如果没有补救手段,RegionServer 每次宕机都会丢几分钟数据,对一个标榜可靠的存储系统是不可接受的。WAL 的解法直接明了:写 MemStore 之前,先把这次修改顺序追加到一份磁盘日志里。数据落了 HFile 之后,对应日志作废;没落盘就宕机,重启或接管时把日志重放一遍,内存态就恢复了。这正是数据库领域经典的预写日志(Write-Ahead Log)思路——MySQL 的 redo log、PostgreSQL 的 WAL 同源。

代价也很清楚:每次写多一次磁盘 IO。但 WAL 是纯顺序追加,而顺序写在现代磁盘(含 HDD)上的吞吐比随机写高一两个数量级,所以这一步的开销可控——这就是 HBase"写得快"的第一个支柱。

WAL 的结构与一次真实追加

每个 RegionServer 上所有 Region 的写入共享若干 WAL 文件(HBase 1.x 起支持多 WAL,生产上常按 RegionServer 配 2 到 4 个 writer 提高吞吐,2.x 还有基于 Erlang 的异步实现 AsyncFSWAL)。日志条目(WALEdit)的关键字段:

WALEdit ├── 序号 sequence id:单调递增,恢复与 MemStore 排序都靠它 ├── region 信息:这条修改属于哪个 Region ├── 编辑列表 edits:一行的多个 KeyValue 原样打包 └── 类型:普通写 / 区域事件如 flush 标记 / compaction 标记

用 Web UI 或日志能观察到 WAL 的实际行为。持续灌数据时翻 RegionServer 的日志,会看到滚动记录:

INFO wal.AsyncFSWAL: Rolled WAL /hbase/WALs/vm1,16020,1724055/...wal.1, entries=182034, size=268 MB INFO regionserver.HRegion: Flushing orders,,1724055.1 ... under replay from WAL

两条日志对应两件事:一是 WAL 文件按大小或周期滚动(默认约每 60 分钟或文件到上限),旧文件在确认无未落盘数据后被清理;二是 Flush 前会先在 WAL 里写一条 flush 标记,恢复时回放到这条标记就知道"此前的日志已经安全落盘",可以直接跳过。

图 2.1-1 WAL 生命周期:追加、滚动与宕机切分回放

图 2.1-1 WAL 生命周期:追加、滚动与宕机切分回放

sync 策略:持久性与延迟的交易

"写 WAL 成功"分两级语义。追加(append)只是把数据写进 JVM 缓冲;真正落盘要调用文件系统的 sync(HDFS 上对应 hflush,让 DataNode 落盘确认)。客户端返回成功前必须等 sync 完成,否则宕机时数据实际还悬在缓冲区里。相关开关在客户端 Put 上:

Put put = new Put(Bytes.toBytes("u1001-1724055123")); put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("status"), Bytes.toBytes("PAID")); put.setDurability(Durability.SYNC_WAL); // 默认:sync 后才返回,不丢 // put.setDurability(Durability.ASYNC_WAL); // 异步,吞吐高,宕机可能丢最后若干条 // put.setDurability(Durability.SKIP_WAL); // 跳过 WAL,只进 MemStore,最危险

三种选择的适用面:

策略 语义 适合
SYNC_WAL(默认) sync 完成才应答 订单、账务等不容丢失的数据
ASYNC_WAL 先应答,攒批异步 sync 监控指标、日志类,可容忍秒级丢失
SKIP_WAL 完全绕过 WAL 数据可随时重算的缓存型场景,慎用

⚠️ 常见坑:把 ASYNC_WAL 当成性能优化默认项全表开启,等一次 RegionServer 崩溃后丢了几秒数据才追悔。我的建议是按数据价值分级设置,宁可在容量规划上多花机器,也别在持久性上省钱。

宕机之后:日志切分与回放

RegionServer 挂掉后,ZooKeeper 的临时会话节点消失(1.3 节的机制),Master 感知后启动恢复流程,核心是 WAL 切分(log splitting):读取宕机机器上的所有 WAL,按"条目属于哪个 Region"把日志拆开,写成每 Region 一份的回放文件,放到 HDFS 的恢复目录;随后这些 Region 被重新分配到别的 RegionServer,新宿主打开 Region 时发现有待回放的文件,就把日志重放进 MemStore——数据由此复活。HBase 2.x 进一步做了分布式切分与过场 Region(工序上的优化),把恢复时间从分钟级压到秒级,但思路不变。

回放为什么不会造成重复数据?因为每个 KeyValue 带单调递增的 sequence id,MemStore 与 HFile 里若已有更大序号,旧条目直接丢弃——幂等恢复。

💡 关键直觉:WAL 是 RegionServer 级而非 Region 级的单位,一个 Region 迁到哪台机器,恢复责任就跟到哪台机器。理解这一点,"宕机恢复为什么慢、慢在切分还是慢在回放"这类问题就有了分析框架。

本节要点回顾

  • 先日志后内存:WAL 弥补 MemStore 未落盘窗口,宕机靠回放恢复,思路与数据库 redo log 同源;
  • 顺序追加:WAL 快是因为纯顺序写,这是 LSM 系统写吞吐的第一支柱;
  • sync 三档:SYNC_WAL 默认不丢,ASYNC 换吞吐,SKIP_WAL 仅限可重算数据;
  • 滚动与清理:WAL 按大小周期滚动,Flush 标记之前的旧日志可安全删除;
  • 恢复是切分加回放:按 Region 拆日志、新宿主重放,sequence id 保证幂等。

WAL 记完,数据终于获准进入内存。下一节看 MemStore:它怎么攒数据、什么时候 Flush、以及最要命的——水位阻塞。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U