1. 程式人生 > >NameNode啟動過程詳細剖析 NameNode中幾個關鍵的資料結構 FSImage

NameNode啟動過程詳細剖析 NameNode中幾個關鍵的資料結構 FSImage

Namenode會將HDFS的檔案和目錄元資料儲存在一個叫fsimage的二進位制檔案中,每次儲存fsimage之後到下次儲存之間的所有hdfs操作,將會記錄在editlog檔案中,當editlog達到一定的大小(bytes,由fs.checkpoint.size引數定義)或從上次儲存過後一定時間段過後(sec,由fs.checkpoint.period引數定義),namenode會重新將記憶體中對整個HDFS的目錄樹和檔案元資料刷到fsimage檔案中。Namenode就是通過這種方式來保證HDFS中元資料資訊的安全性。

Fsimage是一個二進位制檔案,當中記錄了HDFS中所有檔案和目錄的元資料資訊,在我的hadoop的HDFS版中,該檔案的中儲存檔案和目錄的格式如下:

當namenode重啟載入fsimage時,就是按照如下格式協議從檔案流中載入元資料資訊。從fsimag的儲存格式可以看出,fsimage儲存有如下資訊:

1.         首先是一個image head,其中包含:

a)         imgVersion(int):當前image的版本資訊

b)        namespaceID(int):用來確保別的HDFS instance中的datanode不會誤連上當前NN。

c)         numFiles(long):整個檔案系統中包含有多少檔案和目錄

d)        genStamp(long):生成該image時的時間戳資訊。

2.         接下來便是對每個檔案或目錄的源資料資訊,如果是目錄,則包含以下資訊:

a)         path(String):該目錄的路徑,如”/user/build/build-index”

b)        replications(short):副本數(目錄雖然沒有副本,但這裡記錄的目錄副本數也為3)

c)         mtime(long):該目錄的修改時間的時間戳資訊

d)        atime(long):該目錄的訪問時間的時間戳資訊

e)         blocksize(long):目錄的blocksize都為0

f)         numBlocks(int):實際有多少個檔案塊,目錄的該值都為-1,表示該item為目錄

g)        nsQuota(long):namespace Quota值,若沒加Quota限制則為-1

h)        dsQuota(long):disk Quota值,若沒加限制則也為-1

i)          username(String):該目錄的所屬使用者名稱

j)          group(String):該目錄的所屬組

k)        permission(short):該目錄的permission資訊,如644等,有一個short來記錄。

3.         若從fsimage中讀到的item是一個檔案,則還會額外包含如下資訊:

a)         blockid(long):屬於該檔案的block的blockid,

b)        numBytes(long):該block的大小

c)         genStamp(long):該block的時間戳

當該檔案對應的numBlocks數不為1,而是大於1時,表示該檔案對應有多個block資訊,此時緊接在該fsimage之後的就會有多個blockid,numBytes和genStamp資訊。

因此,在namenode啟動時,就需要對fsimage按照如下格式進行順序的載入,以將fsimage中記錄的HDFS元資料資訊載入到記憶體中。

BlockMap

從以上fsimage中載入如namenode記憶體中的資訊中可以很明顯的看出,在fsimage中,並沒有記錄每一個block對應到哪幾個datanodes的對應表資訊,而只是儲存了所有的關於namespace的相關資訊。而真正每個block對應到datanodes列表的資訊在hadoop中並沒有進行持久化儲存,而是在所有datanode啟動時,每個datanode對本地磁碟進行掃描,將本datanode上儲存的block資訊彙報給namenode,namenode在接收到每個datanode的塊資訊彙報後,將接收到的塊資訊,以及其所在的datanode資訊等儲存在記憶體中。HDFS就是通過這種塊資訊彙報的方式來完成 block -> datanodes list的對應表構建。Datanode向namenode彙報塊資訊的過程叫做blockReport,而namenode將block -> datanodes list的對應表資訊儲存在一個叫BlocksMap的資料結構中。

BlocksMap的內部資料結構如下:   

              

如上圖顯示,BlocksMap實際上就是一個Block物件對BlockInfo物件的一個Map表,其中Block物件中只記錄了blockid,block大小以及時間戳資訊,這些資訊在fsimage中都有記錄。而BlockInfo是從Block物件繼承而來,因此除了Block物件中儲存的資訊外,還包括代表該block所屬的HDFS檔案的INodeFile物件引用以及該block所屬datanodes列表的資訊(即上圖中的DN1,DN2,DN3,該資料結構會在下文詳述)。

因此在namenode啟動並載入fsimage完成之後,實際上BlocksMap中的key,也就是Block物件都已經載入到BlocksMap中,每個key對應的value(BlockInfo)中,除了表示其所屬的datanodes列表的陣列為空外,其他資訊也都已經成功載入。所以可以說:fsimage載入完畢後,BlocksMap中僅缺少每個塊對應到其所屬的datanodes list的對應關係資訊。所缺這些資訊,就是通過上文提到的從各datanode接收blockReport來構建。當所有的datanode彙報給namenode的blockReport處理完畢後,BlocksMap整個結構也就構建完成。

