當數據量增大到超出了單個物理計算機存儲容量時,有必要把它分開存儲在多個不同的計算機中。那些管理存儲在多個網絡互連的計算機中的文件系統被稱為“分布式文件系統”。由于這些計算機是基于網絡連接的,所以網絡編程的那些復雜性都會涉及,這也造成了分布式文件系統比一般的磁盤存儲文件系統更復雜。例如,其中最大的一個難題是如何使文件系統因其中一個節點失敗而不造成數據丟失。
Hadoop使用的分布式文件系統稱為HDFS,即Hadoop Distributed Filesystem。在非正式或早期文檔或配置文件中見到DFS也指的是HDFS。HDFS是Hadoop最重要的文件系統,是這章節要講的核心。但是Hadoop實際上具有通用文件系統抽象層,所以我們也順便看一下Hadoop如何與其它存儲系統集成,例如本地文件系統和Amazon S3。
HDFS的設計
設計HDFS的目的是為了能夠存儲非常大體積的文件,這些文件能夠以流的方式訪問,并能夠運行于一般日常用的硬件設備集群中。讓我們更詳細地說明一下這句話的意思。
非常大體積的文件
這里的"非常大"意思是文件的大小是幾百M,幾百G或者幾百T。今天的運行的Hadoop集群能夠存儲P級數據。
流式數據訪問
HDFS認為一次寫入,多次讀取的模式是最高效的處理模式,它圍繞著這個模式建立。數據集通常都是自生成或從源數據復制,然后隨著時間推移會進行多次分析。每次分析可能不是所有數據,但也會是占數據集很大比例的數據,所以讀取整個數據集所花的時間比讀取第一條數據延遲的時間更重要。
日常用的硬件設備
Hadoop不要求昂貴的,高可靠的硬件。它被設計能夠運行于一般常用的硬件之上。常用的硬件指的是能夠從多個供應商購買到的一般的可用的硬件。這種便件集群中節點失敗的可能性更大,至少對于大的集群來說是這樣。HDFS被設計的目的就是當節點失敗時能夠繼續運行,而不會讓用戶察覺到明顯的中斷。
并不是所有應用領域HDFS都有出色的表現,雖然這可能在將來改變。有如下領域HDFS不太適合:
- 低延遲數據讀取
那些要求數據讀取幾十毫秒延遲的應用不適合使用HDFS。記住,HDFS對于傳輸高吞吐量數據進行了優化,這也許以延遲為代價。HBase當前是低延遲比較好的選擇,見第20章節。 - 大量的小文件
由于文件元數據存儲在內存中的名稱結點中,所以內存中名稱節點大小決定了能夠存儲文件的數量。根據經驗,每一個文件名,目錄或塊名占用150字節,所以如果你有一百萬個文件,每一個文件占一塊,你將至少需要300M內存空間。雖然存儲幾百萬個文件是沒問題的,但存儲十億個文件就超出了當前硬件能夠容納的數量。 - 多個寫入器,文件任意修改
HDFS的文件只有一個寫入器,并且只在文件結束時以追加的方式寫入。不支持多個寫入器或者能在文件當中任意一個位置修改。這也許在將來被支持,但它們很可能相對低效一些。
Hadoop概念
塊
硬盤中的塊指一次讀取或寫入的最小單位數量。基于此的單塊硬盤的文件系統處理多塊中的數據,處理的塊大小是單塊大小的整數倍。文件系統塊大小通常是幾M大小,而硬盤中的塊大小正常是512字節。這對于操作文件系統的用戶來說是透明的。這些用戶僅僅需要讀取或寫入任意長度的文件即可,不用關心塊大小。然后,有一些工具可以維護文件系統,例如df和fsck.這些工具直接在塊級別操作。
HDFS也有塊的概念。但它的塊單元要大小一些,一般默認是128MB。和單塊硬盤的文件系統一樣,HDFS中的文件也會按照塊大小被拆分獨立存儲,而不同的是,比一塊大小小的數據不會占用一塊的空間,例如塊大小是128M,而文件大小是1M,則此文件只使用了1M空間,而不是128M。沒有特殊說明,本書中的"塊"指的是HDFS中的塊。
為什么HDFS中的塊默認這么大?
HDFS塊跟比硬盤中塊要大的多,目的是為了減少查找的開銷。如果塊足夠大,將數據從硬盤中讀取出來的時間將會比尋找塊起始地址所花的時候要多的多。因此傳輸一個由多個塊組成的大文件時,傳輸時間主要取決于硬盤的傳輸速率。
讓我們簡單計算一下,如果尋址時間是10ms,傳輸速率是100M/s。為了使尋址時間占傳輸時間的1%,我們設置塊大小為100M。默認塊大小時128M,而一些HDFS安裝說明建議設置更大的塊。隨著新一代的硬盤驅動安裝,傳輸速率增加,這個值會越來越大。然而這個值不能設置的太大,因為MapReduce中的map任務通常一次在一塊上執行,如果你有很多map任務,比集群中的節點還多,這時候,作業運行的時候比它應該運行的時間要慢一些。
對分布系統來說,有抽象的塊有幾個優勢。第一個優勢也是最明顯的:一個文件可能比互聯網中任意一個單塊硬盤容量都要大,也沒有理由要求文件的塊都存儲在一塊硬盤中。所以可以利用集群中所有的硬盤。事實上,一個文件的塊可以占滿集群中所有硬盤的空間,雖然這不常發生。
第二,將塊做為抽象單元而不是文件簡化了存儲子系統。簡單是所有系統都努力追求的。這對于分布式系統來說尤其重要,因為分布式系統失敗的情況各種各樣。存儲子系統操作塊僅僅是簡單地管理存儲,因為塊的大小固定,所以很容易計算出指定的硬盤中能夠有多少塊。并且存儲子系統不需要關心元數據。因為塊僅存儲數據,文件的元數據,例如權限信息不需要存儲在塊中,有另外一個系統單獨管理。
最后,塊對于容災和數據獲取都表現不錯。為了應對塊,硬盤或計算機損塊,每一個塊中的數據都會被復制到獨立的幾個物理計算機中(通常是3個)。如果某一塊中的數據不能獲取,可以以某一種方法從另外一個位置讀取塊的復本。這些操作對客戶來說是透明的。而且如果一個塊數據不能讀取,Hadoop就會從其它替代位置讀取塊內容到另外一個正在運行的計算機中以便讓復制參數回到正常水平(可以看"數據健壯性"那一章節了解更多應對數據損多方法)。類似的,許多應用對于經常使用的文件選擇設置高的復制參數,以便在集群中更多地方可以讀取到。
像文件系統中的fsck一樣,hadoop的fsck命令也能操作塊,例如運行下面命令:
% hdfs fsck / -files -blocks
就會列出文件系統中包含文件數據的所有塊(可以參看"文件系統檢查(fsck)"章節)。
名稱節點和數據節點
一個HDFS集群有兩種節點類型。它們以主-從形式工作。一個名稱節點(主)和多個數據節點(從)。名稱節點管理文件系統命名空間。維護文件系統樹和樹中所有文件和目錄的元信息。這些信息以命名空間鏡像和更改日志兩種形式永久存儲在本地硬盤中。從名稱節點可以查到數據節點。這些數據節點存儲著文件的塊數據。然而這些塊并不會永久存儲。因為當系統啟動時,這些塊會重新在數據節點中建立。
代表用戶的客戶端通過與名稱節點和數據節點溝通操作文件系統。客戶端會提供類似可移植操作系統接口(Portable Operating System Interface POSIX)的文件系統接口。所以用戶開發的時候不需要知道怎么操作名稱節點和數據節點。
數據節點是文件系統中苦力勞作者。他們存儲塊數據,并按照客戶端或名稱節點的要求返回塊數據。他們會定期地向名稱節點返回他們存儲的塊列表。
沒有名稱節點,文件系統無法使用。事實上,如果運行名稱節點的計算機徹底損毀了,所有文件將會丟失。因為根本沒法知道怎么樣根據數據節點中的塊重新生成文件。由于這個原因,能夠在當名稱節點損壞后恢復顯得非常重要。Hadoop提供了兩種機制達到這個目的。
第一種方法是備份存儲著文件元信息的文件。能夠配置Hadoop使名稱節點中的數據能夠自動地同步地寫入多個文件系統。通常的配置是一份存儲在本地系統,另外一份存儲在遠程的NFS系統中。
第二種方法是運行另外一個名稱節點,盡管它叫做名稱節點,但它和名稱節點的作用不一樣。它的作用主要是根據更改日志合并名稱節點鏡像文件,以免更改日志過大。第二名稱節點通常在單獨的一個物理機中運行,因為它需要大量占用CPU,并且需要與名稱節點一樣多的內存空間以便執行合并。它還保持著合并后名稱節點的復本,以便當名稱節點失敗后能夠使用。然而,由于第二名稱節點的狀態更新比主計算機慢,所以當主計算機完全損壞時,數據幾乎肯定會丟失。這種情況發生時,通常的做法是從遠程NFS復制一份名稱節點元數據文件到第二節點,并把第二節點所在的計算機做為主計算機運行(注意:可以運行一個熱備用名稱節點而不使用第二節點,如“HDFS高可用性”中所討論的那樣),可以參看"文件系統鏡像和更改日志"章節了解更詳細信息。
塊緩存
正常情況下,數據節點會從硬盤中讀取塊數據。但是對于需要頻繁讀取的文件,這些塊數據可以被緩存在非堆棧的數據節點內存中。雖然在以文件為基礎的系統中,可以配置一個塊數據緩存在幾個數據節點中,但默認情況下,一個塊數據僅僅緩存在一個數據節點內存中。作業調試器(例如:MapReduce,Spark或者其它框架)在緩存了塊數據的數據節點上運行任務時能夠利用這些緩存的塊數據以提高讀取性能。例如,一個小的用于連接查詢的表就比較適合于緩存。
用戶或應用通過向緩存池中發送一個緩存命令告訴名稱節點那些文件需要緩存,緩存多久。緩存池是一個管理型組織,管理著緩存權和資源使用權。
HDFS聯盟
名稱節點會在內存中保存文件系統中所有文件和塊的引用。也就是說在有著非常多的文件的大集群中,內存的大小存為了集群擴充的限制(看"一個名稱節點需要多大內存?"章節)。HDFS聯盟是Hadoop 2.x系統介紹的一種解決方法。它允許集群可以通過增加名稱節點擴充。每一個名稱節點管理著文件系統的一部分。例如:一個名稱節點管理著/user目錄下所有文件,另一個名稱節點管理著/share目錄下所有文件。
在聯盟形式下,每一個名稱空間管理一個命名空間卷和一個塊池。命名空間卷由命名空間的元信息組成。塊池則包括命名空間下所有文件的所有塊數據。命名空間卷相互獨立,意味著名稱節點相互獨立,更進一步地講,某一個名稱節點毀壞了不會影響到被其它名稱節點管理的命名空間的數據獲取。然而,塊池不是分區的,所以集群中數據節點可以被注冊在任意一個名稱節點中,并且可以存儲來自多個塊池中的塊。
為了配置一個HDFS聯盟的集群,客戶端需使用存放在客戶端的表來把文件路徑映射每一個名稱節點。可以通過ViewFileSystem和viewfs://URIs進行配置。
HDFS高可用性
將名稱節點中保存在多個文件系統中和使用第二名稱節點創建檢查點,這兩者的目的都是為了防止數據丟失,然后它并不能保證文件系統的高可用性。名稱節點仍然會有單點故障。如果出現故障,所有的客戶端包括MapReduce作業等將將不能讀取,寫入數據或者顯示文件。因為名稱節點是元數據和文件與塊對應關系存儲的唯一倉庫。如果出現如此情況,整個Hadoop系統將很快中斷服務,直到一個新的名稱節點啟用。
在名稱節點失敗后,為了恢復,管理者必須從所有文件系統元數據備份中選擇一個備份作為主名稱節點啟用,并配置數據節點和客戶端使用這個新的名稱節點。啟用后這個新節點并不能立即投入使用。直到(1)節點中命名空間鏡像載入內存;(2)重新根據更改日志執行一遍失敗的操作;(3)收到足夠多的來自數據節點中塊的報告表明其已離開安全模式,這三步完成后才會啟用。在有大量文件和塊的集群中,冷啟動一個名稱節點需要花費30分鐘或更多。
長的恢復時間對于運維來說是一個問題。事實上,名稱節點不可預料的失敗發生的情況少之又少,而計劃的停機事件在實際中顯得更重要。
Hadoop2通過提供HDFS高可用性(HA)改善了這種狀況。實現上是有兩個名稱節點,一個激活狀態,一個備用狀態。當激活狀態的名稱節點失敗之后,備用名稱節點立即會接替它的任務,服務客戶端的請求。客戶端不會感覺到明顯的中斷。要想實現HA,需要做一些結構上的改變。
- 兩個名稱節點必須能夠使用高速訪問的存儲空間共享更改日志。當備用的名稱節點運行的時候,它會讀取更改日志所有內容,并同步狀態,然后當激活名稱節點寫入新內容時,再讀取新的狀態同步。
- 數據節點必須將塊報告發送給這兩個名稱節點,因為塊之間的映射關系存儲在名稱節點內存中,而不是在硬盤中。
- 使用一種對用戶透明的機制,客戶端必須要被設置成能夠處理名稱節點的失敗后的備援。
- 這個備用的名稱節點包含了第二節點的角色,會對激活的名稱節點中的命名空間進行定期檢查。
對于高訪問的共享存儲有兩種選擇:NFS和QJM(a quorum journal manager)。QJM是專門為HDFS實現的,設計的唯一目的就是能夠快速訪問更改日志,它是大部分HDFS安裝說明推薦的選擇。QJM以日志節點組形式運行,每一次更改都會被寫入大量的日志節點中。通常會有三個日志節點,所以系統能夠容忍它們其中的一個損壞。這樣的方式與ZooKeeper工作方式類似,但是QJM的實現并沒有使用ZooKeeper。然后,需要注意的是,HDFS HA確實使用了ZooKeeper來選擇激活的名稱節點,下一部分會講到。
如果激活的名稱節點失敗了,備用名稱節點一般會在幾十秒之內替代失敗的節點。因為還需要獲取最新的更改日志和更新的塊映射關系。實際觀察到的替代時間將會更長,一般在一分鐘左右,因為系統需要確定激活的名稱節點確實失敗了。
還有一種不太可能發生的情況,當激活的名稱節點失敗后,備用的也停止了工作,管理員仍然能夠冷啟動備用名稱節點。這也比沒有HA的情況要好。從可操作性角度來看,這是一個進步,因為這個過程是一個內嵌在Hadoop中的標準操作過程。
失敗備援(Failover)和筑圍(Fencing)
從激活的名稱節點切換到備用節點由系統中"失敗備援控制器"管理。有各種各樣的失敗備援控制器,但是默認是使用ZooKeeper確保只有一個名稱節點是激活的。每一個名稱節點都對應運行一個輕量級的失敗備援控制器進程,這些控制器進程的作用是通過簡單心跳的機制監視名稱節點,看它是否失敗,并激活備用節點。
失敗備援也能夠由管理員發起,例如在日常維護中。這被稱為"優雅的失敗備援"。因為控制器會在在這兩個名稱節點間進行有序地過渡以交換角色。然而在不優雅地失敗備援情況下,不可能確定失敗的名稱節點已經停止運行了。例如,緩慢的網絡或網絡不通都能觸發失敗備援切換。被切換掉的前一個激活的名稱節點仍然在運行,仍然認為它自己是激活的節點。HA的實例會使用叫做"筑圍(Fencing)"的方法盡全力確保先前的激活節點不能夠對系統造成任何損害或引起系統癱瘓。
QJM僅僅允許同一時間有一個名稱節點編輯更改日志。然而先前激活的名稱節點仍然可能會響應切換前來自客戶端的請求。所以好的辦法是啟動一個SSH筑圍命令殺死這個名稱節點的進程。當使用NFS做為更改日志存儲的時候,需要更強大的筑圍,因為此時不可能保證同一時間只有一個名稱節點編輯更改日志(這也是推薦使用QJM的原因)。這種更強大的筑圍機制的作用包括撤消名稱節點訪問共享存儲目錄權限(通常情況下使用供應商提供的NFS命令)和通過遠程管理命令關閉它的網絡端口。還有最后一種方法,使用被大眾所熟知的“STONITH”技術(shoot the other node in the head),它會通過專業的電源分配單元強制關閉主機電源。
失敗備援由客戶端庫透明處理,最簡單的實現方法是配置客戶端的配置文件。在配置文件中,HDFS URI使用一個邏輯主機名,并把它映射到兩個名稱節點地址。客戶端庫會嘗試每一個名稱節點地址直到操作成功完成。
命令行接口
我們將以命令行的方式來看一看怎么樣與HDFS交互。有很多其它針對HDFS的接口,但是命令行是最簡單的方式之一,也是許多開發者歡迎的方式。
我們首先在一臺服務器上運行HDFS,按照附錄A中的說明搭建一臺偽分布式的Hadoop服務器。稍后,我們將在集群中運行HDFS,讓它具備可擴展和容錯性。
配置偽分布的系統,需要配置兩個屬性。第一個是屬性是fs.defaultFS,設置成hdfs://localhost/,這個屬性用于設置HDFS默認的文件系統。文件系統通過URI來指定,這里我們配置了hdfs URI,讓Hadoop默認使用HDFS。HDFS將根據這個屬性得到主機名和端口,給HDFS名稱節點使用。HDFS將會在localhost,默認8020端口上運行。客戶端也能根據這個屬性知道名稱節點在哪里運行,以便客戶端能連接到名稱節點。
第二個屬性dfs.replication設置成1,這樣HDFS不會按照默認值3復制文件系統塊。當在單個數據節點上運行時,HDFS不能夠將數據塊復制到3個數據節點中時,它將會一直警告塊需要復制。配置成1就解決了這個問題。
基本的文件系統操作
當文件系統準備好的時候,我們就能夠進行一些常規的文件操作了。例如讀取文件,創建目錄,移動文件,刪除數據,列出文件目錄等操作。你可以在每一個命令后鍵入hadoop fs -help得到命令詳細幫助信息。
將本地硬盤上的一個文件復制到HDFS中:
% hadoop fs -copyFromLocal input/docs/quangle.txt \
hdfs://localhost/user/tom/quangel.txt
這條命令使用了Hadoop文件系統Shell命令fs。這個命令包含一些子命令。我們剛才用-copyFromLocal 來表示將quangle.txt復制到HDFS中的/user/tom/quangle.txt中。事實上,我們可以隱去URI中的協議和主機名,hadoop會默認去core-site.xml中去取hdfs://localhost.
% hadoop fs -copyFromLocal input/docs/quangle.txt /user/tom/quangle.txt
我們也可以使用相對路徑,將文件復制到HDFS的根目錄中。我們這個例子中根目錄是/user/tom:
% hadoop fs -copyFromLocal input/docs/quangle.txt quangle.txt
讓我們再把文件從HDFS中復制回本地文件系統,并檢查一下他們是否一樣。
% hadoop fs -copyToLocal quangle.txt quangle.copy.txt
%md5 input/docs/quangle.txt quangle.copy.txt
MD5 (input/docs/quangle.txt) = e7891a2627cf263a079fb0f18256ffb2
MD5 (quangle.copy.txt) = e7891a2627cf263a079fb0f18256ffb2
可以看出MD5碼是一樣的,表明這個文件成功復制到HDFS后,仍然完好無損地復制回來了。
最后,讓我們看一下一個列舉HDFS文件的命令。我們首先創建了一個目錄,然后看看怎么列舉文件:
% hadoop fs -mkdir books
% hadoop fs -ls
drwxr-xr-x - tom supergroup 0 2014-10-04 13:22 books
-rw-r--r-- 1 tom supergroup 119 2014-10-04 13:21 quangle.txt
返回的信息跟Unix命令ls -l返回的信息很相似。但有一些小的區別。第一列顯示文件權限模式,第二列顯示文件的復制參數(這是傳統的Unix文件系統沒有的)。還記得我們在站點范圍的配置文件中配置的默認復制參數是1吧,這就是為什么我們能在這里看見了相同的值。這個值對于目錄來說是空的,因為復制不會應用到目錄,目錄屬于元數據,它們被存儲在名稱節點中,不是數據節點。第三和第四列分別顯示這個文件的所有者和所屬的組。第五列以字節形式顯示這個文件的大小,目錄大小為0。第6和第7列顯示文件或目錄最后被編輯的日期和時間。最后,第8列顯示文件或目錄的名字。
HDFS中文件的權限
HDFS對于文件和目錄有一種權限控制模式,就像POSIX一樣。有三種權限:讀權限(r),寫權限(w),,執行權限(x)。讀權限可以用于讀取文件或列舉目錄下的所有文件內容。寫權限可以用于編輯文件,對于目錄來說,可以創建或刪除目錄中的文件或目錄。HDFS中的文件沒有執行權限,因為HDFS不允許執行文件,這與POSIX不一樣,至于目錄,執行權限可以用于獲取子目錄。
每一個文件或目錄都有一個所有者,一個組和一個模型。這個模型由三部分用戶地權限組成,一部分是所有者權限,一部分是組中成員權限,還有一部分是既不是所有者也不是組成員的用戶權限。
默認情況下,Hadoop沒有開啟安全驗證功能,這就意味著客戶的身份不會被驗證。因為客戶是遠程的,客戶就可以簡單地通過創建賬號變成任意一個用戶。如果開啟了安全驗證功能,這就不可能發生,詳細信息見"安全性"章節。還有另外一個值得開啟安全驗證的原因,那就是為了避免文件系統的重要部分遭到意外的修改或刪除,不管是被用戶或者自動修改的工具或程序修改。
權限驗證開啟后,如果客戶端的用戶是所有者,則使用所有者權限驗證,如果客戶端用戶是組中的一個成員,則使用組權限驗證,如果都不是,則使用其它設定的權限驗證。
Hadoop文件系統
Hadoop的文件系統是一個抽象概念。HDFS僅僅是其中一個實現。org.apache.hadoop.fs.FileSystem這個Java抽象類定義了客戶訪問Hadoop文件的一系統接口。有很多具體的文件系統,表3-1列舉出了幾個適用于Hadoop的文件系統。
文件系統 | URI協議 | Java的實現(所有類在包org.apache.hadoop下) | 描述 |
---|---|---|---|
Local | file | fs.LocalFileSystem | 一個用于本地的具體客戶端校驗硬盤的文件系統。對于沒有校驗的硬盤使用RawLocal FileSystem。見"本地文件系統" |
HDFS | hdfs | hdfs.DistributedFileSystem | Hadoop的分布式文件系統。HDFS被設計用于和MapReduce連接進行高效地工作 |
WebHDFS | webhdfs | hdfs.web.WebHdfsFileSystem | 提供對基于HTTP讀寫HDFS進行權限驗證的文件系統,見"HTTP" |
安全的WebHDFS | swebhdfs | hdfs.web.SWebHdfsFileSystem | WebHDFS的HTTPS版本 |
HAR | har | fs.HarFileSystem | 在另一個文件系統之上的一個文件系統,用于歸檔文件。Hadoop歸檔用于將HDFS中的文件打包歸檔進一個文件中,以減少名稱節點所占的內存。使用hadoop archive命令創建HAR文件 |
View | viewfs | viewfs.ViewFileSystem | 一個客戶端掛載表,作用于另外一個Hadoop文件系統,通常用于對聯盟名稱節點創建掛載點。見"HDFS聯盟" |
FTP | ftp | fs.ftp.FTPFileSystem | 基于FTP服務的文件系統 |
S3 | s3a | fs.s3a.S3AFileSystem | 基于Amazon S3的文件系統,代替舊的s3n(S3 native) |
Azure | wasb | fs.azure.NativeAzureFileSystem | 基于微軟Azure的文件系統 |
Swift | swift | fs.swift.snative.SwiftNativeFile | 基于OpenStack Swift 的文件系統 |
Hadoop提供了很多接口用于操作文件系統,通常使用URI協議來選擇正確的文件系統實例進行交互。我們之前所使用的文件系統shell適用于hadoop所有文件系統。例如為了列舉本地硬盤根目錄下所有文件,使用如下命令:
% hadoop fs -ls file:///
雖然可以運行MapReduce程序從以上任意一個文件系統獲取數據,有時甚至非常方便。但當我們處理非常大批量的數據時,我們應該選擇能夠進行數據本地優化的分布式文件系統,尤其是HDFS(見"擴展"內容)。
接口
Hadoop是用JAVA開發的,所以大多數的Hadoop文件系統交互都是以JAVA API作為中間溝通的橋梁。例如文件系統shell就是一個JAVA應用程序,這個應用程序使用JAVA類FileSystem來操作文件。其它的文件系統接口也會在這一塊簡單地討論。這些接口大多數通常在HDFS中使用,因為HDFS中一般都有現存的訪問底層文件系統的接口,例如FTP客戶端訪問FTP,S3工具使用S3等等。但是他們中的一些適用于任意的hadoop文件系統。
HTTP
Hadoop系統的文件系統接口是用Java開發的,這就使用非JAVA應用很難與HDFS交互。當其它語言需要與HDFS交互時,我們可以使用WebHDFS提供的HTTP REST API接口,這將會容易許多。但是要注意的是HTTP接口會比原生的JAVA客戶端慢,所以如果可以的話,應盡量避免進行大數據量傳輸。
通過HTTP協議,有兩種與HDFS交互的方式。一種是直接通過HTTP與HDFS交互,還有一種是通過代理方式。客戶端訪問代理,代理再代表客戶,通常使用DistributedFileSystem API訪問HDFS。圖3-1說明了這兩種方式,這兩種方式都是使用了WebHDFS協議.使用第一種方法時,內嵌在名稱節點和數據節點中的webservice作為WebHDFS協議的終結點(WebHDFS默認是啟動的,因為dfs.webhdfs.enabled設置成了true)。文件元數據由名稱節點處理,文件的讀或寫操作請求會首先傳給名稱節點,然后名稱節點會向客戶端返回一個HTTP重啟向鏈接,指向數據節點,以便進行文件的流式操作。
使用第二種方法時,通過使用一個或多個獨立的代理服務基于HTTP訪問HDFS。這些代理是無狀態的,所以它們能夠在標準的負載均衡器之后。所以傳向集錄的請求必須經過代理,所以客戶端不會直接與名稱節點和數據節點交互。我們可以在代理層加入更嚴格的防火墻和帶寬限制策略。通常Hadoop集群分布在不同的數據中心時或者需要訪問外部網絡云中的集群時,使用代理來傳輸數據。
HTTPFS代理暴露了與WebHDFS一樣的HTTP(HTTPS)接口。所以客戶端能夠通過webhdfs(swebhdfs) URIs訪問二者。HTTPFS使用httpfs.sh.script啟動,并獨立于名稱節點和數據節點服務器,默認使用一個不同的端口監聽,一般是14000端口。
C
Hadoop提供了一個叫做libhdfs的C函數庫,與Java FileSystem接口功能相同。盡管它是一個訪問HDFS的C函數庫,但卻能被用于訪問任何任意的hadoop文件系統。它通過使用JNI調用Java文件系統接口。與上面講解的WebHDFS接口類似,對應地有一個libwebhdfs庫。
C API與JAVA很像,但它不如JAVA API。因為一些新的特性不支持。你可以在頭文件hdfs.h中看到。這個頭文件位于Apache Hadoop二進制文件分布目錄中。
Apache Hadoop二進制文件中已經有為64位LInux系統預先構建好的libhdfs二進制文件。但對其它系統,你需要自己構建,可以按照原始樹目錄頂層的BUILDING.txt說明來構建。
NFS
通過使用Hadoop的NFSv3網關可以將HDFS掛載到本地的文件系統。然后就可以使用Unix工具,(例如ls和cat)來與文件系統交互,上傳文件。通常還可以使用任意編程語言調用POSIX函數庫與文件系統交互。可以向文件中追加內容,但不能隨機修改文件,因為HDFS僅僅可以在文件末尾寫入內容。
可以看Hadoop文檔了解如何配置運行NFS網關以及怎么樣從客戶端連接它。
FUSE
用戶空間文件系統(FileSystem in userspace)允許用戶空間中實現的因為有人系統可以被集成進Unix文件系統。Hadoop的Fuse-DFS模塊可以使HDFS或任意其它文件系統掛載成一個標準的本地文件系統。Fuse-DFS使用C語言實現,通過libhdfs與HDFS交互。當需要寫數據的時間,Hadoop NFS網關仍然是掛載HDFS更有效的解決方案,所以應該優先于Fuse-DFS考慮。
JAVA接口
這部分,我們將會深入了解與Hadoop文件系統交互的Hadoop FileSystem類。雖然我們一般主要關注對于HDFS的實現即DistributedFileSystem,但是通常來說,你應該基于FileSystem抽象類實現你自己的代碼,能夠盡可能地跨文件系統。例如當測試程序的時候,這顯示非常有用。因為你能夠快速地測試在本地文件系統的數據。
從Hadoop URL讀取數據
從hadoop文件系統讀取文件最簡單的方法之一是使用java.net.URL類,這個類會打開文件的流用于讀取。一般的寫法是:
InputStream in = null;
try {
in = new URL("hdfs://host/path").openStream();
// process in
} finally {
IOUtils.closeStream(in);
}
我們還需要做一些工作讓Java能夠識別Hadoop的hdfs的URL。調用URL的setURLStreamHandlerFactory()方法,傳遞一個FsUrlStreamHandlerFactory類的實例。一個JVM只允許調用一次這個方法。所以它通常在靜態塊中執行。這個限制意味著如果你的程序中某一部分,也許是不在你控制范圍內的第三方組件設置了URLStreamHandlerFactory,你就不能通過這種途徑從Hadoop中讀取數據,下一部分將討論另一種方法。
示例3-1顯示了從Hadoop文件系統中讀取文件并顯示在標準輸出中,就像Unix的cat命令一樣。
public class URLCat {
static {
URL.setURLStreamHandlerFactory(new
FsUrlStreamHandlerFactory());
}
public static void main(String[] args) throws
Exception {
InputStream in = null;
try {
in = new URL(args[0]).openStream();
IOUtils.copyBytes(in, System.out, 4096, false);
} finally {
IOUtils.closeStream(in);
}
}
}
我們充分利用Hadoop提供的現成的IOUtils類用于在finally語句中關閉流,也能用于從輸入流中復制字節并輸出到指定的輸出流中(示例中是System.out)。copyBytes最后面兩個參數是字節數大小和當復制完成后是否關閉輸入流。我們自己手工關閉輸入流,System.out不需要關閉。
看一下示例的調用:
% export HADOOP_CLASSPATH=hadoop-examples.jar
% hadoop URLCat hdfs://localhost/user/tom/quangle.txt
On the top of the Crumpetty Tree
The Quangle Wangle sat,
But his face you could not see,
On account of his Beaver Hat.
使用FileSystem API讀取數據
正如上一部分所講的那樣,有時我們不能夠使用SetURLStreamHandlerFactory。這時候,我們就需要使用文件系統的API打開一個文件的輸入流。
Hadoop文件系統中的文件由一個Hadoop路徑對象表示(不是java.io.File對象,雖然它的語義與本地文件系統很接近)。你可以把一個路徑想象成Hadoop文件系統URI,例如:hdfs://localhost/user/tom/quangle.txt。
FileSystem是常用的文件系統API。所以第一步獲取一個FileSystem實例。本例中,需要獲取操作HDFS的FileSystem實例。有幾個靜態方法可以獲取FileSystem實例。
public static FileSystem get(Configuration conf) throws IOException
public static FileSystem get(URI uri, Configuration conf) throws IOException
public static FileSystem get(URI uri, Configuration conf, String user)
throws IOException
Configuration對象封裝了客戶端或服務器的配置。這些配置來自于classpath指定路徑下的配置文件,例如:etc/hadoop/core-site.xml。第一個方法返回默認的filesystem對象(core-site指定的對象,如果沒指定,則默認是本地的filesystem對象)。第二個方法使用給定的URI協議和權限決定使用的filesystem,如果URI中沒有指定協議,則按照配置獲取filesystem。第三個方法獲取指定用戶的filesystem,這對于上下文的安全性很重要。可以參看"安全"章節。
在某些情況下,你需要獲取一個本地文件系統的實例對象,這時,你可以方便地使用getLocal()方法即可。
public static LocalFileSystem getLocal(Configuration conf) throws IOException
獲得了filesystem實例對象后,我們可以使用open()方法獲取一個文件的輸入流。
public FSDataInputStream open(Path f) throws IOException
public abstract FSDataInputStream open(Path f, int bufferSize) throws IOException
第一個方法使用默認的buffer大小:4KB.
將以上方法合起來,我們可以重寫示例3-1,見示例3-2:
示例:3-2
public class FileSystemCat {
public static void main(String[] args) throws Exception {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
InputStream in = null;
try {
in = fs.open(new Path(uri));
IOUtils.copyBytes(in, System.out, 4096, false);
} finally {
IOUtils.closeStream(in);
}
}
}
程序的運行結果如下:
% hadoop FileSystemCat hdfs://localhost/user/tom/quangle.txt
On the top of the Crumpetty Tree
The Quangle Wangle sat,
But his face you could not see,
On account of his Beaver Hat.
FSDataInputStream
FileSystem的open()方法實際返回了一個FSDataInputStream對象而不是標準的java.io.class對象。這個類繼承了java.io.DataInputStream,支持隨機訪問,所以你可以從文件流任意部分讀取。
package org.apache.hadoop.fs;
public class FSDataInputStream extends DataInputStream
implements Seekable, PositionedReadable {
// 實現部分省略
}
Seekable接口允許定位到文件中的某個位置并且提供了一個方法查詢當前位置距離文件開始位置的位移。
public interface Seekable {
void seek(long pos) throws IOException;
long getPos() throws IOException;
}
如果調用seek()方法傳入了一個比文件長度長的值,則會拋出IOException異常。Java.io.InputStream中方法skip()方法也可以傳入一個位置,但這個位置必須在當前位置之后,而seek()能夠移動到文件任意一個位置。
簡單對示例3-2修改一下,見示例3-3.將文件中內容兩次寫入標準輸出。在第一次寫入后,跳回到文件起始位置,再寫一次。
示例:3-3
public class FileSystemDoubleCat {
public static void main(String[] args) throws Exception {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
FSDataInputStream in = null;
try {
in = fs.open(new Path(uri));
IOUtils.copyBytes(in, System.out, 4096, false);
in.seek(0); // 返回到文件起始位置
IOUtils.copyBytes(in, System.out, 4096, false);
} finally {
IOUtils.closeStream(in);
}
}
}
本次運行結果如下:
% hadoop FileSystemDoubleCat hdfs://localhost/user/tom/quangle.txt
On the top of the Crumpetty Tree
The Quangle Wangle sat,
But his face you could not see,
On account of his Beaver Hat.
On the top of the Crumpetty Tree
The Quangle Wangle sat,
But his face you could not see,
On account of his Beaver Hat.
FSDataInputStream也實現了PositionedReadable接口,可以基于給定的位置,讀取文件的一部分。
public interface PositionedReadable {
public int read(long position, byte[] buffer, int offset, int length)
throws IOException;
public void readFully(long position, byte[] buffer, int offset, int length)
throws IOException;
public void readFully(long position, byte[] buffer) throws IOException;
}
read()方法從給定的position位置處,讀取length長度的字節放到給定的offset位移開始的buffer中。返回值是實際讀取的字節數。調用者應該檢查這個值,因為它也許比length小。
readFully()方法讀取length長度的字節放入buffer中,對于是字節數組的buffer,讀取buffer.length長度字節數到buffer中。如果到文件末尾,就中斷操作,拋出一個EOFException異常。
所有這些方法都能保持文件當前位移的占有,是線程安全的。所以它們在讀取文件內容的時候,還提供了一個獲取文件文件元信息的方法。但FSDataInputStream設計時不是線程安全的,因此最好還是創建多個實例。
最后,記住調用sekk()方法是一個相當耗時的操作,所以應該盡量少調用。你應該將你的應用中訪問文件的模式結構化,使用流數據的形式,例如使用MapReduce,而不是執行大量的seek。
寫數據
FileSystem有許多創建文件的方法。最簡單的方法是傳入一個文件路徑,返回文件輸出流,然后向輸出流中寫入數據。
public FSDataOutputStream create(Path f) throws IOException
這個方法還有一些重載的方法,可以讓你指定是否強制覆蓋存在的文件,文件的復制參數(復制到幾個節點),向文件寫數據時buffer的大小,文件所用塊的大小以及文件權限。
如果文件所在的父路徑中目錄不存在,create()方法將會創建它。雖然這很方便,但是這種形為也許是不希望發生的。你希望如果父目錄不存在,就不寫入數據,那么就應該在調用這個方法之前,先調用exists()方法檢查一下父目錄是否存在。另一種方法,你可以使用FileContext類,它可以讓你控制父目錄不存在時,創建還是不創建目錄。
仍然有一個重載方法,接收一個回調接口,Progressable。實現此接口后,當數據寫入數據節點時,你可以知道數據寫入的進度。
package org.apache.hadoop.util;
public interface Progressable {
public void progress();
}
再介紹另外一個創建文件的方法,可以使用append()方法向已經存在的文件中添加內容。當然這個方法也有許多重載方法。
public FSDataOutputStream append(Path f) throws IOException
這個append操作允許一個writer操作一個已經存在的文件,打開并從文件末尾處開始寫入數據。使用這個方法,那些能夠生成沒有大小限制的文件(例如日志文件)的應用可以在關閉文件后仍然能寫入數據。append操作是可選的,并不是所有的hadoop文件系統都實現了它,例如HDFS實現了,而S3文件系統沒有實現。
示例3-4顯示了怎么樣將本地的一個文件復制到Hadoop文件系統中。當Hadoop每次調用progress方法的時候,我們通過打印輸出句號顯示進度(當每一次有64KB數據寫入數據節點通道后,hadoop就會調用progress方法)。注意這種特殊的行為并不是create()方法要求的,它僅僅是想讓你知道有事情正在發生,這在下一個Hadoop版本中將有所改變。
public class FileCopyWithProgress {
public static void main(String[] args) throws Exception {
String localSrc = args[0];
String dst = args[1];
InputStream in = new BufferedInputStream(new FileInputStream(localSrc));
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(dst), conf);
OutputStream out = fs.create(new Path(dst),
new Progressable() {
public void progress() {
System.out.print(".");
}
});
IOUtils.copyBytes(in, out, 4096, true);
}
}
調用示范:
% hadoop FileCopyWithProgress input/docs/1400-8.txt
hdfs://localhost/user/tom/1400-8.txt
.................
目前除了HDFS,沒有其它Hadoop文件系統會在寫數據的過程中調用progress()。進度在MapReduce應用中是重要的,你將在接下來的章節中還會看到。
FSDataOutputStream
FileSystem的create()方法返回一個FSDataOutputStream實例,和FSDataInputStream類似,它有一個查詢文件當前位置的方法。
package org.apache.hadoop.fs;
public class FSDataOutputStream extends DataOutputStream implements Syncable {
public long getPos() throws IOException {
// implementation elided
}
// implementation elided
}
然而,和FSDataInputStream不一樣的是,它不允許尋址(seeking)。因為HDFS僅僅允許連續地向打開的文件中寫入內容或者向一個可寫入內容的文件中追加內容。換句話說,不允許隨意地要任意位置寫入內容,只能在文件末尾寫入。所以寫入的時候尋址沒有意義。
目錄
FileSystem提供了一個創建目錄的方法
public boolean mkdirs(Path f) throws IOException
這個方法會創建所有必要的父目錄,如果它們不存在的話,就像java.io.File的mkdirs()方法一樣。當目錄(或所有的父目錄)創建成功后,返回true。
一般,你不需要顯式地創建一個目錄,因為當調用create()方法創建一個文件時會自動地創建任何父目錄。
文件系統查詢
文件元數據:文件狀態
任何文件系統都有一個重要的特性。那就是能夠進行目錄結構導航和獲取它所存儲的文件或目錄的信息。FileStatus類封裝了文件和目錄的元信息,包括文件長度,塊大小,復制參數,修改時間,所有者和權限信息。
FileSystem中的getFileStatus()方法提供了一個獲取某個文件或目錄狀態的FileStatus對象的方法。示例3-5顯示了它的使用方法。
public class ShowFileStatusTest {
private MiniDFSCluster cluster; // use an in-process HDFS cluster for testing
private FileSystem fs;
@Before
public void setUp() throws IOException {
Configuration conf = new Configuration();
if (System.getProperty("test.build.data") == null) {
System.setProperty("test.build.data", "/tmp");
}
cluster = new MiniDFSCluster.Builder(conf).build();
fs = cluster.getFileSystem();
OutputStream out = fs.create(new Path("/dir/file"));
out.write("content".getBytes("UTF-8"));
out.close();
}
@After
public void tearDown() throws IOException {
if (fs != null) { fs.close(); }
if (cluster != null) { cluster.shutdown(); }
}
@Test(expected = FileNotFoundException.class)
public void throwsFileNotFoundForNonExistentFile() throws IOException {
fs.getFileStatus(new Path("no-such-file"));
}
@Test
public void fileStatusForFile() throws IOException {
Path file = new Path("/dir/file");
FileStatus stat = fs.getFileStatus(file);
assertThat(stat.getPath().toUri().getPath(), is("/dir/file"));
assertThat(stat.isDirectory(), is(false));
assertThat(stat.getLen(), is(7L));
assertThat(stat.getModificationTime(),
is(lessThanOrEqualTo(System.currentTimeMillis())));
assertThat(stat.getReplication(), is((short) 1));
assertThat(stat.getBlockSize(), is(128 * 1024 * 1024L)); assertThat(stat.getOwner(),
is(System.getProperty("user.name")));
assertThat(stat.getGroup(), is("supergroup"));
assertThat(stat.getPermission().toString(), is("rw-r--r--"));
}
@Test
public void fileStatusForDirectory() throws IOException {
Path dir = new Path("/dir");
FileStatus stat = fs.getFileStatus(dir);
assertThat(stat.getPath().toUri().getPath(), is("/dir"));
assertThat(stat.isDirectory(), is(true));
assertThat(stat.getLen(), is(0L));
assertThat(stat.getModificationTime(),
is(lessThanOrEqualTo(System.currentTimeMillis())));
assertThat(stat.getReplication(), is((short) 0));
assertThat(stat.getBlockSize(), is(0L));
assertThat(stat.getOwner(), is(System.getProperty("user.name")));
assertThat(stat.getGroup(), is("supergroup"));
assertThat(stat.getPermission().toString(), is("rwxr-xr-x"));
}
}
如果文件或目錄不存在,則會拋出一個FileNotFoundException。然而,如果你僅僅關注文件或目錄是否存在,FileSystem的exists()方法會更方便。
public boolean exists(Path f) throws IOException
列舉文件
查詢單個文件或目錄的信息是有用的,但你也經常需要列舉目錄下的內容,那就是FileSystem的listStatus()方法所做的:
public FileStatus[] listStatus(Path f) throws IOException
public FileStatus[] listStatus(Path f, PathFilter filter) throws IOException
public FileStatus[] listStatus(Path[] files) throws IOException
public FileStatus[] listStatus(Path[] files, PathFilter filter)
throws IOException
當參數是單個文件的時候,最簡單變量的那個方法返回一個FileStatus對象數組,長度為1.當參數是一個目錄的時候,返回零或多個FileStatus對象,代表該目錄下的所有文件或目錄。
重載的方法中,允許傳入一個PathFileter對象,限制匹配的文件或目錄。你將在“PathFilter”部分看到一個示例。最后,如果你傳入一個路徑數組,相當于對每個路徑都調用listStatus()方法,然后將每個路徑返回的FileStatus對象合并到一個數組中。這將非常有用,當Input文件夾中的文件來自文件系統中不同路徑時。示例3-6就是這方面簡單的應用示例。注意其中使用了Hadoop的FileUtil類中的stat2Paths()方法將FileStatus數組轉換成Path對象數組。
示例:3-6 顯示來自Hadoop文件系統中多個路徑的文件狀態使用示例
public class ListStatus {
public static void main(String[] args) throws Exception {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
Path[] paths = new Path[args.length];
for (int i = 0; i < paths.length; i++) {
paths[i] = new Path(args[i]);
}
FileStatus[] status = fs.listStatus(paths);
Path[] listedPaths = FileUtil.stat2Paths(status);
for (Path p : listedPaths) {
System.out.println(p);
}
}
}
運行結果如下:
% hadoop ListStatus hdfs://localhost/ hdfs://localhost/user/tom
hdfs://localhost/user
hdfs://localhost/user/tom/books
hdfs://localhost/user/tom/quangle.txt
文件模式
我們經常需要同時操作大量文件。例如,一個處理日志的MapReduce作業也許需要分析一個月的日志文件,這些文件存放在多個目錄中。通常我們可以很方便地使用一個通配符表達式匹配多個文件,而不是遍歷每一個目錄下的每一個文件。Hadoop提供了兩個使用通配符表達式的方法。
public FileStatus[] globStatus(Path pathPattern) throws IOException
public FileStatus[] globStatus(Path pathPattern, PathFilter filter)
throws IOException
globStatus()方法會返回符合通配符表達式的FileStatus對象數組,并按路徑排序。可選的PathFileter參數能夠進一步限制匹配的路徑。
Hadoop支持與Unix Shell一樣的通配符集合,見表3-2
通配符 | 名稱 | 匹配項 |
---|---|---|
* | 星號 | 匹配零或多個字符 |
? | 問號 | 匹配單個字符 |
[ab] | 字符集 | 匹配在集合{a,b}中的某個字符 |
[^ab] | 排除字符集 | 匹配不在集合{a,b}中單個的字符 |
[a-b] | 字符范圍 | 匹配在范圍[a,b]內的單個字符,a要小于或等于b |
[^a-b] | 排除字符范圍 | 匹配不在范圍[a,b]內的單個字符,a要小于等于b |
{a,b} | 二選一 | 匹配表達式a或b中一個 |
\c | 轉義字符 | 當c是特殊字符時,使用\c匹配c字符 |
假設日志文件按照日期以層級結構形式存儲在目錄下。例如:2007最后一天的日志文件存儲在目錄2007/12/31下。假設完整的文件列表如下:
下面是一些文件通配符和它們的匹配結果:
通配符 | 匹配結果 |
---|---|
/* | /2007 /2008 |
// | /2007/12 /2008/01 |
//12/ | /2007/12/30 /2007/12/31 |
/200? | /2007 /2008 |
/200[78] | /2007 /2008 |
/200[7-8] | /2007 /2008 |
/200[^01234569] | /2007 /2008 |
///{31,01} | /2007/12/31 /2008/01/01 |
///3{0,1} | /2007/12/30 /2007/12/31 |
/*/{12/31,01/01} | /2007/12/31 /2008/01/01 |
路徑過濾(PathFilter)
通配符并不是總能夠獲取你想要的文件集合。例如,使用通配符不太可能排除某些特殊的文件。FileSystem的listStatus()和globStatus()方法都可以接受一個可選的PathFilter參數,允許通過編程控制能夠匹配的文件。
package org.apache.hadoop.fs;
public interface PathFilter {
boolean accept(Path path);
}
PathFilter和java.io.FileFilter類對于Path對象的操作功能一樣,而與File類不一樣。示例3-7排除符合正則表達式的路徑
示例:3-7
public class RegexExcludePathFilter implements PathFilter {
private final String regex;
public RegexExcludePathFilter(String regex) {
this.regex = regex;
}
public boolean accept(Path path) {
return !path.toString().matches(regex);
}
}
這個過濾器僅僅允許不符合正則表達式的文件通過。globStatus()方法接收一個初始化的文件集合后,使用filter過濾出符合條件的結果。
fs.globStatus(new Path("/2007/*/*"), new RegexExcludeFilter("^.*/2007/12/31$"))
將得到結果:/2007/12/30
過濾器僅僅作用于以路徑表示的文件名,不能使用文件的屬性例如創建時間作為過濾條件。然而,他們可以匹配通配符和正則表達式都不能夠匹配的文件,例如如果你將文件存儲在按照日期分類的目錄下,你就可以使用PathFileter篩選出某個日期范圍之間的文件。
刪除數據
使用FileSystem的delete()方法可以永久地刪除文件或目錄。
public boolean delete(Path f, boolean recursive) throws IOException
如果f是一個文件或一個空目錄,則recursive值被忽略。如果recursive值為true,則一個非空目錄連同目錄下的內容都會被刪除,否則,拋出IOException異常。
由于本章節內容較多,達到了簡書單頁最大長度限制,本章其它內容將另起一篇書寫,見Hadoop分布式文件系統(2)。
本文是筆者翻譯自《OReilly.Hadoop.The.Definitive.Guide.4th.Edition》第一部分第3章,后續將繼續翻譯其它章節。雖盡力翻譯,但奈何水平有限,錯誤再所難免,如果有問題,請不吝指出!希望本文對你有所幫助。