2.2 MemStore与Flush:内存如何落地


文档摘要

2.2 MemStore 与 Flush:内存里的数据如何落地 本节摘要:MemStore 是每个 Store(列族)一份的内存写缓冲,内部是按行键排序的跳表;写满触发 Flush 生成 HFile。本节讲 Flush 的四种触发条件、RegionServer 级水位的连锁阻塞,以及"写入突然 hang 住"的完整因果链——这是 HBase 写路径上最容易出生产事故的一段。 跳表:内存里的有序区 回顾 1.2 节:HFile 里的数据按行键字典序存放。那么写进来的顺序是乱的,怎么保证 Flush 出的文件天然有序?答案是 MemStore 的选型——跳表(skiplist,具体是 Java 的 ConcurrentSkipListMap)。

2.2 MemStore 与 Flush:内存里的数据如何落地

本节摘要:MemStore 是每个 Store(列族)一份的内存写缓冲,内部是按行键排序的跳表;写满触发 Flush 生成 HFile。本节讲 Flush 的四种触发条件、RegionServer 级水位的连锁阻塞,以及"写入突然 hang 住"的完整因果链——这是 HBase 写路径上最容易出生产事故的一段。

跳表:内存里的有序区

回顾 1.2 节:HFile 里的数据按行键字典序存放。那么写进来的顺序是乱的,怎么保证 Flush 出的文件天然有序?答案是 MemStore 的选型——跳表(skiplist,具体是 Java 的 ConcurrentSkipListMap)。跳表插入是 O(log n) 且并发友好,任意时刻整个结构都是有序的,Flush 就是把它顺序遍历一遍写出去,不需要额外排序。

所以 MemStore 不是"缓存"而是"整理器":它把乱序到达的写入整理成有序,攒够一批再一次性顺序落盘。这个"攒"的动作是 LSM-Tree 的核心——写吞吐越高,摊薄到每条记录的落盘成本越低。

每个 Region 的每个列族各有一个 MemStore。一台 RegionServer 上通常有成百上千个 MemStore 共享同一块堆内存(写缓冲占总堆的默认 40%),这就引出了本节的主角:Flush 不是单个 MemStore 的私事,而是整机的资源协调

Flush 的四种触发条件

把条件从局部到全局排一遍:

条件一:单 MemStore 达到上限。 参数 hbase.hregion.memstore.flush.size,默认 128 MB。某个列族的 MemStore 攒满即触发该 Region 的 Flush(注意粒度是 Region:它会把该 Region 所有列族的 MemStore 一起刷,保持行完整性)。

条件二:RegionServer 全局水位。 所有 MemStore 占用内存之和达到堆的 40%(hbase.regionserver.global.memstore.size)之前,有提前刷新机制;真正到上限则进入保护模式。

条件三:定期 Flush。 MemStore 最长存活 1 小时(hbase.regionserver.optionalcacheflushinterval),防止低流量表的数据永远悬在内存、WAL 无限增长。

条件四:手动触发。 Shell 里 flush 'orders' 或 Web UI 按钮,运维与测试常用。

Flush 的产物是一个新 HFile,追加到该 Store 的文件列表头部。这里埋下第 3 章的引子:Flush 越频繁,小文件越多,读路径越宽——所以 128 MB 这个默认值不是越大越好也不是越小越好,第 7 章调优再展开。

阻塞水位:写入 hang 住的完整因果链

现在把最容易出事故的机制讲透。全局水位之上还有一道阻塞线hbase.hregion.memstore.block.multiplier(默认 4,即 128 MB × 4 = 512 MB 区域级累积,全局对应堆的约 55% 上限区间的组合判断)。一旦某 Region 的 MemStore 大小超过 flush.size × multiplier 且 Flush 还没完成,该 Region 的写入直接被阻塞,客户端表现为请求超时。

完整因果链通常长这样:

大促流量突增 → 写入速率远超 Flush 落盘速率(HDFS 慢或磁盘忙) → MemStore 占用冲过全局水位,Flush 队列排起长队 → 触达阻塞线,写入全面阻塞 → 客户端超时重试,流量进一步放大 → RegionServer 堆内存吃紧,最坏 Full GC 或 OOM

日志里的特征行:

WARN regionserver.MemStoreFlusher: Global memstore size ... above high water mark; blocking updates for 3200 ms WARN regionserver.HRegion: Blocking updates for table orders ... memstore size 512.4 MB is >= than blocking 512.0 MB size

图 2.2-1 MemStore 水位阶梯:从顺畅写入到全局阻塞

图 2.2-1 MemStore 水位阶梯:从顺畅写入到全局阻塞

应急手段:降低写入速率或批量暂停写入方、手动 flush 热点 Region、必要时 kill 掉最重的 RegionServer 让 Master 重新均衡(最后手段)。根本解法在容量:要么加 RegionServer 分摊,要么调大堆与 flush 线程数,要么减少列族数(每个 Region 的 Flush 文件数 = 列族数,列族越多 HDFS 写压力越大,Flush 越慢)。

⚠️ 常见坑:建表时顺手建三四个列族,平时没事,高峰期 Flush 产物数量成倍放大、HDFS 写队列拥堵,触发全局阻塞。列族数量控制在 1–2 个,是写路径健康的第一道保险。

用实验观察 Flush

单机环境灌一批数据,亲眼看一次 Flush:

hbase:020:0> create 'flush_demo', {NAME => 'cf', VERSIONS => 1} hbase:021:0> (1..200000).each { |i| hbase:022:1* put 'flush_demo', "row-#{format('%08d', i)}", 'cf:v', "payload-#{'x'*100}" hbase:023:1> }

两千万字节的 payload 撑爆 128 MB 上限后,Web UI 的 Region 页会出现一次 Flush 记录,Store 下多出第一个 HFile,MemStore 大小归零。再灌一轮,第二个 HFile 出现——现在这个 Store 有两个文件,同一行键的数据可能分散在两个文件里,读的时候怎么办?这个悬念留给第 3 章的读路径。

顺带验证 WAL 的清理:Flush 完成后,WAL 里对应区段已无用,日志目录里旧文件被移除,这解释了 2.1 节"Flush 标记之前的日志可安全删除"的实现。

Flush 与 Region 迁移的联动

还有一个容易被忽略的细节:MemStore 快满时若恰逢 Region 迁移(第 3 章),HBase 有 memstore merging / flush-before-operation 机制——迁移前先把 MemStore 落盘再走文件,避免带着内存态搬家。也就是说,你观察到的"某次手动迁移后小文件突然变多",其实就是搬迁前的应急 Flush 产物,之后会由 Compaction 收编。

💡 关键直觉:把 MemStore 当作"整机的共享写预算"来监控,而不是每个 Region 自己的小池塘。全局水位、阻塞线、Flush 队列长度这三个指标(第 7 章监控节会点名)是写路径健康的三面仪表盘。

本节要点回顾

  • 跳表保证有序:MemStore 随时可顺序遍历,Flush 即有序落盘,无需排序;
  • Flush 四触发:单区 128 MB、全局水位、一小时兜底、手动命令;
  • 阻塞线是熔断器:MemStore 超上限且刷不动时直接阻塞写入,防止 OOM,代价是客户端超时;
  • 因果链要背下来:写入超速 → Flush 排队 → 阻塞 → 重试放大,排查写问题时按图索骥;
  • 列族越少越健康:Flush 产物数 = 列族数,一两个列族是工程共识。

数据落成了 HFile,写路径走完。下一节拆开这个文件:块、索引、布隆过滤器,以及多版本和墓碑在磁盘上的真实长相。


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