Hbase MemStore Flush
熟悉Hbase写流程的同学一定知道,我们Hbase写数据的时候,都是先写WAL日志的,然后再写到一个叫 MemStore 的内存结构里面,最终,MemStore 里面的数据需要 flush 持久化到磁盘,生成 HFile。有次来说,MemStore 的flush触发和flush策略极其重要。
MemStore Flush 的触发条件
- 单个 Region 中所有 MemStore 占用的内存总和超过相关阈值
- 整个 RegionServer 的 MemStore 占用内存中和超过相关阈值
- WAL 数据量超过相关阈值
- 触发了 MemStore 的定期 Flush 时间
- 数据更新超过一定阈值
- 手动触发 Flush
先一个一个来看。单个 Region 中所有 MemStore 占用的内存总和超过相关阈值
当一个 Region 中的所有 MemStore 占用的内存大小超过hbase.hregion.memstore.flush.size
值时,会触发 Flush。该值默认为 128M。 每次调用 put,delete等操作均会检查此条件。
另外,如果数据增加的很快,达到了上面该值的多倍(即 hbase.hregion.memstore.flush.size
* hbase.hregion.memstore.block.multiplier
) 时,除了触发 Flush 外,还会阻塞所有写入该 MemStore 的请求,如果还继续写就会抛 RegionTooBusyException
异常。
其中 hbase.hregion.memstore.block.multiplier
控制 MemStore 自我保护的参数,默认为4。即写入数据超过MemStore 四倍大小时,MemStore 会开启自我保护。
整个 RegionServer 的 MemStore 占用内存中和超过相关阈值
每个 RegionServer的写缓存:Hbase 为 RegionServer 的 MemStore 分配了一定大小的写缓存,大小等于 RegionServer 的堆内存(
hbase_heapsize
。 ) *hbase.regionserver.global.memstore.size
。hbase.regionserver.global.memstore.size
的默认值是 0.4,即写缓存占用了整个 JVM 内存的 40%。如果 RegionServer 的所有 MemStore 占用内存总和超过了 写缓存的一定比例,则会触发 Flush。即 总占用内存超过
hbase.regionserver.global.memstore.size.lower.limit
*hbase.regionserver.global.memstore.size
*hbase_heapsize
。其中,hbase.regionserver.global.memstore.size.lower.limit
的默认值是 0.95。RegionServer 级别的 Flush 是每次找到 RegionServer 中占用内存最大的 Region 对之进行 Flush。此操作是循环的,直到 占用总内存小于 总写缓存的比例。
注意!! 如果 触发 的是 RegionServer 级别的 Flush,那么此 RegionServer 的所有写操作均会被阻塞。
WAL 数据量超过相关阈值
WAL (Write-ahead log,预写日志),目的是解决宕机之后的操作恢复问题。数据写入到 MemStore 之前,是需要先写到 WAL 的,如果 WAL 的数量越来越多,也就是 MemStore 中未 Flush 的数据越来越多。如果 RegionServer 此时宕机,那么恢复操作的时间就会变得很长,因此,在 WAL 达到一定数量的时候,需要进行一次 Flush。
触发条件为 hbase.regionserver.maxlogs
,如果未设置这个值,那么取 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize)
。其中,logRollSize
是每个 WAL 文件的大小,取值为 hbase.regionserver.logroll.multiplier * hbase.regionserver.hlog.blocksize
,即 每个 WAL 文件达到块的百分之多少就生成新的 WAL (这个块的大小不是HDFS块的大小,默认是HDFS块大小的50%,2.0之后是 95%)。
定期自动 Flush
RegionServer 在启动的时候会启动一个线程去检查此 RegionServer 的 Region 是否达到了定期 Flush 的时间,这个时间由配置 hbase.regionserver.optionalcacheflushinterval
控制的,默认是36000000,即一小时。需要注意的是,定期刷写是有几分钟的延迟的,目的是防止同时有过多的 MemStore Flush。
数据更新超过一定阈值
如果 RegionServer 的某个 Region 数据更新很频繁,既没有达到自动 Flush 的要求,又没达到内存的使用限制,但是数据条数够多。此时也是会触发 MemStore Flush 的。这个条数的限制是 hbase.regionserver.flush.per.changes
控制的,默认是 30000000 。
手动触发 Flush
那么什么操作会触发 MemStore Flush 呢
常见的操作有put、delete、append、increment、flush、Region 分裂、Region Merge、bulkLoad HFiles 以及表做快照,此类操作都会检查是否符合 Flush 的条件。
MemStore Flush 策略(FlushPolicy)
在 HBase 1.1 之前,MemStore 刷写是 Region 级别的。就是说,如果要刷写某个 MemStore ,MemStore 所在的 Region 中其他 MemStore 也是会被一起刷写的!这会造成一定的问题,比如小文件问题。针对这个问题,HBASE-10201/HBASE-3149 引入列族级别的刷写。我们可以通过 hbase.regionserver.flush.policy
参数选择不同的刷写策略。
目前 HBase 2.0.2 的刷写策略全部都是实现 FlushPolicy
抽象类的。并且自带三种刷写策略:FlushAllLargeStoresPolicy
、FlushNonSloppyStoresFirstPolicy
以及 FlushAllStoresPolicy
。
FlushAllStoresPolicy
这种刷写策略实现最简单,直接返回当前 Region 对应的所有 MemStore。也就是每次刷写都是对 Region 里面所有的 MemStore 进行的,这个行为和 HBase 1.1 之前是一样的。
FlushAllLargeStoresPolicy
在 HBase 2.0 之前版本是 FlushLargeStoresPolicy
,后面被拆分成分 FlushAllLargeStoresPolicy
和FlushNonSloppyStoresFirstPolicy
,参见 HBASE-14920。
这种策略会先判断 Region 中每个 MemStore 的使用内存(OnHeap + OffHeap)是否大于某个阀值,大于这个阀值的 MemStore 将会被刷写。阀值的计算是由 hbase.hregion.percolumnfamilyflush.size.lower.bound
、hbase.hregion.percolumnfamilyflush.size.lower.bound.min
以及 hbase.hregion.memstore.flush.size
参数决定的。计算逻辑如下:
1 | `//region.getMemStoreFlushSize() / familyNumber``//就是 hbase.hregion.memstore.flush.size 参数的值除以相关表列族的个数``flushSizeLowerBound = max(region.getMemStoreFlushSize() / familyNumber, hbase.hregion.percolumnfamilyflush.size.lower.bound.min)` `//如果设置了 hbase.hregion.percolumnfamilyflush.size.lower.bound``flushSizeLowerBound = hbase.hregion.percolumnfamilyflush.size.lower.bound` |
计算逻辑上面已经很清晰的描述了。hbase.hregion.percolumnfamilyflush.size.lower.bound.min
默认值为 16MB,而 hbase.hregion.percolumnfamilyflush.size.lower.bound
没有设置。
比如当前表有3个列族,其他用默认的值,那么 flushSizeLowerBound = max((long)128 / 3, 16) = 42
。
如果当前 Region 中没有 MemStore 的使用内存大于上面的阀值,FlushAllLargeStoresPolicy
策略就退化成 FlushAllStoresPolicy
策略了,也就是会对 Region 里面所有的 MemStore 进行 Flush。
FlushNonSloppyStoresFirstPolicy
HBase 2.0 引入了 in-memory compaction,参见 HBASE-13408。如果我们对相关列族 hbase.hregion.compacting.memstore.type
参数的值不是 NONE
,那么这个 MemStore 的 isSloppyMemStore
值就是 true,否则就是 false。
FlushNonSloppyStoresFirstPolicy
策略将 Region 中的 MemStore 按照 isSloppyMemStore
分到两个 HashSet 里面(sloppyStores
和 regularStores
)。然后
- 判断
regularStores
里面是否有 MemStore 内存占用大于相关阀值的 MemStore ,有的话就会对这些 MemStore 进行刷写,其他的不做处理,这个阀值计算和FlushAllLargeStoresPolicy
的阀值计算逻辑一致。 - 如果
regularStores
里面没有 MemStore 内存占用大于相关阀值的 MemStore,这时候就开始在sloppyStores
里面寻找是否有 MemStore 内存占用大于相关阀值的 MemStore,有的话就会对这些 MemStore 进行刷写,其他的不做处理。 - 如果上面
sloppyStores
和regularStores
都没有满足条件的 MemStore 需要刷写,这时候就FlushNonSloppyStoresFirstPolicy
策略久退化成FlushAllStoresPolicy
策略了。
Flush 过程
MemStore 的刷写过程很复杂,很多操作都可能触发,但是这些条件触发的刷写最终都是调用 HRegion
类中的 internalFlushcache
方法。
1 | protected FlushResultImpl internalFlushcache(WAL wal, long myseqid, |
从上面的实现可以看出,Flush 操作主要分以下几步做的
- prepareFlush 阶段:刷写的第一步是对 MemStore 做 snapshot,为了防止刷写过程中更新的数据同时在 snapshot 和 MemStore 中而造成后续处理的困难,所以在刷写期间需要持有 updateLock 。持有了 updateLock 之后,这将阻塞客户端的写操作。所以只在创建 snapshot 期间持有 updateLock,而且 snapshot 的创建非常快,所以此锁期间对客户的影响一般非常小。对 MemStore 做 snapshot 是
internalPrepareFlushCache
里面进行的。 - flushCache 阶段:如果创建快照没问题,那么返回的
result.result
将为 null。这时候我们就可以进行下一步internalFlushCacheAndCommit
。其实internalFlushCacheAndCommit
里面包含两个步骤:flushCache
和commit
阶段。flushCache 阶段其实就是将prepareFlush
阶段创建好的快照写到临时文件里面,临时文件是存放在对应 Region 文件夹下面的.tmp
目录里面。 - commit 阶段:将
flushCache
阶段生产的临时文件移到(rename
)对应的列族目录下面,并做一些清理工作,比如删除第一步生成的 snapshot。