HFR:在RBF上實現跨NameSpace Rename

小米運維發表於2022-12-05

摘要

隨著HDFS資料量越來越大,NameNode記憶體已不足以裝下全部後設資料,這限制了HDFS的擴充套件。社群提出的RBF(Router Based Federation)比較高效地解決了這個問題。它將使用者視角的目錄拆分到多組互相獨立的子叢集(名字空間或者NameSpace)中,這一過程被稱為掛載。Router層記錄掛載表資訊,並轉發使用者請求,使得RBF看起來和普通HDFS一樣。
  但RBF也有自己的問題:


  • 不支援跨NameSpace的rename。
  • NameSpace隨著使用會逐漸不均衡,社群目前還沒有用於均衡NameSpace的工具。


  為了解決這兩個問題,我們設計了HFR(HDFS Federation Rename)。它支援跨NameSpace rename目錄和檔案,也可以用來做NameSpace負載均衡。
介紹
  HFR的思想是:先把源目錄樹從源NameNode移動到目標NameNode,再將所有資料塊從源pool遷移到目標pool,最後更新Router上的掛載表,之後client就能從新的NameNode訪問到目錄了。在後設資料和檔案資料都不斷變化的情況下來實現遷移非常困難,所以HFR中引入了一個額外的限制來簡化這個問題:HFR作業執行期間,所有會修改後設資料和檔案資料的操作都被禁止掉(禁止寫操作)。後面我們會看到,這個限制大大簡化了HFR的實現。
  HFR作業被劃分為5個階段:Prepare、 SaveTree、 GraftTree、 HardLink和Finish:


  1. Prepare: 完成許可權和quota檢查,並鎖定src-path,禁止寫操作。

  2. SaveTree: 向src-NameNode傳送saveTree() RPC,令其將目錄樹序列化到外部儲存中。

  3. GraftTree: 向dst-NameNode傳送graftTree() RPC,dst-NameNode會讀取並反序列化上一步的目錄樹,並接到自己的目錄樹上。

  4. HardLink: RBF叢集的DataNode是共享的,我們可以使用硬連線來完成資料塊傳輸。本階段會先收集所有塊的位置資訊,生成HardLink計劃,最後向DataNode傳送RPC實現塊的hard link。

  5. Finish: 完成校驗和清理工作,並更新Router的掛載表。


  為了將這5個階段整合起來,我們設計了一個狀態機模型。HFR作業是一個狀態自動機,每一個階段對應一個狀態。如果一個階段失敗了,則跳轉到錯誤處理狀態,否則跳轉到下一階段狀態。我們引入新角色Scheduler來負責HFR作業的啟動、執行、重試和恢復。
  接下來我們會分別介紹SaveTree階段(第二部分)、GraftTree階段(第三部分)、HardLink(第四部分)、Scheduler模型(第五部分)、效能(第六部分)、總結(第七部分)。我們不討論Prepare階段和Finish階段,因為這兩個階段比較簡單,也比較多變,不同的使用者可以根據自己需要實現不同的Prepare和Finish階段。
