Compact作用
当MemStore超过阀值的时候,就要flush到HDFS上生成一个HFile。因此随着不断写入,HFile的数量将会越来越多,根据前面所述,HFile数量过多会降低读性能。为了避免对读性能的影响,可以对这些HFile进行compact操作,把多个HFile合并成一个HFile。compact操作需要对HBase的数据进行多次的重新读写,因此这个过程会产生大量的IO。可以看到compact操作的本质就是以IO操作换取后续的读性能的提高。
Compact两种方式区别
1)Minor操作只用来做部分文件的合并操作以及包括minVersion=0并且设置ttl的过期版本清理,不做任何删除数据、多版本数据的清理工作。
2)Major操作是对Region下的HStore下的所有StoreFile执行合并操作,最终的结果是整理合并出一个文件。
一般情况下都是做Minor合并,Major不少集群都是禁止,然后再集群负载较小时,进行手动Major合并,在我们数据立方这边,也是配置了一个<name>datacube.hregion.majorcompaction</name><value>0</value>,这是配置major的合并周期(默认为7天),很多集群配置成一天,如果配置成0即关闭Major合并。本文重点讨论的是minor合并,因此就不在此多说Major合并,只需要注意,既然Major合并是把所有HFile都合并成一个文件,可想对集群负载不可小觑。
minor则只会选择数个HFile文件compact为一个HFile,minor的过程一般较快,而且IO相对较低。在日常任务时间,都会禁止mjaor操作,只在空闲的时段定时执行。
Compact触发条件
可以请求compact的地方有很多,包括在openregion、MemStore flush等都会判断是否需要进行compact操作(单个HStore的MemStore flush之后,如果触发compact操作,则会对所属HRegion下的所有HStore分别进行compact)。除此之外,HRegionServer.CompactionChecker负责定期10 * 1000s针对所有HRegion的HStore检测是否需要进行compact操作。
查看源码也可看到,有个函数:
publicboolean needsCompaction(final Collection<StoreFile> storeFiles,
final List<StoreFile> filesCompacting) {
int numCandidates = storeFiles.size() - filesCompacting.size();
return numCandidates >= comConf.getMinFilesToCompact();
}
minFilesToCompact由hbase.hstore.compaction.min(老版本是:hbase.hstore.compactionThreshold)控制,默认值为3,即store下面的storeFiles数量 减去 正在compaction的数量 >=3是,需要做compaction。
如果这个值为true,则进行minor合并。如果为false,则再进一步判断是否需要执行major compact。主要是查看一下是否太久没有执行compact操作。
具体判断过程:
1)获得compact时间间隔。hbase.hregion.majorcompaction(默认7天)为base基准时间,hbase.hregion.majorcompaction.jitter(默认5.0)为jitter,公式base +jitter - Math.round(2 * jitter * randomNum) 计算出一个会每次自动抖动的数值作为majorcompact的时间间隔。之所以要一个自动抖动,就是避免在HRegionServer重启的时候大量的major compact出现造成大量的IO。
2)所有HFile最老(时间戳最小)的那个HFile的时间间隔大于这个majorcompact的时间间隔,则执行major compact。另外如果HRegion只有一个HFile,并且这个HFile的所有KeyValue的时间戳都没有超过TTL,则表示无须进行major compact,会跳过这次major compact。
当1或2成立都会分别对CompactSplitThread发送compact请求,不同的是,1会异步选择需要进行compact的HFile,2则会进行同步选择。
MinorCompact过程
当确定为需要MinorCompact时,便开始Region合并过程。
(1)选出待执行Compact的storefiles。由于在Store中的文件可能已经在进行Compacting,因此,这里取出未执行Compacting的文件,将其加入到Candidates中。
(2)执行compactSelection算法,在Candidates中选出需要进行compact的文件,并封装成CompactSelection对象当中(这里面还是很复杂的)。
这一步主要是过滤掉过期的hfiles。过滤minVersion=0,并且storefile.maxTimeStamp+ store.ttl < now_timestamp。这意味着整个文件最大的时间戳的kv,都已经过期了,从而证明整个storefile都已经过期了。CompactSelection如果发现这样的storefile,会优先选择出来,作为Min然后提交给Store进行处理。
(3)判断fileToCompact队列中的文件是否超过了maxCompactSize,如果超过,则过滤掉该文件,避免对于大文件进行compaction。(这一步可以忽略,因为默认maxCompactSize为Long.MaxValue,很少有文件大于这个值。
(4)如果确定Minor Compaction方式执行,会检查经过过滤过的fileToCompact的大小是否满足minFilesToCompact最低标准(默认为3,即超过3个hfile文件则启动合并),如果不满足,忽略本次操作。确定执行的Minor Compaction的操作时,会使用一个smart算法,从filesToCompact当中选出匹配的storefiles。
下面就是重点介绍下smart算法了:
其实很简单,就是几个参数的问题。(具体参考:http://blog.csdn.net/liyanyun/article/details/20134417)
hbase会将队列中的storefile 按照文件年龄排序(older to younger),minor compaction总是从older store file开始选择。
(1)如果该文件小于hbase.hstore.compaction.min.size(为memestoreFlushSize)则一定会被添加到合并队列中。
(2)如果该文件大于hbase.hstore.compaction.max.size(Long.MAX_VALUE)则一定会被排除,这个值很大,一般不会有。
(3)如果该文件的size 小于它后面hbase.hstore.compaction.max(默认为10) 个store file size 之和乘以一个ratio(配置项是hbase.hstore.compaction.ratio,默认为1.2),则该storefile 也将加入到minor compaction 中。当然,如果他后面不足10个文件,那么也就是取他后面几个文件总和*ratio了。
如此,最终选择下来的文件就将进入Minor合并。
MinorCompact实例
首先介绍下我们数据立方的以上相关配置,对于MemstoreFlushSize,在项目中是配得640M,意思即HStore的MemStore达到640M便刷写到磁盘。每个Region的MaxSize为100G,意思即Region达到100G之后才分裂。禁止了Major合并,参数值设为了0。相关配置就这几个,其它基本采取默认。
看第一张图:
这是我们数据立方在入数据时,查看当前正在入的region底下的Hfile文件,列族名字是0,在hdfs上一个列族是一个文件夹的,其中已经刷写合并了很多文件了,所以才有好多个文件。按照道理,文件超过3个就启动Minor合并了,所以基本每次刷写一下都会触发Minor合并,而每隔一段时间就会刷入一个54M左右大小的文件,一会又会合并。
按照时间先后排列可以看到,其实最大的文件也是时间最久的文件,因为是经过重重合并得到的。那个54.3M的文件是从memStore刷写到磁盘上经过压缩的文件,640M压缩到54M左右,本来觉得不信,后来查了一下,纯文本这样的压缩比是很正常的。刚刷入一个54.4M的文件,在此触发一次Minor合并。从最早的文件,66.5G的开始,它显然比它后面几个文件(按配置是10个文件*Ratio的大小),但他后面本不足10个文件,因此直接计入所有文件大小总和*1.2。显然,它的大小超过了这个值,因此不纳入Minor合并,同理一直到666.4M文件,它也不小于54.3M*1.2所以不纳入合并,因此这波参与合并的只有54.3M文件,暂且等待下一个刷写文件。
转眼间,又刷入一个54M的文件,这时进入合并流程。再看下面这图:
这时我们可以看到,其中一下子冒出一个150M的文件,其实这是3个54M一起合并的文件,只是因为太快,所以没注意他们就合并了。接下来还可以看到,继续刷入54M的文件,Minor合并继续,那个666.4M的文件仍然不能纳入合并队列,因为(154+54)M*1.2仍然小于它。但是可以发现,54M*1.2也是小于154M,为什么154M的文件会纳入合并队列呢?其实,这是上文所提的,当文件大小小于hbase.hstore.compaction.min.size(为memestoreFlushSize)则会立即被添加到合并队列。下面再继续看图:
这时又刷进来一个文件,基本都在54M左右,又进入Minor合并流程。继续看下图:
:
还没来得及合并,又出来一个文件,看来接下来是3个文件进行合并了。
基本可以看到,刚才那3个文件一起合并,组成了现在的256.1M的文件了,下面的情况可想而知了,无非是继续刷54M得文件,然后和256.1M合并。直到什么时候才会触及到666.4M的文件,这个还是可以计算的。666.4/1.2=553M,因此,当666.4M后面几个文件的总大小到553M的时候,666.4M就参与合并了。那么,我跳过一段,直接看下图:
几番合并后,现在到358.6M了,又刷进来一个54M文件,再接下来看图:
再次经合并,变成了461.3M的文件了,就快靠近553M的文件了,再看图:
又合并了一次,到461.3M的文件了,快了,此时,我间隔了一会,再看图:
看到什么了,最小的文件已经到1.2G了,可以猜想,这是666.4M文件和它后面的文件一起合并而成的。在这之后,可想而知,1.2G远远大于它后面的文件大小之后乘以1.2,因此不会加入minor合并队列了,得重新等待新刷写文件合并。而它上面的2.2G的文件,也是要等到后面的文件总大小达到:2.2/1.2=1.8G时才会参与合并,合成一个更大的。下面,再附一张图吧,验证下:
可以看到,那个1.2G文件仍然在那里,新的文件又在逐步刷写合并了,直到它后面文件总大小达到1.2/1.2=1G的时候,它才会与后面的文件进行合并,但是不会的,因为当它后面的文件达到0.6G时,那个2.2G的文件就达到合并要求,会一起合并成一个更大的文件。估计得有2.2G+1.8G=4G左右吧,后面上图:
哈哈,下次再合并就到0.6G啦,搞不好已经在Minor合并了呢,这个过程需要点时间的,毕竟合并的文件增多了。等下再看图:
又进行了一次合并,下次就该算上2.2G,1.2G的文件来个总体合并了,这里就不上图啦。
不知不觉,一会没看,真的来了个大合并,见下图:
一下子多出一个25.6G的文件了,通过整个流程可以看出,Hfile就是这样通过不断刷写合并,从而越滚越大,数据立方配置的Region大小是100G,即某个Store大于100G之后,Region即会拆分