HBase读写流程&HBase的Flush&HBase的Compaction

MemStore Flush

MemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,如下所示:

每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。

有以下三种情况可以触发MemStore的Flush动作,需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。据说这是Column Family有个数限制的其中一个原因,估计是因为太多的Column Family一起Flush会引起性能问题?具体原因有待考证。

  1. Memstore级别限制: 当一个HRegion中的所有MemStore的大小总和超过了hbase.hregion.memstore.flush.size的大小,默认128MB。此时当前的HRegion中所有的MemStore会Flush到HDFS中。

  2. Region级别限制: 当region所有的memstore的size和,达到hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size= 128*4=512M 会触发memstore flush,同时会阻塞所有的写入该store的写请求! 继续对该region写请求,会抛错 region too busy exception异常。

  3. RegionServer级别限制:

    • 当rs节点上所有的memstore的size和 ,超过低水位线阈值hbase.regionserver内存大小 * hbase.regionserver.global.memstore.size * hbase.regionserver.global.memstore.size.lower.limit
      =48G * 0.45 * 0.91=19.656G,rs强制执行flush。先flush memstore最大的region,再第二大的,直到总的memstore大小下降到低水位线的阈值。

    • 如果此时写入非常繁忙,导致总的memstore大于hbase.regionserver内存大小 *
      hbase.regionserver.global.memstore.size = 21.6G 。rs会阻塞写读的请求,并强制flush,达到低水位线的阈值(安全阈值)

      注意: hbase.regionserver.global.memstore.size.lower.limit == hbase.regionserver.global.memstore.lowerLimit

  4. Hlog级别限制: 当rs的hlog数量达到hbase.regionserver.max.logs(默认32) 会选择最早的hlog的对应的一个或多个region进行flush。

  5. 定期级别限制: hbase.regionserver.optionalcacheflushinterval(默认1h),为避免所有的memstore在同一个时间点进行flush导致的问题,定期的flush其实会有一定的随机时间延时。设置为0就是禁用。

  6. 手动级别限制: flush命令可以封装脚本。

总结: 在生产上,唯独触发rs级别的限制导致flush,是属于灾难级别的,会阻塞所有落在该rs节点的读写请求,直到总的memstore大小降到低水位线,阻塞时间较长。其他的级别限制,只会阻塞对应的region的读写请求,阻塞时间较短。

写流程

HBase是采取LSM树架构,(天生的适合重写轻读的场景)

对HBase来说put、delete操作,在服务端看来都是写操作。

  • put更新是写一条最新版本数据

  • delete是写一条标记为deleted的kv的数据

  1. 先去zk获取hbase:meta表所在的rs节点
  2. 在hbase:meta表根据rk确定目标rs节点和region
  3. hbase client将写的请求进行预处理,并根据元数据写入所在的rs节点,将请求发送给对应的rs,rs接收到写的请求将数据解析: 先写wal,再写对应的region的store的memstore当memstore达到阈值,会异步的flush,将内存的数据写入文件,为HFile。

注意: memstore是一个内存结构,是一个CF只有1个memstore,其中memstore里面的数据也是对rk进行字典排序的。

读流程

我们知道在HBase写时,相同Cell(RowKey/ColumnFamily/Column相同)并不保证在一起,甚至删除一个Cell也只是写入一个新的Cell,它含有Delete标记,而不一定将一个Cell真正删除了,因而这就引起了一个问题,如何实现读的问题?

要解决这个问题,我们先来分析一下相同的Cell可能存在的位置:首先对新写入的Cell,它会存在于MemStore中;然后对之前已经Flush到HDFS中的Cell,它会存在于某个或某些StoreFile(HFile)中;最后,对刚读取过的Cell,它可能存在于BlockCache中。

既然相同的Cell可能存储在三个地方,在读取的时候只需要扫瞄这三个地方,然后将结果合并即可(Merge Read),在HBase中扫瞄的顺序依次是:BlockCache、MemStore、StoreFile(HFile)。

其中StoreFile的扫瞄先会使用Bloom Filter过滤那些不可能符合条件的HFile,然后使用Block Index快速定位Cell,并将其加载到BlockCache中,然后从BlockCache中读取。我们知道一个HStore可能存在多个StoreFile(HFile),此时需要扫瞄多个HFile,如果HFile过多又是会引起性能问题。

  1. 先去zk获取hbase:meta表所在的rs节点,
  2. 在hbase:meta表根据读rk确定所在的目标rs节点和region
  3. 将读请求封装,发送给目标的rs节点,进行处理。先到blockcache查数据,查不到到memstore查,再查不到就访问磁盘的HFile读数据。

Compaction

MemStore每次Flush会创建新的HFile,而过多的HFile会引起读的性能问题,那么如何解决这个问题呢?HBase采用Compaction机制来解决这个问题,有点类似Java中的GC机制,起初Java不停的申请内存而不释放,增加性能,然而天下没有免费的午餐,最终我们还是要在某个条件下去收集垃圾,很多时候需要Stop-The-World,这种Stop-The-World有些时候也会引起很大的问题,因而设计是一种权衡,没有完美的。还是类似Java中的GC,在HBase中Compaction分为两种:Minor Compaction和Major Compaction。

  1. Minor Compaction是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次Minor Compaction的结果是更少并且更大的StoreFile。
  2. Major Compaction是指将所有的StoreFile合并成一个StoreFile,在这个过程中,标记为Deleted的Cell会被删除,而那些已经Expired(TTL)的Cell会被丢弃,那些已经超过最多版本数的Cell会被丢弃。一次Major Compaction的结果是一个HStore只有一个StoreFile存在。Major Compaction可以手动或自动触发,然而由于它会引起很多的IO操作而引起性能问题,因而生产上尽可能的避免发生major compaction,一般通过关闭自动触发大合并,改为手动触发,在业务低谷时期,执行。(一般在凌晨调度脚本,去执行)

更形象一点,如下面两张图分别表示Minor Compaction和Major Compaction。

为什么要合并?

随着hflie文件越来越多,查询需要更多的IO,读取延迟较大。所以需要compaction,主要为了消费带宽和短时间的IO压力,来换取以后查询的低延迟。

合并的作用?

合并小文件减少文件数量、减小稳定度的延迟、消除无效数据、降低存储空间、提高数据的本地化比率。

合并的触发条件?

先判断是否触发小合并,再判断是否大合并

  1. memstore flush: 合并根源来自flush,当memstore达到阈值或者其他条件就触发flush,将数据写到hflie
    正是因为文件多,才需要合并。每次flush之后,就当前的store文件数进行校验判断,一旦store的总文件数超过hbase.hstore.compactionThreshold(默认3),就触发合并

  2. 后台线程定期检查
    后台线程 compactchecker 定期检查是否需要执行合并。检查周期为 hbase.server.thread.wakefrequency * hbase.server.compactchecker.interval.multiplier = 10000ms * 1000。在不做参数修改情况的下,compactchecker 大概是2h 46min 40s执行一次。

  3. 当文件小于 hbase.hstore.compaction.min.size 会被立即添加到合并的队列,当storefiles数量超过
    hbase.hstore.compaction.min时,就启动小合并。

生产上调参:
hbase.hregion.majorcompaction 0 关闭大合并
hbase.hstore.compactionThreshold 6(默认3) 增加小合并文件数

Author: Tunan
Link: http://yerias.github.io/2020/07/26/hbase/4/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.