Hbase刷写分析

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.sizehbase.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 抽象类的。并且自带三种刷写策略:FlushAllLargeStoresPolicyFlushNonSloppyStoresFirstPolicy 以及 FlushAllStoresPolicy

FlushAllStoresPolicy

这种刷写策略实现最简单,直接返回当前 Region 对应的所有 MemStore。也就是每次刷写都是对 Region 里面所有的 MemStore 进行的,这个行为和 HBase 1.1 之前是一样的。

FlushAllLargeStoresPolicy

在 HBase 2.0 之前版本是 FlushLargeStoresPolicy,后面被拆分成分 FlushAllLargeStoresPolicyFlushNonSloppyStoresFirstPolicy,参见 HBASE-14920

这种策略会先判断 Region 中每个 MemStore 的使用内存(OnHeap + OffHeap)是否大于某个阀值,大于这个阀值的 MemStore 将会被刷写。阀值的计算是由 hbase.hregion.percolumnfamilyflush.size.lower.boundhbase.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 里面(sloppyStoresregularStores)。然后

  • 判断 regularStores 里面是否有 MemStore 内存占用大于相关阀值的 MemStore ,有的话就会对这些 MemStore 进行刷写,其他的不做处理,这个阀值计算和 FlushAllLargeStoresPolicy 的阀值计算逻辑一致。
  • 如果 regularStores 里面没有 MemStore 内存占用大于相关阀值的 MemStore,这时候就开始在 sloppyStores 里面寻找是否有 MemStore 内存占用大于相关阀值的 MemStore,有的话就会对这些 MemStore 进行刷写,其他的不做处理。
  • 如果上面 sloppyStoresregularStores 都没有满足条件的 MemStore 需要刷写,这时候就 FlushNonSloppyStoresFirstPolicy 策略久退化成 FlushAllStoresPolicy 策略了。

Flush 过程

MemStore 的刷写过程很复杂,很多操作都可能触发,但是这些条件触发的刷写最终都是调用 HRegion 类中的 internalFlushcache 方法。

1
2
3
4
5
6
7
8
9
10
11
protected FlushResultImpl internalFlushcache(WAL wal, long myseqid,
Collection<HStore> storesToFlush, MonitoredTask status, boolean writeFlushWalMarker,
FlushLifeCycleTracker tracker) throws IOException {
PrepareFlushResult result =
internalPrepareFlushCache(wal, myseqid, storesToFlush, status, writeFlushWalMarker, tracker);
if (result.result == null) {
return internalFlushCacheAndCommit(wal, status, result, storesToFlush);
} else {
return result.result; // early exit due to failure from prepare stage
}
}

从上面的实现可以看出,Flush 操作主要分以下几步做的

  • prepareFlush 阶段:刷写的第一步是对 MemStore 做 snapshot,为了防止刷写过程中更新的数据同时在 snapshot 和 MemStore 中而造成后续处理的困难,所以在刷写期间需要持有 updateLock 。持有了 updateLock 之后,这将阻塞客户端的写操作。所以只在创建 snapshot 期间持有 updateLock,而且 snapshot 的创建非常快,所以此锁期间对客户的影响一般非常小。对 MemStore 做 snapshot 是 internalPrepareFlushCache 里面进行的。
  • flushCache 阶段:如果创建快照没问题,那么返回的 result.result 将为 null。这时候我们就可以进行下一步 internalFlushCacheAndCommit。其实 internalFlushCacheAndCommit 里面包含两个步骤:flushCachecommit 阶段。flushCache 阶段其实就是将 prepareFlush 阶段创建好的快照写到临时文件里面,临时文件是存放在对应 Region 文件夹下面的 .tmp 目录里面。
  • commit 阶段:将 flushCache 阶段生产的临时文件移到(rename)对应的列族目录下面,并做一些清理工作,比如删除第一步生成的 snapshot。