HDFS學習隨筆

1.流式數據訪問

HDFS的構建思想是這樣的:一次寫入,多次讀取是最高效的訪問模式。數據集通常有數據源生成或從數據源複製而來,接着長時間在此數據集上進行各類分析。每次分析都將設計數據集的大部分數據甚至全部,因此讀取整個數據集的時間延遲比讀取第一條數據的時間延遲更重要。

2.關於時間延遲的數據訪問

要求低時間延遲數據訪問的應用,例如幾十毫秒的範圍,不適合在HDFS上運行,記住,HDFS是爲高數據吞吐量應用優化的,這可能會以高時間延遲爲代價。目前對於低時間延遲的數據訪問應用,HBase是更好的選擇。

3.大量的小文件

由於namenode將文件系統的元文件存儲在內存中,因此該文件系統所能存儲的文件總數受限於namenode的內存容量。根據經驗,每個文件、目錄和數據快的存儲信息大約佔150字節。因此,舉例來說,如果有一百萬個文件,且每個文件佔一個數據塊,那麼至少需要300MB的內存,儘管存儲上百萬的文件是可行的,但是存儲數十億個文件就超出了當前硬件的能力。

4.HDFS中的塊

HDFS中的塊默認大小爲64MB,與單一磁盤上的文件系統類似,HDFS中的文件也被劃分爲塊大小的多個分塊(chunk),作爲獨立的存儲單元。但與其他的文件系統不同的是,HDFS中小於一個塊大小的文件不會佔據整個塊的空間。HDFS的塊比磁盤塊(一般爲512KB)大,目的是爲了最小化尋址開銷。如果塊設置的足夠大,從磁盤傳輸數據的時間明顯大於定位這個塊起始位置所需的時間。這樣傳輸一個由多個塊組成的文件的時間取決於磁盤傳輸速率。但是該參數也不應過大,MapReduce中map任務通常一次處理一個塊的數據,因此如果任務數太少(少於集羣中的節點數),作業的運行速度就會比較慢。

對分佈式文件系統中的塊進行抽象會帶來許多好處。第一個明顯的好處是,一個文件的大小可以大於網絡中任意一個磁盤的容量。文件的所有塊並不需要存儲在同一個磁盤上,因此他們可以利用集羣中的任意一個磁盤進行存儲。事實上,儘管不常見,但對於HDFS集羣而言,也可以僅存儲一個文件,該文件的塊佔滿集羣中的所有磁盤。

第二個好處是,使用塊而非整個文件作爲存儲單元,大大簡化了存儲子系統的設計。

不僅如此,塊非常適合用於數據備份進而提供數據容錯能力和可用性。將每個塊複製到少數幾個獨立的機器上(默認是3個),可以確保塊、磁盤或機器故障時數據不丟失。

與磁盤文件系統類似,HDFS中fsck指令可以顯示塊信息。

hadoop fsck / -files -blocks

在HDFS裏面,data node上的塊大小默認是64MB(或者是128MB或256MB)


問題: 爲什麼64MB(或128MB或256MB)是最優選擇?

  • 爲什麼不能遠少於64MB(或128MB或256MB) (普通文件系統的數據塊大小一般爲4KB)
    • 減少硬盤尋道時間(disk seek time)

          HDFS設計前提是支持大容量的流式數據操作,所以即使是一般的數據讀寫操作,涉及到的數據量都是比較大的。假如數據塊設置過少,那需要讀取的數據塊就比較多,由於數據塊在硬盤上非連續存儲,普通硬盤因爲需要移動磁頭,所以隨機尋址較慢,讀越多的數據塊就增大了總的硬盤尋道時間。當硬盤尋道時間比io時間還要長的多時,那麼硬盤尋道時間就成了系統的一個瓶頸。合適的塊大小有助於減少硬盤尋道時間,提高系統吞吐量。


    • 減少Namenode內存消耗
          對於HDFS,他只有一個Namenode節點,他的內存相對於Datanode來說,是極其有限的。然而,namenode需要在其內存FSImage文件中中記錄在Datanode中的數據塊信息,假如數據塊大小設置過少,而需要維護的數據塊信息就會過多,那Namenode的內存可能就會傷不起了。



  • 爲什麼不能遠大於64MB(或128MB或256MB)
