HBase的split分析

    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机制,防止数据处于不一致的状态。整个步骤可以参考文章的这个图,其中有些细节不同,但大致过程基本一致。

HBase的split分析_第1张图片

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上表的全局写锁,这样两个操作就会互斥避免冲突。

    zk的table全局锁实现:简单来说,就是实现锁请求队列,队列头节点就是获取了锁的节点,readLock和writeLock按照自己在队列的位置判断是否获取锁。具体实现大致为,以某个指定父节点,创建类型为EPHEMERAL_SEQUENTIAL(EPHEMERAL指断开zk连接会自动删除节点,SEQUENTIAL会在子节点名字最后增加单调递增的序号)的子节点,其名字如果readLock以read-作为前缀,writeLock则以write-作为前缀。接着获取这个父节点的所有子节点,进行判断:对于readLock,如果名字最后的序号比自己小的含有write-(表示前面有writeLock请求),则监听这个序号最大的节点,如果被删除了,则证明自己获取了readLock。对于writeLock,如果自己不是序号最小的节点,则监听比自己小的最大序号节点,如果被删除,则自己获取了writeLock。

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/storefileName.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下线,daughterRegion A和B准备上线(暂时没有location信息,因为仍没open)。另外如果修改失败,则会进行回滚操作,把之前创建的.split目录删除,然后HRegionServer会终止服务,然后HMaster会负责清理其余的状态。

4、并行打开A,B两个HRegion。创建对应的HStore,读取Reference(Reference文件有专门的Scanner和reader来限制读取对应HFile的范围),从所有HFile里读取最大的MemStoreTS、SequenceId。HRegion成功打开后,就更新hbase:meta表中A和B的location。然后根据每个HFile的MaxSequenceId进行replay WALEdit,就是把内容重新写入HStore的memstore。如果WALEdit的logNum里小于MaxSequenceId则表明已经写入HFile(每条记录对应一个SequenceId),就会跳过。然后会请求一个异步Major compact,从Reference生成真正的HFile(异步Major compact会清理所有的Reference,参考这里)。这个时候,这两个daughterRegion已经可以对外提供读服务。

5、 更新zk节点状态。在zk上之前创建的/hbase/region-in-transition/regionName节点的type改为RS_ZK_REGION_SPLIT,通知HMaster完成split操作。另外要注意,由于zk可能会丢失消息,因此这里需要不断循环节点状态,当节点被HMaster删除当时候才表明HMaster收到通知。

6、释放表的全局读锁

     这样,在CompactSplitThread的split操作就完成,在这个时候,两个daughterRegion对外提供读请求,父HRegion的文件仍然存在。等待major compact请求完成之后,两个daughterRegion的Refernece文件就会被删除。HMaster会定期监控hbase:meta表,一旦发现存在parentRegion且daughterRegion已经没有Reference文件,则会删除parentRegion的相关内容。这样就最终完成了整个split的操作。

预Split操作

    在实际实践过程中,如果可以根据业务大致确认表的数据规模,则可以使用预Split把表分成指定多个HRegion。这样做可以避免Split过程带来的IO开销,并且在大批量导入数据的时候,可以让集群的多个节点分流写请求,加快导入效率。具体做法如下:

    首先是关闭Split操作。从Split的过程可以看到,在判断是否需要split的时候,有两种可选的策略,我们这里采取ConstantSizeRegionSplitPolicy,然后把hbase.hregion.max.filesize设置到一个非常大的值,这样HBase就几乎不会对任何HRegion采取split操作。

    然后就是在create表的时候,需要指定多个splitKey范围,如:

hbase(main):015:0> create 'test_table', 'f1', SPLITS=> ['a', 'b', 'c']
    这样就会创建4个HRegion,rowKey范围分别为[负无穷,a)、[a, b)、[b, c)、[c, 正无穷)。在导入数据的时候,客户端就会根据自己的rowKey插入到对应的范围里。当然也要注意某些热点数据会导致某个HRegion特别大,最好要监控好HRegion的读写请求数(可以从master提供的web页面查看状态)。如果发现某些热点数据,可以利用命令行手动设置这个HRegion的split操作,HBase就会对这个HRegion进行split,保持负载平衡。

总结

    split操作提供了表自动sharding的功能,但这是以额外的IO消耗为代价的。我们可以根据自己的业务需求进行预split或者手动split等操作。无论是哪种,这些split操作都相对较方便,能够免除维护sharding带来的一系列数据同步问题。

    split的操作依赖zookeeper保留transition info以及master和regionserver的通信。在zk之前曾经使用heartbeat向Master汇报状态,但在不同地方出现了很多状态不一致的问题。后来改用zk保证状态一致性,但zk是one-time-trigger,即触发监听后必须重新设置监听,重新监听过程中可能产生消息丢弃(例如SplitTransaction.transitionZKNode采用多次循环判断zk的节点状态,确认HMaster接受信息)。另外,hbase的zk节点允许第三方app查看状态,有安全性问题。目前社区也有Master中实现一个zab、raft、paxos等一致性库的想法,以避免依赖zk产生的问题。具体可以参考https://issues.apache.org/jira/browse/HBASE-10296

你可能感兴趣的:(大数据,hbase)