【HDFS】存儲balancer到底咋回事

最近集羣存儲傾斜,個別節點存儲超過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);
    }

  }

我擦,一看,是熟悉的ToolRunner,不用說Balancer就是個tool,重寫run方法。

進去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。

好了到這裏一輪迭代計劃完成了,下面就要執行派送了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章