HDFS用了這個優化後,效能直接翻倍

語言: CN / TW / HK


【背景】


前段時間在HDFS的dn節點規模1000+的環境中,並且有1億block資料量的情況下, 進行大量併發寫檔案測試時,發現部分客戶端寫異常並導致最終僅寫入了部分資料,本文就該問題進行分析總結。


【表面現象分析】


出現該問題時,首先查看了客戶端的日誌,發現這些客戶端的日誌中,都出現了NotReplicatedYetException的告警日誌,其他全部成功寫入的客戶端中有的也有這樣的日誌。


既然都出現了這個告警日誌,那為什麼有的能全部成功寫入,有的就直接退出不寫了呢?


這個現象背後的原理其實很簡單:當客戶端寫檔案過程中,向nn傳送申請新的block的rpc請求時,如果nn以錯誤形式返回NotReplicatedYetException,客戶端的rpc處理會根據該錯誤構造對應的異常並向上丟擲異常,介面呼叫的處理中又會捕獲該異常,然後睡眠一段時間,並再次傳送申請block的rpc請求。當重試到達一定次數後,仍舊是失敗的,那麼就不再繼續嘗試,直接退出。


關鍵程式碼如下:

protected LocatedBlock locateFollowingBlock(DatanodeInfo[] excluded,
      ExtendedBlock oldBlock)
 throws IOException
{
    final DfsClientConf conf = dfsClient.getConf();
    // 預設5次
    int retries = conf.getNumBlockWriteLocateFollowingRetry();
    // 首次重試睡眠時間為 400ms
    long sleeptime = conf.getBlockWriteLocateFollowingInitialDelayMs();
    while (true) {
      long localstart = Time.monotonicNow();
      while (true) {
        try {
          return dfsClient.namenode.addBlock(src, dfsClient.clientName,
              oldBlock, excluded, stat.getFileId(), favoredNodes,
              addBlockFlags);
        } catch (RemoteException e) {
          IOException ue =
              e.unwrapRemoteException(FileNotFoundException.class,
                  AccessControlException.class,
                  NSQuotaExceededException.class,
                  DSQuotaExceededException.class,
                  QuotaByStorageTypeExceededException.class,
                  UnresolvedPathException.class);
          if (ue != e) {
            throw ue; // no need to retry these exceptions
          }


          if (NotReplicatedYetException.class.getName().
              equals(e.getClassName())) {
            if (retries == 0) {
              throw e;
            } else {
              --retries;
              LOG.info("Exception while adding a block", e);
              long elapsed = Time.monotonicNow() - localstart;
              if (elapsed > 5000) {
                LOG.info("Waiting for replication for "
                    + (elapsed / 1000) + " seconds");
              }
              try {
                LOG.warn("NotReplicatedYetException sleeping " + src
                    + " retries left " + retries);
                Thread.sleep(sleeptime);
                sleeptime *= 2;
              } catch (InterruptedException ie) {
                LOG.warn("Caught exception", ie);
              }
            }
          } else {
            throw e;
          }

        }
      }
    }
  }


因此,雖然有的客戶端有告警日誌,但都全部成功寫入,只能說明出現了重試,但最終還是成功響應;而有的則連續5次均重試失敗,導致僅寫入了部分資料。


【問題深入】


既然知道了是因為nn對客戶端請求block的rpc請求返回了錯誤,並且是連續多次請求都返回錯誤,最終引發客戶端終止寫入,那麼nn為什麼會一直返回NotReplicatedYetException的錯誤呢?


這裡就涉及到NN中block的狀態,以及申請新block的rpc請求處理邏輯了。


在NN內部,每個block的初始狀態為underConstruction,然後依次變為committed,最終變為complete。


客戶端寫同一個檔案,申請新的block時,nn預設將上一個block的狀態置為committed,等dn通過增量塊彙報將block副本的finalized狀態上報時,nn將該block的狀態置為complete。


另外,nn在處理客戶端申請新的block的rpc請求時,需要檢查上上一個block的狀態是否為complete。如果不是,則返回NotReplicatedYetException


也就是說,寫同一個檔案時,在申請第3個block時,會檢查第1個block的狀態是否為complete,如果不是則返回錯誤,後面的依次類推。


實際測試過程中出現問題時,必然是前面的block狀態不對導致的。


【問題根因】


瞭解了錯誤的產生原因之後,自然會追問,為什麼nn中block的狀態一直沒有達到complete狀態,是因為dn沒有傳送增量塊彙報請求嗎?


然而,從dn的日誌來看,確實都有傳送增量塊彙報的請求。


在問題分析過程中,沒有頭緒時卻注意到了另外一個細節。在整個測試中,在nn的web頁面上,看到有部分dn出現了離線的情況


