回顾一下hbase写流程 1、HBase是一种Log-Structured Merge Tree架构模式, 2、用户数据写入先写WAL,再写缓存, 3、满足一定条件后缓存数据会执行flush操作真正落盘,形成一个数据文件HFile。

什么叫Compaction (压缩合并) 1、随着数据写入不断增多,flush次数也会不断增多,进而HFile数据文件就会越来越多。 2、然而,太多数据文件会导致数据查询IO次数增多,因此HBase尝试着不断对这些文件进行合并,这个合并过程称为Compaction。

Compaction (压缩合并)小细节 1、Compaction会从一个region的一个store中选择一些hfile文件进行合并。 2、合并说来原理很简单,先从这些待合并的数据文件中读出KeyValues,再按照由小到大排列后写入一个新的文件中。 3、之后,这个新生成的文件就会取代之前待合并的所有文件对外提供服务。 4、HBase根据合并规模将Compaction分为了两类:MinorCompaction(次要)和MajorCompaction(主要)

Minor Compaction 1、Minor Compaction是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile, 2、在这个过程中不会处理已经Deleted或Expired的Cell。 3、一次Minor Compaction的结果是更少并且更大的StoreFile。 Major Compaction 1、Major Compaction是指将所有的StoreFile合并成一个StoreFile, 2、这个过程还会清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。 4、另外,一般情况下,Major Compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。 5、因此线上业务都会将关闭自动触发Major Compaction功能,改为手动在业务低峰期触发。

Compaction作用 | 副作用

Compaction作用 1、随着hfile文件数不断增多,一次查询就可能会需要越来越多的IO操作,延迟必然会越来越大, 2、如下图一所示,随着数据写入不断增加,文件数不断增多,读取延时也在不断变大。 3、而执行compaction会使得文件数基本稳定,进而IO Seek次数会比较稳定,延迟就会稳定在一定范围。 4、然而,compaction操作重写文件会带来很大的带宽压力以及短时间IO压力。 5、因此可以认为,Compaction就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。//compaction的作用 //从图上来看,就是延迟有很大的毛刺,但总体趋势基本稳定不变

为了换取后续查询的低延迟,除了短时间的读放大之外,Compaction对写入也会有很大的影响。 1、我们首先假设一个现象:当写请求非常多,导致不断生成HFile,但compact的速度远远跟不上HFile生成的速度,这样就会使HFile的数量会越来越多,导致读性能急剧下降。 2、为了避免这种情况,在HFile的数量过多的时候会限制写请求的速度: 3、在每次执行MemStore flush的操作前,如果HStore的HFile数超过hbase.hstore.blockingStoreFiles (默认7),则会阻塞flush操作hbase.hstore.blockingWaitTime时间, 4、在这段时间内,如果compact操作使得HStore文件数下降到回这个值,则停止阻塞。 5、另外阻塞超过时间后,也会恢复执行flush操作。 6、这样做就可以有效地控制大量写请求的速度,但同时这也是影响写请求速度的主要原因之一。 //cdh中 HStore 阻塞存储文件 hbase.hstore.blockingStoreFiles = 10 HStore 阻塞等待时间 hbase.hstore.blockingWaitTime = 90秒 小结:可见,Compaction会使得数据读取延迟一直比较平稳,但付出的代价是大量的读延迟毛刺和一定的写阻塞。

Compaction流程 //见图

compaction触发-》选择合适hfile合并 -》挑选合适的线程池 -》执行hfile文件合并 1、整个Compaction始于特定的触发条件,比如flush操作、周期性地Compaction检查操作等。 2、一旦触发,HBase会将该Compaction交由一个独立的线程处理,该线程首先会从对应store中选择合适的hfile文件进行合并,这一步是整个Compaction的核心, 3、选取文件需要遵循很多条件,比如文件数不能太多、不能太少、文件大小不能太大等等,最理想的情况是,选取那些承载IO负载重、文件小的文件集 4、实际实现中,HBase提供了多个文件选取算法:RatioBasedCompactionPolicy、ExploringCompactionPolicy和StripeCompactionPolicy等,用户也可以通过特定接口实现自己的Compaction算法; 5、选出待合并的文件后,HBase会根据这些hfile文件总大小挑选对应的线程池处理,最后对这些文件执行具体的合并操作。

(1)触发时机 HBase中可以触发compaction的因素有很多,最常见的因素有这么三种: 1、Memstore Flush 2、后台线程周期性检查、3、手动触发。