這裏主要從上層的MapReduce框架來討論
    • Map崩潰問題:

系統需要重新啓動,啓動過程需要重新加載數據,數據塊越大,數據加載時間越長,系統恢復過程越長。


    • 監管時間問題:

         主節點監管其他節點的情況,每個節點會週期性的把完成的工作和狀態的更新報告回來。如果一個節點保持沉默超過一個預設的時間間隔,主節點記錄下這個節點狀態爲死亡,並把分配給這個節點的數據發到別的節點。對於這個“預設的時間間隔”,這是從數據塊的角度大概估算的。假如是對於64MB的數據塊,我可以假設你10分鐘之內無論如何也能解決了吧,超過10分鐘也沒反應,那就是死了。可對於640MB或是1G以上的數據,我應該要估算個多長的時間內?估算的時間短了,那就誤判死亡了,分分鐘更壞的情況是所有節點都會被判死亡。估算的時間長了,那等待的時間就過長了。所以對於過大的數據塊,這個“預設的時間間隔”不好估算。


    • 問題分解問題:

數據量大小是問題解決的複雜度是成線性關係的。對於同個算法,處理的數據量越大,它的時間複雜度也就越大。


    • 約束Map輸出:

         在Map Reduce框架裏,Map之後的數據是要經過排序才執行Reduce操作的。想想歸併排序算法的思想,對小文件進行排序,然後將小文件歸併成大文件的思想,然後就會懂這點了....


5.namenode和datanode

HDFS集羣中有兩類節點,並以管理者-工作者模式運行,即一個namenode(管理者)和多個datanode(工作者)。

namenode管理文件系統的命名空間。它維護文件系統樹和文件系統數中所有文件和目錄。這些信息以兩種方式永久保存在本地磁盤上:命名空間鏡像文件和編輯日誌文件。

datanode是文件系統的工作者。它們存儲並提供定位塊的服務(被用戶或名稱節點調用時),並且定時的向名稱節點發送它們存儲的塊的列表。

沒有namenode,文件系統將無法使用。如果namenode機器損壞,那麼文件系統上的文件將會丟失,因此對實現namenode的容錯非常重要,Hadoop爲此提供了兩種機制:

第一種機制是備份那些組成文件系統元數據持久狀態的文件。一般配置是,將持久態寫入本地磁盤的同時,寫入一個遠程掛在的網絡文件系統(NFS)。

另一種方式是運行一個輔助的namenode。雖然它不能作爲名稱節點使用。這個二級名稱節點的重要作用就是定期的通過編輯日誌合併命名空間鏡像,以防止編輯日誌過大。這個二級名稱節點一般在其他單獨的物理計算機上運行,因爲它也需要佔用大量 CPU 和內存來執行合併操作。它會保存合併後的命名空間鏡像的副本,在名稱節點失效後就可以使用。
但是,二級名稱節點的狀態是比主節點滯後的,所以主節點的數據若全部丟失,損失仍在所難免。在這種情況下,一般把存在 NFS 上的主名稱節點元數據複製到二級名稱節點上並將其作爲新的主名稱節點運行。

6.HDFS基本操作

hadoop fs -copyFromLocal /home/ubuntu/a.txt hdfs://namenode:9000/user/ubuntu/a.txt

hadoop fs -copyToLocal hdfs://namenode:9000/user/ubuntu/a.txt /home/ubuntu/a.copy.txt

hadoop fs -mkdir hdfs://namenode:9000/user/ubuntu/input

hadoop fs -ls hdfs://namenode:9000/user/ubuntu/

hadoop distcp hdfs://namenode1/foo hdfs://namenode2/bar (並行複製)把namenode1集羣的foo目錄(和它的內容)複製到namenode2集羣的bar目錄下。

7.客戶端讀取HDFS中的數據

(1)客戶端通過調用FileSystem對象的open()方法來打開希望讀取的文件,對於HDFS來說,這個對象是分佈式文件系統的一個實例。

