Hadoop源代碼分析(完整圖文版) part 2

Hadoop源代碼分析(一八)

DataNode的介紹基本告一段落。我們開始來分析NameNode。相比於DataNode,NameNode比較複雜。系統中只有一個NameNode,作爲系統文件目錄的管理者和“inode表”(熟悉UNIX的同學們應該瞭解inode)。爲了高可用性,系統中還存在着從NameNode。

先前我們分析DataNode的時候,關注的是數據塊。NameNode作爲HDFS中文件目錄和文件分配的管理者,它保存的最重要信息,就是下面兩個映射:

文件名à數據塊

數據塊àDataNode列表

其中,文件名à數據塊保存在磁盤上(持久化);但NameNode上不保存數據塊àDataNode列表,該列表是通過DataNode上報建立起來的。

下圖包含了NameNode和DataNode往外暴露的接口,其中,DataNode實現了InterDatanodeProtocol和ClientDatanodeProtocol,剩下的,由NameNode實現。

 

 

ClientProtocol提供給客戶端,用於訪問NameNode。它包含了文件角度上的HDFS功能。和GFS一樣,HDFS不提供POSIX形式的接口,而是使用了一個私有接口。一般來說,程序員通過org.apache.hadoop.fs.FileSystem來和HDFS打交道,不需要直接使用該接口。

DatanodeProtocol:用於DataNode向NameNode通信,我們已經在DataNode的分析過程中,瞭解部分接口,包括:register,用於DataNode註冊;sendHeartbeat/blockReport/blockReceived,用於DataNode的offerService方法中;errorReport我們沒有討論,它用於向NameNode報告一個錯誤的Block,用於BlockReceiver和DataBlockScanner;nextGenerationStamp和commitBlockSynchronization用於lease管理,我們在後面討論到lease時,會統一說明。

NamenodeProtocol用於從NameNode到NameNode的通信。

下圖補充了接口裏使用的數據的關係。


Hadoop源代碼分析(一九)

我們先分析INode*.java,類INode*抽象了文件層次結構。如果我們對文件系統進行面向對象的抽象,一定會得到和下面一樣類似的結構圖(類INode*):

 

INode是一個抽象類,它的兩個字類,分別對應着目錄(INodeDirectory)和文件(INodeFile)。INodeDirectoryWithQuota,如它的名字隱含的,是帶了容量限制的目錄。INodeFileUnderConstruction,抽象了正在構造的文件,當我們需要在HDFS中創建文件的時候,由於創建過程比較長,目錄系統會維護對應的信息。

INode中的成員變量有:name,目錄/文件名;modificationTime和accessTime是最後的修改時間和訪問時間;parent指向了父目錄;permission是訪問權限。HDFS採用了和UNIX/Linux類似的訪問控制機制。系統維護了一個類似於UNIX系統的組表(group)和用戶表(user),並給每一個組和用戶一個ID,permission在INode中是long型,它同時包含了組和用戶信息。

INode中存在大量的get和set方法,當然是對上面提到的屬性的操作。導出屬性,比較重要的有:collectSubtreeBlocksAndClear,用於收集這個INode所有後繼中的Block;computeContentSummary用於遞歸計算INode包含的一些相關信息,如文件數,目錄數,佔用磁盤空間。

INodeDirectory是HDFS管理的目錄的抽象,它最重要的成員變量是:

private List<INode> children;

就是這個目錄下的所有目錄/文件集合。INodeDirectory也是有大量的get和set方法,都很簡單。INodeDirectoryWithQuota進一步加強了INodeDirectory,限制了INodeDirectory可以使用的空間(包括NameSpace和磁盤空間)。

INodeFile是HDFS中的文件,最重要的成員變量是:

protected BlockInfo blocks[] = null;

這是這個文件對應的Block列表,BlockInfo增強了Block類。

INodeFileUnderConstruction保存了正在構造的文件的一些信息,包括clientName,這是目前擁有租約的節點名(創建文件時,只有一個節點擁有租約,其他節點配合這個節點工作)。clientMachine是構造該文件的客戶端名稱,如果構造請求由DataNode發起,clientNode會保持相應的信息,targets保存了配合構造文件的所有節點。

上面描述了INode*類的關係。下面我們順便考察一下一些NameNode上的數據類。

BlocksMap保存了Block和它在NameNode上一些相關的信息。其核心是一個map:Map<Block, BlockInfo>。BlockInfo擴展了Block,保存了該Block歸屬的INodeFile和DatanodeDescriptor,同時還包括了它的前繼和後繼Block。有了BlocksMap,就可以通過Block找對應的文件和這個Block存放的DataNode的相關信息。

 

接下來我們來分析類Datanode*。DatanodeInfo和DatanodeID都定義在包org.apache.hadoop.hdfs.protocol。DatanodeDescriptor是DatanodeInfo的子類,包含了NameNode需要的附加信息。DatanodeID只包含了一些配置信息,DatanodeInfo增加了一些動態信息,DatanodeDescriptor更進一步,包含了DataNode上一些Block的動態信息。DatanodeDescriptor包含了內部類BlockTargetPair,它保存Block和對應DatanodeDescriptor的關聯,BlockQueue是BlockTargetPair隊列。

DatanodeDescriptor包含了兩個BlockQueue,分別記錄了該DataNode上正在複製(replicateBlocks)和Lease恢復(recoverBlocks)的Block。同時還有一個Block集合,保存的是該DataNode上已經失效的Block。DatanodeDescriptor提供一系列方法,用於操作上面保存的隊列和集合。也提供get*Command方法,用於生成發送到DataNode的命令。

當NameNode收到DataNode對現在管理的Block狀態的彙報是,會調用reportDiff,找出和現在NameNode上的信息差別,以供後續處理用。

readFieldsFromFSEditLog方法用於從日誌中恢復DatanodeDescriptor。

Hadoop源代碼分析(二零)

前面我們提過關係:文件名à數據塊持久化在磁盤上,所有對目錄樹的更新和文件名à數據塊關係的修改,都必須能夠持久化。爲了保證每次修改不需要從新保存整個結構,HDFS使用操作日誌,保存更新。

現在我們可以得到NameNode需要存儲在Disk上的信息了,包括:

[hadoop@localhost dfs]$ ls -R name
name:
current  image  in_use.lock

name/current:
edits  fsimage  fstime  VERSION

name/image:
fsimage

in_use.lock的功能和DataNode的一致。fsimage保存的是文件系統的目錄樹,edits則是文件樹上的操作日誌,fstime是上一次新打開一個操作日誌的時間(long型)。

image/fsimage是一個保護文件,防止0.13以前的版本啓動(0.13以前版本將fsimage存放在name/image目錄下,如果用0.13版本啓動,顯然在讀fsimage會出錯J)。

我們可以開始討論FSImage了,類FSImage如下圖:

 

分析FSImage,不免要跟DataStorage去做比較(上圖也保留了類DataStorage)。前面我們已經分析過DataStorage的狀態變化,包括升級/回滾/提交,FSImage也有類似的升級/回滾/提交動作,而且這部分的行爲和DataStorage是比較一致,如下狀態轉移圖。圖中update方法和DataStorage的差別比較大,是因爲處理數據庫和處理文件系統名字空間不一樣,其他的地方都比較一致。FSImage也能夠管理多個Storage,而且還能夠區分Storage爲IMAGE(目錄結構)/EDITS(日誌)/IMAGE_AND_EDITS(前面兩種的組合)。

 

我們可以看到,FSImage和DataStorage都有recoverTransitionRead方法。FSImage的recoverTransitionRead方法主要步驟是檢查系統一致性(analyzeStorage)並嘗試恢復,初始化新的storage,然後根據啓動NameNode的參數,做升級/回滾等操作。

FSImage需要支持參數-importCheckpoint,該參數用於在某一個checkpoint目錄里加載HDFS的目錄信息,並更新到當前系統,該參數的主要功能在方法doImportCheckpoint中。該方法很簡單,通過讀取配置的checkpoint目錄來加載fsimage文件和日誌文件,然後利用saveFSImage(下面討論)保存到當前的工作目錄,完成導入。

loadFSImage(File curFile)用於在fsimage中讀入NameNode持久化的信息,是FSImage中最重要的方法之一,該文件的結構如下:

 