Memstore Flush: 1、Memstore Flush: 应该说compaction操作的源头就来自flush操作, 2、memstore flush会产生HFile文件,文件越来越多就需要compact。 3、因此在每次执行完Flush操作之后,都会对当前Store中的文件数进行判断, 4、一旦文件数# > ,就会触发compaction。 5、需要说明的是,compaction都是以Store为单位进行的,而在Flush触发条件下, 6、整个Region的所有Store都会执行compact,所以会在短时间内执行多次compaction。

后台线程周期性检查 1、后台线程周期性检查:后台线程CompactionChecker定期触发检查是否需要执行compaction,检查周期为:hbase.server.thread.wakefrequency*hbase.server.compactchecker.interval.multiplier。 //cdh HBase Server 线程唤醒步骤 hbase.server.thread.wakefrequency = 10秒

2、和flush不同的是,该线程优先检查文件数#是否大于,一旦大于就会触发compaction。 3、如果不满足,它会接着检查是否满足major compaction条件(大合并), 4、简单来说,如果当前store中hfile的最早更新时间早于某个值mcTime,就会触发major compaction,HBase预想通过这种机制定期删除过期数据。 5、上文mcTime是一个浮动值,浮动区间默认为[7-70.2,7+70.2],其中7为hbase.hregion.majorcompaction,0.2为hbase.hregion.majorcompaction.jitter, //HBase HRegion 最大化压缩抖动 hbase.hregion.majorcompaction.jitter = 0.5 HBase HRegion 最大化压缩 hbase.hregion.majorcompaction = 7天 6、可见默认在7天左右就会执行一次major compaction。用户如果想禁用major compaction,只需要将参数hbase.hregion.majorcompaction设为0

手动触发合并 1、手动触发:一般来讲,手动触发compaction通常是为了执行major compaction, 2、原因有三,其一是因为很多业务担心自动major compaction影响读写性能,因此会选择低峰期手动触发; 3、其二也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发major compaction; 4、其三是HBase管理员发现硬盘容量不够的情况下手动触发major compaction删除大量过期数据; 5、无论哪种触发动机,一旦手动触发,HBase会不做很多自动化检查,直接执行合并。

//大型业务设置为手动触发合并 也就是关闭自动触发合并 可见默认在7天左右就会执行一次major compaction。用户如果想禁用major compaction,只需要将参数hbase.hregion.majorcompaction设为0

(2)选择合适HFile合并 //见博客 ***** 1、选择合适的文件进行合并是整个compaction的核心,因为合并文件的大小以及其当前承载的IO数直接决定了compaction的效果。 2、最理想的情况是,这些文件承载了大量IO请求但是大小很小,这样compaction本身不会消耗太多IO,而且合并完成之后对读的性能会有显著提升。//现实生产环境不会是这样的 3、在0.96版本和0.98版本,分别提出了两种选择策略,在充分考虑整体情况的基础上选择最佳方案。无论哪种选择策略,都会首先对该Store中所有HFile进行一一排查,排除不满足条件的部分文件: 1.排除当前正在执行compact的文件及其比这些文件更新的所有文件(SequenceId更大) 2.排除某些过大的单个文件,如果文件大小大于hbase.hzstore.compaction.max.size(默认Long最大值),则被排除,否则会产生大量IO消耗 经过排除的文件称为候选文件,HBase接下来会再判断是否满足major compaction条件,如果满足,就会选择全部文件进行合并。判断条件有下面三条,只要满足其中一条就会执行major compaction:

  1. 用户强制执行major compaction
  2. 长时间没有进行compact(CompactionChecker的判断条件2)且候选文件数小于hbase.hstore.compaction.max(默认10)
  3. Store中含有Reference文件,Reference文件是split region产生的临时文件,只是简单的引用文件,一般必须在compact过程中删除 如果不满足major compaction条件,就必然为minor compaction,HBase主要有两种minor策略:RatioBasedCompactionPolicy和ExploringCompactionPolicy,下面分别进行介绍: RatioBasedCompactionPolic:从老到新逐一扫描所有候选文件,满足其中条件之一便停止扫描: (1)当前文件大小 < 比它更新的所有文件大小总和 * ratio,其中ratio是一个可变的比例,在高峰期时ratio为1.2,非高峰期为5,也就是非高峰期允许compact更大的文件。那什么时候是高峰期,什么时候是非高峰期呢?用户可以配置参数hbase.offpeak.start.hour和hbase.offpeak.end.hour来设置高峰期 (2)当前所剩候选文件数 <= hbase.store.compaction.min(默认为3) 停止扫描后,待合并文件就选择出来了,即为当前扫描文件+比它更新的所有文件

