最近集群存储倾斜,个别节点存储超过85%,启动balancer之后效果明显,但是有时候balancer启动也不能解决问题。从运维阶段就知道有这么一个balancer,今天终于憋了一口气看看balancer到底咋回事。版本还是1.0.3
首先balancer在org.apache.hadoop.hdfs.server.balancer,balancer作为一个独立程序启动,听说之前是在namenode内部,这个还真没见过。
public static void main(String[] args) { try { System.exit( ToolRunner.run(null, new Balancer(), args) ); } catch (Throwable e) { LOG.error(StringUtils.stringifyException(e)); System.exit(-1); } }
进去balancer的run看看吧。
run代码相对比较长,不全部贴出来了,一步步看一下吧。
init(parseArgs(args));首先是解析程序传递参数,然后执行init方法。什么程序参数?很简单,因为balancer是这么用的bin/start-balancer.sh -threshold 5,看到了吗,有个阈值,需要管理员指定。
这个阈值是啥下面再说,这个解析参数,就是拿到咱们threshold的值,这里是5,那就把5传给init。
this.threshold = threshold; this.namenode = createNamenode(conf);//得到集群namenode对象 this.client = DFSClient.createNamenode(conf);//弄一个客户端对象 this.fs = FileSystem.get(conf);//文件系统对象这就是init主要的方法,剩下的安全认证的这里就不说了,这里主要是弄个namenode的代理,弄个hdfs的客户端,然后弄个hdfs文件系统的对象。好了,继续:
out = checkAndMarkRunningBalancer(); /*任意时刻只能有一个balancer存在,如何判断?就是往集群写一个文件/system/balancer.id,写的时候本身会有已存在的报错返回null * 往里写的应该是byte类型的数据,即balancer所在的机器信息。 * */ if (out == null) { System.out.println("Another balancer is running. Exiting..."); return ALREADY_RUNNING; }这里一看,就是检查一下有木有balancer进程已经在工作了,这个方法很简单,程序写死了要往hdfs的/system/balancer.id这个文件写进去点东西,写的东西是balancer所在机器的信息:
private OutputStream checkAndMarkRunningBalancer() throws IOException { try { DataOutputStream out = fs.create(BALANCER_ID_PATH); out. writeBytes(InetAddress.getLocalHost().getHostName()); out.flush(); return out;看到了吗。然后写的过程中判断这个文件是不是存在,如果存在说明有其它的balancer在运行,因为balancer在启动的时候创建,在退出的时候删除。这是一个办法,但是如果运行的时候咱手动给它删了,还是可以启动balancer的,这样就有多个balancer,这是有危害的,后面再说。
这样一个简单地判断后,balancer就可耻地启动了,进入一个大循环。 直到集群“平衡”了,进程就退出了。
ok,进去看看吧。
while (true ) { /* get all live datanodes of a cluster and their disk usage * decide the number of bytes need to be moved */ long bytesLeftToMove = initNodes(); if (bytesLeftToMove == 0) { System.out.println("The cluster is balanced. Exiting..."); return SUCCESS; } else { LOG.info( "Need to move "+ StringUtils.byteDesc(bytesLeftToMove) +" bytes to make the cluster balanced." ); } /* Decide all the nodes that will participate in the block move and * the number of bytes that need to be moved from one node to another * in this iteration. Maximum bytes to be moved per node is * Min(1 Band worth of bytes, MAX_SIZE_TO_MOVE). */ long bytesToMove = chooseNodes(); //到这一步源和端的对应关系即传输计划全部建立完毕 if (bytesToMove == 0) { System.out.println("No block can be moved. Exiting..."); return NO_MOVE_BLOCK; } else { LOG.info( "Will move " + StringUtils.byteDesc(bytesToMove) + "bytes in this iteration"); } formatter.format("%-24s %10d %19s %18s %17s\n", DateFormat.getDateTimeInstance().format(new Date()), iterations, StringUtils.byteDesc(bytesMoved.get()), StringUtils.byteDesc(bytesLeftToMove), StringUtils.byteDesc(bytesToMove) ); /* For each pair of <source, target>, start a thread that repeatedly * decide a block to be moved and its proxy source, * then initiates the move until all bytes are moved or no more block * available to move. * Exit no byte has been moved for 5 consecutive iterations. */ if (dispatchBlockMoves() > 0) { notChangedIterations = 0; } else { notChangedIterations++; if (notChangedIterations >= 5) { System.out.println( "No block has been moved for 5 iterations. Exiting..."); return NO_MOVE_PROGRESS; } } // clean all lists resetData(); try { Thread.sleep(2*conf.getLong("dfs.heartbeat.interval", 3)); } catch (InterruptedException ignored) { } iterations++; }我擦,先计算有多少数据要balance,这部分在initNodes方法中完成,然后chooseNodes,建立传输方案。
看上面这个图,0-100代表集群的存储率。avg是集群的平均存储值(所有活dn),balancer通过客户端请求跟namenode要块汇报数据,namenode返回datanodeInfo给balancer,balancer我日就那这计算集群的平均存储率,所有活dn的DfsUsed加和除以所有dn的capacity(所有dn配置的data.dir的容量之和),这样得出图上的avg,threshold是启动的时候传递进来的,就是指集群平均存储率的上下浮动值,比如threshold=5,就是上下5%,很好理解是吧。ok,看上面的图,threshold给出之后,不同存储使用率的dn就被分为四种,非别为ABCD,啥意思就不用说了吧,D区域的dn就是存储比较高的,例如平均70,阈值5,D区域就是高于75%的dn,对吧;A就是低于65%的dn.
initNodes方法就是要把dn分成ABCD四等,前面看了,initNodes方法返回一个数值,叫做待move的数据总量,什么叫待move的数据?
还是看上面的图,处在D区域的dn,可以说都有需要move的数据,比如前面的例子,平均70,阈值为5,如果一个dn存储是85%,需要挪的数据,就是85-(70+5)=10,10%就是指那台机器自己总量的10%。ok,D区里边这样的量都要加和,总值叫做overLoadedBytes——过载数据。
到这里,initNodes方法还没完事,还要计算一下underLoadedBytes——空载,我擦,名字自己起的,凑合着理解哈。什么是underLoadedBytes?这个东西跟过载数据是对应的,就是在A区域,这个地方的dn单机存储率低于集群平均水平,还是前面的例子,如果A区域有个dn存储率是50%,那么70-5-50=15,就是说这个dn有15%的空间是空载,还可以往里填东西。underLoadedBytes就是A区所有dn这样的数据量的总和。
现在有了过载数据量和空载数据量,好了,那待挪数据总量咋取,balancer取的是其中的较大值,无论过载大还是空载大,都多挪一点,集群更均衡,呵呵,大概是这个意思,并且balancer不是一回合就结束,后面会看到,它是一个迭代的过程。
ok,初始化完毕之后,我们知道集群有多少数据需要挪动,但是到底咋挪?这一步等于知道了需求,下一步就要制定可行的方案。
chooseNodes方法完成这个功能。
前面在initnodes的时候,需要统计出来四个集合,这时候需要一个类来描述这种需要转移数据的节点,BalancerDatanode,这个类创建的时候传进去三个参数,一个就是datanodeInfo对象,是dn的完整信息,然后把集群平均使用率和阈值告诉它,这个BalancerDatanode我擦就开始计算了,按照上面那个图,计算最大可移动数据。
private BalancerDatanode( DatanodeInfo node, double avgUtil, double threshold) { datanode = node; utilization = Balancer.getUtilization(node); /*哦,这个threshold就是集群平均使用率上下浮动值,前面传进来的avgUtil是百分比乘以100了,说以threshold就是百分比*/ if (utilization >= avgUtil+threshold//假如当前集群70%,threshold是10,那么这个dn大于80或者小于60的 || utilization <= avgUtil-threshold) { maxSizeToMove = (long)(threshold*datanode.getCapacity()/100); //需要挪的数据大小就是其配置容量的10%,即等于阈值 } else {//否则如果在60和80之间,那么需要挪的是x-70的绝对值,比如65,那么需要挪的是5%的数据量,这个数据量小于阈值 maxSizeToMove = (long)(Math.abs(avgUtil-utilization)*datanode.getCapacity()/100); } if (utilization < avgUtil ) {//如果集群整体很高,但是单机比集群轻, maxSizeToMove = Math.min(datanode.getRemaining(), maxSizeToMove); } maxSizeToMove = Math.min(MAX_SIZE_TO_MOVE, maxSizeToMove); /*一台机器要挪的数据量最大不能超过10G*/ }对于D区或A区的节点,最大可移动数据就是阈值大小,如果在BC区,就取其和平均值的差值的绝对值作为最大可移动数据,这个数据明显不超过阈值大小。
假如dn的capacity是20T,10%是多少?2T,我擦这么大的数据是不允许一次挪完的,系统给了限制,一次最大可移动数量不能超过10G。
还有这个最大可移动大小,对于AB区的节点就是最大能接收的数据,在CD区就是最大可送走的数据量。
ok,BalancerDatanode知道了,从构造函数来看,四个区域的节点都适用,那现在不够,现在想构建具体的执行计划,什么叫执行计划,就是明确的源端对<src,target>
从src往target发送数据,发多少,这些需要建立。基于这个需求,让Source类继承BalancerDatanode就好了,在initNode的过程中,把所有的dn归类,处于CD去的节点都封装成Source对象,分别放进aboveAvgUtilizedDatanodes,overUtilizedDatanodes集合中。AB区的则直接就封装成BalancerDatanode对象,分别扔到underUtilizedDatanodes,belowAvgUtilizedDatanodes集合中。
这样,CD区域的的Source们都准备好了,该给他们找target:
private void chooseTargets( Iterator<BalancerDatanode> targetCandidates, boolean onRackTarget ) { for (Iterator<Source> srcIterator = overUtilizedDatanodes.iterator(); srcIterator.hasNext();) {//拿最大限的作为要挪的对象src,往存储低的里挪 Source source = srcIterator.next(); while (chooseTarget(source, targetCandidates, onRackTarget)) { }//找到一轮的所有可以接收数据的节点 if (!source.isMoveQuotaFull()) { srcIterator.remove(); } } return; }先为D区的Source们在A区找target,因为A区最宽裕,D区最贫瘠,这样更容易均衡。继续往下说之前不得不补充一点,就是BalancerDatanode这种描述需要转移或者接收数据的节点的对象还必须有个数据控制其已转移量,因为前面说到假如D区有2T要送走,那得多次,所以下次要送走的量就变少了,这个事都有个成员记录下来,它就是scheduledSize代表了接收或者送走的数据总量。好了,继续:
找target的具体过程是这样的,从source们遍历,拿出一个,然后从target候选集合中挨个检查能不能作为这个source的target,也就是一个source可以对应多个target,两个集合能怎么匹配,只能双循环去挨个匹配!咋匹配?
比如拿到一个D区的节点了,然后拿到一个A区的节点,先要看看两个节点能移动的数据还有多大,什么叫还能移动的数据,D的就是能送走的,A的就是能接收的,怎么算这个?前边说了scheduledSize,用最大可移动值减去这个就是了,然后比较src和target,哪个小,就按哪个来,要不然能送走的多,能接收的小,按大的来,不就破坏规则了吗!
这时候把这个target和它本次迭代要送走的数据量数值绑定成NodeTask对象,加到source的派送队列里。source回头就按这个派送队列派送数据。凡是被source加到派送队列的target,都要增加已接收量,source相应增加以派送量。这个东西尽管现在还是计划,只是执行计划,派送并未成功,但是在一次迭代里还是要记录。然后把这些所有的建立好派送计划的source们和target们分别添加到balancer管理的sources和targets集合里。
派送方向,源端选择都在图上的箭头标明了。
需要注意的是,先在同机架建立派送关系,再往机架之间建立派送关系,因为倾斜严重的时候机架之前派送基本上会破坏机架放置策略,这样对于数据安全不利。
前面initNodes的时候,计算出了一个CD区节点需要送走的数据总量,那是个毛量,就是你想挪,但是不一定能全部派送完的量,chooseNodes方法则返回了派送计划要派送的所有数据量。这个量是一次迭代实际要传输的数据总量。前边那个叫need to move,这个执行计划做好后的叫做will to move。
好了到这里一轮迭代计划完成了,下面就要执行派送了。