順著這個現象進行分析:nn感知dn下線,肯定是心跳超時,而dn的增量塊彙報和心跳是在同一個執行緒中複用同一個tcp連線序列傳送的。因此心跳超時,增量塊彙報請求肯定也會受影響。


再次測試復現時,發現dn的心跳執行緒的堆疊卡在傳送增量塊彙報的函式中,而在nn節點上通過netstat觀察對應的tcp連線情況,發現連線的Recv-Q的數值一直很高。檢視nn的jmx指標,發現CallQueueLength也一直維持在最大值,由此斷定是rpc處理機制引起的問題。


為什麼是rpc處理引起的,請繼續往下看。


在nn內部,對於每個監聽的埠,都有四種類型的執行緒來處理連線上的請求。

  • 監聽執行緒

    負責在指定埠監聽,當有新連線到來時,負責完成連線的建立,隨後將連線轉交給reader執行緒進行後續的處理。

  • reader執行緒

    負責連線建立之後,接收該連線上的請求(從socket上讀取客戶端傳送的資料),並請請求封裝成callRpc請求物件,然後將請求物件放到請求佇列中。

  • handler執行緒

    通常有多個handler執行緒,負責從請求佇列中取出請求,並進行實際的處理,處理完成後通過連線的socket直接傳送請求響應內容,或者將響應內容放到響應佇列中。

  • 響應傳送執行緒

    負責從響應佇列中取出請求對應的響應,然後通過請求對應連線的socket,傳送響應內容。



注意:

請求佇列的長度是有上限的,具體上限為handler執行緒的個數乘以每個handler執行緒的佇列長度。


當請求佇列達到上限時,reader執行緒將請求放到佇列的動作會被阻塞,繼而所有連線過來的請求堆在tcp連線的接收緩衝區中,而不會被處理。


按照預設的配置,10個handler執行緒,每個執行緒的佇列長度100,那麼該監聽埠上的請求佇列長度為1000。


當有10000的併發寫檔案時,那麼就有10000個申請block的rpc請求,這些請求瞬間將請求佇列塞滿(nn的jmx指標中CallQueueLenght表示該佇列的實際長度),剩餘的請求堆積在連線的接收緩衝區中(netstat看到的Recv-Q),等待reader執行緒讀取並處理,並且隨著一個block的寫完,還會繼續寫新的block,也就是繼續產生新的rpc請求,因此觀察nn的jmx指標發現rpc請求佇列持續達到最大值。


而dn的增量塊彙報請求本質上也是一個rpc請求,這些請求和客戶端申請block的請求都發往同一個埠,等待reader執行緒從連線的接收緩衝區讀取,或在請求佇列中等待handler執行緒進行實際處理


因此就可能出現,某些dn的增量塊彙報請求,雖然成功傳送,但在連線的接收緩衝區上的請求一直未被reader讀取處理。所以,在nn內部block的狀態也就沒有變化,導致客戶端申請新的block時返回錯誤。同樣,也能解釋為什麼有的客戶端出現了離線的情況。


【問題優化】


知道了問題的所在,剩下來就是進行優化了。


首先採用了增加handler執行緒數的方法。加大請求併發處理,也變相加大了rpc請求佇列的長度。這也是網上提得比較多的方案。


然而實際效果卻不太理想,因為對於申請block請求的處理,內部有一把大鎖鎖住,這會導致其他handler執行緒頻繁等鎖。因此雖然增加handler執行緒看是提升了併發處理能力,但是實際多數handler執行緒都在等鎖,導致dn的增量塊彙報請求不能及時被處理。其提升的效果不明顯。


我們注意到:將dn的增量塊彙報請求和客戶端申請block的請求混在一個佇列中,是無法保證有限處理dn的增量塊彙報請求的,因此考慮將其進行分離,即dn的rpc請求和客戶端的rpc請求分別發往不同的埠。這隻需要在nn上修改相應的配置即可。


埠分離後的測試效果:併發寫檔案的數量相比分離之前,直接翻倍提升,從併發15000提升到30000+(受限於客戶端所在機器的效能,沒有繼續往上壓,實際網路頻寬,nn的各項指標表明併發讀還可以繼續增加),到此問題得到優化解決。


【總結】


來小結一下,本文通過實際測試過程中遇到的問題,結合hdfs中的客戶端的請求邏輯、nn內部blocks狀態、申請block請求的處理機制、rpc處理機制等原理,一步步進行分析,最終定位問題,並給出優化方案,且優化效果明顯。


【你可能感興趣的文章】


《5000字12張圖講解nn記憶體中的元資料資訊》

《HDFS——塊彙報》

《HDFS——寫檔案流程》

《Hadoop中的RPC》


好了,本文就介紹到這裡,如果覺得本文對你有些幫助,來個點贊,在看吧,也歡迎分享轉發加我微信交流~


本文分享自微信公眾號 - hncscwc(gh_383bc7486c1a)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。