(2)DistributedFileSystem通過RPC(遠程過程調用)來調用namenode,以確定文件起始塊的位置。對於每一個塊,namenode返回存有該塊副本的datanode地址。此外這些datanode根據他們與namenode的距離來排序。

          DistributedFileSystem類返回一個FSDataInputStream對象(一個支持文件定位的輸入流)給客戶端並讀取數據。FSDataInputStream類轉而封裝DFSInputStream對象,該對象管理着namenode和datanode的I/O。

(3)接着客戶端對這個輸入流調用read()方法。存儲着文件起始塊的datanode地址的DFSInputStream隨即連接距離最近的datanode。

(4)通過對數據流反覆調用read()方法,可以將數據從datanode傳輸到客戶端。

(5)到達塊的末端時,DFSInputStream會關閉與該datanode的連接,然後尋找下一個塊的最佳datanode。客戶端只需連續的讀取連續的流,並且對於客戶端都是透明的。

          客戶端從流中讀取數據時,塊是按照打開DFSInputStream與datanode新建連接的順序讀取的。它也需要詢問namenode來檢索下一批所需塊的datanode的位置。

(6)一旦客戶端完成讀取,就對DFSInputStream調用close()方法。

          在讀取數據的時候,如果DFSInputStream與datanode的通信出現錯誤,它便會嘗試從這個塊的另外一個最鄰近的datanode讀取數據。它也會記住出現故障的datanode,以保證以後不會反覆讀取該節點上後續的塊。DFSInputStream也會通過“校驗和”確認從datanode發來的數據是否完整。如果發現一個損壞的塊,它就會在DFSInputStream試圖從其他datanode讀取一個塊副本之前通知namenode。

8.寫入HDFS

我們要考慮的情況是如何創建一個新文件,並把數據寫入該文件,最後關閉該文件。

(1)客戶端對DistributedFileSystem對象調用create()方法來創建文件。

(2)DistributedFileSystem對namenode創建一個RPC調用,在文件系統的命名空間中創建一個新文件,此時該文件中還沒有相應的數據塊。namenode執行各種檢查以確保這個文件不存在,並且客戶端有創建該文件的權限。如果這些檢查均通過,namenode就會爲創建新文件記錄一條記錄;否則,創建失敗,並向客戶端拋出一個IOException異常。DistributedFileSystem向客戶端返回一個FSDataOutputStream對象,由此客戶端可以開始寫數據。就像讀取數據一樣,FSDataOutputStream封裝一個DFSOutputStream對象,該對象負責處理datanode和namenode之間的通信。

(3)在客戶端寫入數據時,DFSOutputStream將他們分成一個個的數據包,並寫入內部隊列,成爲數據隊列(data queue)。

(4)DataStreamer處理數據隊列,它的責任是根據datanode隊列來要求namenode分配合適的新塊來存儲數據備份。這一組datanode組成一個管線——我們假設副本數量爲3,所以管線中有3個節點。DataStreamer將數據包流式的傳輸到管線的第一個datanode,該datanode存儲數據並將數據發送到管線的第二個datanode。同樣的,第二個datanode存儲該數據包併發送給管線中的第三個(也就是最後一個)datanode。

(5)FSOutputStream也維護着一個內部數據包隊列來等待datanode的收到確認回執,成爲“確認隊列”(ack queue)。當管線中所有datanode確認信息後,該數據包纔會從確認隊列中刪除。

          如果在寫入期間,datanode遇到故障,則執行一下操作,這對於寫入客戶端是透明的。首先關閉管線,確認把隊列中的任何數據包都放回到數據隊列的最前端,以保證故障點下游的datanode不會漏掉任何一個數據包。爲存儲在另一個正常datanode的當前數據塊指定一個新的標識,並將標識傳遞給namenode,以便故障datanode在恢復後可以刪除存儲的部分數據塊。從管線中刪除故障節點並把餘下的數據塊寫入管線中的兩個正常datanode。namenode注意到塊副本量不足,會在另一個節點上創建一個新的副本。後續的數據塊繼續正常接受處理。

(6)客戶端完成寫入後,會對數據流調用close()方法。該操作將剩餘的所有數據包寫入datanode管線中,並在聯繫namenode且發送文件寫入完成信號之前,等待確認。

(7)namenode已經知道文件由那些數據塊組成(通過DataStreamer詢問數據塊的分配),所以它在返回成功之前只需等待數據塊進行最小量的複製。