SaveTree

  在SaveTree階段,我們引入了saveTree() RPC,它會儲存src-path目錄樹到外部儲存中,包括樹的結構、INodes、以及Blocks。saveTree() RPC還有一個特點是,它假定src-path已經被鎖住且處於不可變的狀態,因此不會做額外措施來保護目錄樹不被改變,即不會做禁寫。下面分別討論儲存src-path到外部儲存的過程和saveTree()不做禁寫的原因。


  saveTree()呼叫會產生兩個檔案:TREE-FILE和TREE-META,這兩個檔案被儲存在外部儲存中。TREE-FILE中儲存了整個目錄樹,TREE-META則儲存目錄樹的各種統計資訊。saveTree() RPC首先深度優先遍歷src-path,並將所有INodeDirectory和INodeFile序列化到TREE-FILE中。序列化的方式和NameNode生成Image的方式相同,這樣目錄的全部屬性(ACL、Xattr等)都會被保留。INode的寫入順序與遍歷順序相同,因此目錄樹的結構也可以被保留。遍歷的同時,saveTree() RPC會計算src-path的name消耗、space消耗和塊總數,並將它們寫入到TREE-META中(後面的GraftTree階段會解釋為什麼需要生成TREE-META)。


  SaveTree階段不做禁寫操作有兩個原因,一是要保持簡單。saveTree() RPC只是簡單地遍歷目錄樹並寫兩個檔案,整個過程都是無鎖的,也沒有edit log,是非常輕量級的操作。二是保持靈活,我們可以讓SaveTree之前的階段來負責禁寫,它可以根據自身需要用任何它想使用的手段,比如簡單的取消src-path的x許可權,或是複雜些的逐個子目錄取消w許可權等等。這些自定義的階段都可以與SaveTree階段組合,來滿足不同需求。saveTree()的結果檔案也不被限制只能用於HFR,也可以被用來其他用途,譬如DEBUG。如果使用者願意,他們可以在沒有禁寫的目錄上呼叫saveTree(),這樣做雖然不會損壞NameNode,但也無法保證寫到外部儲存的目錄樹的完整性。

HFR:在RBF上實現跨NameSpace Rename
Figure 1: The process of saveTree.
GraftTree
  GraftTree階段是HFR的核心,graftTree() RPC讀取TREE-FILE和TREE-META並構造dst-path。過程如下:


  1. 讀取TREE-META檔案。
  2. 合法性檢查,包括路徑名合法性、許可權、quota等。
  3. 預分配INode Id和Block Id。
  4. 讀取TREE-FILE,反序列化並構造目錄樹。
  5. 將構造好的目錄樹接到NameNode根目錄樹上,將所有Id新增到對應Map。


