Hadoop 基石HDFS 一文了解文件存儲系統
@
前言:淺談Hadoop
Hadoop作為大數據入門的基石內容,其中HDFS更是所有生態的地基,so,我們有必要更深入去理解HDFS,以及HDFS在高可用的演變過程。如果有小可愛說hadoop和HDFS有啥區別的。の。。。,那容我之後在做背書來說明,暖男行為的先提一下:目前我們所說的Hadoop更多是指Hadoop的生態,包括hadoop本身及其他組件,如flume、kafka、hive、Hbase等等,如下圖所示:
其中hadoop本身的結構(以Hadoop2.x為例,3.x亦復如是)如下:
Hadoop的發展歷程
1.1 Hadoop產生背景
Hadoop最早起源於Nutch 。Nutch是一個開源的網路搜索引擎,由Doug Cutting於2002年創建。Nutch的設計目標是構建一個大型的全網搜索引擎,包括網 頁抓取、索引、查詢等功能,但隨著抓取網頁數量的增加,遇到了嚴重的可擴展性問 題,即不能解決數十億網頁的存儲和索引問題。之後,Google發表的兩篇論文為該問題 提供了可行的解決方案。一篇是2003年發表的關於Google分散式文件系統(GFS)的論 文。該論文描述了Google搜索引擎網頁相關數據的存儲架構,該架構可解決Nutch遇 到的網頁抓取和索引過程中產生的超大文件存儲需求的問題。但由於Google僅開源了思 想而未開源程式碼,Nutch項目組便根據論文完成了一個開源實現,即Nutch的分散式 文件系統(NDFS)。另一篇是2004年發表的關於Google分散式計算框架MapReduce 的論文。該論文描述了Google內部最重要的分散式計算框架MapReduce的設計藝術, 該框架可用於處理海量網頁的索引問題。同樣,由於Google未開源程式碼,Nutch的開發 人員完成了一個開源實現。由於NDFS和MapReduce不僅適用於搜索領域,2006年 年初,開發人員便將其移出Nutch,成為Lucene [4]的一個子項目,稱為Hadoop。大 約同一時間,Doug Cutting加入雅虎公司,且公司同意組織一個專門的團隊繼續發 展Hadoop。同年2月,Apache Hadoop項目正式啟動以支援MapReduce和HDFS 的獨立發展。2008年1月,Hadoop成為Apache頂級項目,迎來了它的快速發展期。
文字必定是比較枯燥的,貼心為大家準備一個時間樹:
1.引入HDFS設計
HDFS作為Hadoop的旗艦級文件系統(也就是說除了HDFS,Hadoop也可以與其他文件存儲系統集成,所以不能簡單認為HDFS和Hadoop是有等價關係的哦。),被定義為以流式數據訪問模式來存儲超大文件,運行於廉價的商用硬體集群上。
我們解讀一下其定義:
- 超大文件 。即超過幾百MB或者GB甚至幾百TB大小的文件來充分發揮其優勢;
- 流式數據訪問:也是HDFS的構建思想:一次寫入、多次讀取是最高效的訪問模式。數據集通常由數據源生成或者從數據源複製而來,此後長時間在此數據集上進行各種分析,因此其讀取數據的時間延遲不滿足即席查詢。HDFS是為高數據吞吐量應用優化的,對於低時間延遲的數據訪問是不適合的,這是其優點也在某種程度看是缺點。
1.1 HDFS主要特性
- 支援超大文件。超大文件這裡指的是幾百MB、幾百GB甚至幾TB大小的文件,一般來說,一個Hadoop文件系統會存儲T(1T=1024GB)、P(1P=1024TB)級別的數據。Hadoop需要能夠支援這種級別的大文件;
- 檢測和快速應對硬體故障:在大量通用硬體平台上構建集群時,故障,特別是硬體故障是常見的問題。一般的HDFS系統是由數百台甚至上千台存儲者數據文件的伺服器組成,這麼多的伺服器意味著高故障率。因此,過賬檢測和自動恢復是HDFS的一個設計目標;
- 流式數據訪問:HDFS處理的數據規模都比較大,應用一般需要訪問大量的數據。同時,這些應用一般是批量處理,而不是用戶互動式處理。HDFS使應用程式能夠以流的形式訪問數據集,注重的是數據的吞吐量,而不是數據訪問的速度;
- 簡化的一致性模型:大部分的HDFS程式操作文件是需要一次寫入,多次讀取。在HDFS中,一個文件一旦經過創建、寫入、關閉後,一半就不需要修改了。這樣簡單的一致性模型,有利於提供高吞吐量的數據訪問模型。
2.HDFS體系結構
在一個全配置的集群上,「運行HDFS」意味著在網路分布的不同伺服器上運行一些守護進程(daemon),這些進程有各自的特殊角色,並相互配合,一起形成一個分散式文件系統。
HDFS採用了主從(Master/Slaver)體系結構,名位元組點 NameNode、數據節點DataNode和客戶端Client是HDFS的三個重要的角色。
- NameNode 管理文件系統的命名空間,維護者文件系統及整棵樹內所有的文件和目錄。這些資訊以兩個文件形式永久保存在本地磁碟中:命名空間鏡像文件(Fsimagexxx)和編輯日誌文件(editxxx)。NameNode 也記錄著每個文件中各個塊所在的數據節點資訊,但是並不會永久保存塊的位置資訊,而是在系統啟動的時候根據數據節點資訊重建(在系統啟動的時候,各個DataNode會向NameNode進行註冊,同時彙報數據節點的資訊)。
- SecondaryNameNode,也稱SNN或2NN,是用於定期合併命名空間鏡像和鏡像編輯日誌的輔助守護進程。其與NameNode的區別是,2NN不接受或者說不記錄HDFS的任何實時變化,而只是根據集群配置的時間間隔,不停的獲取HDFS某一個時間點的命名空間鏡像和鏡像編輯日誌,合併得到一個新的命名空間鏡像。該新鏡像會上傳到NameNode,替換原有的命名空間鏡像,並清空上述日誌。2NN配合NameNode,為NameNode提供了一個簡單的檢查點(checkpoint)機制,並避免出現編輯日誌過大,導致NameNode啟動時間過長的問題。
- DataNode 是文件系統的工作節點,再其內部都會駐留一個數據節點的守護進程,來執行分散式文件系統中最忙碌的部分:將HDFS數據塊寫到Linux本地文件系統的事件文件中,或者從這些實際文件讀取數據塊。它們根據需要存儲並檢索數據塊(受客戶端和NameNode調度),並定期(心跳為每三秒一次,當NameNode10分鐘沒有接受到DataNode的心跳就認為節點不可用,發送資訊為每一小時一次)向NameNode發送他們所存儲的塊的列表。
通常DataNode從磁碟讀取數據塊,但是對於訪問頻繁的文件,其對應的塊可能被顯式的快取在DataNode的記憶體中,以堆外快快取的形式存在。默認情況下一個塊僅快取一個DataNode的記憶體中。作業調度器(用於MapReduce、Spark和其他框架的)通過在快取塊的DataNode上運行任務,可以利用塊快取的優勢提高讀操作的性能。 - 客戶端:用戶與HDFS進行交互的手段。HDFS提供了各種各樣的客戶端,包括命令行介面、JavaAPI、用戶文件系統等等。
此外,由於NameNode將文件系統的元數據存儲在記憶體中,因此一個HDFS文件系統所能存儲的文件總數受限於NameNode的記憶體容量(一般一個文件、目錄和數據塊的存儲資訊即元數據大約佔150位元組)。因此當有一百萬個文件,且每個文件佔一個數據塊,至少需要300M的記憶體。注意一點:Hadoop的數據是分塊在物理存儲的(hadoop2.x及之後版本,塊默認其大小為128M,塊的劃分大小隻和磁碟的傳輸速率強正相關),但是在DataNode節點實際佔用的空間大小和文件真實大小一致,而不是佔據整個塊的空間(當一個1MB的文件存在一個一個128MB的塊中時,文件只是用1MB的磁碟空間,而不是128MB)。
簡單的速算,當定址時間為10ms,磁碟傳輸速率為100M/S,一般經驗認為定址時間占傳輸時間的1%為佳,so,傳輸時間即為1s,故大小為100M,存儲單位都是二進位,故設置為128MB,如果磁碟傳輸速率為500M時,那麼相應的塊大小可以設置為512MB,可以通過配置參數dfs.blocksize 來設置。
有必要說明一下:分散式文件系統中的塊抽象帶來的好處:
1.一個文件的大小可以大於網路中任意一個磁碟的容量,(也就是說我們需要的文件有2T,但是單個節點的存儲只有1T,這也是Hadoop應用的得意之處)。文件的塊並不需要存儲在同一個磁碟上,我們將大文件按塊劃分為多個文件,並將這些塊存儲在任意的節點磁碟進行儲存。
2.使用抽象塊而不是整個文件作為存儲單元,大大簡化了存儲子系統的設計。將存儲子系統的處理獨享設置為塊,可簡化存儲管理,由於塊的大小是固定,因此計算單個磁碟可以存儲多少個塊就相對簡單。同事也消除了對元數據的顧慮,塊只是來存儲數據,並不存儲文件的元數據,就可以將數據和元數據分離,單獨管理元數據。
3.塊的抽象非常適合數據備份,將每個塊複製到幾個物理上互相獨立的節點(默認為3),可以確保在塊、磁碟或者機器發生故障後數據不會丟失。如果發現一個塊不可用,系統會從其他地方讀取另一個副本,而這個過程對用戶是透明的。且當一個損壞或機器故障而丟失的塊可以從其他存儲的節點複製到另一台正常運行的機器上,保證複本數量是滿足設定的。進而提供了系統數據容錯能力和可用性。
HDFS工作流程機制
當我們向HDFS上傳數據的時候是怎麼進行,HDFS是如何工作的呢?基於NameNode和DataNode分別都做了那些工作呢?我們一一道來。
1.各個節點是如何互通有無的?
分散式系統,如瀏覽器和伺服器端的通訊,需要在不同的實體中顯示交換資訊,處理諸如消息的編解碼、發送和接收等具體任務。Hadoop中各個實體間的交互通過遠程過程調用(RPC),讓用戶可以像調用本地方法一樣調用另外一個應用程式提供的服務,而不必關注具體的實現。從而提升了可操作性和交互性。那我們先來了解一下什麼是RPC。
RPC原理
簡要地說,RPC就是允許程式調用位於其他機器上的過程(也可以是同一台機器上的不同的進程)。當機器A上的進程調用機器B上的進程時,A上的調用進程被掛起,而B上的被調用進程開始執行。調用方使用參數將資訊傳送給到被調用方,然後通過傳回的結果得到資訊。在這個過程中,A是RPC客戶端,B是RPC服務端。同時我們不用關注任何消息的傳遞,就像在一個過程到另一個過程的調用一樣,如同方法的調用。
class ProgressDemo{
public static void main(String[] args){
...
func(a1,a2,...,an);
...
}
public static int func(int p1,int p2,... ,int pn){
...
}
}
上邊是一個簡單的常規過程調用
RPC調用示例
RPC的Server運行時會阻塞在接受消息的調用上,當接到客戶端的請求後,它會解包以獲取請求參數,類似於傳統過程調用,被調用函數從棧中接受參數,然後確定調用過程的名字並調用相應過程。調用結束後,返回值通過主程式打包並發送回客戶端,通知客戶端調用結束。
我們對於RPC先有一個大致映象,幫助我們理解後續的一些內容,具體的RPC實現可以參考分散式系統相關內容。
客戶端操作文件與目錄
我們從客戶端到NameNode有大量的元數據操作,比如修改文件名,創建子目錄等。這些操作只涉及到客戶端和NameNode的交互。
剖析文件寫入HDFS
-
1 (步驟1)客戶端通過對Distributed FileSystem對象調用create() 來新建文件
-
2(步驟2、3).Distributed FileSystem 對NameNode 創建一個RPC(Remote Procedure Call Protocol 遠程過程調用協議)調用(關於RPC調用,可看這裡什麼是RPC調用),讓NameNode執行同名方法,在文件系統中的命名空間中新建一個文件,此時該文件還沒有相應的數據塊。NameNode創建新文件時,執行各種不同的檢查以確保這個文件不存在以及客戶端有新建該文件的許可權等等。如果這些檢查通過,NameNode就會構建一個新文件,並記錄創建操作到編輯日誌edits中;否則,當用戶沒有許可權或者默認情況下沒有說明覆蓋文件的情況下,會發生文件創建失敗並向客戶端拋出一個IOException異常。通過檢查之後,DistributedFileSystem向客戶端返回一個FSDataOutputStream對象,FSDataOutputStream封裝一個DFSOutputStream對象(DFSOutputStream是對應的實例對象),此對象處理DataNode和NameNode之間的通訊。
用更通俗的話來說,這一步可以細分為兩個步驟,Distributed FileSystem 一下簡寫DFS。- I 客戶端通過DFS對象對NameNode發送請求創建一個和上傳文件同名的空文件,此時NameNode會檢測是否有同名文件以及請求方是否有創建文件的許可權。這個時候會根據檢查結果返回不同的響應結果。
- II 若檢查通過,DFS對象會給客戶端一個FSDataOutputStream對象,就像是給客戶端一個寫入數據的入口,客戶端不必關心具體是向那些DataNode發送數據的,這些工作都交給FSDataOutputStream中的DFSOutputStream實例對象來完成,客戶端就向此對象寫入數據流即可。
-
3(步驟4、5) 在客戶端寫入數據時,DFSOutputStream將它分成一個個數據包,並寫入內部隊列,稱為「數據隊列」。DataStreamer處理數據隊列,其責任是挑選出適合存儲數據複本的一組DataNode(此處挑選DataNode的原則要素為複本數量和節點就近距離(拓撲網路)),並要求NameNode分配新的數據塊,因為上步我們只是創建了一個空文件,所以DFSOutputStream實例首先向NameNode節點申請數據塊,申請成功之後(內部調用addBlock()方法,返回是否成功),就獲得對應的數據塊對象(此對象包含數據塊的數據塊表示和版本號)。這一組DataNode就構成了一個管線,複本數量決定了管線中節點的數量,默認複本數為3,則節點有三台。DataStreamer將數據包流式傳輸到管線中第一個DataNode,該DataNode存儲數據包並將它發送到管線中的第2個DataNode。同樣,第2個DataNode存儲該數據包並且發送給第3個(也就是最後一個)DataNode。
-
4(步驟6)DFSOutputStream也維護著一個內部數據包隊列來等待DataNode的收到確認回執,成為「確認隊列(ack queue)」。收到管道中所有DataNode確認資訊後,該數據包才會從確認隊列刪除。
如果任何DataNode在數據寫入期間發生故障,則執行以下操作(對客戶端是透明的)。首先關閉管線,確認把隊列中的所有數據包都添加回數據隊列的最前端,以確保故障節點下游的DataNode不會漏掉任何一個數據包。為存儲在另一正常DataNode的當前數據塊指定一個新的標識,並將該標識傳送給NameNode,以便故障DataNode在恢復後可以刪除存儲的部分數據塊。從管線中刪除故障DataNode,基於正常的DataNode重新構建一條新管線。餘下的數據塊寫入管線中正常的DataNode。NameNode注意到塊複本量不足時,會在另一個節點上創建一個新的複本。
-
5(步驟7、8)客戶端完成數據寫入後,對數據流調用close()方法,意味著客戶單不會再向六中寫入數據。該操作將剩餘的所有數據包寫入DataNode管線,並在聯繫到NameNode告知其文件寫入完成之前,等待確認(步驟8)。NameNode已經知道文件有哪些塊組成(因為DataStreamer請求分配數據塊),所以他在返回成功前只需要等待數據塊進行最小量的複製。
剖析HDFS文件讀取
為了了解客戶端及與之交互的HDFS、NameNode和DataNode之間的數據流,我們參考上圖解釋一下讀取文件是發生的事件順序。
- 1)客戶端通過調用FileSystem(Java API )對象的open()方法來打開希望讀取的文件,對於HDFS來說,該對象是DistributedFileSystem的一個實例;
- 2)DFS(DistributedFileSystem)通過使用遠程過程調用(RPC)來調用NameNode,以確定文件起始塊的位置,對於每一個塊,NameNode返回存有該副本的DataNode地址。DataNode的選擇依舊是依據集群的網路拓撲排序。(如果該客戶端本身就是一個DataNode【比如:在一個MapReduce任務中】,那麼客戶端就會從保存有相應數據塊複本的本地DataNode讀取數據);
- 3)DFS類返回一個FSDataInputStream對象(一個支援文件的輸入流)給客戶端以便讀取數據。FSDataInputStream 類內封裝DFSInputStream對象,由該對象管理DataNode和NameNode的I/O。接著客戶端對這個輸入流調用read()方法。
- 4)存儲文件起始幾個塊的DataNode地址的DFSInputStream隨機連接距離最近的文件中第一個塊所在的DataNode。通過對數據流反覆調用read()方法,將數據從DataNode傳輸到客戶端。到達塊的末端時,DFSInputStream關閉與該DataNode的連接,然後尋找下一個塊的最佳DataNode。所有操作對於客戶端都是透明的,對客戶端而言都是讀取一個連續的流。
- 5)客戶端從流中讀取數據時,塊是按照打開DFSInputStream與DataNode新建連接的順序讀取的。會根據需要詢問NameNode來檢索下一批數據塊的DataNode位置。一旦客戶端完成讀取,就對FSDataInputStream調用close()方法。
在整個讀取過程中,如果DFSInputStream與DataNode通訊時遇到錯誤,會嘗試從這個快的另外一個最近鄰DataNode讀取數據,也會記住故障DataNode,保證以後不會反覆讀取該節點上後續的塊。DFSInputStream也會通過校驗和確認從DataNode發來的數據是否完整。如果發現有損壞的塊,DFSInputStream會試圖從其他DataNode讀取其複本,同時會將損壞的塊通知NameNode。
結論
因而,沒有NameNode,整個HDFS將無法使用。事實上,如果運行NameNode服務的及其損壞,文件系統上所有的文件將會丟失,因為我們不知道如何根據DataNode的塊重建文件。
HDFS是怎麼保證運行的?
NameNode 容錯機制
因此,對於NameNode實現容錯非常重要,Hadoop為此提供了兩種機制。
- 1.第一種機制是備份那些組成文件系統元數據持久狀態的文件。Hadoop可以通過配置使NameNode在多個文件系統上保存元數據的持久狀態。這些寫操作是實時同步且是原子性的。當時NameNode的處理效率會變低,實時去持久化是不被接受的。一般的配置:將持久狀態寫入本地磁碟的同時,寫入一個遠程掛在的網路文件系統(NFS)。
- 2運行一個輔助NameNode,但不能被用作NameNode。這個輔助NameNode的重要作用是 定期合併編輯日誌與命名空間鏡像,以防止編輯日誌過大。這個輔助NameNode一般在另一台單獨的物理電腦上運行,他需要佔用大量CPU時間,並且需要和NameNode一樣多的記憶體來執行合併操作。他會保存合併後的命名空間鏡像的副本,並在NameNode發生故障時啟動。但是輔助NameNode保存的狀態總是滯後於主節點,所以在主節點全部失效時,難免會丟失部分數據。這種情況下,一般是將存儲在NFS的NameNode元數據複製到輔助NameNode並作為新的主NameNode運行。
方式二的部分也正是hadoop2.x就有的方式,配置NameNode(NN)和secondNameNode(2NN)。且2NN可以將NameNode的鏡像文件和編輯日誌文件合併過程接手處理,減少NameNode的額外開銷。具體的NN和2NN之間的處理關係會稍後詳細在講。
如何NN突破記憶體限制?聯邦HDFS設計思想
NameNode在記憶體中保存文件系統中每個文件和每個數據塊的引用關係。意味著對於一個擁有大量文件的超級集群而言,單台NameNode記憶體限制了系統橫向擴展的瓶頸。
且根據NameNode的架構局限性:
1)Namespace(命名空間)的限制
由於NameNode在記憶體中存儲所有的元數據(metadata),因此單個NameNode所能存儲的對象(文件+塊)數目受到NameNode所在JVM的heap size的限制。50G的heap能夠存儲20億(200million)個對象,這20億個對象支援4000個DataNode,12PB的存儲(假設文件平均大小為40MB)。隨著數據的飛速增長,存儲的需求也隨之增長。單個DataNode從4T增長到36T,集群的尺寸增長到8000個DataNode。存儲的需求從12PB增長到大於100PB。
2)隔離問題
由於HDFS僅有一個NameNode,無法隔離各個程式,因此HDFS上的一個實驗程式就很有可能影響整個HDFS上運行的程式。
3)性能的瓶頸
由於是單個NameNode的HDFS架構,因此整個HDFS文件系統的吞吐量受限於單個NameNode的吞吐量。
在2.x的發行版本引入了 HDFS Federation(聯邦HDFS),該方案允許系統通過添加NameNode實現擴展,其中每個NameNode管理文件系統命名空間中的一部分。例如,一個NameNode可能管理/user目錄下的所有文件,而另一個NameNode管理/share 目錄下的所有文件。
如何解決單點故障問題?
通過聯合使用在多個文件系統中備份NameNode的元數據和通過備用NameNode創建檢測點能防止數據丟失,但是依舊無法實現文件系統的高可用性。NameNode依舊存在單點失效(SPOF,single point of failure )的問題。如果NameNode失效了,那麼所有的客戶端包括MapReduce作業,均無法工作,以為內NameNode是唯一存儲元數據與文件到數據塊映射的地方。
想要一個失效的NameNode恢復,系統管理員的啟動一個擁有文件系統元數據副本的新的NameNode,並配置DataNode和客戶端使用這個新的NameNode。新的NameNode需要滿足以下情況才能響應服務:
- 1)將命名空間的映像導入記憶體中
- 2) 重演編輯日誌;
- 3) 接受到足夠多的來自DataNode的數據塊報告並退出安全模式。
對於一個大型並擁有大量文件和數據塊的集群,NameNode的冷啟動需要30分鐘,或者更長時間。系統恢復時間太長,也會影響到日常維護。
Hadoop2 針對以上問題增加了對HDFS的高可用性(HA)的支援。通過配置一對活動-備用 NameNode。當活動NameNode失效, 備用NameNode就會接管他的任務並開始服務與來自客戶端的請求,不會有任何明顯的中斷。也就是NN和2NN之間的處理邏輯。HA會在後邊再開一篇來討論它的實現以及好處。
我們先來了解一下NN和2NN的工作機制。
我們在上邊已經說了NameNode的元數據存儲是通過FsImage和Edits日誌文件來完成的。為什麼會有這樣的設計?
我們不妨假設,如果元數據存儲在NameNode節點的磁碟中,因為經常需要進行隨機訪問,還有響應客戶請求,必然是效率過低。因此,元數據需要存放在記憶體中。
但如果只存在記憶體中,一旦斷電,元數據丟失,整個集群就無法工作了。因此產生在磁碟中備份元數據的FsImage。這樣又會帶來新的問題,當在記憶體中的元數據更新時,如果同時更新FsImage,就會導致效率過低,但如果不更新,就會發生一致性問題,一旦NameNode節點斷電,就會產生數據丟失。
因此,引入Edits文件(只進行追加操作,效率很高)。每當元數據有更新或者添加元數據時,修改記憶體中的元數據並追加到Edits中。這樣,一旦NameNode節點斷電,可以通過FsImage和Edits的合併,合成元數據。
但是,如果長時間添加數據到Edits中,會導致該文件數據過大,效率降低,而且一旦斷電,恢復元數據需要的時間過長。因此,需要定期進行FsImage和Edits的合併,如果這個操作由NameNode節點完成,又會效率過低。因此,引入一個新的節點SecondaryNamenode,專門用於FsImage和Edits的合併。
NameNode啟動
- (1)第一次啟動NameNode格式化後,創建Fsimage和Edits文件。如果不是第一次啟動,直接載入編輯日誌和鏡像文件到記憶體。
- (2)客戶端對元數據進行增刪改的請求。
- (3)NameNode記錄操作日誌,更新滾動日誌。
- (4)NameNode在記憶體中對元數據進行增刪改。
Secondary NameNode工作
- (1)Secondary NameNode詢問NameNode是否需要CheckPoint(checkpoint在2NN默認每隔一小時執行一次checkpoint檢測,查看是否需要execute checkpoint,並每一分鐘進行一次操作數檢測,當操作數達100萬時,2NN執行checkpoint)。直接帶回NameNode是否檢查結果。
- (2)Secondary NameNode請求執行CheckPoint。
- (3)NameNode滾動正在寫的Edits日誌。
- (4)將滾動前的編輯日誌和鏡像文件拷貝到Secondary NameNode。
- (5)Secondary NameNode載入編輯日誌和鏡像文件到記憶體,併合並。
- (6)生成新的鏡像文件fsimage.chkpoint。
- (7)拷貝fsimage.chkpoint到NameNode。
- (8)NameNode將fsimage.chkpoint重新命名成fsimage。