HBase源码分析4—Region Balance
易于水平拓展是HBase高性能的重要原因之一。但是随着数据分散到不同节点,容易出现数据倾斜的问题,使得整个集群的效率下降。本章详细讨论下HBase中负载均衡的问题。
入口
在HBase源码分析3—HMaster启动过程中提到过,在HMaster启动的最后阶段 finishActiveMasterInitialization 中,HBase会启动一些定时任务,其中就包括负责Region负载均衡的BalanceChore
1 2 3 4 5 |
// HMaster.java line:828 //initialize load balancer this.balancer.setMasterServices(this); this.balancer.setClusterStatus(getClusterStatusWithoutCoprocessor()); this.balancer.initialize(); |
而balancer对象是在 initializeZKBasedSystemTrackers 中产生的
1 |
this.balancer = LoadBalancerFactory.getLoadBalancer(conf); |
HBase提供了很多种Balancer,曾经默认的叫 SimpleLoadBalancer ,现在默认的是 StochasticLoadBalancer (随机梯度……啊不是,随机负载均衡)。两种balancer的实现都在包 org.apache.hadoop.hbase.master.balancer 中。
Balance过程
BalanceChore 通过调用 HMaster.balance() 来启动balance过程。
首先函数做了一些检查,看集群是否满足做balance的条件,比如正在进行region splitting的时候就不能进行balance,有其他balance进行的时候也不能再进行balance( this.balancer 是用 syncronized 修饰的)。
宏观上看,balance的过程主要分三步:
- 获取集群信息
- 获得每个表的balance plan
- 执行所有plan
plan指的是一个balance的步骤,其中保存了需要移动的region,当前所在的serverName和目的地serverName。
下面展开分析一下三个步骤。
获取集群信息
1 2 |
Map<TableName, Map<ServerName, List<HRegionInfo>>> assignmentsByTable = this.assignmentManager.getRegionStates().getAssignmentsByTable(); |
assignmentManager 是一个与zk集群交互的组件,包含了一些关于node分配方面的函数。 getRegionStates() 方法返回regionStates,这个是 assignmentManager 内使用的管理region状态的一个数据结构,里面的内容跟 hbase:meta 表的内容是一致的。 getAssignmentsByTable() 返回的是以table为聚合维度的region信息,返回的结构为 Map<TableName, Map<ServerName, List<HRegionInfo>>> ,比较直观。
获得balance plan
1 2 3 4 5 6 |
//Give the balancer the current cluster state. this.balancer.setClusterStatus(getClusterStatusWithoutCoprocessor()); for (Entry<TableName, Map<ServerName, List<HRegionInfo>>> e : assignmentsByTable.entrySet()) { List<RegionPlan> partialPlans = this.balancer.balanceCluster(e.getKey(), e.getValue()); if (partialPlans != null) plans.addAll(partialPlans); } |
getClusterStatusWithoutCoprocessor() 用于获取集群信息。为什么要without coprocessor以后再找机会分析下。
接下来就是对每个tabel计算它的balance plan。以默认的方案 StochasticLoadBalancer为例看下这个方案是怎么得到的。
实际调用的方法是 org.apache.hadoop.hbase.master.balancer 包中的 balanceCluster 。
step 1 balanceMasterRegions()
顾名思义就是对master上的region做平衡操作。分两种:把不该出现在master上的region换出去,把该在master上的region换回来。
这个方法的前半部分实现了“换出去”操作,对maste上的每个region判断一下,如果不必要,就随便找个region server丢了(通过生成RegionPlan实现的,这里并不做真正操作)。如何判断是否要存在master region server上呢?通过方法 shouldBeOnMaster(region)实现。这个方法会去检查regionName是否在一个“免死列表”中( tablesOnMaster ),这个列表中默认包含 hbase:meta,如果想增加其他的region需要在配置中通过配置 hbase.balancer.tablesOnMaster 实现。
方法的后半部分实现的是“换回来”操作,会遍历所有server的所有region进行判断(还用刚才那个函数),如果该回来就生成相应的plan。
如果在这一步中,master region需要做balance,那么cluster的balance plan就不再继续生成了,这一轮就只做master region的balance。
step 2 判断是否有balance的必要
如果除了master region server之外,就剩一个Server了,那就没必要做balance了。在后面几行还有一个 needsBalance(cluster) ,用于计算当前cluster的负载是否需要做balance,具体是对 costFunctions 中的所有估价函数记性计算求和(带权重的),看是否小于某个阈值。估价函数包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
costFunctions = new CostFunction[]{ new RegionCountSkewCostFunction(conf), new PrimaryRegionCountSkewCostFunction(conf), new MoveCostFunction(conf), localityCost, rackLocalityCost, new TableSkewCostFunction(conf), regionReplicaHostCostFunction, regionReplicaRackCostFunction, regionLoadFunctions[0], regionLoadFunctions[1], regionLoadFunctions[2], regionLoadFunctions[3], }; |
每个函数都考虑了某方面的问题,比如locality、备份复杂度等等……
step 3 生成balance方案
生成方案的流程很暴力:首先算出一个当前的负载(用needBalance里一样的方法),然后随机生成一个动作,计算操作后是不是比之前更好,如果好就保留,不行就回滚。这种随机的尝试动作有次数上限。最后将cluster的变化组合成plan返回。
执行balance plan
代码来到HMaster.java的1489行左右。之前我们已经获得了一个RegionPlan的ArrayList,下面就是要顺序执行里面的所有plan。主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// balance开始时间 long balanceStartTime = System.currentTimeMillis(); // balance结束时间阈值 long cutoffTime = balanceStartTime + this.maxBlancingTime; if (plans != null && !plans.isEmpty()) { int balanceInterval = this.maxBlancingTime / plans.size(); for (RegionPlan plan : plans) { this.assignmentManager.balance(plan); rpCount++; // 控制balance节奏 balanceThrottling(balanceStartTime + rpCount * balanceInterval, maxRegionsInTransition, cutoffTime); // 超过cutoffTime直接结束balance if (rpCount < plans.size() && System.currentTimeMillis() > cutoffTime) { break; } } } |
this.assignmentManager.balance(plan) 这行进行了plan的实际执行操作,具体是(删减了无用代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public void balance(final RegionPlan plan) { HRegionInfo hri = plan.getRegionInfo(); TableName tableName = hri.getTable(); if (tableStateManager.isTableState(tableName, ZooKeeperProtos.Table.State.DISABLED, ZooKeeperProtos.Table.State.DISABLING)) { return; } String encodedName = hri.getEncodedName(); ReentrantLock lock = locker.acquireLock(encodedName); try { if (!regionStates.isRegionOnline(hri)) { return; } synchronized (this.regionPlans) { this.regionPlans.put(plan.getRegionName(), plan); } unassign(hri, false, plan.getDestination()); } finally { lock.unlock(); } } |
读到这自然地会有个疑问:就把plan put到一个array里就完了???region的移动到底谁干的???
其实实际的代码在 unassign 这个方法里,这个方法看起来是“把某个region下线”的意思,但是实际上他带了一个 dest 参数,默认是null。当这个 dest 是null的时候,他干的就是字面的unassign的事情;但是如果不为空的时候,会顺带把move的事情给干了。追进去还会发现一个东西:
1 2 |
if (serverManager.sendRegionClose(server, region, versionOfClosingNode, dest, transitionInZK)) { |
sendRegionClose 这个东西表面上看起来是给region发close rpc指令的,但是注意他参数里也有个dest,当他不为null的时候,也会顺便给移动下。
这俩方法写的。。不说不对吧,但是名字确实起的有点让人误解,在这里卡了一段时间才发现搞的什么鬼。。
这部分具体的实现逻辑要追的非常深,region server那边还不是特别了解,暂时马克一下,以后再具体看。
region balance的代码基本就分析到这里。另外看了下其他的balancer,感觉各有各的道理,但是目前的stochastic balancer大概是在性能和效果之间有trade off吧,后面看有没有相关资料可以研究下。