- Compaction是以Store为单位进行的
- Compaction使读取延迟更加稳定,但是读取时间产生了很大的毛刺
- Compaction 操作的主要源头来自flush操作
- 根据待合并HFile文件的选择策略可以得知,正在进行Compaction的HFile文件不会重复被Compaction
Compaction 是从一个 Region 的一个 Store 中选择部分 HFile 文件进行合并,即Compaction是以Store为单位的。。
合并原理是:先从这些待合并的数据文件中依次读出 KeyValue,再由小到大排序后写入一个新的文件。之后,这个新生成的文件就会取代之前已合并的所有文件对外提供服务。
HBase 根据合并规模将 Compaction 分为两类:Minor Compaction
和 Major Compaction
。
Minor Compaction
是指选取部分小的、相邻的HFile,将它们合并成一个更大的HFile。Major Compaction
是指将一个Store中所有的HFile合并成一个HFile,在HBase的体系架构下,Compaction有以下核心作用:
随着HFile文件数不断增多,查询可能需要越来越多的IO操作,读取延迟必然会越来越大。
执行Compaction会使文件个数基本稳定,进而读取IO的次数会比较稳定,延迟就会稳定在一定范围。从图上看,虽然数据读取延迟相比上图稳定了一些,但是读取响应时间有了很大的毛刺,这是因为Compaction在执行的时候占用系统资源导致业务读取性能受到一定波及。
Compaction 的一个重要作用是提高数据的本地化率。本地化率越高,在 HDFS 上访问数据时延迟就越小;相反,本地化率越低,访问数据就可能大概率需要通过网络访问,延迟必然会比较大。
Compaction 合并小文件的同时会将落在远程 DataNode 上的数据读取出来重新写入大文件,合并后的大文件在当前 DataNode 节点上有一个副本,因此可以提高数据的本地化率。极端情况下,Major Compaction 可以将当前 Region 的本地化率提高到100%。这也是最常用的一种提高数据本地化率的方法。
Compaction 在执行过程中有个比较明显的副作用:Compaction 操作重写文件会带来很大的带宽压力以及短时间 IO 压力。这点比较容易理解,要将小文件的数据读出来需要 IO,很多小文件数据跨网络传输需要带宽,读出来之后又要写成一个大文件,因为是三副本写入,必然需要网络开销,当然写入 IO 开销也避免不了。因此可以认为,Compaction 就是使用短时间的 IO 消耗以及带宽消耗换取后续查询的低延迟。
HBase中Compaction只有在特定的触发条件才会执行,一旦触发,HBase会将该Compaction交由一个独立的线程处理,该线程首先会从对应Store中选择合适的HFile文件进行合并,这一步是整个Compaction的核心。选出待合并的文件后,HBase会根据这些HFile文件总大小挑选对应的线程池处理,最后对这些文件执行具体的合并操作。
HBase 中触发 Compaction 的时机有很多,最常见的时机有如下三种:MemStore Flush、后台线程周期性检查以及手动触发。
hbase.hstore.compactionThreshold
,就会触发 Compaction。需要说明的是,Compaction 都是以 Store 为单位进行的,而在 flush 触发条件下,整个 Region 的所有 Store 都会执行 compact 检查,所以一个 Region 有可能会在短时间内执行多次 Compaction。参数解释:
hbase.hstore.compactionThreshold
:默认值为3,如果在任何一个Store中存在超过这个数量的HFile,运行Compaction将所有HFile重写为单个HFile。将该参数设置成较大的值会延迟Compaction,但是当Compaction运行时,则需要更长的时间。
CompactionChecker
,定期触发检查对应 Store 是否需要执行 Compaction,检查周期为 hbase.server.thread.wakefrequency
* hbase.server.compactchecker.interval.multiplier
,大概2小时40分左右执行一次。hbase.hstore.compactionThreshold
,一旦大于阀值就会触发 Compaction;如果不大于阀值,接着检查是否满足 Major Compaction
条件。简单来说,如果当前 Store 中 HFile 的最早更新时间早于某个值 mcTime,就会触发 Major Compaction。hbase.hregion.majorcompaction
的值,0.5为参数 hbase.hregion.majorcompaction.jitter
的值,可见默认在7天左右就会执行一次 Major Compaction。参数解释:
hbase.server.thread.wakefrequency
:默认值为10000ms,作为服务线程(如日志roller)的睡眠时间间隔。hbase.server.compactchecker.interval.multiplier
:默认值为1000,用于决定周期任务频率的值,以确定是否需要Compaction。通常情况下,Compaction是在一些事件(如memstore刷新)之后执行的,但是如果Region在一段时间内没有收到很多写操作,或者由于不同的Compaction策略,可能需要定期检查它。hbase.hregion.majorcompaction
:默认值为604800000ms,即7天,表示两次Major Compaction之间的时间间隔。设置为0可以禁用基于时间的自动Major Compaction,但手动触发和基于Store中HFile文件数量的Major Compaction仍将运行。hbase.hregion.majorcompaction.jitter
:默认值为0.5,这个值乘以hbase.hregion.majorcompaction
使Compaction在给定的时间窗口中以某种随机的时间开始。该参数的值越小,Major Compaction就会越接近hbase.hregion.majorcompaction区间。
选择合适的文件进行合并是整个 Compaction 的核心,因为合并文件的大小及其当前承载的 IO 数直接决定了 Compaction 的效果以及对整个系统其他业务的影响程度。理想的情况是,选择的待合并 HFile 文件集合承载了大量 IO 请求但是文件本身很小,这样Compaction 本身不会消耗太多 IO,而且合并完成之后对读的性能会有显著提升。然而现实中可能大部分 HFile 文件都不会这样。
要选择待合并的HFile文件,首先会对该Store中所有HFile逐一进行排查,排除不满足条件的部分文件,排除条件如下:
hbase.hstore.compaction.max.size
,则被排除,否则会产生大量IO消耗。参数解释:
hbase.hstore.compaction.max.size
:默认值:Long.MAX_VALUE,以字节表示,大于这个大小的HFile文件将被排除在Compaction之外。当Compaction发生得太频繁时,可以尝试提高这个值。
经过排除后留下来的文件称为候选文件,接下来 HBase 再判断侯选文件是否满足 Major Compaction 条件,如果满足,就会选择全部文件进行合并。判断条件如下所列,只要满足其中一条就会执行 Major Compaction:
CompactionChecker
条件二,且候选文件数小于 hbase.hstore.compaction.max
。参数解释:
hbase.hstore.compaction.max
:默认值为10,执行单次Minor Compaction过程可以被选择的HFile的最大数量,而与符合条件的HFile的数量无关。实际上,该参数的值控制完成一次Compaction所需的时间长度。将其设置得更大意味着Compaction中包含更多的HFile文件。对于大多数情况,默认值是合适的。
如果满足Major Compaction条件,文件选择这一步就结束了,待合并HFile文件就是Store中所有HFile文件。如果不满足Major Compaction条件,就必然为Minor Compaction。
HBase主要有两种Minor Compaction文件选择策略,一种是RatioBasedCompactionPolicy
,另一种是 ExploringCompactionPolicy
。后者在前者的基础上做了进一步修正。
从老到新逐一扫描所有候选文件,满足其中条件之一便停止扫描:
hbase.hstore.compaction.ratio
控制,非高峰期ratio为5,受参数hbase.hstore.compaction.ratio.offpeak
控制,也就是非高峰期允许compact更大的文件。hbase.hstore.compaction.min
。参数解释:
hbase.hstore.compaction.ratio
:默认值为1.2F,对于Minor Compaction,这个比率用于确定给定的HFile文件大于等于hbase.hstore.compaction.min.size
情况下是否适合进行Compaction。它的作用是限制对较大的HFile的进行Compaction。推荐取值范围为1.0和1.4之间的中等值。对于大多数情况,默认值是合适的。hbase.hstore.compaction.ratio.offpeak
:默认值为5.0F,工作原理同hbase.hstore.compaction.ratio
参数,但是该值用于非高峰时间段内的Mimor Compaction。默认情况下非高峰时间段是关闭的,因此该参数的值不会生效。hbase.offpeak.start.hour
:默认值为-1,非高峰时间的开始,表示为0到23之间的整数。设置为-1禁用非峰值,即默认禁用。hbase.offpeak.end.hour
:默认值为-1,非高峰时间的结束,表示为0到23之间的整数。设置为-1禁用非峰值,即默认禁用。hbase.hstore.compaction.min
:默认为3,在运行Compaction之前必须符合Compaction条件的HFile文件的最小数量。调优该参数的的目的是避免产生太多需要Compaction的小HFile文件。将该值设置为2将导致每次在一个Store中有两个HFile文件时进行Minor Compaction,这可能不合适。如果您将该值设置得过高,则需要相应地调整所有其他值。对于大多数情况,默认值是合适的。在HBase的早期版本中,该参数被命名为hbase.hstore.compactionThreshold
。hbase.hstore.compaction.min.size
:默认值为134217728,即128M,小于此大小的HFile文件将始终适合进行Minor Compaction。大于等于此大小的HFiles文件由hbase.hstore.compact.ratio评估,以确定它们是否符合条件。
停止扫描后,待合并文件就选择出来了,即当前扫描文件以及比它更新的所有文件。
实际情况下的RatioBasedCompactionPolicy
算法效果很差,经常引发大面积的 Minor Compaction,而Minor Compaction过程中不能写入数据,经常因为Compaction而影响IO。
该策略思路基本和 RatioBasedCompactionPolicy 相同,不同的是,Ratio策略在找到一个合适的文件集合之后就停止扫描了,而Exploring策略会记录所有合适的文件集合,并在这些文件集合中寻找最优解。最优解可以理解为:待合并文件数最多或者待合并文件数相同的情况下文件较小,这样有利于减少Compaction带来的IO消耗。
a.修改待合并文件的挑选条件
不再武断地认为,某个文件满足条件就把更加新的文件全部合并进去。确切地说,现在的遍历不强调顺序性了,是把所有的文件都遍历一遍之后每一个文件都去考虑。如果当前文件大小小于最小Compaction大小,则直接进入待合并列表。最小合并大小的配置项:hbase.hstore.compaction.min.size
。如果没设定该配置项,则使用hbase.hregion.memstore.flush.size
。
如果不小于最小Compaction大小,则根据“该文件大小 < (所有文件大小总和 - 该文件大小) * ratio”条件判断符合条件而进入待合并列表的文件。
b.以组合作为计算单元
新的算法不再按文件为单元进行比较了,而是挑出多个文件组合。 挑选组合的条件是:
被挑选的文件必须能通过以上提到的筛选条件,并且组合内含有的文件数必须大于hbase.hstore.compaction.min
,小于hbase.hstore.compaction.max
。
文件太少了没必要合并,还浪费资源;文件太多了太消耗资源,怕机器受不了。
挑选完组合后,比较哪个文件组合包含的文件更多,就合并哪个组合。如果出现平局,就挑选那个文件尺寸总和更小的组合。
HBase实现中有一个专门的类org.apache.hadoop.hbase.regionserver.CompactSplit
负责接收Compaction请求和Split请求,而且为了能够独立处理这些请求,这个类内部构造了三个线程池:longCompactions
、shortCompactions
以及splits
。splits
线程池负责处理所有的split请求,longCompactions
用来处理大性Compaction,shortCompactions
负责处理小型Compaction。
这里需要明确三点:
上述设计目的是能够将请求独立处理,提高系统的处理性能。
大型Compaction并不是Major Compaction,小型Compaction 也并不是MinorCompaction。HBase 定义了一个阈值hbase.regionserver.thread.compaction.throttle
,如果Compaction合并的总文件大小超过这个阈值就认为是大型Compaction,否则认为是小型Compaction。大Compaction会分配给longCompactions
线程池处理,小Compaction会分配给shortCompactions
线程池处理。
longCompactions
线程池和shortCompactions
线程池默认都只有一个线程,用户可以通过参数hbase.regionserver.thread.compaction.large
和hbase.regionserver.thread.compaction.small
进行配置。
参数解释:
hbase.regionserver.thread.compaction.throttle
:默认值为2684354560,即2G,Compaction有两个不同的线程池,一个用于大型Compaction,另一个用于小型Compaction。这有助于保持精简表(如hbase:meta)的快速Compaction。如果Compaction大于此阈值,则将进入大型Compaction池。在大多数情况下,默认值是合适的。默认值是2 *hbase.hstore.compaction.max
*hbase.hregion.memstore.flush.size
。
选出待合并的HFile集合,再选出合适的处理线程,接下来执行合并流程。
合并流程主要分为如下几步:
分别读出待合并 HFile 文件的 KeyValue,进行归并排序处理,之后写到region目录下的.tmp
目录下新创建HFile文件中。以下两种数据不会被读取出来:
(1)如果数据过期了(达到 TTL 所规定的时间),那么这些数据不会被读取出来。
(2)如果是Major Compaction,那么数据带了墓碑标记也不会被读取出来。
将/
目录下新创建的HFile文件移动到对应列族数据目录。
将Compaction的输入文件路径和输出文件路径封装为KV写入HLog日志,并打上 Compaction标记,最后强制执行sync。
将对应列族数据目录下的Compaction输入文件全部删除。
上述4个步骤看起来简单,但实际是很严谨的,具有很强的容错性和幂等性:
其实HBase一直拖到Major Compaction的时候才真正把带墓碑标记的数据删掉,并不是因为性能要求,而是之前真的做不到。HBase是建立在HDFS这种只有增加删除而没有修改的文件系统之上的,所以就连用户删除这个动作,在底层都是由新增实现的:
现在会遇到一个问题:当用户删除数据的时候之前的数据已经被刷写到磁盘上的另外一个HFile了。这种情况很常见,也就是说,墓碑标记和原始数据这两个KeyValue 压根就不在同一个HFile上,如下图所示:
在查询的时候Scan指针其实是把所有的HFile都看过了一遍,它知道了有这条数据,也知道它有墓碑标记,而在返回数据的时候选择不把数据返回给用户,这样在用户的Scan操作看来这条数据就是被删掉了。如果带上RAW=>true
参数来Scan,就可以查询到这条被打上墓碑标记的数据。
这是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来记录。Compaction时创建的Scan在查询数据的时候,根据“当前时间now - cell的timestamp > TTL”公式来判断cell是否过期。
如果过期了就不返回这条数据。这样当Compaction完成后,过期的数据因为没有被写入新文件,自然就消失了。