HFR:在RBF上實現跨NameSpace Rename
Figure 2: The process of GraftTree.
  在graftTree()中涉及到很多NameNode後設資料操作,因此必須獲取寫鎖。但整個RPC過程又有很多IO操作,所以不能像其他RPC那樣簡單地在RPC開始時獲取寫鎖並在RPC結束的時候釋放掉。這裡我們使用了一個比較巧妙的辦法避免拿著寫鎖做IO,實際上只需要在第2步獲取讀鎖,在第3和5步獲取寫鎖就夠了,其餘步驟都是無鎖的。因為在第3和5步要獲取寫鎖,而在第4步不需要任何鎖,所以中間會有一個放棄鎖再恢復鎖的過程。為此我們引入了一個計數器,在第4步開始的時候,開始一邊釋放鎖一邊計數,直到所有鎖都被釋放掉,然後執行第4步,執行完成後再根據計數器恢復被釋放掉的鎖。下面解釋一下鎖設計的正確性:


  • 第1步只是讀取TREE-META檔案,包含IO操作且不需要讀寫NameNode自身後設資料,自然應該是無鎖的。
  • 第2步需要做合法性檢查,涉及到讀取NameNode後設資料,和所有其他讀操作一樣,這一步是要拿讀鎖的。
  • 第3步預分配Id,這一步會修改NameNode後設資料,因此必須拿寫鎖。
  • 第4步構造目錄樹,構造過程需要讀取TREE-FILE檔案,並給新建INode和Block分Id,因為所有Id都在第三步分配好了,所以這一步雖然有IO但不需要拿任何鎖。
  • 第5步將目錄樹接到NameNode上,還要新增Id到Map,這都會改變NameNode後設資料,必須拿寫鎖。


  現在我們可以解釋為什麼SaveTree階段還會儲存一個TREE-META檔案了。在第二步quota檢查時我們需要知道目錄樹的name和space大小,在第三步Id預分配時我們需要知道INode總數和Block總數,有了TREE-META我們就不必讀取整個TREE-FILE來自己計算了,只要在第一步讀取TREE-META即可。
  現在我們討論至關重要的錯誤處理部分。在這裡我們的思路是”不要undo”,即GraftTree階段的5個步驟不論哪一個出錯了,都不要做回滾。這是因為回滾操作非常複雜,每一步都可能失敗,回滾本身也可能失敗。這裡我們同樣使用了一個比較巧妙的辦法,透過引入兩階段edit log解決了這個問題,完全避免了回滾操作,這兩個edit log是:


  • Pre-allocation edit log,在第3步預分配Id成功時,記錄分配了哪些Id。
  • Graft-done edit log,在第5步整個RPC成功時,記錄graftTree()引數列表和id對映表。


  我們將NameNode重放edit log後的狀態叫做重放狀態,將NameNode記錄完edit log那一刻的狀態叫做標準狀態,只要重放狀態與標準狀態一致,那麼無論發生failover、重啟、或是standby節點追日誌,NameNode狀態都是正確的。下面簡單證明一下為什麼兩階段edit log可以保證重放狀態與標準狀態一致。我們用e表示NameNode原有的edit log,用S表示標準狀態,用R表示重放狀態。NameNode當前每條edit log(不包含Pre-allocation和Graft-done)都滿足重放狀態等於標準狀態:對於任意一個ei和初始狀態S,我們都有Si=Ri,其中Si表示S狀態儲存ei之後的標準狀態,Ri表示S狀態重放ei之後的重放狀態。將上一步稍微推廣一下,對於一個序列{e1,...,ei,...,en}和初始狀態S,數學歸納法易證對於任意i屬於[1,n],Si=Ri。
  從兩階段edit log過程我們知道,其實edit log序列只有三種情況(ep表示Pre-allocation edit log,eg表示Graft-done edit log):無ep無eg、有ep無eg和有ep有eg。接下來我們逐個討論3種情況下標準狀態與重放狀態一致性:


  • 無ep無eg(步驟1,2,3其中一個失敗),edit log序列={e1,...,en},屬於原始的NameNode edit log序列,顯然成立。
  • 有ep無eg(步驟4或5失敗),edit log序列={e1,...,ei,ep,...,en}。考慮對於任意起始狀態S,當從Si->Sp時,NameNode會從可用Id集合中去除預分配Id並將去掉的Id記錄到ep,當重放ep時NameNode會將ep記錄的Id從可用Id集合中去除掉。對於Si->Sp和Si->Rp兩個過程來說,起始可用Id集合相同又去除了相同的Id,因此Sp=Rp,進而對任意x,都有Sx=Rx。
  • 有ep有eg(全部步驟成功),edit log序列={e1,...,ei,ep,...,ej,eg,...,en}。在Sj->Sp時,NameNode完成了步驟4和5,並記錄graftTree()引數和預分配Id到eg。步驟4和5可以看作有兩個輸入的函式:f(狀態Sj、預分配Id)。當重放ep時NameNode也重做步驟4、5,其中步驟4使用的Id記錄在eg中,與Sj->Sp時使用的Id完全相同。可見Sj->Sp和Sj-Rp時兩者輸入均相同,因此Sg=Rg。結合上一步證明可知對e1~eg均有Si=Ri,進而對任意x有Sx=Rx。


  在graftTree() RPC過程中,NameNode的狀態改變涉及到Id生成器、根目錄樹和Id對映表三部分。其中"Id被新增到Id對映表"和"dst-path目錄樹被新增到根目錄樹"同時發生,所以可以用根目錄樹的新增來表示Id對映表新增。下圖展示了三種情況下NameNode狀態與對應的edit log狀態:
