HBase Flush,Split,Comact 三大动 作流程源码
- HBase Flush
- HBase Split
- HBase Comact
作流程源码)
HBase Flush
总是要回顾下前面的,不妨回看
上一篇好了,不多说,上干货
看图
MemStore 不是无限大的,当写入数据达到一定阈值条件的时候,就需要执行 Flush 动作,将数据持久化到磁盘。而负责帮助 MemStore 执行 Flush 到磁盘的组件
就是 MemStoreFlusher 组件。
MemStore 的默认实现是:DefaultMemStore 它的内部有一个成员变量:MemStoreFlusher
代码入口:HRegion.requestFlushIfNeeded()
// MemStoreFlusher 负责 HRegion 的 Flush 操作
class MemStoreFlusher implements FlushRequester {
// Flush 队列,FlushQueueEntry 每需要执行一次 flush 的时候,就会提交一个 FlushQueueEntry 到 flushQueue
private final BlockingQueue<FlushQueueEntry> flushQueue = new DelayQueue<>();
// 存储了,region 和 flush 之间的映射关系
private final Map<Region, FlushRegionEntry> regionsInQueue = new HashMap<>();
// 负责消费 flushQueue 队列的 Flush 线程
private final FlushHandler[] flushHandlers;
// Flush 线程的工作 = 消费者
private class FlushHandler extends Thread {
public void run() {
while(!server.isStopped()) {
// 获取 Flush 信息
FlushQueueEntry fqe = flushQueue.poll(threadWakeFrequency, TimeUnit.MILLISECONDS);
FlushRegionEntry fre = (FlushRegionEntry) fqe;
// 执行 Flush
if(!flushRegion(fre)) {
break;
}
}
}
}
// 生产者
public boolean requestFlush(HRegion r, List<byte[]> families, FlushLifeCycleTracker tracker) {
synchronized(regionsInQueue) {
if(!regionsInQueue.containsKey(r)) {
// 封装一个 Flush 抽象
FlushRegionEntry fqe = new FlushRegionEntry(r, families, tracker);
// Regoin Flush 登记
this.regionsInQueue.put(r, fqe);
// 添加到队列
this.flushQueue.add(fqe);
}
}
}
上面的代码只是帮助我们去统一 flush 的入口。
触发 flush 的条件有哪些?
具体的 flush 流程是怎样的呢?
1.1. HBase Flush 触发条件
HBase 会在如下几种情况下触发 flush 操作,需要注意的是 MemStore 的最小 flush 单元是 HRegion 而不是单个 MemStore。可想而知,如果一个 HRegion 中
Memstore 过多,每次 flush 的开销必然会很大,因此建议在进行表设计的时候尽量减少 ColumnFamily 的个数。
- Memstore 级别限制:当 Region 中任意一个 MemStore 的大小达到了上限(hbase.hregion.memstore.flush.size,默认 128MB),会触发 Memstore
Flush。 - Region 级别限制:当 Region 中所有 Memstore 的大小总和达到了上限(hbase.hregion.memstore.block.multiplier *
hbase.hregion.memstore.flush.size,默认 4 * 128M = 512M),会触发 memstore 刷新。 - RegionServer 级别限制:当一个 Region Server 中所有 Memstore 的大小总和达到了上限(hbase.regionserver.global.memstore.upperLimit *
hbase_heapsize,默认 40% 的 JVM 内存使用量),会触发部分 Memstore 刷新。Flush 顺序是按照 Memstore 由大到小执行,先 Flush Memstore 最大的
Region,再执行次大的,直至总体 Memstore 内存使用量低于阈值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默认 38% 的
JVM 内存使用量)。 - 当一个 RegionServer 中 HLog 数量达到上限(可通过参数 hbase.regionserver.maxlogs 配置)时,系统会选取最早的一个 HLog 对应的一个或多个 Region进
行 Flush - HBase 定期刷新 Memstore:默认周期为 1 小时,通过 AbstractWALRoller 线程来完成。确保 Memstore 不会长时间没有持久化。为避免所有的 MemStore
在同一时间都进行 flush 导致的问题,定期的 flush 操作有 20000 左右的随机延时。 - 手动执行 flush:用户可以通过 shell 命令 flush ‘tablename’ 或者 flush ‘region name’ 分别对一个表或者一个 Region 进行 flush。
1.2. HBase Flush 步骤
为了减少 flush 过程对读写的影响,HBase 采用了类似于两阶段提交的方式,将整个 flush 过程分为三个阶段:
- prepare 阶段:遍历当前 Region 中的所有 Memstore,将 Memstore 中当前数据集 kvset 做一个快照 snapshot,然后再新建一个新的 kvset。后期的所有写
入操作都会写入新的 kvset 中,而整个 flush 阶段读操作会首先分别遍历 kvset 和 snapshot,如果查找不到再会到 HFile 中查找。prepare 阶段需要加一把
updateLock 对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。 - flush 阶段:遍历所有 Memstore,将 prepare 阶段生成的 snapshot 持久化为临时文件,临时文件会统一放到目录 .tmp 下。这个过程因为涉及到磁盘 IO 操
作,因此相对比较耗时。 - commit 阶段:遍历所有的 Memstore,将 flush 阶段生成的临时文件移到指定的 ColumnFamily 目录下,针对 HFile 生成对应的 storefile 和 Reader,把
storefile 添加到 HStore 的 storefiles 列表中,最后再清空 prepare 阶段生成的 snapshot。
看源码:
43 }
HRegion.internalFlushcache(){
// 针对 MemStore 拍摄快照
PrepareFlushResult result = internalPrepareFlushCache(wal, myseqid, storesToFlush, status, writeFlushWalMarker, tracker){
// 准备
MemStoreSize snapshotSize = flush.prepare(){
// 针对 MemStore 拍摄快照
this.snapshot = memstore.snapshot(){
// 对于 Active MutableSegment 拍摄快照,其实就是创建 ImmutableSegment
ImmutableSegment immutableSegment = SegmentFactory.instance().createImmutableSegment(getActive(), ...){
return new CSLMImmutableSegment(segment, memstoreSizing){
super(segment){
super(segment){
// 将目标 Segment 的 CellSet 赋值给当前创建的快照 Segment
this.cellSet.set(segment.getCellSet());
}
}
}
}
// 赋值
this.snapshot = immutableSegment;
// 重新创建一个新的 MutableSegment
resetActive(){
active = SegmentFactory.instance().createMutableSegment(conf, comparator, memstoreAccounting);
}
}
}
}
// 将 MemStore 中的快照 ImmutableSegment 执行 flush 动作
return internalFlushCacheAndCommit(wal, status, result, storesToFlush){
// 执行 Flush
for(StoreFlushContext flush : storeFlushCtxs.values()) {
// 第二步
flush.flushCache(status){
// 执行 Flush,生成临时 StoreFile 文件
tempFiles = HStore.this.flushCache(cacheFlushSeqNum, snapshot, status, throughputController, tracker){
// 内部具体实现分为四大核心步骤
List<Path> pathNames = flusher.flushSnapshot(snapshot, logCacheFlushId, status, throughputController, ...){
// 1、创建 Scanner,MemStoreScanner
InternalScanner scanner = createScanner(snapshot.getScanners(), tracker);
// 2、创建 Writer = StoreFileWriter
writer = store.createWriterInTmp(cellsCount, ....){
StoreFileWriter.Builder builder = new StoreFileWriter.Builder(conf, ....);
return builder.build();
}
// 3、执行 flush
// 如果这一步骤执行完了,仅仅只是代表,我们的 MemStore 真是 Cell 数据已经都被写入到 HFile 临时文件了。
// 这个方法是负责写入 HFile 四段数据的第一段:DataBlock 段
performFlush(scanner, writer, throughputController){
do{
// 读取数据
// HFile 有一段是专门存储真实数据的,就是 DataBlock 段,这一段中,包含了多个 HFileBlock
hasMore = scanner.next(kvs, scannerContext);
// 执行 append 到 HFile 临时文件
for(Cell c : kvs) {
sink.append(c);
}
} while (hasMore)
}
// 4、写 HFile 除了真是数据之外的索引数据,文件元数据。最后写入文件的其他部分
finalizeWriter(writer, cacheFlushId, status);
}
}
}
// 第三步
// 提交 HStoreFile
for(Map.Entry<byte[], StoreFlushContext> flushEntry : storeFlushCtxs.entrySet()) {
StoreFlushContext sfc = flushEntry.getValue();
boolean needsCompaction = sfc.commit(status);
}
}
}
}
1.3. HFile 结构
如图:
HFile 文件主要分为 4 个部分:Scanned block 部分、Non-scanned block 部分、Load-on-open 部分和 Trailer。
Scanned Block 部分:表示顺序扫描 HFile 时所有的数据块将会被读取。这个部分包含 3 种数据块:Data Block,Leaf Index Block 以及 BloomBlock。其中
Data Block 中存储用户的 KeyValue 数据,Leaf Index Block 中存储索引树的叶子节点数据,Bloom Block 中存储布隆过滤器相关数据。
Non-scanned Block 部分:表示在 HFile 顺序扫描的时候数据不会被读取,主要包括 Meta Block 和 Intermediate Level Data Index Blocks 两部分。
Load-on-open 部分:这部分数据会在 RegionServer 打开 HFile 时直接加载到内存中,包括 FileInfo、布隆过滤器 MetaBlock、Root Data Index 和
MetaIndexBlock。
Trailer 部分:这部分主要记录了 HFile 的版本信息、其他各个部分的偏移值 offset 和 寻址信息。
// 执行 Flush
for(StoreFlushContext flush : storeFlushCtxs.values()) {
// 第二步
flush.flushCache(status){
// 执行 Flush,生成临时 StoreFile 文件
tempFiles = HStore.this.flushCache(cacheFlushSeqNum, snapshot, status, throughputController, tracker){
// 内部具体实现分为四大核心步骤
List<Path> pathNames = flusher.flushSnapshot(snapshot, logCacheFlushId, status, throughputController, ...){
// 1、创建 Scanner,MemStoreScanner
InternalScanner scanner = createScanner(snapshot.getScanners(), tracker);
// 2、创建 Writer = StoreFileWriter
writer = store.createWriterInTmp(cellsCount, ....){
StoreFileWriter.Builder builder = new StoreFileWriter.Builder(conf, ....);
return builder.build();
}
// 3、执行 flush
// 如果这一步骤执行完了,仅仅只是代表,我们的 MemStore 真是 Cell 数据已经都被写入到 HFile 临时文件了。
// 这个方法是负责写入 HFile 四段数据的第一段:DataBlock 段
performFlush(scanner, writer, throughputController){
do{
// 读取数据
// 这个 KVS 就是一个 DataBlock
// HFile 有一段是专门存储真实数据的,就是 DataBlock 段,这一段中,包含了多个 HFileBlock
hasMore = scanner.next(kvs, scannerContext);
// 执行 append 到 HFile 临时文件
for(Cell c : kvs) {
sink.append(c);
}
} while (hasMore)
}
// 4、写 HFile 除了真是数据之外的索引数据,文件元数据。最后写入文件的其他部分
// HFile 当中的后三段,都是在这个方法中写入的
finalizeWriter(writer, cacheFlushId, status);
}
}
}
// 第三步
// 提交 HStoreFile
for(Map.Entry<byte[], StoreFlushContext> flushEntry : storeFlushCtxs.entrySet()) {
StoreFlushContext sfc = flushEntry.getValue();
boolean needsCompaction = sfc.commit(status);
}
}
}
}
HFile 文件的写机制: 先写 DataBlock 段,每个 DataBlock 的 offset 就有了,存储在事先创建好的 Trailler 对象中,然后写第二端,第三段,也都能得到每一段的
offset,最终把 Trailer 对象写到 HFile 文件的末尾
HFile 结构中的 DataBlock 的组成结构如下:DataBlock 的默认大小是: 64KB
太大了,方便范围查询,但是不方便单点查询
太小了,方便单点查询,不方便范围查询
HBase 是一张四维表:
table.get(【rowkey, cf, quafiler, timestamp】) = value
溢写之后生成的 HFile 内部写进去的 Cell 结构如下:一个 Cell 对象是不能超过 10M 的
代码体现:
HBase Flush 内容总结:
触发条件 6 个点
Flush 相关工作组件
核心三个步骤,其中中间的第二个步骤 flushCache 又分为四步,最重要的就是 performFlush
HFile 的结构
KeyValue 的结构
HBase Split
HBase Split 处理
HBase Split 是 HBase 根据一定的触发条件和一定的分裂策略将 HBase 的一个 region 进行分裂成两个子 region 并对父 region 进行清除处理的过程。在 HBase
中,Split 其实是进行 sharding 的一种技术手段,通过 HBase 的 Split 条件和 Split 策略,将 region 进行合理的 Split,再通过 HBase 的 balance 策略,将分裂的
region 负载均衡到各个 regionserver 上,最大化的发挥分布式系统的优点。
最大目的:为了确保每个 Region 不至于增长数据变得过大,导致从某一个 Region 执行查询的需要扫描的文件过多导致效率低
硬性标准:不管使用哪种分裂策略,都会保证任何一个 Region 的总大小不超过 10G
整个 HBase 的设计理念:通过分布式并行数据集(类似于 shard, 所有 shard 全局有序)的管理方式,通过三次网络来回,始终将要进行扫描查询的数据范围降低
到 10G 以内
/
/ 完成一个 Cell 对象的写出
KeyValueUtil.oswrite(final Cell cell, final OutputStream out, final boolean withTags){
short rlen = cell.getRowLength();
byte flen = cell.getFamilyLength();
int qlen = cell.getQualifierLength();
int vlen = cell.getValueLength();
int tlen = cell.getTagsLength();
int size = 0;
// 1 写出 Key Length
int klen = keyLength(rlen, flen, qlen);
ByteBufferUtils.putInt(out, klen);
// 2 写出 Value Length
ByteBufferUtils.putInt(out, vlen);
// 3 写出 Rokwey Length
StreamUtils.writeShort(out, rlen);
// 4 写出 Rowkey
out.write(cell.getRowArray(), cell.getRowOffset(), rlen);
// 5 写出 Column Family Length
out.write(flen);
// 6 写出 Column Family
out.write(cell.getFamilyArray(), cell.getFamilyOffset(), flen);
// 7 写出列 Qualifier
out.write(cell.getQualifierArray(), cell.getQualifierOffset(), qlen);
// 8 写出时间戳 TimeStamp
StreamUtils.writeLong(out, cell.getTimestamp());
// 9 写出类型 Type
out.write(cell.getTypeByte());
// 10 写出真实数据 value
out.write(cell.getValueArray(), cell.getValueOffset(), vlen);
}
2.1. HBase RegionSplitPolicy 解析
详细解释:
ConstantSizeRegionSplitPolicy:在 0.94 版本之前 ConstantSizeRegionSplitPolicy 是默认和唯一的 split 策略,当一个 Region 的大小达到:
hbase.hregion.max.filesize = 10737418240 的时候就会执行 Split
IncreasingToUpperBoundRegionSplitPolicy:按照逐步递增的方式来执行 split,有一个计算算法:
所以:
第一次:256 * 1^3 = 256M
第二次:256 * 2^3 = 2048M
第三次:256 * 3^3 = 6912M
第四次:256 * 4^3 = 16384M,由于大于 10G 了,则取 10G
后续,都按照 10G 大小作为阈值执行 Split
SteppingSplitPolicy:它是 IncreasingToUpperBoundRegionSplitPolicy 的子类,主要是为了防止小表产生多个 Region
第一次:256M
第二次:10G
后续,都按照 10G 大小作为阈值执行 Split
2.2. HBase split 流程
HBase 实现中有一个专门的类 CompactSplit 负责接收 Compaction 请求和 split 请求。
先来看它的构造方法:
protected long getSizeToCheck(final int tableRegionsCount) {
// safety check for 100 to avoid numerical overflow in extreme cases
// 如果极端情况,就按照 10G 整,比如 table 的 region 个数大于 100
// 否则:按照 initialSize * tableRegionsCount * tableRegionsCount * tableRegionsCount 进行计算
// initialSize = 2 * MemStore Flush Size = 2 * 128M = 256M
return tableRegionsCount == 0 || tableRegionsCount > 100 ?
getDesiredMaxFileSize() :
Math.min(getDesiredMaxFileSize(), initialSize * tableRegionsCount * tableRegionsCount * tableRegionsCount);
}
protected long getSizeToCheck(final int tableRegionsCount) {
// 如果就一个 Region ,则使用初始化值,如果有多个 Region 就使用 10G
return tableRegionsCount == 1 ? this.initialSize : getDesiredMaxFileSize();
}
// CompactSplit 构造方法
CompactSplit(HRegionServer server) {
// 创建负责 compact 的线程池
createCompactionExecutors(){
// 负责大的 compaction 不是 major
this.longCompactions = new ThreadPoolExecutor(largeThreads, largeThreads, ...);
// 负责小的 compaction 不是 minor
this.shortCompactions = new ThreadPoolExecutor(smallThreads, smallThreads, ...);
}
// 创建负责 split 的线程池
但是这三个线程池的默认线程数都是 1!
然后看它的核心结构:
接着我们来看 SplitRequest 线程的工作:
当 HMaster 接收到来自于 RegionServer 的 region split 的 RPC 请求 reportRegionStateTransition 之后,会调用 AssignmentManager 来完成具体的 Region Split
的工作。
SplitTableRegionProcedure 在创建的时候,会创建对应的两个子 Region 的元数据,和 RegionSplitPolicy 实例:
createSplitExcecutors(){
this.splits = (ThreadPoolExecutor) Executors.newFixedThreadPool(splitThreads, ...);
}
}
public class CompactSplit implements CompactionRequester, PropagatingConfigurationObserver {
public synchronized boolean requestSplit(final Region r) {
// 执行检查,内部会调用 RegionSplitPolicy 来执行判断
byte[] midKey = hr.checkSplit().orElse(null){
// 判断是否要执行 split
splitPolicy.shouldSplit();
// 获取分割点
splitPolicy.getSplitPoint();
}
if(midKey != null) {
// 执行 split
requestSplit(r, midKey){
// 提交一个 SplitRequest 到 splits 线程池
this.splits.execute(new SplitRequest(r, midKey, this.server, user));
}
return true;
}
}
}
class SplitRequest implements Runnable {
public void run() {
doSplitting();
}
private void doSplitting() {
requestRegionSplit(){
final TableName table = parent.getTable();
// 子 Region A
final RegionInfo hri_a = RegionInfoBuilder.newBuilder(table).setStartKey(parent.getStartKey()).setEndKey(midKey)
.build();
// 子 Region B
final RegionInfo hri_b = RegionInfoBuilder.newBuilder(table).setStartKey(midKey).setEndKey(parent.getEndKey())
.build();
// HMaster 最终完成 Region Split
server.reportRegionStateTransition(new RegionStateTransitionContext(.....)){
// 创建请求对象
final ReportRegionStateTransitionRequest request = createReportRegionStateTransitionRequest(context);
// 搞定 RPC 链接
createRegionServerStatusStub();
// 发送 RPC 请求给 HMaster
ReportRegionStateTransitionResponse response = rss.reportRegionStateTransition(null, request);
}
}
}
}
// HMaster 内部的 AssignManager 完成 RegionSplit
AssignmentManager.reportRegionStateTransition(req){
reportRegionStateTransition(builder, serverName, req.getTransitionList()){
final RegionInfo parent = ProtobufUtil.toRegionInfo(transition.getRegionInfo(0));
final RegionInfo splitA = ProtobufUtil.toRegionInfo(transition.getRegionInfo(1));
final RegionInfo splitB = ProtobufUtil.toRegionInfo(transition.getRegionInfo(2));
updateRegionSplitTransition(serverName, transition.getTransitionCode(), parent, splitA, splitB){
// 提交一个 SplitTableRegionProcedure 给 HMaster 内部的 ProcedureExecutor 来处理
master.getMasterProcedureExecutor().submitProcedure(createSplitProcedure(parent, splitKey));
}
}
}
关于 ProcedureExecutor 的工作机制,咱们之前讲过,接下来直接进入到 SplitTableRegionProcedure 的 executeFromState 方法:
public SplitTableRegionProcedure(final MasterProcedureEnv env, final RegionInfo regionToSplit,
final byte[] splitRow) throws IOException {
// 子 Region
this.daughterOneRI = RegionInfoBuilder.newBuilder(table).setStartKey(regionToSplit.getStartKey())
.setEndKey(bestSplitRow).setSplit(false).setRegionId(rid).build();
// 子 Region
this.daughterTwoRI = RegionInfoBuilder.newBuilder(table).setStartKey(bestSplitRow)
.setEndKey(regionToSplit.getEndKey()).setSplit(false).setRegionId(rid).build();
// 构建 RegionSplitPolicy = SteppingSplitPolicy
if(tableDescriptor.getRegionSplitPolicyClassName() != null) {
Class<? extends RegionSplitPolicy> clazz = RegionSplitPolicy.getSplitPolicyClass(tableDescriptor, conf);
this.splitPolicy = ReflectionUtils.newInstance(clazz, conf);
}
}
public class SplitTableRegionProcedure extends AbstractStateMachineRegionProcedure<SplitTableRegionState> {
// 四大核心
private RegionInfo daughterOneRI;
private RegionInfo daughterTwoRI;
private byte[] bestSplitRow;
private RegionSplitPolicy splitPolicy;
// 核心业务方法:executeFromState
protected Flow executeFromState(MasterProcedureEnv env, SplitTableRegionState state) throws InterruptedException {
switch(state) {
case SPLIT_TABLE_REGION_PREPARE:
if(prepareSplitRegion(env)) {
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_PRE_OPERATION);
break;
} else {
return Flow.NO_MORE_STATE;
}
case SPLIT_TABLE_REGION_PRE_OPERATION:
preSplitRegion(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_CLOSE_PARENT_REGION);
break;
// 3 解分配: 此时,这个 parent region 必定还是由某个 regionserver 在执行管理
case SPLIT_TABLE_REGION_CLOSE_PARENT_REGION:
addChildProcedure(createUnassignProcedures(env));
setNextState(SplitTableRegionState.SPLIT_TABLE_REGIONS_CHECK_CLOSED_REGIONS);
break;
case SPLIT_TABLE_REGIONS_CHECK_CLOSED_REGIONS:
checkClosedRegions(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_CREATE_DAUGHTER_REGIONS);
break;
// 5 创建 子 Region
case SPLIT_TABLE_REGION_CREATE_DAUGHTER_REGIONS:
removeNonDefaultReplicas(env);
createDaughterRegions(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_WRITE_MAX_SEQUENCE_ID_FILE);
break;
case SPLIT_TABLE_REGION_WRITE_MAX_SEQUENCE_ID_FILE:
writeMaxSequenceIdFile(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_PRE_OPERATION_BEFORE_META);
break;
case SPLIT_TABLE_REGION_PRE_OPERATION_BEFORE_META:
preSplitRegionBeforeMETA(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_UPDATE_META);
break;
// 8 更新元数据
case SPLIT_TABLE_REGION_UPDATE_META:
updateMeta(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_PRE_OPERATION_AFTER_META);
break;
case SPLIT_TABLE_REGION_PRE_OPERATION_AFTER_META:
preSplitRegionAfterMETA(env);
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_OPEN_CHILD_REGIONS);
break;
// 10 分配 Region
case SPLIT_TABLE_REGION_OPEN_CHILD_REGIONS:
addChildProcedure(createAssignProcedures(env));
setNextState(SplitTableRegionState.SPLIT_TABLE_REGION_POST_OPERATION);
break;
case SPLIT_TABLE_REGION_POST_OPERATION:
postSplitRegion(env);
return Flow.NO_MORE_STATE;
上述 11 步工作:
- HRegionServer 触发在本地进行 split,并准备 split。第一步就是在 ZooKeeper 的 /hbase/region-in-transition/region-name 的节点下创建一个 znode 节
点,并置为 SPLITTING 状态; - HBase 的 HMaster 是一直监听着 ZooKeeper 的 znode,发现 Parent Region 需要 split。
- HRegionServer 在 HDFS 的 Parent Region 的目录下创建一个名为“.splits”的子目录。
- HRegionServer 关闭 Parent Region 。强制 flush 缓存,并且在本地数据结构中标记 region 为下线状态。如果这个时候客户端刚好请求到 parent region,会
抛出 NotServingRegionException。这时客户端会进行重试。 - HRegionServer 在 .split 目录下分别为两个 daughter region(A,B)创建目录和必要的数据结构。然后创建两个引用文件指向 parent regions 的文件。
- HRegionServer 在 HDFS 中,创建真正的 region目录,并且把引用文件移到对应的目录下。
- HRegionServer 发送一个 put 的请求到 .META. 表中,并且在 .META. 表中设置 parent region 为下线状态,并添加关于 daughter regions 的信息。但是这个
时候在 .META. 表中 daughter region 还不是独立的 row,客户端在此时 scan .META. 表时会发现 parent region 在 split,但是还不能获得 daughter region的
信息,直到她们独立的出现 .META. 表中。如果此时这个往 .META. 表中的 put 操作成功,parent region 会高效的 split,如果此时 HRegionServer 在 RPC请
求成功前失败,Master 和下一个 HRegionServer 会重新打开这个 parent region 并将之前产生的 split 的脏数据清掉,.META. 表成功更新后,HBase 继续进
行下面 split 的流程。 - HRegionServer 并行打开两个 daughter region 接受写操作。
- HRegionServer 在 .META. 表中增加 daughters A 和 B region 的相关信息,在这以后,client 就能发现这两个新的 regions 并且能发送请求到这两个新的
region 了。client 本地具体有 .META. 表的缓存,当他们访问到 parent region 的时候,发现 parent region 下线了,就会重新访问 .META. 表获取最新的信
息,并且更新本地缓存。 - HRegionServer 更新 znode 的状态为 SPLIT。master 就能知道状态更新了,master 的平衡机制会判断是否需要把 daughter regions 分配到其他
HRegionServer 中。 - 在 split 之后,meta 和 HDFS 依然会有引用指向 parentregion. 当 compact 操作发生在 daughter regions 中,会重写数据 file,这个时候引用就会被逐渐的去
掉。GC 任务会定时检测 daughter regions 是否还有引用指向 parent files,如果没有引用指向 parent files 的话,parent region 就会被删除。
2.3. HBase Split 优化
为了减小 HBase 的 split 带来的性能影响,可以在预估数据的规模和增长情况之后,通过预分区来优化。
HBase Comact
HBase Compact 处理
HBase 实现中有一个专门的类 CompactSplit 负责接收 Compaction 请求和 split 请求。
3.1. HBase Compact 概念
随着 MemStore 不停的 Flush 产生新的 HFile,会导致查询数据时磁盘 IO 越来越大,需要进行优化,方案就是把 HFile 执行合并成大文件,如果 HFile 文件过多,如图
则会按照某种策略来执行一个选择。具体的合并方式,就是从 HFile 中读取 KeyValue,排序之后,再写入到一个新的 HFile 文件中。
HBase 根据合并规模将 Compaction 分为两类:Minor Compaction 和 Major Compaction。一般情况下,Major Compaction 持续时间会比较长,整个过程会消耗
大量系统资源,对上层业务有比较大的影响,所以线上环境基本都是关闭 Major Compaction,而推荐在集群负载低谷期通过手动的方式来触发 Major
Compaction。
Minor Compaction 是指选取部分小的、相邻的 HFile,将它们合并成一个更大的 HFile。
Major Compaction 是指将一个 Store 中所有的 HFile 合并成一个HFile,这个过程还会完全清理三类无意义数据:被删除的数据、TTL 过期数据、版本号超过
设定版本号的数据。如果想禁用 major Compact,将参数 hbase.hregion.majorcompaction 设为 0 即可。默认是 7 天左右一次。
在 HBase 的体系架构下,Compaction 有以下核心作用:
合并小文件,减少文件数,稳定随机读延迟。
清除无效数据,减少数据存储量。
3.2. HBase Compact 触发时机
HBase Compact 有三种常见的触发时机:
MemStore Flush 之后,会多出来一个 HFile,Store 中总文件数大于 hbase.hstore.compactionThreshold,默认是3,就会触发 Compaction
CompactionChecker 线程的周期性检查,检查周期为:hbase.server.thread. wakefrequency * hbase.server.compactchecker.interval.multiplier,如果在
源码中没有找到,到 hbase-default.xml 这个默认配置文件中寻找。
手动触发,主要目的是第一,自动触发 major compact 由于比较耗时会影响高峰期的业务,第二,用户修改表之后,希望立即生效。第三,需要清除大量无效
数据,恢复部分磁盘空间
default:
throw new UnsupportedOperationException(this + " unhandled state=" + state);
}
return Flow.HAS_MORE_STATE;
}
}
3.3. HBase Compact 基本流程
HBase Compact 基本流程如下:
HBase 会将该 Compaction 交由一个独立的线程 CompactionRunner 处理,该线程首先会从对应 Store 中选择合适的 HFile 文件进行合并,然后再选择合适的线程
池,最后执行 HFile 的合并。
HBase 的 RegionServer 在初始化的时候,会创建一个 CompactSplit 的组件来负责具体的 Split 和 Compact 动作。
接着来看 CompactionRunner 的工作:
// 通过 CompactSplit 来完成具体的 Compact 动作
public class CompactSplit implements CompactionRequester, PropagatingConfigurationObserver {
private void requestCompactionInternal(HRegion region, .....){
// 其实,Region 的 Compact 是每个 HStore 独立完成的
for(HStore store : region.stores.values()) {
// Compact 内部实现
requestCompactionInternal(region, store, why, priority, selectNow, tracker, completeTracker, user){
// 第一步:挑选合适的 HFile 集合
selectCompaction(region, store, priority, tracker, completeTracker, user){
store.requestCompaction(priority, tracker, user){
final CompactionContext compaction = storeEngine.createCompaction();
compaction.select(this.filesCompacting, isUserCompaction, mayUseOffPeak,
forceMajor && filesCompacting.isEmpty()){
// 底层最终根据 CompactionPolicy 来执行 HFile 的选择
request = compactionPolicy.selectCompaction(storeFileManager.getStorefiles(), ....);
}
}
}
// 第二步:挑选合适的线程池
hreadPoolExecutor pool = store.throttleCompaction(compaction.getRequest().getSize()) ?
longCompactions : shortCompactions;
// 第三步:提交一个 Compact 任务
pool.execute(new CompactionRunner(store, region, compaction, tracker, completeTracker, pool, user));
}
}
}
}
CompactionRunner.run(){
// 执行合并
doCompaction(user){
// 第一: 挑选 HFile
Optional<CompactionContext> selected = selectCompaction(this.region, this.store, ...);
// 第二: 挑选线程池
ThreadPoolExecutor pool = store
.throttleCompaction(c.getRequest().getSize()) ? longCompactions : shortCompactions;
// 第三: 执行 region 的 compact
boolean completed = region.compact(c, store, compactionThroughputController, user){
// 执行 Store 的 Compact
store.compact(compaction, throughputController, user){
//
compaction.compact(throughputController, user){
// 具体实现
compactor.compact(request, throughputController, user){
// 创建每个 HFile 对应的 Scanner
List<StoreFileScanner> scanners = createFileScanners(request.getFiles(), smallestReadPoint, dropCache);
// 创建 Writer
writer = sinkFactory.createWriter(scanner, fd, dropCache);
// 执行 Compact
finished = performCompaction(fd, scanner, writer, smallestReadPoint, cleanSeqId, throughputController,
request.isAllFiles(), request.getFiles().size()){
do{
hasMore = scanner.next(cells, scannerContext);
总结,HBase 的 flush 动作其实 compact 的核心逻辑几乎一致
创建对应的 Scanner
flush: MemStoreScanner
compact: StoreFileScanner
创建临时 HFile 文件对应的 StoreFileWriter
扫描数据
写入 HFile 文件