HBase – Memstore Flush深度解析

Memstore是HBase框架中非常重要的组成部分之一,是HBase能够实现高性能随机读写至关重要的一环。深入理解Memstore的工作原理、运行机制以及相关配置,对hbase集群管理、性能调优都有着非常重要的帮助。


Memstore 概述

HBase中,Region是集群节点上最小的数据服务单元,用户数据表由一个或多个Region组成。在Region中每个ColumnFamily的数据组成一个Store。每个Store由一个Memstore和多个HFile组成,如下图所示:

0

之前我们提到,HBase是基于LSM-Tree模型的,所有的数据更新插入操作都首先写入Memstore中(同时会顺序写到日志HLog中),达到指定大小之后再将这些修改操作批量写入磁盘,生成一个新的HFile文件,这种设计可以极大地提升HBase的写入性能;另外,HBase为了方便按照RowKey进行检索,要求HFile中数据都按照RowKey进行排序,Memstore数据在flush为HFile之前会进行一次排序,将数据有序化;还有,根据局部性原理,新写入的数据会更大概率被读取,因此HBase在读取数据的时候首先检查请求的数据是否在Memstore,写缓存未命中的话再到读缓存中查找,读缓存还未命中才会到HFile文件中查找,最终返回merged的一个结果给用户。

可见,Memstore无论是对HBase的写入性能还是读取性能都至关重要。其中flush操作又是Memstore最核心的操作,接下来重点针对Memstore的flush操作进行深入地解析:首先分析HBase在哪些场景下会触发flush,然后结合源代码分析整个flush的操作流程,最后再重点整理总结和flush相关的配置参数,这些参数对于性能调优、问题定位都非常重要。


Memstore Flush触发条件

HBase会在如下几种情况下触发flush操作,需要注意的是MemStore的最小flush单元是HRegion而不是单个MemStore。可想而知,如果一个HRegion中Memstore过多,每次flush的开销必然会很大,因此我们也建议在进行表设计的时候尽量减少ColumnFamily的个数。

  1. Memstore级别限制:当Region中任意一个MemStore的大小达到了上限(hbase.hregion.memstore.flush.size,默认128MB),会触发Memstore刷新。
  2. Region级别限制:当Region中所有Memstore的大小总和达到了上限(hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size,默认 2* 128M = 256M),会触发memstore刷新。
  3. Region Server级别限制:当一个Region Server中所有Memstore的大小总和达到了上限(hbase.regionserver.global.memstore.upperLimit * hbase_heapsize,默认 40%的JVM内存使用量),会触发部分Memstore刷新。Flush顺序是按照Memstore由大到小执行,先Flush Memstore最大的Region,再执行次大的,直至总体Memstore内存使用量低于阈值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默认 38%的JVM内存使用量)。
  4. 当一个Region Server中HLog数量达到上限(可通过参数hbase.regionserver.maxlogs配置)时,系统会选取最早的一个 HLog对应的一个或多个Region进行flush
  5. HBase定期刷新Memstore:默认周期为1小时,确保Memstore不会长时间没有持久化。为避免所有的MemStore在同一时间都进行flush导致的问题,定期的flush操作有20000左右的随机延时。
  6. 手动执行flush:用户可以通过shell命令 flush ‘tablename’或者flush ‘region name’分别对一个表或者一个Region进行flush。


Memstore Flush流程

为了减少flush过程对读写的影响,HBase采用了类似于两阶段提交的方式,将整个flush过程分为三个阶段:

  1. prepare阶段:遍历当前Region中的所有Memstore,将Memstore中当前数据集kvset做一个快照snapshot,然后再新建一个新的kvset。后期的所有写入操作都会写入新的kvset中,而整个flush阶段读操作会首先分别遍历kvset和snapshot,如果查找不到再会到HFile中查找。prepare阶段需要加一把updateLock对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
  2. flush阶段:遍历所有Memstore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时。
  3. commit阶段:遍历所有的Memstore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,把storefile添加到HStore的storefiles列表中,最后再清空prepare阶段生成的snapshot。


上述flush流程可以通过日志信息查看:

/******* prepare阶段 ********/
2016-02-04 03:32:41,516 INFO  [MemStoreFlusher.1] regionserver.HRegion: Started memstore flush for sentry_sgroup1_data,{\xD4\x00\x00\x01|\x00\x00\x03\x82\x00\x00\x00?\x06\xDA`\x13\xCAE\xD3C\xA3:_1\xD6\x99:\x88\x7F\xAA_\xD6[L\xF0\x92\xA6\xFB^\xC7\xA4\xC7\xD7\x8Fv\xCAT\xD2\xAF,1452217805884.572ddf0e8cf0b11aee2273a95bd07879., current region memstore size 128.9 M

/******* flush阶段 ********/
2016-02-04 03:32:42,423 INFO  [MemStoreFlusher.1] regionserver.DefaultStoreFlusher: Flushed, sequenceid=1726212642, memsize=128.9 M, hasBloomFilter=true, into tmp file hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/.tmp/021a430940244993a9450dccdfdcb91d

/******* commit阶段 ********/
2016-02-04 03:32:42,464 INFO  [MemStoreFlusher.1] regionserver.HStore: Added hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/d/021a430940244993a9450dccdfdcb91d, entries=643656, sequenceid=1726212642, filesize=7.1 M

整个flush过程可能涉及到compact操作和split操作,因为过于复杂,在此暂时略过不表。


Memstore Flush对业务读写的影响

上文介绍了HBase在什么场景下会触发flush操作以及flush操作的基本流程,想必对于HBase用户来说,最关心的是flush行为会对读写请求造成哪些影响以及如何避免。因为不同触发方式下的flush操作对用户请求影响不尽相同,因此下面会根据flush的不同触发方式分别进行总结,并且会根据影响大小进行归类:

影响甚微

正常情况下,大部分Memstore Flush操作都不会对业务读写产生太大影响,比如这几种场景:HBase定期刷新Memstore、手动执行flush操作、触发Memstore级别限制、触发HLog数量限制以及触发Region级别限制等,这几种场景只会阻塞对应Region上的写请求,阻塞时间很短,毫秒级别。

影响较大

然而一旦触发Region Server级别限制导致flush,就会对用户请求产生较大的影响。会阻塞所有落在该Region Server上的更新操作,阻塞时间很长,甚至可以达到分钟级别。一般情况下Region Server级别限制很难触发,但在一些极端情况下也不排除有触发的可能,下面分析一种可能触发这种flush操作的场景:

相关JVM配置以及HBase配置:

maxHeap = 71
hbase.regionserver.global.memstore.upperLimit = 0.35
hbase.regionserver.global.memstore.lowerLimit = 0.30

基于上述配置,可以得到触发Region Server级别的总Memstore内存和为24.9G,如下所示:

2015-10-12 13:05:16,232 INFO  [regionserver60020] regionserver.MemStoreFlusher: globalMemStoreLimit=24.9 G, globalMemStoreLimitLowMark=21.3 G, maxHeap=71 G

假设每个Memstore大小为默认128M,在上述配置下如果每个Region有两个Memstore,整个Region Server上运行了100个region,根据计算可得总消耗内存 = 128M * 100 * 2 = 25.6G > 24.9G,很显然,这种情况下就会触发Region Server级别限制,对用户影响相当大。

根据上面的分析,导致触发Region Server级别限制的因素主要有一个Region Server上运行的Region总数,一个是Region上的Store数(即表的ColumnFamily数)。对于前者,根据读写请求量一般建议线上一个Region Server上运行的Region保持在50~80个左右,太小的话会浪费资源,太大的话有可能触发其他异常;对于后者,建议ColumnFamily越少越好,如果从逻辑上确实需要多个ColumnFamily,最好控制在3个以内。


总结

本文主要介绍了HBase引擎中至关重要的一个组件-Memstore,主要介绍了Memstore Flush的几种触发条件、Flush完整流程以及各种不同场景下Flush对业务读写的影响。希望通过此篇文章可以对Memstore有一个更深入的了解。

范欣欣

就职于网易杭州研究院后台技术中心数据库技术组,从事HBase开发、运维,对HBase相关技术有浓厚的兴趣。邮箱:libisthanks@gmail.com

在 “HBase – Memstore Flush深度解析” 上有 45 条评论

  1. 博主的文章很靠谱,正在按照时间顺序看一遍。有一个问题,“还有,根据局部性原理,新写入的数据会更大概率被读取,因此HBase在读取数据的时候首先检查请求的数据是否在Memstore,写缓存未命中的话再到读缓存中查找,读缓存还未命中才会到HFile文件中查找,最终返回merged的一个结果给用户。”

    我在StoreScanner里面找到了if (scan instanceof InternalScan) 的判断,但是如果是InternalScan而且mem only,那么只会在memstore里面找,否则就会包含HFile。请问这个“memstore没有->读缓存”的递进关系在哪?

    1. HBase读取这块逻辑有点复杂 这里涉及到内部scanner的实现 StoreScanner由StoreFileScanner和MemStoreScanner组成 hbase首先会在memstorescanner中查找 如果没有找到 才会雇佣storefilescanner查找 storefilescanner封装了查找读缓存和hfile的逻辑 首先在blockcache中国年查找 没找到再到hfile中查找

      希望有所帮助

      1. 感谢回复。

        我表达的意思就是在scanner实现里面。比如get操作的在server端的逻辑,是hregion.get –> new RegionScannerImpl() –> store.getScanner –> new StoreScanner –> scanners = getScannersNoCompaction(); resetKVHeap(scanners); 然后实际用这个heap去进行真正的查找。而getScannersNoCompaction调用selectScannersFrom(hstore.getScanners())。hstore.getScanners()将memstore/hfile的scanner不加区分地封装在一起,selectScannersFrom根据ts或者bloom过滤掉不可能的scanner,没有对先到memstorescanner查找的印象。

        1. 从代码上看不出来先到memstorescanner查找的 这个需要考虑storescanner的排序 排序规则是按照时间戳进行的 自然memstorescanner会排在前面

          1. 可能是这个理解不一样,我理解是,建立heap的过程中已经seek了HFile,就已经发生了对HFile的读取(部分,至少读了load-on-open和索引部分,以及对应的block)。

      2. 除此之外,先查cache再查HFile的逻辑应该是错的。在关闭cache on write的情况下,一开始a=0,写到了hfile;读取a的值,然后cache住对应block;写a=1;flush对应memstore;再次读取a的时候,此时memstore里面没有a,cache里面有a=0,这时候直接返回a=0显然是错误的。

        1. 先查cache再查hfile的逻辑是没问题的,可以参考HFileReaderV2文件中的readBlock方法。至于你说的这个问题,后续更新a的操作会flush成一个新的文件,并不是修改的blockcache中那个a。假设当前a这个数据更新了一次,有两个版本:v1和v2,按照你的说法应该是v1版本的a在blockcache中(对应的文件是hfile1) ,v2版本的a在hfile2中,如果查询最新版本的a,只会查询hfile2的a;如果查询所有版本,首先会在cache中查,查不到再到hfile2中查,查到v2版本。接着会查v1版本的,直接在cache中就可以找到。

          希望有所帮助

          1. 你好,不知道我理解对不对?
            一个查询需要查一个Store中的mem和file。
            如果发现block在blockcache中会去blockcache中读取,就可以不需要去读取那个file。
            mem是肯定要被读取(否则读取的数据有问题)。
            cache和file两者不一定,但是新生成的file如果没有被读取过肯定不在blockcache中。

          2. 对于另一篇文章中的问题,确实感觉blockcache最大的隐患来自于文件的更改(包括合并,flush),他会导致blockcache的缓存不一致,这个问题的关键是不是应该在于,文件的更改能否让StoreFileScanner的可见性达到需要的程度。ps:一个刚接触hbase一周的人,还没开始看源码,如果说错或问错,请不要喷小白。

          3. blockcache缓存不存在不一致的问题 memstore更新到hfile的数据是以append形式追加写的(形成多版本) 不是update原有数据 这点对于理解blockcache的那个问题至关重要

          4. 扫一眼Hstore,感觉ReentrantReadWriteLock org.apache.hadoop.hbase.regionserver.HStore.lock 这个锁的控制很关键,还没非常细看。这个锁的控制应该能解释另外那篇文章的问题。
            感觉对这个问题的理解方向有点不一致。评论貌似只能到的五层。==!

  2. hbase.regionserver.global.memstore.upperLimit * hbase_heapsize,默认 40%的JVM内存使用量),会触发部分Memstore刷新。Flush顺序是按照Memstore由大到小执行,先Flush Memstore最大的Region,再执行次大的,直至总体Memstore内存使用量低于阈值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默认 38%的JVM内存使用量)这个是不是有问题?
    这部分低于阈值hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize是有问题的吧,这个阈值应该是hbase.regionserver.global.memstore.lowerLimit * globalMemStoreLimit(hbase.regionserver.global.memstore.upperLimit * hbase_heapsize)吧

    1. 再次确认了下,阈值是hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,不清楚hbase.regionserver.global.memstore.lowerLimit * globalMemStoreLimit是怎么来的?

  3. 如果总是isAboveLowWaterMark,那么会一直进行flush吧,而其中的引用到的globalMemStoreLimitLowMark的赋值如下:
    this.globalMemStoreLimitLowMark =(long) (this.globalMemStoreLimit * this.globalMemStoreLimitLowMarkPercent); 具体可以参看MemStoreFlusher的构造函数。难道是我理解的有问题?不知道你是从哪看到flush的条件的?

          1. 很多人用qq有道理啊 可以有交流群号 微信只有二维码 anyway 二维码发你邮箱你361197893@qq.com

  4. flush是Region级别的。而一个region含有多个store的。为何各个store自己独立flush呢。
    官方也推荐一个region不要有太多的column family,因为flush的时候会导致大量相邻的column family一起flush的,
    造成大量的IO操作。
    那么请教下,是否可以每个store独立flush呢。
    以region做store的考虑点是什么呢。毕竟在sequenceId中,也是每个store自己维护自己的id的。

    1. 0.98版本的HBase中sequenceId还是region级别的,所以flush就是region级别的。记得再1.1版本中把sequenceId做成store级别的,这样就可以实现store级别的flush,1.1以上版本flush默认策略是FlushLargeStoresPolicy

  5. 因此HBase在读取数据的时候首先检查请求的数据是否在Memstore,写缓存未命中的话再到读缓存中查找,读缓存还未命中才会到HFile文件中查找,最终返回merged的一个结果给用户————请问如果一个rowkey的数据部分在Memstore,部分数据在hdfs中,这种情况岂不是之返回Memstore的数据了?

  6. 在memstore flush 机制中
    4、当一个Region Server中HLog数量达到上限(可通过参数hbase.regionserver.maxlogs配置)时,系统会选取最早的一个 HLog对应的一个或多个Region进行flush。

    “一个Region Server中HLog数量” 不就只有一个嘛,为什么一个region server 的hlog数量之说,而且博主在“HBase - 数据写入流程解析”里面也说了“每个Region Server拥有一个HLog日志”。这边描述是否有点问题。不是很明白,还是博主解答。

    1. 《HBase - 数据写入流程解析》中说到“每个Region Server拥有一个HLog日志”是想强调所有Region共用HLog。一个Region Server中所有Region都会向同一个HLog写日志,当HLog大小超过阈值就会新生成一个HLog文件接收新的写入。所以HLog文件个数实际上有多个的。

  7. 你好,regionserver中出现这样的日志“INFO org.apache.hadoop.hbase.regionserver.HRegionServer: my-hbase-srv3,60020,1506017784241-MemstoreFlusherChore requesting flush of regionInfoKey,1499557430610.4ac8c97fadd652f676a803688d2ff07e. because cfInfo has an old edit so flush to free WALs after random delay 241441ms”,可以这么理解吗?由于写入量太大,导致flush不及时(代码中会批量手动flush),最后触发定时刷新?

    1. because cfInfo has an old edit so flush to free WALs 这个是flush的最终原因。这个memstore长时间没有flush导致已经写入到memstore中的数据在WAL中太老,对应的WALEdits之后的日志都不能被释放,最终导致系统WAL日志数据太大。所以需要赶紧将这个memstore flush掉,对应的WAL Edit就可以清理掉了,这个Edit清理掉之后,之后的Edit也就可以清理掉了

      1. 对于写流程,可以这么理解吗?客户端设置writerbuffersize(假如6M),满了后会flush写入memstore和WAL,然后memstore满了后,会flush到HFile,对于memstore中残余的数据(最后的未满128M数据),就等待定时flush,此时出现日志信息“has an old edit so flush to free WALs”?

李鹏进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注