HFR:在RBF上實現跨NameSpace Rename
Figure 3: The NameNode states and the edit log.
  下面討論一些實踐中的細節。首先討論步驟4,我們知道步驟4是無鎖的,這意味著它不可以改變NameNode的後設資料。步驟4構造的dst-path目錄樹不能被接到根目錄樹上,而是作為整個RPC過程的一個區域性變數儲存在記憶體中。一旦RPC失敗,這棵樹就會被自動回收掉,不需要額外的清理工作。下面來看一下graftTree() RPC的最後一步,我們先將所有Id新增到block-map和inode-map,然後將dst-path目錄樹接到根目錄樹上,最後寫下edit log。這三個操作必須同時完成或同時不做,只有部分完成則NameNode要殺死自己。最後看一下BLOCK-MAP檔案,在GraftTree階段,我們會寫一個新檔案BLOCK-MAP。它對映了源NameNode的Id到目標NameNode的Id,包括INodeId、BlockId和GenerationStamp。這個檔案是在第4步構造目錄樹時,一邊深度遍歷一邊寫成的,我們在下一步做HardLink的時候會用上它。另外在我們的實踐中,Graft-done edit log並沒有記錄所有預分配的Id,重放這條edit log時我們是透過讀BLOCK-MAP來獲取預分配資訊的。
HardLink
  HardLink階段負責將所有副本hard link到新block pool。一個塊可以被pool id、block id和gs(generation stamp)唯一確定,進而一個塊的hard link可以看作是一個6元組(src-pool, src-id, src-gs, dst-pool, dst-id, dst-gs)。我們給DataNode新增了一個新RPC介面,批次接收hard link 6元組,完成hard link並IBR(增量塊上報,與之相對的是全量塊上報)給NameNode。所有成功hard link的塊都會返回給Client。
  HardLink過程分為兩個步驟:


  1. 收集塊位置資訊,並生成hard link計劃。
  2. 執行hard link並處理hard link失敗的情況。


HFR:在RBF上實現跨NameSpace Rename
Figure 4: The process of HardLink.
  在第1步我們用多執行緒的方式收集塊位置資訊。首先我們定義一個pendingQueue用來儲存未被處理的路徑,之後我們啟動多執行緒來消費這個佇列。當一個執行緒拿到一個路徑的時候,它首先判斷路徑類別,如果是目錄就將它的所有子路徑加入到佇列中,如果是檔案就收集它的塊位置資訊,並新增到hard link map。這個map的key是DataNode,value是這個DataNode上一系列要做hard link的塊。
HFR:在RBF上實現跨NameSpace Rename
Figure 5: The thread model of collecting locations.
  第2步我們使用一個執行緒池來做hard link,每一個執行緒負責一個DataNode,分批地將block發給對應DataNode,並收集hard link結果。如果一個塊成功hard link的次數滿足了最小複製因子,就認為這個塊hard link完成。否則我們需要去目標NameNode檢查它的副本數,這是因為NameNode也會做複製,如果塊的副本數已經滿足了最小複製因子,就不需要再hard link了。如果檢查完NameNode塊副本數仍舊沒有達到最小複製因子,就對這些未達標塊重試整個流程。
HFR:在RBF上實現跨NameSpace Rename
Figure 6: The thread model of hardlink.
Scheduler模型
  Scheduler模型包括一個作業模型和一個排程器。一個作業是一個狀態自動機,可以用如下圖來表示。一個作業(Job)包括有限個任務(Task),任務間的有向邊表示任務執行流程。標記為Start的任務是作業的初始任務,標記為None的灰色任務是一個特殊任務,表示作業的結束。Job Context儲存了作業的上下文資訊,當作業被執行時Job Context會被逐個傳遞給每個任務,每個任務都可以根據需要來修改它。每一次任務結束時Job Context都會被序列化並儲存到外部儲存中,用於恢復失敗的作業。
HFR:在RBF上實現跨NameSpace Rename
Figure 7: The job model.
  Scheduler管理了所有作業的生命週期,下圖表示了Scheduler的執行緒模型。每一個作業都有四種狀態:Running、Pending、Delay、Recovering。當一個作業被提交給Scheduler後,它就被加入到pendingQueue中,處於pending狀態。Worker執行緒不斷地從pendingQueue中獲取作業並執行,這時作業就進入running狀態。當作業執行時,它可以透過設定delay time並丟擲TaskRetryException的方式來觸發重試。Worker捕捉到這個重試異常,就會給它加入到delayQueue中。當delay time時間到,作業會被Rooster執行緒取出並加回到pendingQueue,作業再次進入pending狀態。如果作業執行過程中有未知異常發生,作業就會被加入到recoverQueue中。Recover執行緒負責從recoverQueue中獲取作業,並根據job context來恢復它。當Scheduler啟動的時候,它會自動掃描外部儲存來查詢所有未完成作業,並將它們加入到recoverQueue中。
