一、写过程

  1. Client访问ZK,根据ROOT表获取meta表所在Region的位置信息,并将该位置信息写入Client Cache。(注:为了加快数据访问速度,我们将元数据、Region位置等信息缓存在Client Cache中。)
  2. Client读取meta表,再根据meta表中查询得到的Namespace、表名和RowKey等相关信息,获取将要写入Region的位置信息(此过程即Region三层定位,如下图),最后client端会将meta表写入Client Cache。
  3. Client向上一步HRegionServer发出写请求,HRegionServer先将操作和数据写入HLog(预写日志,Write Ahead Log,WAL),再将数据写入MemStore,并保持有序。(联想:HDFS中也是如此,EditLog写入时机也是在真实读写之前发生)
  4. 当MemStore的数据量超过阈值时,将数据溢写磁盘,生成一个StoreFile文件。

当Store中StoreFile的数量超过阈值时,将若干小StoreFile合并(Compact)为一个大StoreFile。
当Region中最大Store的大小超过阈值时,Region分裂(Split),等分成两个子Region。
image.png

二、读过程

image.png
1、获取将要读取Region的位置信息(同读的1、2步)。
2、Client向HRegionServer发出读请求。
3、HRegionServer先从MemStore读取数据,如未找到,再从StoreFile中读取(但是最后会合并磁盘和内存的值,返回时间戳最新的数据)。
所以hbase的读一般都会比写慢,因为每次读都要去读磁盘。

三、Hbase快速响应数据

hbase上的数据是以storefile(HFile)二进制流的形式存储在HDFS上block块中;但是HDFS并不知道的hbase存的是什么,它只把存储文件是为二进制文件,也就是说,hbase的存储数据对于HDFS文件系统是透明的。
不同的region会被Master分配给相应的RegionServer进行管理:
HBase HRegion servers集群中的所有的region的数据在服务器启动时都是被打开的,并且在内冲初始化一些memstore,相应的这就在一定程度上加快系统响应;而Hadoop中的block中的数据文件默认是关闭的,只有在需要的时候才打开,处理完数据后就关闭,这在一定程度上就增加了响应时间。
从根本上说,HBase能提供实时计算服务主要原因是由其架构和底层的数据结构决定的,即由LSM-Tree + HTable(region分区) + Cache决定——客户端可以直接定位到要查数据所在的HRegion server服务器,然后直接在服务器的一个region上查找要匹配的数据,并且这些数据部分是经过cache缓存的。
image.png
HBase中有两张特殊的Table,-ROOT-和.META.
.META.:记录了用户表的Region信息,.META.可以有多个regoin,以及RegionServer的服务器地址。
-ROOT-:记录了.META.表的Region信息,-ROOT-只有一个region
Zookeeper中记录了-ROOT-表的location

Client访问用户数据之前需要首先访问zookeeper,然后访问-ROOT-表,接着访问.META.表,最后才能找到用户数据的位置去访问,中间需要多次网络操作,不过client端会做cache缓存。
1、Client会通过内部缓存的相关的-ROOT-中的信息和.META.中的信息直接连接与请求数据匹配的HRegion server;
2、然后直接定位到该服务器上与客户请求对应的region,客户请求首先会查询该region在内存中的缓存——memstore(memstore是是一个按key排序的树形结构的缓冲区);
3、如果在memstore中查到结果则直接将结果返回给client;
4、在memstore中没有查到匹配的数据,接下来会读已持久化的storefile文件中的数据。storefile也是按key排序的树形结构的文件——并且是特别为范围查询或block查询优化过的,;另外hbase读取磁盘文件是按其基本I/O单元(即 hbase block)读数据的。具体就是过程就是:
如果在BlockCache中能查到要造的数据则这届返回结果,否则就读去相应的storefile文件中读取一block的数据,如果还没有读到要查的数据,就将该数据block放到HRegion Server的blockcache中,然后接着读下一block块儿的数据,一直到这样循环的block数据直到找到要请求的数据并返回结果;如果将该region中的数据都没有查到要找的数据,最后接直接返回null,表示没有找的匹配的数据。当然blockcache会在其大小大于一的阀值(heapsize hfile.block.cache.size 0.85)后启动基于LRU算法的淘汰机制,将最老最不常用的block删除。

四、MemStore Flush

4.1、概念

image.png
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相关的配置参数,这些参数对于性能调优、问题定位都非常重要。

4.2、什么时候触发 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。

4.3、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。

4.4、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操作的场景:

五、StoreFile Compaction

image.png
由于memstore每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(PUT/Delete)有可能会分布到不同的HFile中,因此查询时需要遍历所有的HFile。为了减少HFile的个数,以及清理掉过期和删掉的数据,会进行StoreFile Compaction。
HBase每间隔一段时间都会进行一次合并(Compaction),合并的对象为HFile文件。Compaction分为两种,分别是Minor Compaction Major Compaction。Minor Compaction会将临近的若干个较小的HFile合并成一个较大的HFile,但不会清理过期和删除的数据。Major Compaction 会将一个Store下的所有HFile合并成一个大HFile,并且会清理掉过期和删除的数据。

