HDFS NameNode內存全景
一、概述
從整個HDFS系統架構上看,NameNode是其中最重要、最復雜也是最容易出現問題的地方,而且一旦NameNode出現故障,整個Hadoop集群就將處于不可服務的狀態,同時隨著數據規模和集群規模地持續增長,很多小量級時被隱藏的問題逐漸暴露出來。所以,從更高層次掌握NameNode的內部結構和運行機制尤其重要。除特別說明外,本文基于社區版本Hadoop-2.4.1[1][2],雖然2.4.1之后已經有多次版本迭代,但是基本原理相同。
NameNode管理著整個HDFS文件系統的元數據。從架構設計上看,元數據大致分成兩個層次:Namespace管理層,負責管理文件系統中的樹狀目錄結構以及文件與數據塊的映射關系;塊管理層,負責管理文件系統中文件的物理塊與實際存儲位置的映射關系BlocksMap,如圖1所示[1]。Namespace管理的元數據除內存常駐外,也會周期Flush到持久化設備上FsImage文件;BlocksMap元數據只在內存中存在;當NameNode發生重啟,首先從持久化設備中讀取FsImage構建Namespace,之后根據DataNode的匯報信息重新構造BlocksMap。這兩部分數據結構是占據了NameNode大部分JVM Heap空間。
圖1 HDFS結構圖
除了對文件系統本身元數據的管理之外,NameNode還需要維護整個集群的機架及DataNode的信息、Lease管理以及集中式緩存引入的緩存管理等等。這幾部分數據結構空間占用相對固定,且占用較小。
測試數據顯示,Namespace目錄和文件總量到2億,數據塊總量到3億后,常駐內存使用量超過90GB。
二、內存全景
如前述,NameNode整個內存結構大致可以分成四大部分:Namespace、BlocksMap、NetworkTopology及其它,圖2為各數據結構內存邏輯分布圖示。
圖2 NameNode內存全景圖
Namespace:維護整個文件系統的目錄樹結構及目錄樹上的狀態變化;
BlockManager:維護整個文件系統中與數據塊相關的信息及數據塊的狀態變化;
NetworkTopology:維護機架拓撲及DataNode信息,機架感知的基礎;
其它:
LeaseManager:讀寫的互斥同步就是靠Lease實現,支持HDFS的Write-Once-Read-Many的核心數據結構;
CacheManager:Hadoop 2.3.0引入的集中式緩存新特性,支持集中式緩存的管理,實現memory-locality提升讀性能;
SnapshotManager:Hadoop 2.1.0引入的Snapshot新特性,用于數據備份、回滾,以防止因用戶誤操作導致集群出現數據問題;
DelegationTokenSecretManager:管理HDFS的安全訪問;
另外還有臨時數據信息、統計信息metrics等等。
NameNode常駐內存主要被Namespace和BlockManager使用,二者使用占比分別接近50%。其它部分內存開銷較小且相對固定,與Namespace和BlockManager相比基本可以忽略。
三、內存分析
3.1 Namespace
與單機文件系統相似,HDFS對文件系統的目錄結構也是按照樹狀結構維護,Namespace保存了目錄樹及每個目錄/文件節點的屬性。除在內存常駐外,這部分數據會定期flush到持久化設備上,生成一個新的FsImage文件,方便NameNode發生重啟時,從FsImage及時恢復整個Namespace。圖3所示為Namespace內存結構。前述集群中目錄和文件總量即整個Namespace目錄樹中包含的節點總數,可見Namespace本身其實是一棵非常巨大的樹。
圖3 Namespace內存結構
在整個Namespace目錄樹中存在兩種不同類型的INode數據結構:INodeDirectory和INodeFile。其中INodeDirectory標識的是目錄樹中的目錄,INodeFile標識的是目錄樹中的文件。由于二者均繼承自INode,所以具備大部分相同的公共信息INodeWithAdditionalFields,除常用基礎屬性外,其中還提供了擴展屬性features,如Quota、Snapshot等均通過Feature增加,如果以后出現新屬性也可通過Feature方便擴展。不同的是,INodeFile特有的標識副本數和數據塊大小組合的header(2.6.1之后又新增了標識存儲策略ID的信息)及該文件包含的有序Blocks數組;INodeDirectory則特有子節點的列表children。這里需要特別說明children是默認大小為5的ArrayList,按照子節點name有序存儲,雖然在插入時會損失一部分寫性能,但是可以方便后續快速二分查找提高讀性能,對一般存儲系統,讀操作比寫操作占比要高。具體的繼承關系見圖4所示。
圖4 INode繼承關系
3.2 BlockManager
BlocksMap在NameNode內存空間占據很大比例,由BlockManager統一管理,相比Namespace,BlockManager管理的這部分數據要復雜的多。Namespace與BlockManager之間通過前面提到的INodeFile有序Blocks數組關聯到一起。圖5所示BlockManager管理的內存結構。
圖5 BlockManager管理的內存結構
每一個INodeFile都會包含數量不等的Block,具體數量由文件大小及每一個Block大小(默認為64M)比值決定,這些Block按照所在文件的先后順序組成BlockInfo數組,如圖5所示的BlockInfo[A~K],BlockInfo維護的是Block的元數據,結構如圖6所示,數據本身是由DataNode管理,所以BlockInfo需要包含實際數據到底由哪些DataNode管理的信息,這里的核心是名為triplets的Object數組,大小為3*replicas,其中replicas是Block副本數量。triplets包含的信息:
- triplets[i]:Block所在的DataNode;
- triplets[i+1]:該DataNode上前一個Block;
- triplets[i+2]:該DataNode上后一個Block;
其中i表示的是Block的第i個副本,i取值[0,replicas)。
圖6 BlockInfo繼承關系
從前面描述可以看到BlockInfo幾塊重要信息:文件包含了哪些Block,這些Block分別被實際存儲在哪些DataNode上,DataNode上所有Block前后鏈表關系。
如果從信息完整度來看,以上數據足夠支持所有關于HDFS文件系統的正常操作,但還存在一個使用場景較多的問題:不能通過blockid快速定位Block,所以引入了BlocksMap。
BlocksMap底層通過LightWeightGSet實現,本質是一個鏈式解決沖突的哈希表。為了避免rehash過程帶來的性能開銷,初始化時,索引空間直接給到了整個JVM可用內存的2%,并且不再變化。集群啟動過程,DataNode會進行BR(BlockReport),根據BR的每一個Block計算其HashCode,之后將對應的BlockInfo插入到相應位置逐漸構建起來巨大的BlocksMap。前面在INodeFile里也提到的BlockInfo集合,如果我們將BlocksMap里的BlockInfo與所有INodeFile里的BlockInfo分別收集起來,可以發現兩個集合完全相同,事實上BlocksMap里所有的BlockInfo就是INodeFile中對應BlockInfo的引用;通過Block查找對應BlockInfo時,也是先對Block計算HashCode,根據結果快速定位到對應的BlockInfo信息。至此涉及到HDFS文件系統本身元數據的問題基本上已經解決了。
前面提到部分都屬于靜態數據部分,NameNode內存中所有數據都要隨讀寫情況發生變化,BlockManager當然也需要管理這部分動態數據。主要是當Block發生變化不符合預期時需要及時調整Blocks的分布。這里涉及幾個核心的數據結構:
excessReplicateMap:若某個Block實際存儲的副本數多于預設副本數,這時候需要刪除多余副本,這里多余副本會被置于excessReplicateMap中。excessReplicateMap是從DataNode的StorageID到Block集合的映射集。
neededReplications:若某個Block實際存儲的副本數少于預設副本數,這時候需要補充缺少副本,這里哪些Block缺少多少個副本都統一存在neededReplications里,本質上neededReplications是一個優先級隊列,缺少副本數越多的Block之后越會被優先處理。
invalidateBlocks:若某個Block即將被刪除,會被置于invalidateBlocks中。invalidateBlocks是從DataNode的StorageID到Block集合的映射集。如某個文件被客戶端執行了刪除操作,該文件所屬的所有Block會先被置于invalidateBlocks中。
corruptReplicas:有些場景Block由于時間戳/長度不匹配等等造成Block不可用,會被暫存在corruptReplicas中,之后再做處理。
前面幾個涉及到Block分布情況動態變化的核心數據結構,這里的數據實際上是過渡性質的,BlockManager內部的ReplicationMonitor線程(圖5標識Thread/Monitor)會持續從其中取出數據并通過邏輯處理后分發給具體的DatanodeDescriptor對應數據結構(3.3 NetworkTopology里會有簡單介紹),當對應DataNode的心跳過來之后,NameNode會遍歷DatanodeDescriptor里暫存的數據,將其轉換成對應指令返回給DataNode,DataNode收到任務并執行完成后再反饋回NameNode,之后DatanodeDescriptor里對應信息被清除。如BlockB預設副本數為3,由于某種原因實際副本變成4(如之前下線的DataNode D重新上線,其中B正好有BlockB的一個副本數據),BlockManager能及時發現副本變化,并將多余的DataNode D上BlockB副本放置到excessReplicateMap中,ReplicationMonitor線程定期檢查時發現excessReplicateMap中數據后將其移到DataNode D對應DatanodeDescriptor中invalidateBlocks里,當DataNode D下次心跳過來后,隨心跳返回刪除Block B的指令,DataNode D收到指令實際刪除其上的Block B數據并反饋回NameNode,此后BlockManager將DataNode D上的Block B從內存中清除,至此Block B的副本符合預期,整個流程如圖7所示。
圖7 副本數異常時處理過程
3.3 NetworkTopology
前面多次提到Block與DataNode之間的關聯關系,事實上NameNode確實還需要管理所有DataNode,不僅如此,由于數據寫入前需要確定數據塊寫入位置,NameNode還維護著整個機架拓撲NetworkTopology。圖8所示內存中機架拓撲圖。
圖8 NetworkTopology內存結構
從圖8可以看出這里包含兩個部分:機架拓撲結構NetworkTopology和DataNode節點信息。其中樹狀的機架拓撲是根據機架感知(一般都是外部腳本計算得到)在集群啟動完成后建立起來,整個機架的拓撲結構在NameNode的生命周期內一般不會發生變化;另一部分是比較關鍵的DataNode信息,BlockManager已經提到每一個DataNode上的Blocks集合都會形成一個雙向鏈表,更準確的應該是DataNode的每一個存儲單元DatanodeStorageInfo上的所有Blocks集合會形成一個雙向鏈表,這個鏈表的入口就是機架拓撲結構葉子節點即DataNode管理的DatanodeStorageInfo。此外由于上層應用對數據的增刪查隨時發生變化,隨之DatanodeStorageInfo上的Blocks也會動態變化,所以NetworkTopology上的DataNode對象還會管理這些動態變化的數據結構,如replicateBlocks/recoverBlocks/invalidateBlocks,這些數據結構正好和BlockManager管理的動態數據結構對應,實現了數據的動態變化由BlockManager傳達到DataNode內存對象最后通過指令下達到物理DataNode實際執行的流動過程,流程在3.2 BlockManager已經介紹。
這里存在一個問題,為什么DatanodeStorageInfo下所有Block之間會以雙向鏈表組織,而不是其它數據結構?如果結合實際場景就不難發現,對每一個DatanodeStorageInfo下Block的操作集中在快速增加/刪除(Block動態增減變化)及順序遍歷(BlockReport期間),所以雙向鏈表是非常合適的數據結構。
3.4 LeaseManager
Lease 機制是重要的分布式協議,廣泛應用于各種實際的分布式系統中。HDFS支持Write-Once-Read-Many,對文件寫操作的互斥同步靠Lease實現。Lease實際上是時間約束鎖,其主要特點是排他性。客戶端寫文件時需要先申請一個Lease,一旦有客戶端持有了某個文件的Lease,其它客戶端就不可能再申請到該文件的Lease,這就保證了同一時刻對一個文件的寫操作只能發生在一個客戶端。NameNode的LeaseManager是Lease機制的核心,維護了文件與Lease、客戶端與Lease的對應關系,這類信息會隨寫數據的變化實時發生對應改變。
圖9 LeaseManager的內存數據結構
圖9所示為LeaseManager內存結構,包括以下三個主要核心數據結構:
sortedLeases:Lease集合,按照時間先后有序組織,便于檢查Lease是否超時;
leases:客戶端到Lease的映射關系;
sortedLeasesByPath:文件路徑到Lease的映射關系;
其中每一個寫數據的客戶端會對應一個Lease,每個Lease里包含至少一個標識文件路徑的Path。Lease本身已經維護了其持有者(客戶端)及該Lease正在操作的文件路徑集合,之所以增加了leases和sortedLeasesByPath為提高通過Lease持有者或文件路徑快速索引到Lease的性能。
由于Lease本身的時間約束特性,當Lease發生超時后需要強制回收,內存中與該Lease相關的內容要被及時清除。超時檢查及超時后的處理邏輯由LeaseManager.Monitor統一執行。LeaseManager中維護了兩個與Lease相關的超時時間:軟超時(softLimit)和硬超時(hardLimit),使用場景稍有不同。
正常情況下,客戶端向集群寫文件前需要向NameNode的LeaseManager申請Lease;寫文件過程中定期更新Lease時間,以防Lease過期,周期與softLimit相關;寫完數據后申請釋放Lease。整個過程可能發生兩類問題:(1)寫文件過程中客戶端沒有及時更新Lease時間;(2)寫完文件后沒有成功釋放Lease。兩個問題分別對應為softLimit和hardLimit。兩種場景都會觸發LeaseManager對Lease超時強制回收。如果客戶端寫文件過程中沒有及時更新Lease超過softLimit時間后,另一客戶端嘗試對同一文件進行寫操作時觸發Lease軟超時強制回收;如果客戶端寫文件完成但是沒有成功釋放Lease,則會由LeaseManager的后臺線程LeaseManager.Monitor檢查是否硬超時后統一觸發超時回收。不管是softLimit還是hardLimit超時觸發的強制Lease回收,處理邏輯都一樣:FSNamesystem.internalReleaseLease,邏輯本身比較復雜,這里不再展開,簡單的說先對Lease過期前最后一次寫入的Block進行檢查和修復,之后釋放超時持有的Lease,保證后面其它客戶端的寫入能夠正常申請到該文件的Lease。
NameNode內存數據結構非常豐富,這里對幾個重要的數據結構進行了簡單的描述,除了前面羅列之外,其實還有如SnapShotManager/CacheManager等,由于其內存占用有限且有一些特性還尚未穩定,這里不再展開。
四、問題
隨著集群中數據規模的不斷積累,NameNode內存占用隨之成比例增長。不可避免的NameNode內存將逐漸成為集群發展的瓶頸,并開始暴漏諸多問題。
1、啟動時間變長。NameNode的啟動過程可以分成FsImage數據加載、editlogs回放、Checkpoint、DataNode的BlockReport幾個階段。數據規模較小時,啟動時間可以控制在~10min以內,當元數據規模達到5億(Namespace中INode數超過2億,Block數接近3億),FsImage文件大小將接近到20GB,加載FsImage數據就需要~14min,Checkpoint需要~6min,再加上其它階段整個重啟過程將持續~50min,極端情況甚至超過60min,雖然經過多輪優化重啟過程已經能夠穩定在~30min,但也非常耗時。如果數據規模繼續增加,啟動過程將同步增加。
2、性能開始下降。HDFS文件系統的所有元數據相關操作基本上均在NameNode端完成,當數據規模的增加致內存占用變大后,元數據的增刪改查性能會出現下降,且這種下降趨勢會因規模效應及復雜的處理邏輯被放大,相對復雜的RPC請求(如addblock)性能下降更加明顯。
3、NameNode JVM FGC(Full GC)風險較高。主要體現在兩個方面:(1)FGC頻率增加;(2)FGC時間增加且風險不可控。針對NameNode的應用場景,目前看CMS內存回收算法比較主流,正常情況下,對超過100GB內存進行回收處理時,可以控制到秒級別的停頓時間,但是如果回收失敗被降級到串行內存回收時,應用的停頓時間將達到數百秒,這對應用本身是致命的。
4、超大JVM Heap Size調試問題。如果線上集群性能表現變差,不得不通過分析內存才能得到結論時,會成為一件異常困難的事情。且不說Dump本身極其費時費力,Dump超大內存時存在極大概率使NameNode不可服務。
針對NameNode內存增長帶來的諸多問題,社區和業界都在持續關注并嘗試不同的解決方案。整體上兩個思路:(1)擴展NameNode分散單點負載;(2)引入外部系統支持NameNode內存數據。
從2010年開始社區就投入大量精力持續解決,Federation方案[3]通過對NameNode進行水平擴展分散單點負載的方式解決NameNode的問題,經過幾年的發展該方案逐漸穩定,目前已經被業界廣泛使用。除此之外,社區也在嘗試將Namespace存儲值外部的KV存儲系統如LevelDB[4],從而降低NameNode內存負載。
除社區外,業界也在嘗試自己的解決方案。Baidu HDFS2[5]將元數據管理通過主從架構的集群形式提供服務,本質上是將原生NameNode管理的Namespace和BlockManagement進行物理拆分。其中Namespace負責管理整個文件系統的目錄樹及文件到BlockID集合的映射關系,BlockID到DataNode的映射關系是按照一定的規則分到多個服務節點分布式管理,這種方案與Lustre有相似之處(Hash-based Partition)。Taobao HDFS2[6]嘗試過采用另外的思路,借助高速存儲設備,將元數據通過外存設備進行持久化存儲,保持NameNode完全無狀態,實現NameNode無限擴展的可能。其它類似的諸多方案不一而足。
盡管社區和業界均對NameNode內存瓶頸有成熟的解決方案,但是不一定適用所有的場景,尤其是中小規模集群。結合實踐過程和集群規模發展期可能遇到的NameNode內存相關問題這里有幾點建議:
-
合并小文件。正如前面提到,目錄/文件和Block均會占用NameNode內存空間,大量小文件會降低內存使用效率;另外,小文件的讀寫性能遠遠低于大文件的讀寫,主要原因對小文件讀寫需要在多個數據源切換,嚴重影響性能。
-
調整合適的BlockSize。主要針對集群內文件較大的業務場景,可以通過調整默認的Block Size大小(參數:dfs.blocksize,默認128M),降低NameNode的內存增長趨勢。
-
HDFS Federation方案。當集群和數據均達到一定規模時,僅通過垂直擴展NameNode已不能很好的支持業務發展,可以考慮HDFS Federation方案實現對NameNode的水平擴展,在解決NameNode的內存問題的同時通過Federation可以達到良好的隔離性,不會因為單一應用壓垮整集群。
五、總結
NameNode在整個HDFS系統架構中占據舉足輕重的位置,內部數據和處理邏輯相對復雜,本文簡單梳理了NameNode的內存全景及對其中幾個關鍵數據結構,從NameNode內存核心數據視角對NameNode進行了簡單的解讀,并結合實際場景介紹了隨著數據規模的增加,NameNode內存可能遇到的問題及業界各種可借鑒的解決方案
六、參考
[1] Apache Hadoop, 2016, https://hadoop.apache.org/ .
[2] Apache Hadoop Source Code, 2014, https://github.com/apache/hadoop/tree/branch-2.4.1/ .
[3] HDFS Federation, 2011, https://issues.apache.org/jira/browse/HDFS-1052 .
[4] NemeNode Scalability, 2013, https://issues.apache.org/jira/browse/HDFS-5389 .
[5] Baidu HDFS2, 2013, http://static.zhizuzhefu.com/wordpress_cp/uploads/2013/04/a9.pdf .
[6] Taobao HDFS2, 2012, https://github.com/taobao/ADFS .
來自:http://tech.meituan.com/namenode.html