HBase的紧缩与分裂过程是对数据管理中必不可少的功能,正是因为它们的存在为HBase拥有高效读写性能及集群的负载均衡提供了保证,接下来对这两个过程作一下详细的介绍。
HBase的紧缩与分裂的发生都在在Region中进行的,了解过HBase的伙伴应该不会陌生,这里再对Region的结构功能作一个简单介绍。Region是 HBase中分布式存储和负载均衡的最小单元,不同的region可以分配在不同的Region Server上,Region的数据存储依赖的是HDFS的文件系统
Compaction是从一个Region的一个Store中选择部分HFile文件进行合并。合并原理是先从这些待合并文件中依次读出KeyValue数据,再由小到大排序后写入一个新的文件中,之后这个新生成的文件就会取代合并之前的文件对外提供服务。
HBase根据合并规模将Compaction分为两类:
指选取部分小的、相邻的HFile,将他们合并成一个更大的HFile,合并持续时间较短,合并过程中只是对文件进行合并而不会清理被标记删除或过期的数据
指将一个Store中的所有HFile合并成一个HFile,在合并过程中会清理掉被删除的数据,TTL过期数据及版本号超过设定版本号的数据。Major_compaction持续时间比较长,整个过程会消耗大量的系统资源,对上层业务影响较大。因此,线上部分数据量较大的业务通常推荐关闭自动触发Major_compaction功能,改在业务相对空闲时手动触发。
<property>
<name>hbase.hregion.majorcompact.begintimename>
<value>00:00:00value>
property>
<property>
<name>hbase.hregion.majorcompact.endtimename>
<value>01:30:00value>
property>
<property>
<name>hbase.hregion.majorcompactionname>
<value>0value>
property>
HBase的Compaction在特定的条件下才会触发,一旦满足一些条件触发后就按照一定的流程执行合并,Compaction的过程会由一个单独的线程处理
基本流程可以分为以下几部分:
Compaction的触发
Compaction触发的条件有多种,最常见的有三种:
MemStore Flush
MemStore达到一定条件后会Flush产生HFile文件,执行完后会对当前Store中的文件数判断,如果Store的文件数大于hbase.hstore.compactionThreshold配置项,就会触发Compaction。整个region的多个Store同时进行flush时,就有可能在短时间内执行多次Compaction
定期检查触发
RegionServer会在后台启动一个线程CompactionChecker,定期触发检查对应Store是都需要执行Compaction,检查周期为hbase.server.thread.wakefrequency * hbase.server.compactchecker.interval.multiplier。先检查Store中总文件数是否大于阈值hbase.hstore.compactionThreshold,大于就触发Compaction;否则,检查当前Store的HFile最早的更新时间是否大于mcTime([7-7×0.2,7+7×0.2],7为hbase.hregion.majorcompaction,0.2为hbase.hregion.majorcompaction.jitter),默认7天左右执行一次Major_compaction
手动触发
手动触发Compaction是为了执行Major_compaction
- Major_compaction会影响读写性能,选择低峰期手动触发
- 用户希望在alter之后立即生效
- Hbase硬盘容量不够时,清理过期数据
选择合适的HFile文件
Compaction的核心,合并文件的大小及其当前承载的IO数直接决定了Compaction的效果以及对整个系统其他业务的影响程度
<property>
<name>hbase.hstore.compaction.max.sizename>
<value>2147483648value>
property>
<property>
<name>hbase.hstore.compaction.maxname>
<value>200value>
property>
候选文件不满足Major_compaction,即为Minor_compaction
Minor的文件选择策略
RatioBasedCompactionPolicy
从老到新逐一扫描候选文件,满足条件停止
ExploringCompactionPolicy
从老到新逐一扫描候选文件,找到合适的文件集就停止,合适的文件集的理解为:
待合并文件数最多或者待合并文件数相同的情况下文件较小,这样有利于减少Compaction带来的IO消耗。
选择合适的合并线程池
HBase实现中有一个专门的类CompactSplitThead负责接收Compaction请求和split请求,为了能够独立处理请求提高系统的处理性能,构造了多个线程池:splits线程池负责处理所有的split请求,largeCompactions用来处理大Compaction,smallCompaction负责处理小Compaction
注:Hbase定义了hbase.regionserver.thread.compaction.throttle决定是否为大小Compaction
largeCompactions线程池和smallCompactions线程池默认都只有一个线程,允许用户配置
<property>
<name>hbase.regionserver.thread.compaction.largename>
<value>4value>
property>
<property>
<name>hbase.regionserver.thread.compaction.smallname>
<value>4value>
property>
执行HFile文件合并
合并的流程主要有以下几步:
合并小文件,减少文件数,稳定随机读延迟
Hbase的数据存储依赖于HDFS,首先当MemStore向磁盘刷入多个小文件时,多个小文件的元数据信息会对HDFS中的NameNode的物理内存容量产生限制;其次,读数据时,访问大量的小文件会导致访问不断的从一个DataNode到另一个DataNode去检索,会消耗大量的系统资源(IO,网络)
提高数据的本地化率
在进行Compaction合并小文件的同时会读取远程DataNode上的数据写入大文件,并在当前DataNode节点上保存一个副本,提高数据的本地化率。本地化率越高,在HDFS上访问数据时延迟就越小,本地化率越低,访问数据时大概率通过网络访问,延迟必然会大
清理无效的数据,减少数据存储量
HBase中的无效数据包含:
- 执行删除的数据时,数据不会即时删除,而是会对删除数据做tombStone Marker(“墓碑标记”),但数据仍在HFile中
# 查看删除的数据,会被标记 scan 'hrecord',{RAW=>true,VERSION=>3} ROW COLUMN+CELL 001 column=info:address, timestamp=2,type=Delete 001 column=info:address, timestamp=3,value=record_id
- 在HBase数据管理中,会根据业务需求设置数据TTL(生存时间),它是设置一个基时间戳的临界值,内部管理自动检查超过TTL值的数据会被自动删除
# 建表设定 create 'hrecord',{NAME => 'f', TTL=>'86400'} # 修改TTL,作用于列族f alter "hrecord",NAME=>'f',TTL=>'FOREVER'
- HBase分裂过程中会产生reference临时文件,reference文件是region分裂产生的临时文件,一般必须在Compaction过程中清理
Region分裂是HBase最核心的功能之一,是实现分布式可扩展性的基础。最初,每个Table只有一个Region,随着数据的不断写入,HBase根据一定的触发条件和一定的分裂策略将Hbase的一个region分裂成两个子region并对父region进行清除,通过HBase的balance机制,实现分裂后的region负载均衡到对应RegionServer上
目前常见的HBase分裂方式有三种:
Per-Spliting指的是在HBase创建Table时,指定好Table的Region的个数,生成多个Region。这么做的好处是一方面可以避免热点数据的写入问题(只有一个region,写数据量大时其余RegionServer就是空闲的),另一方面减少Region的Split几率,同时减少消耗集群的系统资源(IO,网络),减少因Split暂停Region的上线造成对HBase读写数据的影响。
HBase默认建表时只有一个Region,此时的RowKey是没有边界的,即没有StartKey和EndKey。进行预分区时,需要配置切分点,使得HBase知道在哪个RowKey点做切分。
HBase自带了两种pre-split的算法,分别是 HexStringSplit 和 UniformSplit 。如果我们的row key是十六进制的字符串作为前缀的,就比较适合用HexStringSplit,作为pre-split的算法。我们使用HexHash(prefix)作为row key的前缀,其中Hexhash为最终得到十六进制字符串的hash算法。
自动分裂指的是随着不断向表中写入数据,Region也会不断增大,HBase根据触发的分裂策略自动分裂Region,当前HBase已经有6中分裂触发的策略,不同版本中配置的分裂策略不同,介绍一下常用三种:
ConstantSizeRegionSplitPolicy
0.94版本之前默认的分裂策略。表示一个Region中最大Store的大小超过设置阈值(hbase.hregion.max.filesize)之后就会触发分裂。
弊端:分裂策略对于大表比较友好,小表达不到阈值永远不分裂,如果设置为对小表友好就可能产生大量的region,对集群管理、资源使用都不好
hbase.hregion.max.filesize
5368709120
IncreasingToUpperBoundRegionSplitPolicy
0.94~2.0版本默认分裂策略。表示一个Region中最大Store的大小超过设置阈值(hbase.hregion.max.filesize)之后就会触发分裂。但是阈值不是一个固定值,而是在一定条件下不断调整,调整后的阈值等于(#regions) *(#regions) *(#regions) * f lush size * 2,(#regions) 表示Region所属表在当前RegionServer上的Region个数,阈值最大值为用户配置的MaxRegionFileSize,这种策略自适应大小表
弊端:在大集群场景下,很多小表产生大量region分散在整个集群
SteppingSplitPolicy
2.0版本默认分裂策略。表示一个Region中最大Store的大小超过设置阈值(hbase.hregion.max.filesize)之后就会触发分裂。分裂阈值大小和待分裂Region所属表在当前RegionServer上的Region个数有关,如果Region个数为1,分裂阈值为flush size * 2,否则为MaxRegionFileSize,小表不会再产生大量的小Region
DisableSplitPolicy
禁止Region Split
KeyPrefixRegionSplitPolicy
切分策略依然依据默认切分策略,根据 Rowkey 指定长度的前缀来切分 Region,保证相同的前缀的行保存在同一个 Region 中。由 KeyPrefixRegionSplitPolicy.prefix_length 属性指定 Rowkey 前缀长度。按此长度对splitPoint进行截取。此种策略比较适合有固定前缀的 Rowkey。当没有设置前缀长度,切分效果等同与 IncreasingToUpperBoundRegionSplitPolicy。
DelimitedKeyPrefixRegionSplitPolicy
切分策略依然依据默认切分策略,同样是保证相同 RowKey 前缀的数据在一个Region中,但是是以指定分隔符前面的前缀为来切分 Region。
客户端手动运行split命令执行分裂
split 'forced_table', 'b' //其中forced_table 为要split的table , ‘b’ 为split 点
我们还可以通过配置hbase.regionserver.region.split.policy指定自己的split策略
<property>
<name>hbase.regionserver.region.split.policyname>
<value>org.apache.hadoop.hbase.regionserver.TimingRegionSplitPolicyvalue>
property>
<property>
<name>hbase.regionserver.region.split.startTimename>
<value>02:00:00value>
property>
<property>
<name>hbase.regionserver.region.split.endTimename>
<value>04:00:00value>
property>
HBase将整个分裂过程包装成了一个事务,目的是保证分裂事务的原子性。
整个分裂事务过程分为三个阶段:prepare、execute和rollback。
prepare
在内存中初始化两个子Region,具体生成两个HRegionInfo对象,包含tableName、regionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录分裂的进展,具体见rollback阶段。
execute
RegionServer将ZooKeeper节点上hbase/region-in-transition/parent_region_name中该Region的状态更改为SPLITING
Master通过watch节点/region-in-transition检测到Region状态改变,并修改内存中Region的状态,在Master页面RIT模块可以看到Region执行split的状态信息
在父Region的HDFS存储目录下新建临时文件夹.split,保存split后的daughter region信息
关闭父Region。父Region关闭数据写入并触发f lush操作,将写入Region的数据全部持久化到磁盘。此后短时间内客户端落在父Region上的请求都会抛出异常NotServingRegionException
在.split文件夹下新建两个子文件夹,称为daughter A、daughter B,并在文件夹中生成reference文件,分别指向父Region中对应文件。这个步骤是所有步骤中最核心的一个环节,会生成reference文件日志,可以通过reference日志文件查看指向的是哪个父Region中的哪个HFile文件,并且reference文件内容也很重要,可以通过Hadoop命令查看文件内容
# StoreOpener名在reference日志中可以查看 hdfs dfs -cat /hbase-rsgroup/data/default/music/StoreOpener名/cf/reference文件名
reference文件是一个引用文件(非Linux链接文件)文件内容不是用户数据,由两部分构成:
- 分裂点SplitKey
- 一个boolean类型的变量,true表示该reference文件引用的是父文件的上半部分(top),false表示引用的是下半部分(bottom)
父Region分裂为两个子Region后,将daughter A、daughter B拷贝到HBase根目录下,形成两个新的Region
父Region通知修改hbase:meta表后下线,不再提供服务。下线后父Region在meta表中的信息并不会马上删除,而是将split列、off line列标注为true,并记录两个子Region
开启daughter A、daughter B两个子Region。通知修改hbase:meta表,split列、off line列标注为tfalse,正式对外提供服务
rollback阶段
如果execute阶段出现异常,则执行rollback操作。为了实现回滚,整个分裂过程分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。
由以上分裂过程可知:
分裂后父Region的数据没有立即删除,子Region通过reference文件对数据的查询,子Region文件实际没有任何用户数据,文件中存储的仅是一些元数据信息(分裂点rowkey等)
父Region的数据什么时候迁移到子Region中的呢?
迁移发生在子Region执行Major Compaction时。
根据Compaction原理,从一系列小文件中依次由小到大读出所有数据并写入一个大文件,完成之后再将所有小文件删掉,因此Compaction本身就是一次数据迁移。分裂后的数据迁移完全可以借助Compaction实现,子Region执行Major Compaction后会将父目录中属于该子Region的所有数据读出来,并写入子Region目录数据文件中。
父Region是什么时候被删除的呢?
Master会启动一个线程定期遍历检查所有处于splitting状态的父Region,确定父Region是否可以被清理。
说起HBase分裂的作用首先要理解分布式集群的负载均衡,实现了负载均衡,集群的扩展性才能得到有效的保证。HBase的负载均衡就是依赖Region的Split将不断写入数据增大的Region合理切分,分配给对应的RegionServer实现的。其次,HBase中Region的迁移、合并等操作也是实现负载均衡的保障机制。
在实际业务场景中,HBase的Region分裂,能够有效的避免热点数据的访问问题,提高数据的读写效率。