相关配置参数:

  1. --------------------------------Compact--------------------------------
  2. <!-- 一个region进行 major compaction合并的周期,在这个点的时候, 这个region下的所有hfile会进行合并,默认是7天,但是时间是不固定的,有0.5的摇摆范围.可能3.5天就触发,如果正执行高并发的操作,忽然HBase启动了major compaction操作.那么会影响系统性能(IO压力很大,写入非常慢). ,major compaction非常耗资源,建议生产关闭(设置为0),在应用空闲时间手动触发(比如说每天夜里一两点不是很忙了,那么可以写个定时脚本来触发一下) -->
  3. <property>
  4. <name>hbase.hregion.majorcompaction</name>
  5. <value>604800000</value>
  6. <description>The time (in miliseconds) between 'major' compactions of
  7. all
  8. HStoreFiles in a region. Default: Set to 7 days. Major compactions tend to
  9. happen exactly when you need them least so enable them such that they
  10. run at
  11. off-peak for your deploy; or, since this setting is on a periodicity that is
  12. unlikely to match your loading, run the compactions via an external
  13. invocation out of a cron job or some such.
  14. </description>
  15. </property>
  16. <!-- 一个store里面允许存的hfile的个数,超过这个个数会被写到新的一个hfile里面 也即是每个region的每个列族对应的memstore在fulsh为hfile的时候,默认情况下当超过3个hfile的时候就会对这些文件进行合并重写为一个新文件,设置个数越大可以减少触发合并的时间,但是每次合并的时间就会越长 -->
  17. <property>
  18. <name>hbase.hstore.compactionThreshold</name>
  19. <value>3</value>
  20. <description>
  21. If more than this number of HStoreFiles in any one HStore
  22. (one HStoreFile is written per flush of memstore) then a compaction
  23. is run to rewrite all HStoreFiles files as one. Larger numbers
  24. put off compaction but when it runs, it takes longer to complete.
  25. </description>
  26. </property>
  27. <!-- 每个minor compaction操作的 允许的最大hfile文件上限 -->
  28. <property>
  29. <name>hbase.hstore.compaction.max</name>
  30. <value>10</value>
  31. <description>Max number of HStoreFiles to compact per 'minor'
  32. compaction.</description>
  33. </property>
  34. --------------------------------Flush--------------------------------
  35. <!-- regionServer的全局memstore的大小,超过该大小会触发flush到磁盘的操作,默认是堆大小的40%,而且regionserver级别的flush会阻塞客户端读写 -->
  36. <property>
  37. <name>hbase.regionserver.global.memstore.size</name>
  38. <value></value>
  39. <description>Maximum size of all memstores in a region server before
  40. new
  41. updates are blocked and flushes are forced. Defaults to 40% of heap (0.4).
  42. Updates are blocked and flushes are forced until size of all
  43. memstores
  44. in a region server hits
  45. hbase.regionserver.global.memstore.size.lower.limit.
  46. The default value in this configuration has been intentionally left
  47. emtpy in order to
  48. honor the old hbase.regionserver.global.memstore.upperLimit property if
  49. present.
  50. </description>
  51. </property>
  52. <!--可以理解为一个安全的设置,有时候集群的“写负载”非常高,写入量一直超过flush的量,这时,我们就希望memstore不要超过一定的安全设置。
  53. 在这种情况下,写操作就要被阻塞一直到memstore恢复到一个“可管理”的大小, 这个大小就是默认值是堆大小 * 0.4 * 0.95,也就是当regionserver级别的flush操作发送后,会阻塞客户端写,一直阻塞到整个regionserver级别的memstore的大小为 堆大小 * 0.4 *0.95为止 -->
  54. <property>
  55. <name>hbase.regionserver.global.memstore.size.lower.limit</name>
  56. <value></value>
  57. <description>Maximum size of all memstores in a region server before
  58. flushes are forced.
  59. Defaults to 95% of hbase.regionserver.global.memstore.size (0.95).
  60. A 100% value for this value causes the minimum possible flushing to
  61. occur when updates are
  62. blocked due to memstore limiting.
  63. The default value in this configuration has been intentionally left
  64. emtpy in order to
  65. honor the old hbase.regionserver.global.memstore.lowerLimit property if
  66. present.
  67. </description>
  68. </property>
  69. <!-- 内存中的文件在自动刷新之前能够存活的最长时间,默认是1h -->
  70. <property>
  71. <name>hbase.regionserver.optionalcacheflushinterval</name>
  72. <value>3600000</value>
  73. <description>
  74. Maximum amount of time an edit lives in memory before being automatically
  75. flushed.
  76. Default 1 hour. Set it to 0 to disable automatic flushing.
  77. </description>
  78. </property>
  79. <!-- 单个region里memstore的缓存大小,超过那么整个HRegion就会flush,默认128M -->
  80. <property>
  81. <name>hbase.hregion.memstore.flush.size</name>
  82. <value>134217728</value>
  83. <description>
  84. Memstore will be flushed to disk if size of the memstore
  85. exceeds this number of bytes. Value is checked by a thread that runs
  86. every hbase.server.thread.wakefrequency.
  87. </description>
  88. </property>