最開始是版本號(注意,各版本文件佈局不一樣,文中分析的樣本是0.17的),然後是命名空間的ID號,文件個數和最高文件版本號(就是說,下一次產生文件版本號的初始值)。接下來就是文件的信息啦,首先是文件名,然後是該文件的副本數,接下來是修改時間/訪問時間,數據塊大小,數據塊數目。數據塊數目如果大於0,表明這是個文件,那麼接下來就是numBlocks個數據塊(淺藍),如果數據塊數目等於0,那該條目是目錄,接下來是應用於該目錄的quota。最後是訪問控制的一些信息。文件信息一共有numFiles個,接下來是處於構造狀態的文件的信息。(有些版本可能還會保留DataNode的信息,但0.17已經不保存這樣的信息啦)。loadFSImage(File curFile)的對應方法是saveFSImage(File newFile),FSImage中還有一系列的方法(大概7,8個)用於配合這兩個方法工作,我們就不再深入討論了。

loadFSEdits(StorageDirectory sd)用於加載日誌文件,並把日誌文件記錄的內容應用到NameNode,loadFSEdits只是簡單地調用FSEditLog中對應的方法。

loadFSImage()和saveFSImage()是另外一對重要的方法。

loadFSImage()會在所有的Storage中,讀取最新的NameNode持久化信息,並應用相應的日誌,當loadFSImage()調用返回以後,內存中的目錄樹就是最新的。loadFSImage()會返回一個標記,如果Storage中有任何和內存中最終目錄樹中不一致的Image(最常見的情況是日誌文件不爲空,那麼,內存中的Image應該是Storage的Image加上日誌,當然還有其它情況),那麼,該標記爲true。

saveFSImage()的功能正好相反,它將內存中的目錄樹持久化,很自然,目錄樹持久化後就可以把日誌清空。saveFSImage()會創建edits.new,並把當前內存中的目錄樹持久化到fsimage.ckpt(fsimage現在還存在),然後重新打開日誌文件edits和edits.new,這會導致日誌文件edits和edits.new被清空。最後,saveFSImage()調用rollFSImage()方法。

rollFSImage()上來就把所有的edits.new都改爲edits(經過了方法saveFSImage,它們都已經爲空),然後再把fsimage.ckpt改爲fsimage。如下圖:

 

爲了防止誤調用rollFSImage(),系統引入了狀態CheckpointStates.UPLOAD_DONE。

有了上面的狀態轉移圖,我們就很好理解方法recoverInterruptedCheckpoint了。

 

圖中存在另一條路徑,應用於GetImageServlet中。GetImageServlet是和從NameNode進行文件通信的接口,這個場景留到我們分析從NameNode時再進行分析。

 

最後我們分析一下和檢查點相關的一個類,rollFSImage()會返回這個類的一個實例。CheckpointSignature用於標識一個日誌的檢查點,它是StorageInfo的子類,同時實現了WritableComparable接口,出了StorageInfo的信息,它還包括了兩個屬性:editsTime和checkpointTime。editsTime是日誌的最後修改時間,checkpointTime是日誌建立時間。在和從NameNode節點的通信中,需要用CheckpointSignature,來保證從NameNode獲得的日誌是最新的。

Hadoop源代碼分析(二一)

不好意思,突然間需要忙項目的其他事情了,更新有點慢下來,爭取月底搞定HDFS吧。

我們來分析FSEditLog.java,該類提供了NameNode操作日誌和日誌文件的相關方法,相關類圖如下:

 

首先是FSEditLog依賴的輸入/輸出流。輸入流基本上沒有新添加功能;輸出流在打開的時候,會寫入日誌的版本號(最前面的4字節),同時,每次將內存刷到硬盤時,會爲日誌尾部寫入一個特殊的標識(OP_INVALID)。

FSEditLog有打開/關閉的方法,它們都是很簡單的方法,就是關閉的時候,要等待所有正在寫日誌的操作都完成寫以後,才能關閉。processIOError用於處理IO出錯,一般這會導致對於的Storage的日誌文件被關閉(還記得loadFSImage要找出最後寫的日誌文件吧,這也是提高系統可靠性的一個方法),如果系統再也找不到可用的日誌文件,NameNode將會退出。

loadFSEdits是個大傢伙,它讀取日誌文件,並把日誌應用到內存中的目錄結構中。這傢伙大是因爲它需要處理所有類型的日誌記錄,其實就一大case語句。logEdit的作用和loadFSEdits相反,它向日志文件中寫入日誌記錄。我們來分析一下什麼操作需要寫log,還有就是需要log那些參數:

logOpenFile(OP_ADD):申請lease

path(路徑)/replication(副本數,文本形式)/modificationTime(修改時間,文本形式)/accessTime(訪問時間,文本形式)/preferredBlockSize(塊大小,文本形式)/BlockInfo[](增強的數據塊信息,數組)/permissionStatus(訪問控制信息)/clientName(客戶名)/clientMachine(客戶機器名)

logCloseFile(OP_CLOSE):歸還lease

path/replication/modificationTime/accessTime/preferredBlockSize/BlockInfo[]/permissionStatus

logMkDir(OP_MKDIR):創建目錄

path/modificationTime/accessTime/permissionStatus

logRename(OP_RENAME):改文件名

src(原文件名)/dst(新文件名)/timestamp(時間戳)

logSetReplication(OP_SET_REPLICATION):更改副本數

src/replication

logSetQuota(OP_SET_QUOTA):設置空間額度

path/nsQuota(文件空間額度)/dsQuota(磁盤空間額度)

logSetPermissions(OP_SET_PERMISSIONS):設置文件權限位

src/permissionStatus

logSetOwner(OP_SET_OWNER):設置文件組和主

src/username(所有者)/groupname(所在組)

logDelete(OP_DELETE):刪除文件

src/timestamp

logGenerationStamp(OP_SET_GENSTAMP):文件版本序列號

genstamp(序列號)

logTimes(OP_TIMES):更改文件更新/訪問時間

src/modificationTime/accessTime

通過上面的分析,我們應該清楚日誌文件裏記錄了那些信息。

rollEditLog()我們在前面已經提到過(配合saveFSImage和rollFSImage),它用於關閉edits,打開日誌到edits.new。purgeEditLog()的作用正好相反,它刪除老的edits文件,然後把edits.new改名爲edits。這也是Hadoop在做更新修改時經常採用的策略。

Hadoop源代碼分析(二二)

我們開始對租約Lease進行分析,下面是類圖。Lease可以認爲是一個文件寫鎖,當客戶端需要寫文件的時候,它需要申請一個Lease,NameNode負責記錄那個文件上有Lease,Lease的客戶是誰,超時時間(分佈式處理的一種常用技術)等,所有這些工作由下面3個類完成。至於租約過期NameNode需要採取什麼動作,並不是這部分code要完成的功能。


LeaseManager(左)管理着系統中的所有Lease(右),同時,LeaseManager有一個線程Monitor,用於檢查是否有Lease到期。

一個租約由一個holder(客戶端名),lastUpdate(上次更新時間)和paths(該客戶端操作的文件集合)構成。瞭解了這些屬性,相關的方法就很好理解了。LeaseManager的方法也就很好理解,就是對Lease進行操作。注意,LeaseManager的addLease並沒有檢查文件上是否已經有Lease,這個是由LeaseManager的調用者來保證的,這使LeaseManager跟簡單。內部類Monitor通過對Lease的最後跟新時間來檢測Lease是否過期,如果過期,簡單調用FSNamesystem的internalReleaseLease方法。

這部分的代碼比我想象的簡單,主要是大部分的一致性邏輯都存在於LeaseManager的使用者。在開始分析FSNamesystem.java這個4.5k多行的龐然大物之前,我們繼續來掃除外圍的障礙。下面是關於訪問控制的一些類:

 

Hadoop文件保護採用的UNIX的機制,文件用戶分文件屬主、文件組和其他用戶,權限讀,寫和執行(FsAction中抽象了所有組合)。