ExploringCompactionPolicy 该策略思路基本和RatioBasedCompactionPolicy相同,不同的是,Ratio策略在找到一个合适的文件集合之后就停止扫描了,而Exploring策略会记录下所有合适的文件集合,并在这些文件集合中寻找最优解。 最优解可以理解为:待合并文件数最多或者待合并文件数相同的情况下文件大小较小,这样有利于减少compaction带来的IO消耗。

提示:Ratio策略是0.94版本的默认策略,而0.96版本之后默认策略就换为了Exploring策略,在cloudera博文《what-are-hbase-compactions》中,作者给出了一个两者的简单性能对比,基本可以看出后者在节省IO方面会有10%左右的提升: 截止到此,HBase基本上就选择出来了待合并的文件集合,后续通过挑选合适的处理线程,就会对这些文件进行真正的合并 。

(3)挑选合适的线程池 1、HBase实现中有一个专门的线程CompactSplitThead负责接收compact请求以及split请求,而且为了能够独立处理这些请求,这个线程内部构造了多个线程池:largeCompactions、smallCompactions以及splits等, 2、其中splits线程池负责处理所有的split请求,largeCompactions和smallCompaction负责处理所有的compaction请求,其中前者用来处理大规模compaction,后者处理小规模compaction。这里需要明白三点:

  1. 上述设计目的是为了能够将请求独立处理,提供系统的处理性能。
  2. 哪些compaction应该分配给largeCompactions处理,哪些应该分配给smallCompactions处理?是不是Major Compaction就应该交给largeCompactions线程池处理?不对。这里有个分配原则:待compact的文件总大小如果大于值throttlePoint(可以通过参数hbase.regionserver.thread.compaction.throttle配置,默认为2.5G),分配给largeCompactions处理,否则分配给smallCompactions处理。
  3. largeCompactions线程池和smallCompactions线程池默认都只有一个线程,用户可以通过参数hbase.regionserver.thread.compaction.large和hbase.regionserver.thread.compaction.small进行配置

(4)执行HFile文件合并 上文一方面选出了待合并的HFile集合,一方面也选出来了合适的处理线程,万事俱备,只欠最后真正的合并。合并流程说起来也简单,主要分为如下几步:

  1. 分别读出待合并hfile文件的KV,并顺序写到位于./tmp目录下的临时文件中
  2. 将临时文件移动到对应region的数据目录
  3. 将compaction的输入文件路径和输出文件路径封装为KV写入WAL日志,并打上compaction标记,最后强制执行sync
  4. 将对应region数据目录下的compaction输入文件全部删除

上述四个步骤看起来简单,但实际是很严谨的,具有很强的容错性和完美的幂等性:

  1. 如果RS在步骤2之前发生异常,本次compaction会被认为失败,如果继续进行同样的compaction,上次异常对接下来的compaction不会有任何影响,也不会对读写有任何影响。唯一的影响就是多了一份多余的数据。
  2. 如果RS在步骤2之后、步骤3之前发生异常,同样的,仅仅会多一份冗余数据。
  3. 如果在步骤3之后、步骤4之前发生异常,RS在重新打开region之后首先会从WAL中看到标有compaction的日志,因为此时输入文件和输出文件已经持久化到HDFS,因此只需要根据WAL移除掉compaction输入文件即可

小结: 本文重点从减少IO的层面对Compaction进行了介绍,其实Compaction还是HBase删除过期数据的唯一手段。

小伙伴的问题1 Hi ,”然而,compaction操作重写文件会带来很大的带宽压力以及短时间IO压力” 这句话不是太懂,compaction是在一个store内发生的,并不涉及网路操作,那为什么会引起带宽的压力呢? 答: 带宽消耗主要有两方面:

  1. 合并文件第一阶段是读取小文件,这些小文件可能并不在本地,而在其他dn上,需要远程读取
  2. 合并文件第二阶段是写成大文件,写入过程依然是三副本写入,本地写一份,远程写两份,因此也有带宽消耗 //HFile一般有三副本 通常情况下数据写入会将一个副本写到本地(和Region在同一台机器) 如果Region发生迁移 迁移到其他机器上去 文件就可能不在本地了