HFR:在RBF上實現跨NameSpace Rename
Figure 8: The job scheduler model
效能
測試環境
  測試叢集包含2個NameSpace和14個DataNode。伺服器配置如下:


  • Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz, 12 cores.
  • 128GB RAM, 4T x 12 HDD
  • Linux 2.6.32, JDK 8


測試方法


  • 資料集"set x-y"表示一個深度為x的目錄樹,樹的每個非葉子節點都有y個孩子節點,樹的每個葉子節點都是一個檔案。
  • 檔案副本數是3,每個檔案只包含一個塊。Linux中對256MB檔案和1KB檔案做HardLink速度是一樣的,因此這裡做了個最佳化,使用1KB塊檔案代表 256MB塊。下表中File Size是按照每個檔案256MB計算的總檔案大小。


測試結果
Table 1: The performance results.


Data sets
Directories
Files
Blocks
Time costs/ms
File Size
set 7-7
19608
117649
117649
18,001
28.72TB
set 7-8
37449
262144
262144
30,890
64TB
set 8-9
597871
4782969
4782969
577,360
1.14PB


線上環境
  線上環境與測試環境有很大不同,主要是為了保證安全,線上環境我們做了更煩瑣的校驗,這對HFR速度影響很大,會使其耗時更久。線上的校驗需要給NameNode傳送大量rpc,每個rpc的耗時也隨NameNode當時的ops有波動,所以會有大小差不多的目錄耗時卻不同的情況。
  下表展示了部分線上環境遷移案例:
Table 2: The online cluster performance.


Path
Files+Directories
Blocks
Time costs
/user/h_data_platform/platform/isource
1900000+
2523910
1166s
/user/h_data_platform/platform/b2cdc
1600000+
2461348
875s
/user/h_data_platform/platform/fintech
1400000+
1830326
697s



總結
  HFR實現了跨NameSpace的rename。它的速度很快,可以在秒級完成TB資料的rename。HDFS RPC的預設超時是60秒,所以小目錄上的HFR不會引起使用者端RPC超時。HFR在處理跨NameSpace均衡的時候也非常靈活高效,我們經常遇到這樣的問題:有的使用者希望遷移整體儘量快,為此可以接受一段時間的服務不可用;有的則不能接受,他們希望大路徑被拆成很多小路徑一點一點遷移過去,整體時間可以較長,但每一部分的遷移時間要非常短。Scheduler模型是靈活可插拔的,允許我們組合出不同的HFR作業型別來滿足不同需求。HFR的缺點是作業執行期間會禁寫,無法在所有情況下都對使用者透明。
  截止到文章撰寫時(2020年1月中旬),HFR在小米最大離線生產叢集已經工作了2個多月。我們用它來拯救不堪重負的NameNode,超過3100萬檔案被移動到了空閒的NameSpace,為壓力最大的NameNode釋放了10GB記憶體。
  後續我們對HFR的計劃是:


  • 將HFR整合到Router服務中,允許小目錄跨NameSpace rename。
  • HFR支援Hadoop3.1 EC(Erasure Code)編碼檔案。
  • 透過自動分析用量和接入歷史,結合管理員設定的閾值,實現更智慧的負載均衡。


  小米HDFS團隊有很棒的開源氛圍,一直非常積極地參與開源社群,貢獻了大量Patch。我們也在努力將HFR貢獻給社群,相關Jira:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559359/viewspace-2677535/,如需轉載,請註明出處,否則將追究法律責任。

相關文章