我們先分析包org.apache.hadoop.fs.permission的幾個類吧。FsAction抽象了操作權限,FsPermission記錄了某文件/路徑的允許情況,分文件屬主、文件組和其他用戶,同時提供了一系列的轉換方法,applyUMask用於去掉某些權限,如某些操作需要去掉文件的寫權限,那麼可以通過該方法,生成對應的去掉寫權限的FsPermission對象。PermissionStatus用於描述一個文件的文件屬主、文件組和它的FsPermission。

INode在保存PermissionStatus時,用了不同的方法,它用一個long變量,和SerialNumberManager配合,保存了PermissionStatus的所有信息。

SerialNumberManager保存了文件主和文件主號,用戶組和用戶組號的對應關係。注意,在持久化信息FSImage中,不保存文件主號和用戶組號,它們只是SerialNumberManager分配的,只保存在內存的信息。通過SerialNumberManager得到某文件主的文件主號時,如果找不到文件主號,會往對應關係中添加一條記錄。

INode的long變量作爲一個位串,分組保存了FsPermission(MODE),文件主號(USER)和用戶組號(GROUP)。

PermissionChecker用於權限檢查。

Hadoop源代碼分析(二三)

下面我們來分析FSDirectory。其實分析FSDirectory最好的地方,應該是介紹完INode*以後,FSDirectory在INode*的基礎上,保存了HDFS的文件目錄狀態。系統加載FSImage時,FSImage會在FSDirectory對象上重建文件目錄狀態,HDFS文件目錄狀態的變化,也由FSDirectory寫日誌,同時,它保存了文件名à數據塊的映射關係。

FSDirectory只有很少的成員變量,如下:

  final FSNamesystem namesystem;
  final INodeDirectoryWithQuota rootDir;
  FSImage fsImage;
  boolean ready = false;

其中,namesystem,fsImage是指向FSNamesystem對象和FSImage對象的引用,rootDir是文件系統的根,ready初值爲false,當系統成功加載FSImage以後,ready會變成true,FSDirectory的使用者就可以調用其它FSDirectory功能了。

FSDirectory中剩下的,就是一堆的方法(我們不討論和MBean相關的類,方法和過程)。

 

loadFSImage用於加載目錄樹結構,它會去調用FSImage的方法,完成持久化信息的導入以後,它會把成員變量ready置爲true。系統調用loadFSImage是在FSNamesystem.java的initialize方法,那是系統初始化重要的一步。

addFile用於創建文件或追加數據時創建INodeFileUnderConstruction,下圖是它的Call Hierachy圖:

 

addFile首先會試圖在系統中創建到文件的路徑,如果文件爲/home/hadoop/Hadoop.tar,addFile會調用mkdirs(創建路徑爲/home/hadoop,這也會涉及到一系列方法),保證文件路徑存在,然後創建INodeFileUnderConstruction節點,並把該節點加到目錄樹中(通過addNode,也是需要調用一系列方法),如果成功,就寫操作日誌(logOpenFile)。

unprotectedAddFile也用於在系統中創建一個目錄或文件(非UnderConstruction),如果是文件,還會建立對應的block。FSDirectory中還有好幾個unprotected*方法,它們不檢查成員變量ready,不寫日誌,它們大量用於loadFSEdits中(這個時候ready當然是false,而且因爲正在恢復日誌,也不需要寫日誌)。

addToParent添加一個INode到目錄樹中,並返回它的上一級目錄,它的實現和unprotectedAddFile是類似的。

persistBlocks比較有意思,用於往日誌裏記錄某inode的block信息,其實並沒有一個對應於persistBlocks的寫日誌方法,它用的是logOpenFile。這個大家可以去檢查一下logOpenFile記錄的信息。closeFile對應了logCloseFile。

addBlock和removeBlock對應,用於添加/刪除數據塊信息,同時它們還需要更新FSNamesystem.java中對應的信息。

unprotectedRenameTo和renameTo實現了UNIX的mv命令,主要的功能都在unprotectedRenameTo中完成,複雜的地方在於對各種各樣情況的討論。

setReplication和unprotectedSetReplication用於更新數據塊的副本數,很簡單的方法,注意,改變產生的對數據塊的刪除/複製是在FSNamesystem.java中實現。

setPermission,unprotectedSetPermission,setOwner和unprotectedSetOwner都是簡單的方法。

Delete和unprotectedDelete又是一對方法,刪除如果需要刪除數據塊,將通過FSNamesystem的removePathAndBlocks進行。

……(後續的方法和前面介紹的,都比較類似,都是一些過程性的東西,就不再討論了)

Hadoop源代碼分析(二四)

下面輪到FSNamesystem出場了。FSNamesystem.java一共有4573行,而整個namenode目錄下所有的Java程序總共也只有16876行,把FSNamesystem搞定了,NameNode也就基本搞定。

FSNamesystem是NameNode實際記錄信息的地方,保存在FSNamesystem中的數據有:

l           文件名à數據塊列表(存放在FSImage和日誌中)

l           合法的數據塊列表(上面關係的逆關係)

l           數據塊àDataNode(只保存在內存中,根據DataNode發過來的信息動態建立)

l           DataNode上保存的數據塊(上面關係的逆關係)

l           最近發送過心跳信息的DataNode(LRU)

我們先來分析FSNamesystem的成員變量。

  private boolean isPermissionEnabled;
是否打開權限檢查,可以通過配置項dfs.permissions來設置。

 

  private UserGroupInformation fsOwner;
本地文件的用戶文件屬主和文件組,可以通過hadoop.job.ugi設置,如果沒有設置,那麼將使用啓動HDFS的用戶(通過whoami獲得)和該用戶所在的組(通過groups獲得)作爲值。

 

  private String supergroup;
對應配置項dfs.permissions.supergroup,應用在defaultPermission中,是系統的超級組。

 

  private PermissionStatus defaultPermission;
缺省權限,缺省用戶爲fsOwner,缺省用戶組爲supergroup,缺省權限爲0777,可以通過dfs.upgrade.permission修改。

 

  private long capacityTotal, capacityUsed, capacityRemaining;
系統總容量/已使用容量/剩餘容量

 

  private int totalLoad = 0;
系統總連接數,根據DataNode心跳信息跟新。

 

  private long pendingReplicationBlocksCount, underReplicatedBlocksCount, scheduledReplicationBlocksCount;
分別是成員變量pendingReplications(正在複製的數據塊),neededReplications(需要複製的數據塊)的大小,scheduledReplicationBlocksCount是當前正在處理的複製工作數目。

 

  public FSDirectory dir;
指向系統使用的FSDirectory對象。

 

  BlocksMap blocksMap = new BlocksMap();
保存數據塊到INode和DataNode的映射關係

public CorruptReplicasMap corruptReplicas = new CorruptReplicasMap();
保存損壞(如:校驗沒通過)的數據塊到對應DataNode的關係,CorruptReplicasMap類圖如下,類只有一個成員變量,保存Block到一個DatanodeDescriptor的集合的映射和這個映射上的一系列操作:


 

  Map<String, DatanodeDescriptor> datanodeMap = new TreeMap<String, DatanodeDescriptor>();
保存了StorageID à DatanodeDescriptor的映射,用於保證DataNode使用的Storage的一致性。

 

  private Map<String, Collection<Block>> recentInvalidateSets
保存了每個DataNode上無效但還存在的數據塊(StorageID à ArrayList<Block>)。

  Map<String, Collection<Block>> recentInvalidateSets
保存了每個DataNode上有效,但需要刪除的數據塊(StorageID à TreeSet<Block>),這種情況可能發生在一個DataNode故障後恢復後,上面的數據塊在系統中副本數太多,需要刪除一些數據塊。

 

  HttpServer infoServer;

  int infoPort;

  Date startTime;