小伙伴的问题2 范老师,你好!请教一个问题,关于使用RatioBasedCompactionPolicy这个策略来选择哪些storeFile需要进行compact中ratio的是如何进行选择的呢?高峰期与非高峰期如何判断?关于minor compact 文件选择策略,一般推荐使用哪一种策略?另外请教一个关于region的预分split,如果知道已知数据量的大小,如何进行选择region的个数呢? 答: 推荐官方默认的compact策略 0.96之后就是exploring。高峰期设置由业务确定,根据参数hbase.offpeak.start.hour和hbase.offpeak.end.hour确定。 预分区region,一般根据rowkey散列性质来决定,比如如果rowkey是经过md5处理过的,就可以根据数据量大小分成4个或者16个甚至32个,一般设置16个就可以,数据量大了会自动分裂

范老师,预分region的话,一般一个region的大小是多少呢 个人推荐:30g~50g左右 每台rs上差不多100~200左右的region

小伙伴的问题3 请问minor compact和majorcomact期间对读请求有多大影响,应该不对阻塞读请求吧? 答: 不会的 compaction会大量读写文件 对io带宽这些系统资源比较耗费 一定程度上会影响读请求 一般不会阻塞读请求

小伙伴的问题4 范老师,你好,我在CDH5.8.2中hbase 没有找到hbase.hzstore.compaction.max.size这个,也没有找到hbase.regionserver.thread.compaction.large和hbase.regionserver.thread.compaction.small ,不知道CDH中是不是把这些值写到cdc的数据库中了? 答: 这几个参数是社区版本的参数,cdh版本是不是做过调整。可以在网上看看这几个参数的介绍,再对应的在cdh中找找看,个人感觉应该是有的

小伙伴的问题5: 我们现在hbase 有很多region major compact 持续很长时间,大概在十多个小时都不能结束掉,但是日志里又没有出现异常,还有在生产环境中如果关闭掉了major compact 的话,会有什么影响吗? 答: 如果major很长,考虑是不是region特别大,或者磁盘io不行。如果没有大量的update操作,可以关闭自动major,改为周期性的手动major。

小伙伴的问题6 输入文件路径:合并前的几个HFile原始文件 输出文件路径:几个HFile文件合并后,产生的那个新HFile文件

范神的意思是:这个过程相当于 输入 合并前的几个HFile原始文件,然后输出 合并后产生的新HFile文件 答: 是的 归并排序

小伙伴的问题6 我从文中摘抄了两个地方 摘抄1——“1. Memstore Flush: 应该说compaction操作的源头就来自flush操作,memstore flush会产生HFile文件,文件越来越多就需要compact。因此在每次执行完Flush操作之后,都会对当前Store中的文件数进行判断,一旦文件数# > ,就会触发compaction。需要说明的是,compaction都是以Store为单位进行的,而在Flush触发条件下,整个Region的所有Store都会执行compact,所以会在短时间内执行多次compaction。 ”

摘抄2——Store中含有Reference文件,Reference文件是split region产生的临时文件,只是简单的引用文件,一般必须在compact过程中删除

===================== 我的疑问: 上面有一句话你说过:线上业务都会将关闭自动触发Major Compaction功能,改为手动在业务低峰期触发。 如果我现在设置了hbase.hregion.majorcompaction=0关闭了自动触发Major Compaction,但是当Memstore Flush出的HFile文件数很多的情况下,并且Store中含有Reference文件 按照上面这段文字的意思,Memstore Flush在输出HFile文件很多的情况下会检查触发compact,到底是minor compaction还是major compaction,由三个条件来判断,我们正好符合其中一个条件:Store中含有Reference,那么,Store中含有Reference文件就还是会触发Major Compaction功能咯? 答 这个问题我在范神的http://hbasefly.com/2017/08/27/hbase-split/#comment-6041 这个博客的评论中找到答案, 下面我把提问的人和答案复制出来下:

【提问者的问题】 父region的数据什么时候会迁移到子region目录? 答案是子region发生major_compaction时。 如果 region 的 hbase.hregion.majorcompaction 属性是 0 呢?

【范神给的回复】 boolean isAfterSplit = StoreUtils.hasReferences(candidateSelection); CompactionRequest result = createCompactionRequest(candidateSelection, isTryingMajor || isAfterSplit, mayUseOffPeak, mayBeStuck); 如果是split之后进行的compaction,会按照major执行的

案例 案例一:Compaction永久阻塞 导致现象:线上一个集群因为未知原因忽然就卡住了,读写完全进不来了;另外还有很多处于PENDING_CLOSE状态的Region。 http://hbasefly.com/2016/09/08/hbase-rit/

参考链接: http://hbasefly.com/2016/07/13/hbase-compaction-1/