說明
除了標(biāo)注之外,本文純屬原創(chuàng),轉(zhuǎn)載請注明出處:http://www.lxweimin.com/p/ea6ef5f5b868
HDFS架構(gòu)簡介
Hadoop的框架最核心的設(shè)計就是:HDFS和MapReduce。HDFS為海量的數(shù)據(jù)提供了存儲,則MapReduce為海量的數(shù)據(jù)提供了計算。本文基于Hadoop 2.7.3源碼,分析本地文件推送(新建/追加)到的HDFS客戶端邏輯。
- HDFS架構(gòu)主要包含兩種類型的節(jié)點:NameNode和DataNode。
- NameNode,其實就是名字節(jié)點,其功能類似于我們常用的磁盤文件系統(tǒng)中的inode。對于HDFS而言,NameNode相當(dāng)于“目錄管理器”和“inode表”。
- NameNode保存兩類關(guān)鍵的映射表:
- 名字空間表:從文件名到數(shù)據(jù)塊(DataBlock)的映射,這部分?jǐn)?shù)據(jù)保存在NameNode服務(wù)器的磁盤。
- inode表:從數(shù)據(jù)塊(DataBlock)到機器的映射,包括每一個數(shù)據(jù)塊保存在哪一個或者哪幾個機器上。這部分?jǐn)?shù)據(jù)在每次重啟NameNode的時候都會和DataNode通訊并重建。
- 對于Hadoop 2.7.3而言,一個DataBlock默認(rèn)是128MB,所以一個文件可能需要N個DataBlock來存儲,那么名字空間表很可能是一個文件名映射到一個DataBlock的數(shù)組。
- 關(guān)于這兩張表如何協(xié)作定位文件:
- 當(dāng)使用文件名訪問文件時,NameNode會查詢名字空間表,根據(jù)這個文件名獲取它所有內(nèi)容對應(yīng)的DataBlock列表(是不是很類似于單機磁盤的數(shù)據(jù)訪問)。此時inode表會查詢每一個DataBlock的信息,包括它所在的位置(DataNode的IP+端口)、DataBlock的ID和時間戳以及里面數(shù)據(jù)的長度(<=128MB)等。
- 這個DataBlock列表返回到客戶端,客戶端根據(jù)每個DataBlock上的信息(線索),分別連接到每個DataNode上,獲取上面存儲的數(shù)據(jù)。
- 客戶端與NameNode、NameNode與DataNode的連接,全部都是通過ProtoBuf的RPC調(diào)用來實現(xiàn)的。關(guān)于ProtoBuf可以參考這里。例如,下面就是追加文件的append請求的RPC協(xié)議:
//摘自hadoop-hdfs-project/hadoop-hdfs/src/main/proto/ClientNamenodeProtocol.proto
//RPC請求
rpc append(AppendRequestProto) returns(AppendResponseProto);
//請求報文
message AppendRequestProto {
required string src = 1;
required string clientName = 2;
optional uint32 flag = 3; // bits set using CreateFlag
}
//應(yīng)答報文
message AppendResponseProto {
optional LocatedBlockProto block = 1;
optional HdfsFileStatusProto stat = 2;
}
HDFS寫文件Pipeline機制
HDFS在對文件的寫入方面,只允許數(shù)據(jù)追加到文件末尾,而不允許在文件中間修改文件。因為在文件中間修改文件,需要涉及文件鎖、數(shù)據(jù)塊之類的比較復(fù)雜的邏輯。
Hadoop的文件按照DataBlock分塊,并以DataBlock為單位做冗余(負(fù)載均衡)。HDFS可以指定一個復(fù)制因子(replication),默認(rèn)是保存3份,根據(jù)dfs.replication
配置項配置。
下面分析HDFS寫文件的Pipeline流程(藍色線表示用于通訊,紅色線表示數(shù)據(jù)的傳輸路線):
- ①客戶端發(fā)送請求到NameNode,請求寫文件/新建數(shù)據(jù)塊。
- NameNode收到請求后,會給客戶端分配一個數(shù)據(jù)塊,其ID是
blk_123456
,并指明DataBlock各個拷貝所在的各個DataNode的IP和端口(圖中是分別存在于三個DataNode中)。 - 這一系列的DataNode稱為Pipeline,也就是數(shù)據(jù)傳輸?shù)墓艿溃簿褪恰綝ataNode_1:50010, DataNode_2:50010, DataNode_3:50010】。
- ②客戶端收到數(shù)據(jù)塊的信息,開始對DataNode發(fā)起寫的請求,請求報文包括要寫的數(shù)據(jù)塊,要寫的數(shù)據(jù)大小等等。請求成功后,發(fā)送數(shù)據(jù)到第一個DataNode,也就是圖中的DataNode_1,在該請求中包含DataBlock各個拷貝的地址(包含DataNode2和DataNode3的地址):【DataNode_1:50010, DataNode_2:50010, DataNode_3:50010】,發(fā)送完成之后等待DataNode_1返回的ACK報文。
- ③DataNode_1收到數(shù)據(jù)后,保存數(shù)據(jù),并把數(shù)據(jù)發(fā)送到DataNode_2,Pipeline修改為【DataNode_2:50010, DataNode_3:50010】,發(fā)送完成之后等待DataNode_2返回的ACK報文。
- ④DataNode_2收到數(shù)據(jù)后,把數(shù)據(jù)發(fā)送到DataNode_3,Pipeline修改為【DataNode_3:50010】,發(fā)送完成之后等待DataNode_2返回的ACK報文。
- ⑤DataNode_3發(fā)現(xiàn)Pipeline中只有自己,不再有下游的DataNode節(jié)點,于是處理完成之后只需要返回ACK到Pipeline的上游節(jié)點,即DataNode_2。
- ⑥D(zhuǎn)ataNode_2收到DataNode_3的ACK,于是把ACK發(fā)送到Pipeline的上游節(jié)點,即DataNode_1。
- ⑦DataNode_1收到DataNode_2的ACK,把ACK發(fā)送到Pipeline的上游節(jié)點,即客戶端。
數(shù)據(jù)發(fā)送至此完成。
HDFS文件推送客戶端
要把本地文件推送到HDFS,可以通過以下兩個命令實現(xiàn):
hadoop fs -appendToFile <localsrc> ... <dst>
hadoop fs -put [-f] [-p] [-l] <localsrc> ... <dst>
跟蹤調(diào)用堆棧發(fā)現(xiàn),這兩個命令最終是調(diào)用DFSOutputStream.java
中的代碼實現(xiàn)文件的拷貝。
輔助發(fā)送的相關(guān)類和數(shù)據(jù)結(jié)構(gòu)
這份代碼里面包含了一些用于輔助發(fā)送的類:
-
DFSOutputStream
:實現(xiàn)了發(fā)送數(shù)據(jù)的主流程,最主要是繼承自FSOutputSummer
這個虛擬類的接口方法writeChunk
。 -
DataStreamer
:繼承自Daemon
的后臺線程,主要實現(xiàn)數(shù)據(jù)的流式發(fā)送。 -
ResponseProcessor
:同樣繼承自Daemon
的后臺線程,主要實現(xiàn)對已發(fā)送數(shù)據(jù)包的ACK報文的接收。
還有一些保存發(fā)送數(shù)據(jù)相關(guān)信息的數(shù)據(jù)結(jié)構(gòu):
-
DFSPacket
:表示發(fā)送出去的一個數(shù)據(jù)包,包含相應(yīng)的請求頭部以及相關(guān)標(biāo)志位。 -
LinkedList<DFSPacket> dataQueue
:用于保存待發(fā)送的數(shù)據(jù)包。它是主線程DFSOutputStream
和發(fā)送線程DataStreamer
之間生產(chǎn)者-消費者關(guān)系*的共享數(shù)據(jù)結(jié)構(gòu)。 -
LinkedList<DFSPacket> ackQueue
:用于保存已經(jīng)發(fā)送的數(shù)據(jù)包。發(fā)出去的數(shù)據(jù)包還要等待DataNode返回ACK才可以被認(rèn)為是發(fā)送成功。它是發(fā)送線程DataStreamer
與ACK接收線程ResponseProcessor
之間生產(chǎn)者-消費者關(guān)系的共享數(shù)據(jù)結(jié)構(gòu)。 -
BlockConstructionStage stage
:這是一個狀態(tài)變量,整個發(fā)送流程就相當(dāng)于一個狀態(tài)機。
看完上面的數(shù)據(jù)結(jié)構(gòu),整個數(shù)據(jù)發(fā)送流程就很明顯了:
<u>DFSOutputStream
把數(shù)據(jù)組裝成DFSPacket
對象,放入dataQueue
;然后等待發(fā)送線程DataStreamer
發(fā)送到DataNode;DataStreamer
發(fā)送之后,把DFSPacket
對象移動到ackQueue
,等待ACK線程ResponseProcessor
在收到對應(yīng)的ACK之后把該DFSPacket
從隊列移除。</u>
下面主要分析DFSOutputStream.java
這個客戶端代碼的執(zhí)行流程。
數(shù)據(jù)發(fā)送的主要流程
-
newStreamForCreate/newStreamForAppend
這兩個靜態(tài)函數(shù)用于創(chuàng)建DFSOutputStream
對象。一個是用于新建文件,一個用于追加到現(xiàn)有的文件。兩個函數(shù)主要差別在于,前者需要新建一個文件(發(fā)送create的RPC請求到NameNode),后者直接通過發(fā)送append的RPC請求到NameNode,在返回報文中獲取文件最后的一個數(shù)據(jù)塊并開始寫入。 -
newStreamForCreate/newStreamForAppend
這兩個函數(shù)返回一個DFSOutputStream
的對象,然后被org.apache.hadoop.io.IOUtils.copyBytes()
調(diào)用DFSOutputStream
的writeChunk
接口函數(shù),把本地的數(shù)據(jù)塊發(fā)送出去。下面主要看writeChunk
函數(shù)。這個函數(shù)的參數(shù)主要包括數(shù)據(jù)的緩沖區(qū)、要發(fā)送的數(shù)據(jù)在DataBlock中的offset、還有數(shù)據(jù)的校驗等。