BlockMap中datanode列表資料結構

在BlockInfo中,將該block所屬的datanodes列表儲存在一個Object[]陣列中,但該陣列不僅僅儲存了datanodes列表,還包含了額外的資訊。實際上該陣列儲存瞭如下資訊:

上圖表示一個block包含有三個副本,分別放置在DN1,DN2和DN3三個datanode上,每個datanode對應一個三元組,該三元組中的第二個元素,即上圖中prev block所指的是該block在該datanode上的前一個BlockInfo引用。第三個元素,也就是上圖中next Block所指的是該block在該datanode上的下一個BlockInfo引用。每個block有多少個副本,其對應的BlockInfo物件中就會有多少個這種三元組。

       Namenode採用這種結構來儲存block->datanode list的目的在於節約namenode記憶體。由於namenode將block->datanodes的對應關係儲存在了記憶體當中,隨著HDFS中檔案數的增加,block數也會相應的增加,namenode為了儲存block->datanodes的資訊已經耗費了相當多的記憶體,如果還像這種方式一樣的儲存datanode->block list的對應表,勢必耗費更多的記憶體,而且在實際應用中,要查一個datanode上儲存的block list的應用實際上非常的少,大部分情況下是要根據block來查datanode列表,所以namenode中通過上圖的方式來儲存block->datanode list的對應關係,當需要查詢datanode->block list的對應關係時,只需要沿著該資料結構中next Block的指向關係,就能得出結果,而又無需儲存datanode->block list在記憶體中。

NameNode啟動過程

fsimage載入過程

Fsimage載入過程完成的操作主要是為了:

1.         從fsimage中讀取該HDFS中儲存的每一個目錄和每一個檔案

2.         初始化每個目錄和檔案的元資料資訊

3.         根據目錄和檔案的路徑,構造出整個namespace在記憶體中的映象

4.         如果是檔案,則讀取出該檔案包含的所有blockid,並插入到BlocksMap中。

整個載入流程如下圖所示:

如上圖所示,namenode在載入fsimage過程其實非常簡單,就是從fsimage中不停的順序讀取檔案和目錄的元資料資訊,並在記憶體中構建整個namespace,同時將每個檔案對應的blockid儲存入BlocksMap中,此時BlocksMap中每個block對應的datanodes列表暫時為空。當fsimage載入完畢後,整個HDFS的目錄結構在記憶體中就已經初始化完畢,所缺的就是每個檔案對應的block對應的datanode列表資訊。這些資訊需要從datanode的blockReport中獲取,所以載入fsimage完畢後,namenode程序進入rpc等待狀態,等待所有的datanodes傳送blockReports。

blockReport階段

每個datanode在啟動時都會掃描其機器上對應儲存hdfs block的目錄下(dfs.data.dir)所儲存的所有檔案塊,然後通過namenode的rpc呼叫將這些block資訊以一個long陣列的方式傳送給namenode,namenode在接收到一個datanode的blockReport rpc呼叫後,從rpc中解析出block陣列,並將這些接收到的blocks插入到BlocksMap表中,由於此時BlocksMap缺少的僅僅是每個block對應的datanode資訊,而namenoe能從report中獲知當前report上來的是哪個datanode的塊資訊,所以,blockReport過程實際上就是namenode在接收到塊資訊彙報後,填充BlocksMap中每個block對應的datanodes列表的三元組資訊的過程。其流程如下圖所示:

當所有的datanode彙報完block,namenode針對每個datanode的彙報進行過處理後,namenode的啟動過程到此結束。此時BlocksMap中block->datanodes的對應關係已經初始化完畢。如果此時已經達到安全模式的推出閾值,則hdfs主動退出安全模式,開始提供服務。

對namenode的整個啟動過程有了詳細瞭解之後,就可以對其啟動過程中各階段各函式的呼叫耗時進行profiling的採集,資料的profiling仍然分為兩個階段,即fsimage載入階段和blockReport階段。

fsimage載入階段效能資料採集和瓶頸分析

以下是對建庫叢集真實的fsimage載入過程的的效能採集資料:

從上圖可以看出,fsimage的載入過程那個中,主要耗時的操作分別分佈在FSDirectory.addToParentFSImage.readString,以及PermissionStatus.read三個操作,這三個操作分別佔用了載入過程的73%,15%以及8%,加起來總共消耗了整個載入過程的96%。而其中FSImage.readStringPermissionStatus.read操作都是從fsimage的檔案流中讀取資料(分別是讀取String和short)的操作,這種操作優化的空間不大,但是通過調整該檔案流的Buffer大小來提高少許效能。而FSDirectory.addToParent的呼叫卻佔用了整個載入過程的73%,所以該呼叫中的優化空間比較大。

       以下是addToParent呼叫中的profiling資料:

從以上資料可以看出addToParent呼叫佔用的73%的耗時中,有66%都耗在了INode.getPathComponents呼叫上,而這66%分別有36%消耗在INode.getPathNames呼叫,30%消耗在INode.getPathComponents呼叫。這兩個耗時操作的具體分佈如以下資料所示:

可以看出,消耗了36%的處理時間的INode.getPathNames操作,全部都是在通過String.split函式呼叫來對檔案或目錄路徑進行切分。另外消耗了30%左右的處理時間在INode.getPathComponents中,該函式中最終耗時都耗在獲取字串的byte陣列的java原生操作中。

blockReport階段效能資料採集和瓶頸分析

由於blockReport的呼叫是通過datanode呼叫namenode的rpc呼叫,所以在namenode進入到等待blockreport階段後,會分別開啟rpc呼叫的監聽執行緒和rpc呼叫的處理執行緒。其中rpc處理和rpc鑑定的呼叫耗時分佈如下圖所示:

而其中rpc的監聽執行緒的優化是另外一個話題,在其他的issue中再詳細討論,且由於blockReport的操作實際上是觸發的rpc處理執行緒,所以這裡只關心rpc處理執行緒的效能資料。

       在namenode處理blockReport過程中的呼叫耗時效能資料如下:

可以看出,在namenode啟動階段,處理從各個datanode彙報上來的blockReport耗費了整個rpc處理過程中的絕大部分時間(48/49),blockReport處理邏輯中的耗時分佈如下圖:

從上圖資料中可以發現,blockReport階段中耗時分佈主要耗時在FSNamesystem.addStoredBlock呼叫以及DatanodeDescriptor.reportDiff過程中,分別耗時37/48和10/48,其中FSNamesystem.addStoredBlock所進行的操作時對每一個彙報上來的block,將其於彙報上來的datanode的對應關係初始化到namenode記憶體中的BlocksMap表中。所以對於每一個block就會呼叫一次該方法。所以可以看到該方法在整個過程中呼叫了774819次,而另一個耗時的操作,即DatanodeDescriptor.reportDiff,該操作的過程在上文中有詳細介紹,主要是為了將該datanode彙報上來的blocks跟namenode記憶體中的BlocksMap中進行對比,以決定那個哪些是需要新增到BlocksMap中的block,哪些是需要新增到toRemove佇列中的block,以及哪些是新增到toValidate佇列中的block。由於這個操作需要針對每一個彙報上來的block去查詢BlocksMap,以及namenode中的其他幾個map,所以該過程也非常的耗時。而且從呼叫次數上可以看出,reportDiff呼叫在啟動過程中僅呼叫了14次(有14個datanode進行塊彙報),卻耗費了10/48的時間。所以reportDiff也是整個blockReport過程中非常耗時的瓶頸所在。

       同時可以看到,出了reportDiff,addStoredBlock的呼叫耗費了37%的時間,也就是耗費了整個blockReport時間的37/48,該方法的呼叫目的是為了將從datanode彙報上來的每一個block插入到BlocksMap中的操作。從該方法呼叫的執行資料如下圖所示:

從上圖可以看出,addStoredBlock中,主要耗時的兩個階段分別是FSNamesystem.countNode和DatanodeDescriptor.addBlock,後者是java中的插表操作,而FSNamesystem.countNode呼叫的目的是為了統計在BlocksMap中,每一個block對應的各副本中,有幾個是live狀態,幾個是decommission狀態,幾個是Corrupt狀態。而在namenode的啟動初始化階段,用來儲存corrput狀態和decommission狀態的block的map都還是空狀態,並且程式邏輯中要得到的僅僅是出於live狀態的block數,所以,這裡的countNoes呼叫在namenode啟動初始化階段並無需統計每個block對應的副本中的corrrput數和decommission數,而僅僅需要統計live狀態的block副本數即可,這樣countNodes能夠在namenode啟動階段變得更輕量,以節省啟動時間。

2.3 瓶頸分析總結

從profiling資料和瓶頸分歧情況來看,fsimage載入階段的瓶頸除了在分切路徑的過程中不夠優以外,其他耗時的地方几乎都是在java原生介面的呼叫中,如從位元組流讀資料,以及從String物件中獲取byte[]陣列的操作。

       而blockReport階段的耗時其實很大的原因是跟當前的namenode設計以及記憶體結構有關,比較明顯的不優之處就是在namenode啟動階段的countNode和reportDiff的必要性,這兩處在namenode初始化時的blockReport階段有一些不必要的操作浪費了時間。可以針對namenode啟動階段將必要的操作抽取出來,定製成namenode啟動階段才呼叫的方式,以優化namenode啟動效能。