Hadoop基礎-HDFS的讀取與寫入過程
作者:尹正傑
版權聲明:原創作品,謝絕轉載!否則將追究法律責任。
為了了解客戶端及與之交互的HDFS,NameNode和DataNode之間的數據流是什么樣的,我們需要詳細介紹一下HDFS的讀取以及寫入過程,本篇博客的觀點是在我讀《Hadoop權威指南,大數據的存儲與分析》整理的筆記。
一.剖析HDFS文件讀取

上圖顯示了HDFS在讀取文件時事件的發生順序。大致總結為以下幾個步驟:
1>.客戶端通過調用FileSystem對象的open()放啊來打開希望讀取的文件,對於HDFS來說,這個對象DistributedFileSystem的一個實例;
2>.DistributedFileSystem通過使用遠程過程調用(RPC)來調用namenode,以確定文件起始塊的位置;對於每一個塊,NameNode返回存有該塊的副本的DataNode地址。此外,這些DataNode根據它們與客戶端的距離來排序(根據集群的網絡拓撲,詳情請參考:https://www.cnblogs.com/yinzhengjie/p/9142230.html)。如果該客戶端本身就是一個DataNode(比如,在一個MapReduce任務中),那么該客戶端將會從保存有相應的數據塊副本的本地DataNode讀取數據;
3>.DistributedFileSystem類返回一個FSDataInputStream對象(一個支持文件定位的輸入流)給客戶端以便讀取數據。FSDataInputStream類轉而封裝DFSInputStream對象,對該對象管理着DataNode和NameNode的I/O,接着,客戶端對這個輸入流調用read()方法;
4>.存儲着文件起始幾個塊的DataNode地址的DFSInputStream隨即連接距離最近的文件中第一個塊所在的DataNode。通過對數據流反復調用read()方法,可以將數據從DataNode傳輸到客戶端;
5>.到達塊末端時,DFSInputStream關閉該DataNode的連接,然后找尋下一個塊的最佳DataNode。所有這些對於客戶端都是透明的,在客戶看來它一直在讀取一個連續的流;
6>.客戶端從流中讀取數據時,塊是按照打開DFSInputStream與DataNode新建連接的順序讀取的。他也會根據需要詢問NameNode來檢索下一批數據塊的DataNode的位置。一旦客戶端完成讀取,就對FSDataInputStream調用close()方法;
在讀取數據的時候,如果DFSInputStream在與DataNode通信時遇到錯誤,會嘗試這個塊的另一個最近DataNode讀取數據。他也會記住那個故障DataNode,以保證以后不會反復讀取該節點上后續的塊。DFSInputStream也會通過校驗和確認從DataNode發來的數據是否完整。如果發現有損失的塊,DFSInputStream會試圖從其它DataNode讀取其副本,也會將抒懷的塊通知給NameNode。
這個設計一個重點是,客戶端可以直接鏈接到DataNode檢索數據,且NameNode告知客戶端每個塊所在的最佳DataNode。由於數據流分散在集群中所有DataNode,所以這種設計能夠使HDFS擴展到大量的並發客戶端。同時,NameNode只需要相應塊位置的請求(這些信息存儲的在內存中,因而非常高效),無需相應數據請求,否則隨着科幻數量的增長,NameNode會很快稱為瓶頸。
二.剖析HDFS文件寫入
、
接下來我們看看文件是如何寫入HDFS的,盡管比較詳細,但對於理解數據流還是很有用的,因為它清楚說明了HDFS的一致模型。我們要考慮是如何新建一個文件,把數據寫入該文件,最后關閉該文件。上圖顯示了HDFS在寫入文件時事件的發生順序,大致總結為以下幾個步驟:
1>.客戶端通過對DistributedFileSystem對象調用create()來新建文件;
2>.DistributedFileSystem對NameNode創建一個RPC調用,在文件系統的命名空間中新建一個文件,此時該文件中還沒有形影的數據塊。NameNode執行各種不同的檢查以確保這個文件不存在以及客戶端有新建該文件的權限。如果這個檢查均通過,NameNode就會創建新文件記錄一條記錄;否則,文件創建失敗並向客戶端拋出一個IOException 異常。DistributedFileSystem向客戶端返回一個FSDataOutputStream對象,由此客戶端可以開始寫入數據。就像讀取事件一樣,FSDataOutputStream封裝一個DFSoutPutStream對象,該對象負責處理DataNode和NameNode之間的通信;
3>.在客戶端寫入數據時嗎,DFSOutputStream將它分為一個個的數據包(每個packet大小為64k),當包滿了之后,也就是數據大於64k,數據會並寫入內部隊列,稱為“數據隊列”(data queue)。DataStreamer 處理數據隊列,它的責任是挑選出適合存儲數據副本的一組DataNode,並據此來要求NameNode分配新的數據塊。這一組DataNode構成一個管線,我我們假設副本數為3,所有管線中有3個節點。
4>.DataStream將數據包流式傳輸到管線中第一個DataNode,該DataNode存儲數據包並將它發送到管線中的第二個DataNode。同樣,第二個DataNode存儲該數據包並且發送給管線中的第三個(也是最后一個)DataNode;
5>.DFSOutputSteam也維護着一個內部數據包隊列來等待DataNode的收到確認回執,稱為“確認隊列”(ack queue)。收到管道中所有DataNode確認信息后,該數據包才會從確認隊列刪除。如果任何DataNode在數據寫入期間發生故障,則執行一下操作(對寫入數據的客戶單是透明的,也就是說客戶端根本體驗不到這個過程)。首先關閉管線,確認把隊列中的所有數據包都添加回數據隊列的前端,以確保故障節點下游的DataNode不會漏掉任何一個數據包。為存儲在另一正DataNode的當前數據塊指定一個新的標識,並將該標識傳送給NameNode,以便掛賬DataNode在恢復后可以刪除存儲的部分數據塊。從管線中刪除故障DataNode,基於兩個正常DataNode構建一條新管線。余下的數據塊寫入管線中正常的DataNode。NameNode注意到塊副本量不足時,會在另一個節點上穿件一個新的復本。后續的數據塊繼續正常接受處理。在一個塊被寫入期間可能會有多個DataNode同時發生故障,但非常少見。只要寫入了“dfs.namenode.repliocation.min”的副本數(默認為1),寫操作就會成功,並且這個塊在集群中異步復制,直到達到其目標副本數(dfs.replication的默認值為3)。
6>.客戶端完成數據的寫入后,對數據流調用close()方法。
7>.該操作將剩余的所有數據包寫入DataNode管線,對數據流調用close()方法。該操作將剩余的所有數據包寫入DataNode管線,並在聯系到NameNode告知其文件寫入完成之前,等待確認。NameNode已經知道文件由哪些塊組成(因為Datastreamer請求分配數據塊),所以他在返回成功前只需要等待數據塊進行最小量的復制。
三.寫入過程代碼調試片段
1>.DFSPacket的格式說明

上圖是調試代碼時Hadoop的部分源碼注釋,為了方便理解,我們可以大致畫一個DFSPacket結構圖,如下:

上圖你可能會問126是怎么來的?其實用一個計算公式算出來的,一個包的大小是64k,換算成字節則為:65536字節,而一個chunk的大小為512字節,每個chunk都會進行依次校驗,而每個循環冗余校驗大小為4個字節,包頭信息會占用33個字節,因此我們就得到了一個計算DFSPacket個數的公式:(65536 - 33)/(512 + 4) = 126.9437984496124 ,很顯然一個packet裝不下127個chunk,故一個包中,最多能存儲126個chunk。
2>.flushBuffer
我們在FSOutputSummer中的flushBuffer中可以看到writeChecksumChunks(4608)的字樣,說明它是每隔4608字節進行一次flushBuffer,這個4608是怎么來的呢?4608=512x9,而512是對數據進行校驗的基本單位。

四.副本的存儲選擇
NameNode如何選擇在哪個DataNode存儲副本(replica)?我們要從以下三個角度考慮:
1>.可靠性;
2>.寫入帶寬;
3>.讀取帶寬;
接下來我們舉兩個極端的例子,把所有的副本都存儲在一個節點上,損失的寫入寫入帶寬最小,因為復制管線都是在同一個節點上運行。但這個並不提供給真實的冗余,如果節點發生故障,那么該塊中的數據會丟失。同時,同一機架服務器鍵的讀取帶寬是很高的。另一個極端,把復本放在不同的數據中心可能最大限度的提高冗余,但帶寬的損耗非常大。即使在同一個數據中心。到目前為止,所有Hadoop集群軍鄖縣在同一個數據中心內,也有很多可能數據布局策略。
Hadoop的默認布局策略是在運行客戶端的節點上放第一個復本(如果客戶端運行在集群之外,就隨機選擇一個節點,不過系統會避免挑選那些存儲太滿或太忙的節點)。第二個復本放在與第一個不同且隨機另外選擇的機架中節點上(離架)。第3個副本與第二個副本放在同一個機架上,且隨機選擇另一個節點。其它副本放在集群中隨機選擇的節點上,不過系統會盡量避免在同一個機架上放太多的副本。一旦選定副本的存放位置,就根據網絡拓撲創建一個管線,如果副本數為3,則如下圖所示的管線。

總的來說,這一方法不僅提供很好的穩定性(數據塊存儲在兩個機架中)並實現很好的負載均衡,包括寫入帶寬(寫入操作只需要遍歷一個交換機),讀取性能(可以從兩個機架中選擇讀取)和集群中塊中均勻分布(客戶端只在本地機架上寫入一個塊)。