用於內部信息傳輸的HTTP請求服務器(Servlet的容器)。現在有/fsck,/getimage,/listPaths/*,/data/*和/fileChecksum/*,我們後面還會繼續討論。

 

  ArrayList<DatanodeDescriptor> heartbeats;
所有目前活着的DataNode,線程HeartbeatMonitor會定期檢查。

private UnderReplicatedBlocks neededReplications
需要進行復制的數據塊。UnderReplicatedBlocks的類圖如下,它其實是一個數組,數組的下標是優先級(0的優先級最高,如果數據塊只有一個副本,它的優先級是0),數組的內容是一個Block集合。UnderReplicatedBlocks提供一些方法,對Block進行增加,修改,查找和刪除。


  private PendingReplicationBlocks pendingReplications;
保存正在複製的數據塊的相關信息。PendingReplicationBlocks的類圖如下:


其中,pendingReplications保存了所有正在進行復制的數據塊,使用Map是需要一些附加的信息PendingBlockInfo。這些信息包括時間戳,用於檢測是否已經超時,和現在進行復制的數目numReplicasInProgress。timedOutItems是超時的複製項,超時的複製項在FSNamesystem的processPendingReplications方法中被刪除,並從新複製。timerThread是用於檢測複製超時的線程的句柄,對應的線程是PendingReplicationMonitor的一個實例,它的run方法每隔一段會檢查是否有超時的複製項,如果有,將該數據塊加到timedOutItems中。Timeout是run方法的檢查間隔,defaultRecheckInterval是缺省值。PendingReplicationBlocks和PendingBlockInfo的方法都很簡單。

 

  public LeaseManager leaseManager = new LeaseManager(this);
租約管理器。

Hadoop源代碼分析(二五)

繼續對FSNamesystem進行分析。

 

  Daemon hbthread = null;   // HeartbeatMonitor thread

  public Daemon lmthread = null;   // LeaseMonitor thread

  Daemon smmthread = null;  // SafeModeMonitor thread

public Daemon replthread = null;  // Replication thread
NameNode上的線程,分別對應DataNode心跳檢查,租約檢查,安全模式檢查和數據塊複製,我們會在後面介紹這些線程對應的功能。

 

  volatile boolean fsRunning = true;

  long systemStart = 0;

系統運行標誌和系統啓動時間。

 

接下來是一堆系統的參數,比方說系統每個DataNode節點允許的最大數據塊數,心跳檢查間隔時間等… …

  //  The maximum number of replicates we should allow for a single block

  private int maxReplication;

  //  How many outgoing replication streams a given node should have at one time

  private int maxReplicationStreams;

  // MIN_REPLICATION is how many copies we need in place or else we disallow the write

  private int minReplication;

  // Default replication

  private int defaultReplication;

  // heartbeatRecheckInterval is how often namenode checks for expired datanodes

  private long heartbeatRecheckInterval;

  // heartbeatExpireInterval is how long namenode waits for datanode to report

  // heartbeat

  private long heartbeatExpireInterval;

  //replicationRecheckInterval is how often namenode checks for new replication work

  private long replicationRecheckInterval;

  //decommissionRecheckInterval is how often namenode checks if a node has finished decommission

  private long decommissionRecheckInterval;

  // default block size of a file

  private long defaultBlockSize = 0;

 

  private int replIndex = 0;
和neededReplications配合,記錄下一個進行復制的數據塊位置。

public static FSNamesystem fsNamesystemObject;
哈哈,不用介紹了,還是static的。

  private String localMachine;
  private int port;
本機名字和RPC端口。

private SafeModeInfo safeMode;  // safe mode information
記錄安全模式的相關信息。
安全模式是這樣一種狀態,系統處於這個狀態時,不接受任何對名字空間的修改,同時也不會對數據塊進行復制或刪除數據塊。NameNode啓動的時候會自動進入安全模式,同時也可以手工進入(不會自動離開)。系統啓動以後,DataNode會報告目前它擁有的數據塊的信息,當系統接收到的Block信息到達一定門檻,同時每個Block都有dfs.replication.min個副本後,系統等待一段時間後就離開安全模式。這個門檻定義的參數包括:

l           dfs.safemode.threshold.pct:接受到的Block的比例,缺省爲95%,就是說,必須DataNode報告的數據塊數目佔總數的95%,纔到達門檻;

l           dfs.replication.min:缺省爲1,即每個副本都存在系統中;

l           dfs.replication.min:等待時間,缺省爲0,單位秒。

SafeModeInfo的類圖如下:



 

threshold,extension和safeReplication保存的是上面說的3個參數。Reached等於-1表明安全模式是關閉的,0表示安全模式打開但是系統還沒達到threshold。blockTotal是計算threshold時的分母,blockSafe是分子,lastStatusReport用於控制寫日誌的間隔。

SafeModeInfo(Configuration conf)使用配置文件的參數,是NameNode正常啓動時使用的構造函數,SafeModeInfo()中,this.threshold = 1.5f使得系統用於處於安全模式。

enter()使系統進入安全模式,leave()會使系統離開安全模式,canLeave()用於檢查是否能離開安全模式而needEnter(),則判斷是否應該進入安全模式。checkMode()檢查系統狀態,如果必要,則進入安全模式。其他的方法都比價簡單,大多爲對成員變量的訪問。

 

討論完類SafeModeInfo,我們來分析一下SafeModeMonitor,它用於定期檢查系統是否能夠離開安全模式(smmthread就是它的一個實例)。系統離開安全模式後,smmthread會被重新賦值爲null。


Hadoop源代碼分析(二六)

(沒想到需要分頁啦)

private Host2NodesMap host2DataNodeMap = new Host2NodesMap();
保存了主機名(String)到DatanodeDescriptor數組的映射(Host2NodesMap唯一的成員變量爲HashMap<String,DatanodeDescriptor[]> map,它的方法都是對這個map進行操作)。

 

  NetworkTopology clusterMap = new NetworkTopology();
  private DNSToSwitchMapping dnsToSwitchMapping;

定義了HDFS的網絡拓撲,網絡拓撲對應選擇數據塊副本的位置很重要。如在一個層次型的網絡中,接到同一個交換機的兩個節點間的網絡速度,會比跨越多個交換機的兩個節點間的速度快,但是,如果某交換機故障,那麼它對接到它上面的兩個節點會同時有影響,但跨越多個交換機的兩個節點,這種影響會小得多。下面是NetworkTopology相關的類圖:

 

Hadoop實現了一個樹狀的拓撲結構抽象,其中,Node接口,定義了網絡節點的一些方法,NodeBase是Node的一個實現,提供了葉子節點的一些方法(明顯它沒有子節點),而InnerNode則實現了樹的內部節點,如果我們考慮一個網絡部署的話,那麼葉子節點是服務器,而InnerNode則是服務器所在的機架或交換機或路由器。Node提供了對網絡位置信息(採用類似文件樹的方式),節點名稱和Node所在的樹的深度的方法。NodeBase提供了一個簡單的實現。InnerNode是NetworkTopology的內部類,對比NodeBase,它的clildren保存了所有的子節點,這樣的話,就可以構造一個拓撲樹。這棵樹的葉子可能是服務器,也可能是機架,內部則是機架或者是路由器等設備,InnerNode提供了一系列的方法區分處理這些信息。

NetworkTopology的add方法和remove用於在拓撲結構中加入節點和刪除節點,同時也給出一些get*方法,用於獲取一些對象內部的信息,如getDistance,可以獲取兩個節點的距離,而isOnSameRack可以判斷兩個節點是否處於同一個機架。chooseRandom有兩個實現,用於在一定範圍內(另一個還有一個排除選項)隨機選取一個節點。chooseRandom在選擇數據塊副本位置的時候調用。

DNSToSwitchMapping配合上面NetworkTopology,用於確定某一個節點的網絡位置信息,它的唯一方法,可以通過一系列機器的名字找出它們對應的網絡位置信息。目前有支持兩種方法,一是通過命令行方式,將節點名作爲輸入,輸出爲網絡位置信息(RawScriptBasedMapping執行命令CachedDNSToSwitchMapping緩存結果),還有一種就是利用配置參數hadoop.configured.node.mapping靜態配置(StaticMapping)。

 

  ReplicationTargetChooser replicator;

用於爲數據塊備份選擇目標,例如,用戶寫文件時,需要選擇一些DataNode,作爲數據塊的存放位置,這時候就利用它來選擇目標地址。chooseTarget是ReplicationTargetChooser中最重要的方法,它通過內部的一個NetworkTopology對象,計算出一個DatanodeDescriptor數組,該數組就是選定的DataNode,同時,順序就是最佳的數據流順序(還記得我們討論DataXceiver些數據的那個圖嗎?)。

 

  private HostsFileReader hostsReader; 

保存了系統中允許/不允許連接到NameNode的機器列表。

 

  private Daemon dnthread = null;

線程句柄,該線程用於檢測DataNode上的Decommission進程。例如,某節點被列入到不允許連接到NameNode的機器列表中(HostsFileReader),那麼,該節點會進入Decommission狀態,它上面的數據塊會被複制到其它節點,複製結束後機器進入DatanodeInfo.AdminStates.DECOMMISSIONED,這臺機器就可以從HDFS中撤掉。

 

  private long maxFsObjects = 0;          // maximum number of fs objects

系統能擁有的INode最大數(配置項dfs.max.objects,0爲無限制)。

 

  private final GenerationStamp generationStamp = new GenerationStamp();

系統的時間戳生產器。

 

  private int blockInvalidateLimit = FSConstants.BLOCK_INVALIDATE_CHUNK;

發送給DataNode刪除數據塊消息中,能包含的最大數據塊數。比方說,如果某DataNode上有250個Block需要被刪除,而這個參數是100,那麼一共會有3條刪除數據塊消息消息,前面兩條包含了100個數據塊,最後一條是50個。

 

private long accessTimePrecision = 0;

用於控制文件的access時間的精度,也就是說,小於這個精度的兩次對文件訪問,後面的那次就不做記錄了。

Hadoop源代碼分析(二七)

我們接下來分析NameNode.java的成員變量,然後兩個類綜合起來,分析它提供的接口,並配合說明接口上請求對應的處理流程。

前面已經介紹過了,NameNode實現了接口ClientProtocol,DatanodeProtocol和NamenodeProtocol,分別提供給客戶端/DataNode/從NameNode訪問。由於NameNode的大部分功能在類FSNamesystem中實現,那麼NameNode.java的成員變量就很少了。

  public FSNamesystem namesystem;
指向FSNamesystem對象。

 

  private Server server;
NameNode的RPC服務器實例。

 

  private Thread emptier;
處理回收站的線程句柄。

 

  private int handlerCount = 2;
還記得我們分析RPC的服務器時提到的服務器請求處理線程(Server.Handle)嗎?這個參數給出了server中服務器請求處理線程的數目,對應配置參數爲dfs.namenode.handler.count。

 

  private boolean supportAppends = true;
是否支持append操作,對應配置參數爲dfs.support.append。

  private InetSocketAddress nameNodeAddress = null;
NameNode的地址,包括IP地址和監聽端口。

下面我們來看NameNode的啓動過程。main方法是系統的入口,它會調用createNameNode創建NameNode實例。createNameNode分析命令行參數,如果是FORMAT或FINALIZE,調用對應的方法後退出,如果是其他的參數,將創建NameNode對象。NameNode的構造函數會調用initialize,初始化NameNode的成員變量,包括創建RPC服務器,初始化FSNamesystem,啓動RPC服務器和回收站線程。

FSNamesystem的構造函數會調用initialize方法,去初始化上面我們分析過的一堆成員變量。幾個重要的步驟包括加載FSImage,設置系統爲安全模式,啓動各個工作線程和HTTP服務器。系統的一些參數是在setConfigurationParameters中初始化的,其中一些值的計算比較麻煩,而且也可能被其它部分的code引用的,就獨立出來了,如getNamespaceDirs和getNamespaceEditsDirs。initialize對應的是close方法,很簡單,主要是停止initialize中啓動的線程。

對應於initialize方法,NameNode也提供了對應的stop方法,用於初始化時出錯系統能正確地退出。

NameNode的format和finalize操作,都是先構造FSNamesystem,然後利用FSNamesystem的FSImage提供的對應方法完成的。我們在分析FSImage.java時,已經瞭解了這部分的功能。

Hadoop源代碼分析(二八)

萬事俱備,我們可以來分析NameNode上的流程啦。

 

首先我們來看NameNode上實現的ClientProtocol,客戶端通過這個接口,可以對目錄樹進行操作,打開/關閉文件等。

getBlockLocations用於確定文件內容的位置,它的輸入參數爲:文件名,偏移量,長度,返回值是一個LocatedBlocks對象(如下圖),它攜帶的信息很多,大部分字段我們以前都討論過。


 

getBlockLocations直接調用NameSystem的同名方法。NameSystem中這樣的方法首先會檢查權限和對參數進行檢查(如偏移量和長度要大於0),然後再調用實際的方法。找LocatedBlocks先找src對應的INode,然後通過INode的getBlocks方法,可以拿到該節點的Block列表,如果返回爲空,表明該INode不是文件,返回null;如果Block列表長度爲0,以空的Block數組構造返回的LocatedBlocks。

如果Block數組不爲空,則通過請求的偏移量和長度,就可以把這個區間涉及的Block找出來,對於每一個block,執行:

l           通過BlocksMap我們可以找到它存在於幾個DataNode上(BlocksMap.numNodes方法);

l           計算包含該數據塊但數據塊是壞的DataNode的數目(通過NameSystem.countNodes方法,間接訪問CorruptReplicasMap中的信息);

l           計算壞數據塊的數目(CorruptReplicasMap.numCorruptReplicas方法,應該和上面的數相等);

l           通過上面的計算,我們得到現在還OK的數據塊數目;

l           從BlocksMap中找出所有OK的數據塊對應的DatanodeDescriptor(DatanodeInfo的父類);

l           創建對應的LocatedBlock。

收集到每個數據塊的LocatedBlock信息後,很自然就能構造LocatedBlocks對象。getBlockLocations其實只是一個讀的方法,請求到了NameNode以後只需要查表就行了。

create方法,該方法用於在目錄樹上創建文件(創建目錄使用mkdir),需要的參數比較多,包括文件名,權限,客戶端名,是否覆蓋已存在文件,副本數和塊大小。NameNode的create調用NameSystem的startFile方法(startFile需要的參數clientMachine從線程局部變量獲取)。

startFile方法先調用startFileInternal完成操作,然後調用logSync,等待日誌寫完後才返回。

startFileInternal不但服務於startFile,也被appendFile調用(通過參數append區分)。方法的最開始是一堆檢查,包括:安全模式,文件名src是否正確,權限,租約,replication參數,overwrite參數(對append操作是判斷src指向是否存在並且是文件)。租約檢查很簡單,如果通過FSDirectory.getFileINode(src)得到的文件是出於構造狀態,表明有客戶正在操作該文件,這時會拋出異常AlreadyBeingCreatedException。

如果對於創建操作,會通過FSDirectory的addFile往目錄樹上添加一個文件並在租約管理器裏添加一條記錄。

對於append操作,執行的是構造一個新的INodeFileUnderConstruction並替換原有的節點,然後在租約管理器裏添加一條記錄。

總的來說,最簡單的create流程就是在目錄樹上創建一個INodeFileUnderConstruction對象並往租約管理器裏添加一條記錄。

 

我們順便分析一下append吧,它的返回值是LocatedBlock,比起getBlockLocations,它只需要返回數組的一項。appendFile是NameSystem的實現方法,它首先調用上面討論的startFileInternal方法(已經在租約管理器裏添加了一條記錄)然後寫日誌。然後尋找對應文件INodeFile中記錄的最後一個block,並通過BlocksMap.getStoredBlock()方法得到BlockInfo,然後再從BlocksMap中獲得所有的DatanodeDescriptor,就可以構造LocatedBlock了。需要注意的,如果該Block在需要被複制的集合(UnderReplicatedBlocks)中,移除它。

如果文件剛被創建或者是最後一個數據塊已經寫滿,那麼append會返回null,這是客戶端需要使用addBlock,爲文件添加數據塊。

Hadoop源代碼分析(二九)

  public boolean setReplication(String src,
                                      short replication
                                      ) throws IOException;

setReplication,設置文件src的副本數爲replication,返回值爲boolean,在FSNameSystem中,調用方法setReplicationInternal,然後寫日誌。

setReplicationInternal上來自然是檢查參數了,然後通過FSDirectory的setReplication,設置新的副本數,並獲取老的副本數。根據新舊數,決定刪除/複製數據塊。

增加副本數通過調用updateNeededReplications,爲了獲取UnderReplicatedBlocks. update需要的參數,FSNameSystem提供了內部方法countNodes和getReplication,獲得對應的數值(這兩個函數都很簡單)。

proccessOverReplicatedBlock用於減少副本數,它被多個方法調用:

 

主要參數有block,副本數,目標DataNode,源DataNode(用於刪除)。proccessOverReplicatedBlock首先找出block所在的,處於非Decommission狀態的DataNode的信息,然後調用chooseExcessReplicates。chooseExcessReplicates執行:

l           按機架位置,對DatanodeDescriptor進行分組;

l           將DataNode分爲兩個集合,分別是一個機架包含一個以上的數據塊的和剩餘的;

l           選擇可以刪除的數據塊(順序是:源DataNode,同一個機架上的,剩餘的),把它加到recentInvalidateSets中。

  public void setPermission(String src, FsPermission permission
                                 ) throws IOException;

setPermission,用於設置文件的訪問權限。非常簡單,首先檢查是否有權限,然後調用FSDirectory.setPermission修改文件訪問權限。

  public void setOwner(String src, String username, String groupname
      ) throws IOException;

  public void setTimes(String src, long mtime, long atime) throws IOException;

  public void setQuota(String path, long namespaceQuota, long diskspaceQuota)
                      throws IOException;

setOwner,設置文件的文件主和文件組,setTimes,設置文件的訪問時間和修改時間,setQuota,設置某路徑的空間限額和空間額度,和setPermission類似,調用FSDirectory的對應方法,簡單。

  public boolean setSafeMode(FSConstants.SafeModeAction action) throws IOException;

前面我們已經介紹了NameNode的安全模式,客戶端通過上面的方法,可以讓NameNode進入(SAFEMODE_ENTER)/退出(SAFEMODE_LEAVE)安全模式或查詢(SAFEMODE_GET)狀態。FSNamesystem的setSafeMode處理這個命令,對於進入安全模式的請求,如果系統現在不處於安全模式,那麼創建一個SafeModeInfo對象(創建的這個對象有別於啓動時創建的那個SafeModeInfo,它不會自動退出,因爲threshold=1.5f),這標誌着系統進入安全模式。退出安全模式很簡單,將safeMode賦空就可以啦。

  public FileStatus[] getListing(String src) throws IOException;

分析完set*以後,我們來看get*。getListing對應於UNIX系統的ls命令,返回值是FileStatus數組,FileStatus的類圖如下,它其實給出了文件的詳細信息,如大小,文件主等等。其實,這些信息都存在INode*中,我們只需要把這些信息搬到FileStatus中就OK啦。FSNamesystem和FSDirectory中都有同名方法,真正幹活的地方在FSDirectory中。getListing不需要寫日誌。


  public long[] getStats() throws IOException; 

getStatus得到的是文件系統的信息,UNIX對應命令爲du,它的實現更簡單,所有的信息都存放在FSNamesystem對象裏。

  public DatanodeInfo[] getDatanodeReport(FSConstants.DatanodeReportType type)

  throws IOException;

getDatanodeReport,獲取當前DataNode的狀態,可能的選項有DatanodeReportType.ALL, IVE和DEAD。FSNamesystem的同名方法調用getDatanodeListForReport,通過HostsFileReader讀取對應信息。

  public long getPreferredBlockSize(String filename) throws IOException;

getPreferredBlockSize,返回INodeFile.preferredBlockSize,數據塊大小。

  public FileStatus getFileInfo(String src) throws IOException;

和getListing類似,不再分析。

  public ContentSummary getContentSummary(String path) throws IOException;

得到文件樹的一些信息,如下圖:

 

  public void metaSave(String filename) throws IOException;

這個也很簡單,它把系統的metadata輸出/添加到指定文件上(NameNode所在的文件系統)。

Hadoop源代碼分析(三零)

軟柿子都捏完了,我們開始啃硬骨頭。前面已經分析過getBlockLocations,create,append,setReplication,setPermission和setOwner,接下來我們繼續回來討論和文件內容相關的操作。

  public void abandonBlock(Block b, String src, String holder
      ) throws IOException;

abandonBlock用於放棄一個數據塊。普通的文件系統中並沒有“放棄”操作,HDFS出現放棄數據塊的原因,如下圖所示。當客戶端通過其他操作(如下面要介紹的addBlock方法)獲取LocatedBlock後,可以打開到一個block的輸出流,由於從DataNode出錯到NameNode發現這個信息,需要有一段時間(NameNode長時間收到DataNode心跳),打開輸出流可能出錯,這時客戶端可以向NameNode請求放棄這個數據塊。

 

abandonBlock的處理不是很複雜,首先檢查租約(調用checkLease方法。block對應的文件存在,文件處於構造狀態,租約擁有者匹配),如果通過檢查,調用FSDirectory的removeBlock,從INodeFileUnderConstruction/BlocksMap/CorruptReplicasMap中刪除block,然後通過logOpenFile()記錄變化(logOpenFile真是萬能啊)。

 

  public LocatedBlock addBlock(String src, String clientName) throws IOException;

寫HDFS的文件時,如果數據塊被寫滿,客戶端可以通過addBlock創建新的數據塊。具體的創建工作由FSNamesystem的getAdditionalBlock方法完成,當然上來就是一通檢查(是否安全模式,命名/存儲空間限額,租約,數據塊副本數,保證DataNode已經上報數據塊狀態),然後通過ReplicationTargetChooser,選擇複製的目標(如果目標數不夠副本數,又是一個異常),然後,就可以分配數據塊了。allocateBlock創建一個新的Block對象,然後調用addBlock,檢查參數後把數據塊加到BlocksMap對象和對應的INodeFile對象中。allocateBlock返回後,getAdditionalBlock還會繼續更新一些需要記錄的信息,最後返回一個新構造的LocatedBlock。

 

  public boolean complete(String src, String clientName) throws IOException;

當客戶端完成對數據塊的寫操作後,調用complete完成寫操作。方法complete如果返回是false,那麼,客戶端需要繼續調用complete方法。

FSNamesystem的同名方法調用completeFileInternal,它會:

l           檢查環境;

l           獲取src對應的INode;

l           如果INode存在,並且處於構造狀態,獲取數據塊;

l           如果獲取數據塊返回空,返回結果CompleteFileStatus.OPERATION_FAILED,FSNamesystem的complete會拋異常返回;

l           如果上報文件完成的DataNode數不夠系統最小的副本數,返回STILL_WAITING;

l           調用finalizeINodeFileUnderConstruction;

l           返回成功COMPLETE_SUCCESS

其中,對finalizeINodeFileUnderConstruction的處理包括:

l           釋放租約;

l           將對應的INodeFileUnderConstruction對象轉換爲INodeFile對象,並在FSDirectory進行替換;

l           調用FSDirectory.closeFile關閉文件,其中會寫日誌logCloseFile(path, file)。

l           檢查副本數,如果副本數小於INodeFile中的目標數,那麼添加數據塊複製任務。

我們可以看到,complete一個文件還是比較複雜的,需要釋放很多的資源。

  public void reportBadBlocks(LocatedBlock[] blocks) throws IOException;

調用reportBadBlocks的地方比較多,客戶端可能調用,DataNode上也可能調用。 

 

由於上報的是個數組,reportBadBlocks會循環處理,調用FSNamesystem的markBlockAsCorrupt方法。markBlockAsCorrupt方法需要兩個參數,blk(數據塊)和dn(所在的DataNode信息)。如果系統目前副本數大於要求,那麼直接調用invalidateBlock方法。

方法invalidateBlock很簡單,在檢查完系統環境以後,先調用addToInvalidates方法往FSNamesystem.recentInvalidateSets添加一項,然後調用removeStoredBlock方法。

removeStoredBlock被多個方法調用,它會執行:

l           從BlocksMap中刪除記錄removeNode(block, node);

l           如果目前系統中還有其他副本,調用decrementSafeBlockCount(可能的調整安全模式參數)和updateNeededReplications(跟新可能存在的block複製信息,例如,現在系統中需要複製1個數據塊,那麼更新後,需要複製2個數據塊);

l           如果目前系統中有多餘數據塊等待刪除(在excessReplicateMap中),那麼移除對應記錄;

l           刪除在CorruptReplicasMap中的記錄(可能有)。

 

removeStoredBlock其實也是涉及了多處表操作,包括BlocksMap,excessReplicateMap和CorruptReplicasMap。

我們回到markBlockAsCorrupt,如果系統目前副本數小於要求,那麼很顯然,我們需要對數據塊進行復制。首先將現在的數據塊加入到CorruptReplicasMap中,然後調用updateNeededReplications,跟新複製信息。

markBlockAsCorrupt這個流程太複雜了,我們還是畫個圖吧:


Hadoop源代碼分析(三一)

下面是和目錄樹相關的方法。

  public boolean rename(String src, String dst) throws IOException;

更改文件名。調用FSNamesystem的renameTo,幹活的是renameToInternal,最終調用FSDirectory的renameTo方法,如果成功,更新租約的文件名,如下:

      changeLease(src, dst, dinfo);

  public boolean delete(String src) throws IOException;

  public boolean delete(String src, boolean recursive) throws IOException;

第一個已經廢棄不用,使用第二個方法。

最終使用deleteInternal,該方法調用FSDirectory.delete()。

  public boolean mkdirs(String src, FsPermission masked) throws IOException;

在做完一系列檢查以後,調用FSDirectory.mkdirs()。

  public FileStatus[] getListing(String src) throws IOException;

前面我們已經討論了。

 

下面是其它和系統維護管理的方法。

  public void renewLease(String clientName) throws IOException;

就是調用了一下leaseManager.renewLease(holder),沒有其他的事情需要做,簡單。

  public void refreshNodes() throws IOException;

還記得我們前面分析過NameNode上有個DataNode在線列表和DataNode離線列表嗎,這個命令可以讓NameNode從新讀這兩個文件。當然,根據前後DataNode的狀態,一共有4種情況,其中有3種需要修改。

對於從工作狀態變爲離線的,需要將上面的DataNode複製到其他的DataNode,需要調用updateNeededReplications方法(前面我們已經討論過這個方法了)。

對於從離線變爲工作的DataNode,只需要改變一下狀態。

  public void finalizeUpgrade() throws IOException;

finalize一個升級,確認客戶端有超級用戶權限以後,調用FSImage.finalizeUpgrade()。

  public void fsync(String src, String client) throws IOException;

將文件信息持久化。在檢查租約信息後,調用FSDirectory的persistBlocks,將文件的原信息通過logOpenFile(path, file)寫日誌。

Hadoop源代碼分析(三二)

搞定ClientProtocol,接下來是DatanodeProtocol部分。接口如下:


 

public DatanodeRegistration register(DatanodeRegistration nodeReg

                                       ) throws IOException

用於DataNode向NameNode登記。輸入和輸出參數都是DatanodeRegistration,類圖如下:


 

前面討論DataNode的時候,我們已經講過了DataNode的註冊過程,我們來看NameNode的過程。下面是主要步驟:

l           檢查該DataNode是否能接入到NameNode;

l           準備應答,更新請求的DatanodeID;

l           從datanodeMap(保存了StorageID à DatanodeDescriptor的映射,用於保證DataNode使用的Storage的一致性)得到對應的DatanodeDescriptor,爲nodeS;

l           從Host2NodesMap(主機名到DatanodeDescriptor數組的映射)中獲取DatanodeDescriptor,爲nodeN;

l           如果nodeN!=null同時nodeS!=nodeN(後面的條件表明表明DataNode上使用的Storage發生變化),那麼我們需要先在系統中刪除nodeN(removeDatanode,下面再討論),並在Host2NodesMap中刪除nodeN;

l           如果nodeS存在,表明前面已經註冊過,則:

1.      更新網絡拓撲(保存在NetworkTopology),首先在NetworkTopology中刪除nodeS,然後跟新nodeS的相關信息,調用resolveNetworkLocation,獲得nodeS的位置,並從新加到NetworkTopology裏;

2.      更新心跳信息(register也是心跳);

l           如果nodeS不存在,表明這是一個新註冊的DataNode,執行

1.      如果註冊信息的storageID爲空,表明這是一個全新的DataNode,分配storageID;

2.      創建DatanodeDescriptor,調用resolveNetworkLocation,獲得位置信息;

3.      調用unprotectedAddDatanode(後面分析)添加節點;

4.      添加節點到NetworkTopology中;

5.      添加到心跳數組中。

上面的過程,我們遺留了兩個方法沒分析,removeDatanode的流程如下:

l           更新系統的狀態,包括capacityTotal,capacityUsed,capacityRemaining和totalLoad;

l           從心跳數組中刪除節點,並標記節點isAlive屬性爲false;

l           從BlocksMap中刪除這個節點上的所有block,用了(三零)分析到的removeStoredBlock方法;

l           調用unprotectedAddDatanode;

l           從NetworkTopology中刪除節點信息。

unprotectedAddDatanode很簡單,它只是更新了Host2NodesMap的信息。

Hadoop源代碼分析(三三)

下面來看一個大傢伙:

public DatanodeCommand sendHeartbeat(DatanodeRegistration nodeReg,

                                       long capacity,

                                       long dfsUsed,

                                       long remaining,

                                       int xmitsInProgress,

                                       int xceiverCount) throws IOException

DataNode發送到NameNode的心跳信息。細心的人會發現,請求的內容還是DatanodeRegistration,應答換成DatanodeCommand了。DatanodeCommand類圖如下:

前面介紹DataNode時,已經分析過了DatanodeCommand支持的命令:

  DNA_TRANSFER:拷貝數據塊到其他DataNode

  DNA_INVALIDATE:刪除數據塊

  DNA_SHUTDOWN:關閉DataNode

  DNA_REGISTER:DataNode重新註冊

  DNA_FINALIZE:提交升級

  DNA_RECOVERBLOCK:恢復數據塊



 

有了上面這些基礎,我們來看FSNamesystem.handleHeartbeat的處理過程:

l           調用getDatanode方法找對應的DatanodeDescriptor,保存於變量nodeinfo(可能爲null)中,如果現有NameNode上記錄的StorageID和請求的不一樣,返回DatanodeCommand.REGISTER,讓DataNode從新註冊。

l           如果發現當前節點需要關閉(已經isDecommissioned),拋異常DisallowedDatanodeException。

l           nodeinfo是空或者現在狀態不是活的,返回DatanodeCommand.REGISTER,讓DataNode從新註冊。

l           更新系統的狀態,包括capacityTotal,capacityUsed,capacityRemaining和totalLoad;

l           接下來按順序看有沒有可能的恢復數據塊/拷貝數據塊到其他DataNode/刪除數據塊/升級命令(不討論)。一次返回只能有一條命令,按上面優先順序。

 

下面分析應答的命令是如何構造的。

首先是DNA_RECOVERBLOCK(恢復數據塊),那是個非常長的流程,同時需要回去討論DataNode上的一些功能,我們在後面介紹它。

 

對於DNA_TRANSFER(拷貝數據塊到其他DataNode),從DatanodeDescriptor.replicateBlocks中取出儘可能多的項目,放到BlockCommand中。在DataNode中,命令由transferBlocks執行,前面我們已經分析過啦。

 

刪除數據塊DNA_INVALIDATE也很簡單,從DatanodeDescriptor.invalidateBlocks中獲取儘可能多的項目,放到BlockCommand中,DataNode中的動作,我們也分析過。

 

我們來討論DNA_RECOVERBLOCK(恢復數據塊),在討論DataNode的過程中,我們沒有講這個命令是用來幹什麼的,還有它在DataNode上的處理流程,是好好分析分析這個流程的時候了。DNA_RECOVERBLOCK命令通過DatanodeDescriptor.getLeaseRecoveryCommand獲取,獲取過程很簡單,將DatanodeDescriptor對象中隊列recoverBlocks的所有內容取出,放入BlockCommand的Block中,設置BlockCommand爲DNA_RECOVERBLOCK,就OK了。

關鍵是,這個隊列裏的信息是用來幹什麼的。我們先來看那些操作會向這個隊列加東西,調用關係圖如下:


租約有兩個超時時間,一個被稱爲軟超時(1分鐘),另一個是硬超時(1小時)。如果租約軟超時,那麼就會觸發internalReleaseLease方法,如下:

  void internalReleaseLease(Lease lease, String src) throws IOException

該方法執行:

l           檢查src對應的INodeFile,如果不存在,不處於構造狀態,返回;

l           文件處於構造狀態,而文件目標DataNode爲空,而且沒有數據塊,則finalize該文件(該過程在completeFileInternal中已經討論過,租約在過程中被釋放),並返回;

l           文件處於構造狀態,而文件目標DataNode爲空,數據塊非空,則將最後一個數據塊存放的DataNode目標取出(在BlocksMap中),然後設置爲文件現在的目標DataNode;

l           調用INodeFileUnderConstruction.assignPrimaryDatanode,該過程會挑選一個目前還活着的DataNode,作爲租約的主節點,並把<block,block目標DataNode數組>加到該DataNode的recoverBlocks隊列中;

l           更新租約。

上面分析了租約軟超時的情況下NameNode發生租約恢復的過程。DataNode上收到這個命令後,將會啓動一個新的線程,該線程爲每個Block調用recoverBlock方法:recoverBlock(blocks[i], false, targets[i], true)。

  private LocatedBlock recoverBlock(Block block, boolean keepLength,

      DatanodeID[] datanodeids, boolean closeFile) throws IOException

它的流程並不複雜,但是分支很多,如下圖(藍線是上面輸入,沒有異常走的流程):


 

 首先是判斷進來的Block是否在ongoingRecovery中,如果存在,返回,不存在,加到ongoingRecovery中。

接下來是個循環(框內部分是循環體,奇怪,沒找到表示循環的符號),對每一個DataNode,獲取Block的BlockMetaDataInfo(下面還會分析),這需要調用到DataNode間通信的接口上的方法getBlockMetaDataInfo。然後分情況看要不要把信息保存下來(圖中間的幾個判斷),其中包括要進行同步的節點。

根據參數,更新數據塊信息,然後調用syncBlock並返回syncBlock生產的LocatedBlock。

上面的這一圈,對於我們這個輸入常數來說,就是把Block的長度,更新成爲擁有最新時間戳的最小長度值,並得到要更新的節點列表,然後調用syncBlock更新各節點。

getBlockMetaDataInfo用於獲取Block的BlockMetaDataInfo,包括Block的generationStamp,最後校驗時間,同時它還會檢查數據塊文件的元信息,如果出錯,會拋出異常。

syncBlock定義如下:

private LocatedBlock syncBlock(Block block, List<BlockRecord> syncList,

      boolean closeFile)

它的流程是:

l           如果syncList爲空,通過commitBlockSynchronization向NameNode提交這次恢復;

l           syncList不爲空,那麼先NameNode申請一個新的Stamp,並根據上面得到的長度,構造一個新的數據塊信息newblock;

l           對於沒一個syncList中的DataNode,調用它們上面的updateBlock,更新信息;更新信息如果返回OK,記錄下來;

l           如果更新了信息的DataNode不爲空,調用commitBlockSynchronization提交這次恢復;並生成LocatedBlock;

l           如果更新的DataNode爲空,拋異常。

通過syncBlock,所有需要恢復的DataNode上的Block信息都被更新。

DataNode上的updateBlock方法我們前面已經介紹了,就不再分析。

下面我們來看NameNode的commitBlockSynchronization方法,它在上面的過程中用於提交數據塊恢復:

public void commitBlockSynchronization(Block block,

      long newgenerationstamp, long newlength,

      boolean closeFile, boolean deleteblock, DatanodeID[] newtargets

      )

參數分別是block,數據塊;newgenerationstamp,新的時間戳;newlength,新長度;closeFile,是否關閉文件,deleteblock,是否刪除文件;newtargets,新的目標列表。

上面的兩次調用,輸入參數分別是:

commitBlockSynchronization(block, 0, 0, closeFile, true, DatanodeID.EMPTY_ARRAY);

commitBlockSynchronization(block, newblock.getGenerationStamp(), newblock.getNumBytes(), closeFile, false, nlist);

處理流程是:

l           參數檢查;

l           獲取對應的文件,記爲pendingFile;

l           BlocksMap中刪除老的信息;

l           如果deleteblock爲true,從pendingFile刪除Block記錄;

l           否則,更新Block的信息;

l           如果不關閉文件,那麼寫日誌保存更新,返回;

l           關閉文件的話,調用finalizeINodeFileUnderConstruction。

這塊比較複雜,不僅涉及了NameNode和DataNode間的通信,而且還存在對於DataNode和DataNode間的通信(DataNode間的通信就只支持這兩個方法,如下圖)。後面介紹DFSClient的時候,我們還會再回來分析它的功能,以獲取全面的理解。


Hadoop源代碼分析(三四)

繼續對NameNode實現的接口做分析。

public DatanodeCommand blockReport(DatanodeRegistration nodeReg,
                                     long[] blocks) throws IOException

DataNode向NameNode報告它擁有的所有數據塊,其中,參數blocks包含了數組化以後數據塊的信息。FSNamesystem.processReport處理這個請求。一番檢查以後,調用DatanodeDescriptor的reportDiff,將上報的數據塊分成三組,分別是:

l           刪除:其它情況;

l           加入:BlocksMap中有數據塊,但目前的DatanodeDescriptor上沒有對應信息;

l           使無效:BlocksMap中沒有找到數據塊。

對於刪除的數據塊,調用removeStoredBlock,這個方法我們前面已經分析過啦。

對應需要加入的數據塊,調用addStoredBlock方法,處理流程如下:

l           從BlocksMap獲取現在的信息,記爲storedBlock;如果爲空,返回;

l           記錄block和DatanodeDescriptor的關係;

l           新舊數據塊記錄不是同一個(我們這個流程是肯定不是啦):

1.      如果現有數據塊長度爲0,更新爲上報的block的值;

2.      如果現有數據塊長度比新上報的長,invalidateBlock(前面分析過,很簡單的一個方法)當前數據塊;

3.      如果現有數據塊長度比新上報的小,那麼會刪除所有老的數據塊(還是通過invalidateBlock),並更新BlocksMap中數據塊的大小信息;

4.      跟新可用存儲空間等信息;

l           根據情況確定數據塊需要複製的數目和目前副本數;

l           如果文件處於構建狀態或系統現在是安全模式,返回;

l           處理當前副本數和文件的目標副本數不一致的情況;

l           如果當前副本數大於系統設定門限,開始刪除標記爲無效的數據塊。

還是給個流程圖吧:



對於標記爲使無效的數據塊,調用addToInvalidates方法,很簡單的方法,直接加到FSNamesystem的成員變量recentInvalidateSets中。

public void blockReceived(DatanodeRegistration registration,
                            Block blocks[],

                            String[] delHints)

DataNode可以通過blockReceived,向NameNode報告它最近接受到的數據塊,同時給出如果數據塊副本數太多時,可以刪除數據塊的節點(參數delHints)。在DataNode中,這個信息是通過方法notifyNamenodeReceivedBlock,記錄到對應的列表中。

 

 

NameNode上的處理不算複雜,對輸入參數進行檢查以後,調用上面分析的addStoredBlock方法。然後在PendingReplicationBlocks對象中刪除相應的block。

 

  public void errorReport(DatanodeRegistration registration,

                          int errorCode,
                          String msg)

向NameNode報告DataNode上的一個錯誤,如果錯誤是硬盤錯,會刪除該DataNode,其它情況只是簡單地記錄收到一條出錯信息。

  public NamespaceInfo versionRequest() throws IOException;

從NameNode上獲取NamespaceInfo,該信息用於構造DataNode上的DataStorage。

 

 

  UpgradeCommand processUpgradeCommand(UpgradeCommand comm) throws IOException;

我們不討論。

  public void reportBadBlocks(LocatedBlock[] blocks) throws IOException

報告錯誤的數據塊。NameNode會循環調用FSNamesystem的markBlockAsCorrupt方法。處理流程不是很複雜,找對應的INodeFile,如果副本數夠,那麼調用invalidateBlock,使該DataNode上的Block無效;如果副本數不夠,加Block到CorruptReplicasMap中,然後準備對好數據塊進行復制。

目前爲止,我們已經完成了NameNode上的ClientProtocol和DatanodeProtocol的分析了,NamenodeProtocol我們在理解從NameNode的時候,纔會進行分析。

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