HBase在新建一个表的时候,默认会把所有数据都会放在一个HRegion上,主节点HMaster根据一定的策略把HRegion分配到不同的HRegionServer从节点上,客户端在进行读写操作的时候,就会访问对应HRegionServer的HRegion。当HRegion的数据量超过阀值的时候,为了防止单个热点访问带来的压力,HBase就会对HRegion进行split操作,一个父HRegion分为两个子HRegion,后续的数据写入操作就会分配到两个HRegion里,减轻了单个热点的负载。由此可以看到split是一种动态的负载平衡机制。如果对
数据库知识有过一定了解,就会发现这就是数据库常用的sharding技术,只不过HBase提供了自动化处理,减轻维护开销。另外split会产生额外的IO,实际实践中也有手动关闭该特性,按照规模进行预先分配HRegion的做法。
具体实现
split是把数据平均分配的操作:按照父HRegion的SplitKey(约为rowKey范围的中间值)把父HRegion分成两个子HRegion后,同时会把父HRegion的数据也会重新写入到两个子HRegion上。另外子HRegion的rowKey范围是父HRegion的各自一半,这样后面的rowKey就会按照范围插入对应的HRegion。split在执行过程中采取一些做法避免影响读写请求,由于split过程需要较长时间大量的IO操作,如果发生故障就需要有效的failover机制,防止数据处于不一致的状态。整个步骤可以参考文章的这个图,其中有些细节不同,但大致过程基本一致。
split入口
当memstore flush操作后,HRegion写入新的HFile或者HStore刚刚进行完compact操作后,这两个操作都有可能产生较大的HFile,HBase就会调用CompactSplitThread.requestSplit判断是否需要split操作。这个判断如下:
判断整个HRegionServer所有的HRegion数量是否超过hbase.regionserver.regionSplitLimit(默认Integer.MAX_VALUE,即没有限制)。
当前HRegion所有HStore中包含的HFile最小数是否>=1
尝试获取SplitKey:hbase:meta表(记录HRegion信息的HBase表,只有单个HRegion)、或是正在恢复状态的HRegion返回null。然后利用设置的策略判断是否需要split操作。一般使用两种策略:ConstantSizeRegionSplitPolicy以及IncreasingToUpperBoundRegionSplitPolicy(默认)。 ConstantSizeRegionSplitPolicy:如果某个不包含Reference文件的HStore(Reference文件是split后产生的临时引用文件,见后述),总大小(包含HFile的总大小)超过hbase.hregion.max.filesize(默认10G),则返回true。 IncreasingToUpperBoundRegionSplitPolicy:对于HRegionServer内所有属于同一个表的HRegion的数n,如果某个不包含Reference文件的HStore,总大小超过[n*n*n*2*MemStoreFlushSize和hbase.hregion.max.filesize(10G)之间最小值],则返回true。例如,对于如果n=3,则split大小为3^3*2*128M=6912M。可见如果Region数比较少的时候的可以尽早采取split。
返回SplitPoint。返回HRegion里总大小最大HStore的最大HFile的中间rowKey值。
split执行
获得SplitPoint后,CompactSplitThread就把split请求放到线程池执行。整个过程的每一个步骤都会有具体的日志记录,方便在split过程中失败的回滚。具体过程如下:
1、获取zk上的表的全局读锁,默认等待600s。这是为了避免和修改表发生冲突。修改表的操作会发送到HMaster执行,HMaster会获取zk上表的全局写锁,这样两个操作就会互斥避免冲突。
2、创建split后的daughter region,A和B的HRegionInfo对象,用于后续操作。A和B的HRegion在split后对应父HRegion的每个HFile,都有一个Reference文件,内容为指示Top或Bottom的标记,表示被划分文件上/下部分,以及SplitKey。
3、创建Daughter Region。这个过程包含两部分,一个是stepsBeforePONR;另一个是修改hbase:meta表内容。stepsBeforePONR中的PONR指point of no return,也就是不可逆,在这个过程中HBase的操作都会认为是可逆的,在这之后的修改hbase:meta表内容就是非可逆的。
stepsBeforePONR
(1)创建split对应的zk节点。zk创建/hbase/region-in-transition/regionName节点,节点的data为split相关数据,type为RS_ZK_REQUEST_REGION_SPLIT。不断轮询等待HMaster更换状态为RS_ZK_REGION_SPLITTING。就是利用ZK传递region split的信息给HMaster,让HMaster获知正在执行split操作。
(2)在region的目录里创建.splits目录
(3)关闭当前region。停止flush和compact操作,并等待进行中的flush和compact完成。如果所有memstore大于5mb,则flush。并行关闭所属的HStore。
(4)并行split所有的HFiles。 分别创建daughterRegion A和B的Reference文件(不能被split,compact的时候会被删除),文件路径.splits/daughterRegionName/familyName/store?leName.RegionEncodedName,然后用PB格式写入Top/Bottom枚举以及splitKey。Reference文件会在后面的compact操作中被删除,然后才会真正把split的内容写入daughterRegion,这样延迟写入的操作可以避免发生故障需要回滚删除文件从而造成IO浪费,Reference文件非常小,不会造成很明显的影响。
(5)创建daughter region对象。 同时把A和B的regionInfo写到路径.regioninfo下,然后把之前的.splits/daughterRegionName移动到table/region目录,这样daughterRegion就和parent同级目录。然后创建A和B两个HRegion对象。
修改hbase:meta表内容
发送请求到hbase:meta表的HRegion,修改hbase:meta表的内容,表示原HRegion下线,dau