9.複本的佈局

namenode如何選擇在哪個datanode存儲複本(replica)?這裏需要在可靠性,寫入帶寬和讀取帶寬之間進行權衡。

Hadoop的默認佈局策略是在運行客戶端的節點上放第一個複本。第二個複本與第一個不同且隨機另外選擇的機架中節點上(離架)。第三個複本與第二個複本放在相同的機架上,且隨機選擇另外一個節點。其他的複本放在集羣中隨機選擇的節點上,不過系統會避免在相同的機架上放太多複本。

10.一致模型

HDFS提供一個方法來強制所有緩存與數據節點同步,及對DataOutputStream調用sync()方法。當sync()方法放回成功後,對所有新的reader而言,HDFS能保證到目前爲止寫入的數據均一致且可見。

11.HDFS的數據完整性

HDFS會對寫入的所有數據計算校驗和(checksum),並在讀取數據時驗證校驗和。它針對每個有io.bytes.per.checksum指定字節的數據計算校驗和。默認情況下爲512字節,由於CRC -32校驗和是4個字節,所以存儲校驗和的額外開銷小於1%。

datanode負責在驗證收到的數據後存儲數據及其校驗和。它在收到客戶端數據或複製其它datanode數據期間執行這個操作。正在寫數據的客戶端將數據及其校驗和發送到由一系列datanode組成的管線,管線的最後一個datanode負責驗證校驗和。

客戶端從datanode中讀取數據時也會驗證校驗和,將它們與datanode中的校驗和進行比較。每個datanode都會持久化存儲一個用戶驗證的校驗和日誌,所以它知道每個塊最後一次驗證時間。客戶端成功驗證一個數據塊後,會告訴這個datanode,datanode由此更新日誌。

不只是客戶端讀取數據時會驗證校驗和,每個datanode也會在一個後臺線程中運行一個DataBlockScanner,從而定期檢查存儲在這個datanode上的所有數據塊。

可以使用RawLocalFileSystem類來禁用校驗和。

12.壓縮

壓縮格式總結
壓縮格式 工具 算法 文件擴展名 是否包含多個文件 是否可切分
DEFLATE N/A DEFLATE .deflate
Gzip gzip DEFLATE .gz
bzip2 bzip2 bzip2 .bz2
LZO Lzop LZO .lzo
所有壓縮算法都要權衡時間/空間:壓縮和解壓縮速度更快,其代價通常只是節省少量空間。表中列出的壓縮工具都提供9個不同的選項來控制壓縮時必須考慮的權衡:選項-1爲優化速度,-9爲優化壓縮空間。

gzip是一個通用的壓縮工具,在空間/時間權衡中,居於其他兩種壓縮方法之間。bzip2更高效,但是更慢。LZO優化壓縮速度,但是壓縮效率稍遜一籌。

在hadoop中可以使用CompressionCodec對數據流進行壓縮和解壓縮。如果要對寫入輸出流的數據進行壓縮,可用createOutputStream(OutputStream out)方法在在底層的數據流中對需要以壓縮格式寫入在此之前尚未壓縮的數據建立一個CompressionOutputStream對象,相反,對輸入數據流讀取數據進行解壓縮時,調用createInputStream(InputStream in)獲取CompressionInputStream。

13.序列化

所謂序列化(serialization),是將結構化對象轉化成字節流,以便在網絡上傳輸或寫入磁盤永久保存。反序列化,是將字節流轉化回結構化對象的過程。

序列化在分佈式數據處理的兩大領域中廣泛出現:進程間通信(RPC)和永久儲存。

hadoop只用自己的序列化格式Writable,它格式緊湊,速度快。

14.Writable

Writable類的層次結構:

Java類型的Writable封裝
Java基本類型 Writable實現 序列化大小(字節)
boolean BooleanWritable 1
byte ByteWritable 1
int IntWritable 4
  VintWritable 1~5
float FloatWritable 4
long LongWritable 8
  VlongWritable 1~9
double DoubleWritable 8
String(UTF-8) Text  
15.MapReduce作業運行機制

可以只用一行代碼來運行一個MapReduce作業:JobClient.runJob(conf)(如果是較新的版本,其實質也是調用這個方法)。分析其過程細節:

整個過程如圖所示,包含如下4個獨立的實體:

  • 客戶端:提交MapReduce作業。
  • jobtracker:協調作業運行。
  • tasktracker:運行作業劃分後的任務。
  • 分佈式文件系統:用來在其他實體間共享作業文件。

(1)作業的提交:

JobClient的runjob()方法是創建JobClient實例並調用它的submitJob()方法的快捷方式(步驟1)。作業提交後,runJob()每秒輪詢作業的進度,如果發現自上次報告後有變化,便把進度報告到進度臺。作業完成後,如果成功,就顯示作業計數器。如果失敗,導致作業失敗的錯誤被記錄到控制檯。

JobClient實現的submitJob()方法實現的作業提交過程如下:

  • 向jobtracker請求一個新的作業ID。
  • 檢查作業的輸出說明。例如,如果沒有指定作業的輸出目錄或者輸出目錄已存在,就不能提交作業,錯誤拋回MapReduce程序。
  • 計算作業的輸入分片。
  • 將作業所需的資源(包括作業JAR文件,配置文件和計算所得的輸入分片)複製到一個以作業ID命名的目錄下jobtracker的文件系統中。
  • 告知jobtracker作業準備執行。

(2)作業的初始化

當JobTracker接收到jobclient的submitJob()方法調用後,會把此調用放入一個內部隊列中,交由作業調度器(job scheduler)進行調度,並對其進行初始化。初始化包括建立一個正在運行作業的對象——封裝任務和記錄信息,以便跟蹤任務的狀態和進程。

爲了創建任務運行列表,作業調度器首先從共享文件系統獲取JobClient已經計算好的輸入分片信息。然後爲每個分片創建一個map任務。創建的reduce任務數有JobConf的mapred.reduce.task屬性決定。然後調度器創建相應數量的reduce任務。任務ID在此時被指定。

(3)任務的分配

tasktracker運行一個簡單的循環來定期發送“心跳”(heartbeat)給jobtracker。心跳告訴jobtracker,tasktracker是否還存活,同時也充當兩者之間的消息通道。作爲“心跳”的一部分,tasktracker會指明它是否已經準備好運行新的任務,如果是,jobtracker會爲它分配一個任務,並使用“心跳”的返回值與tasktracker進行通信(步驟7)。

在jobtracker爲tasktracker選定任務之前,jobtracker必須先選定任務所在的作業。默認的方法是維護一個簡單的作業優先級列表。當然還有各種調度算法。

對於map任務和reduce任務,tasktracker有固定數量的任務槽。默認調度器會在處理reduce任務槽之前,先填滿map任務槽。

爲了選擇一個reduce任務,jobtracker簡單的從待運行的reduce任務列表中選取下一個來執行,用不着考慮數據的本地化。然後,對於一個map任務,jobtracker會考慮tasktracker的網絡位置,並選取一個距離其輸入分片最近的tasktracker。

(4)任務的執行

現在,tasktracker已經被分配了一個任務,下一步是運行任務。第一步,通過共享文件系統將作業的JAR複製到tasktracker所在的文件系統,從而實現JAR文件本地化。同時,tasktracker將程序所需的全部文件從分佈式緩存複製到本地磁盤(步驟8)。第二步,tasktracker爲任務新建一個本地工作目錄,並把JAR文件解壓到這個文件夾下。第三步,tasktracker新建一個TaskRunner實例來運行該任務。

TaskRunner啓動一個新的JVM(步驟9)來運行每個任務(步驟10),以便用戶定義的map和reduce函數的任何軟件問題都不會影響到tasktracker(例如導致崩潰或掛起等)。但是在不同的任務間共享JVM是可能的。子進程通過umbilical接口與父進程進行通信。任務的子進程每隔幾秒便告訴父進程它的進度,直到任務完成。

(5)作業的完成

當jobtracker收到作業最後一個任務完成的通知後,便把作業的狀態設爲“成功”。然後,JobClient查看作業狀態時,便知道任務已完成,於是JobClient打印一條消息告知用戶,然後從runJob()方法返回。

最後jobtracker清空作業的工作狀態,指示tasktracker也清空工作狀態(如刪除中間輸出等)。

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