當(dāng)數(shù)據(jù)量增大到超出了單個(gè)物理計(jì)算機(jī)存儲(chǔ)容量時(shí),有必要把它分開存儲(chǔ)在多個(gè)不同的計(jì)算機(jī)中。那些管理存儲(chǔ)在多個(gè)網(wǎng)絡(luò)互連的計(jì)算機(jī)中的文件系統(tǒng)被稱為“分布式文件系統(tǒng)”。由于這些計(jì)算機(jī)是基于網(wǎng)絡(luò)連接的,所以網(wǎng)絡(luò)編程的那些復(fù)雜性都會(huì)涉及,這也造成了分布式文件系統(tǒng)比一般的磁盤存儲(chǔ)文件系統(tǒng)更復(fù)雜。例如,其中最大的一個(gè)難題是如何使文件系統(tǒng)因其中一個(gè)節(jié)點(diǎn)失敗而不造成數(shù)據(jù)丟失。
Hadoop使用的分布式文件系統(tǒng)稱為HDFS,即Hadoop Distributed Filesystem。在非正式或早期文檔或配置文件中見到DFS也指的是HDFS。HDFS是Hadoop最重要的文件系統(tǒng),是這章節(jié)要講的核心。但是Hadoop實(shí)際上具有通用文件系統(tǒng)抽象層,所以我們也順便看一下Hadoop如何與其它存儲(chǔ)系統(tǒng)集成,例如本地文件系統(tǒng)和Amazon S3。
HDFS的設(shè)計(jì)
設(shè)計(jì)HDFS的目的是為了能夠存儲(chǔ)非常大體積的文件,這些文件能夠以流的方式訪問,并能夠運(yùn)行于一般日常用的硬件設(shè)備集群中。讓我們更詳細(xì)地說明一下這句話的意思。
非常大體積的文件
這里的"非常大"意思是文件的大小是幾百M(fèi),幾百G或者幾百T。今天的運(yùn)行的Hadoop集群能夠存儲(chǔ)P級(jí)數(shù)據(jù)。
流式數(shù)據(jù)訪問
HDFS認(rèn)為一次寫入,多次讀取的模式是最高效的處理模式,它圍繞著這個(gè)模式建立。數(shù)據(jù)集通常都是自生成或從源數(shù)據(jù)復(fù)制,然后隨著時(shí)間推移會(huì)進(jìn)行多次分析。每次分析可能不是所有數(shù)據(jù),但也會(huì)是占數(shù)據(jù)集很大比例的數(shù)據(jù),所以讀取整個(gè)數(shù)據(jù)集所花的時(shí)間比讀取第一條數(shù)據(jù)延遲的時(shí)間更重要。
日常用的硬件設(shè)備
Hadoop不要求昂貴的,高可靠的硬件。它被設(shè)計(jì)能夠運(yùn)行于一般常用的硬件之上。常用的硬件指的是能夠從多個(gè)供應(yīng)商購(gòu)買到的一般的可用的硬件。這種便件集群中節(jié)點(diǎn)失敗的可能性更大,至少對(duì)于大的集群來說是這樣。HDFS被設(shè)計(jì)的目的就是當(dāng)節(jié)點(diǎn)失敗時(shí)能夠繼續(xù)運(yùn)行,而不會(huì)讓用戶察覺到明顯的中斷。
并不是所有應(yīng)用領(lǐng)域HDFS都有出色的表現(xiàn),雖然這可能在將來改變。有如下領(lǐng)域HDFS不太適合:
- 低延遲數(shù)據(jù)讀取
那些要求數(shù)據(jù)讀取幾十毫秒延遲的應(yīng)用不適合使用HDFS。記住,HDFS對(duì)于傳輸高吞吐量數(shù)據(jù)進(jìn)行了優(yōu)化,這也許以延遲為代價(jià)。HBase當(dāng)前是低延遲比較好的選擇,見第20章節(jié)。 - 大量的小文件
由于文件元數(shù)據(jù)存儲(chǔ)在內(nèi)存中的名稱結(jié)點(diǎn)中,所以內(nèi)存中名稱節(jié)點(diǎn)大小決定了能夠存儲(chǔ)文件的數(shù)量。根據(jù)經(jīng)驗(yàn),每一個(gè)文件名,目錄或塊名占用150字節(jié),所以如果你有一百萬個(gè)文件,每一個(gè)文件占一塊,你將至少需要300M內(nèi)存空間。雖然存儲(chǔ)幾百萬個(gè)文件是沒問題的,但存儲(chǔ)十億個(gè)文件就超出了當(dāng)前硬件能夠容納的數(shù)量。 - 多個(gè)寫入器,文件任意修改
HDFS的文件只有一個(gè)寫入器,并且只在文件結(jié)束時(shí)以追加的方式寫入。不支持多個(gè)寫入器或者能在文件當(dāng)中任意一個(gè)位置修改。這也許在將來被支持,但它們很可能相對(duì)低效一些。
Hadoop概念
塊
硬盤中的塊指一次讀取或?qū)懭氲淖钚挝粩?shù)量。基于此的單塊硬盤的文件系統(tǒng)處理多塊中的數(shù)據(jù),處理的塊大小是單塊大小的整數(shù)倍。文件系統(tǒng)塊大小通常是幾M大小,而硬盤中的塊大小正常是512字節(jié)。這對(duì)于操作文件系統(tǒng)的用戶來說是透明的。這些用戶僅僅需要讀取或?qū)懭肴我忾L(zhǎng)度的文件即可,不用關(guān)心塊大小。然后,有一些工具可以維護(hù)文件系統(tǒng),例如df和fsck.這些工具直接在塊級(jí)別操作。
HDFS也有塊的概念。但它的塊單元要大小一些,一般默認(rèn)是128MB。和單塊硬盤的文件系統(tǒng)一樣,HDFS中的文件也會(huì)按照塊大小被拆分獨(dú)立存儲(chǔ),而不同的是,比一塊大小小的數(shù)據(jù)不會(huì)占用一塊的空間,例如塊大小是128M,而文件大小是1M,則此文件只使用了1M空間,而不是128M。沒有特殊說明,本書中的"塊"指的是HDFS中的塊。
為什么HDFS中的塊默認(rèn)這么大?
HDFS塊跟比硬盤中塊要大的多,目的是為了減少查找的開銷。如果塊足夠大,將數(shù)據(jù)從硬盤中讀取出來的時(shí)間將會(huì)比尋找塊起始地址所花的時(shí)候要多的多。因此傳輸一個(gè)由多個(gè)塊組成的大文件時(shí),傳輸時(shí)間主要取決于硬盤的傳輸速率。
讓我們簡(jiǎn)單計(jì)算一下,如果尋址時(shí)間是10ms,傳輸速率是100M/s。為了使尋址時(shí)間占傳輸時(shí)間的1%,我們?cè)O(shè)置塊大小為100M。默認(rèn)塊大小時(shí)128M,而一些HDFS安裝說明建議設(shè)置更大的塊。隨著新一代的硬盤驅(qū)動(dòng)安裝,傳輸速率增加,這個(gè)值會(huì)越來越大。然而這個(gè)值不能設(shè)置的太大,因?yàn)镸apReduce中的map任務(wù)通常一次在一塊上執(zhí)行,如果你有很多map任務(wù),比集群中的節(jié)點(diǎn)還多,這時(shí)候,作業(yè)運(yùn)行的時(shí)候比它應(yīng)該運(yùn)行的時(shí)間要慢一些。
對(duì)分布系統(tǒng)來說,有抽象的塊有幾個(gè)優(yōu)勢(shì)。第一個(gè)優(yōu)勢(shì)也是最明顯的:一個(gè)文件可能比互聯(lián)網(wǎng)中任意一個(gè)單塊硬盤容量都要大,也沒有理由要求文件的塊都存儲(chǔ)在一塊硬盤中。所以可以利用集群中所有的硬盤。事實(shí)上,一個(gè)文件的塊可以占滿集群中所有硬盤的空間,雖然這不常發(fā)生。
第二,將塊做為抽象單元而不是文件簡(jiǎn)化了存儲(chǔ)子系統(tǒng)。簡(jiǎn)單是所有系統(tǒng)都努力追求的。這對(duì)于分布式系統(tǒng)來說尤其重要,因?yàn)榉植际较到y(tǒng)失敗的情況各種各樣。存儲(chǔ)子系統(tǒng)操作塊僅僅是簡(jiǎn)單地管理存儲(chǔ),因?yàn)閴K的大小固定,所以很容易計(jì)算出指定的硬盤中能夠有多少塊。并且存儲(chǔ)子系統(tǒng)不需要關(guān)心元數(shù)據(jù)。因?yàn)閴K僅存儲(chǔ)數(shù)據(jù),文件的元數(shù)據(jù),例如權(quán)限信息不需要存儲(chǔ)在塊中,有另外一個(gè)系統(tǒng)單獨(dú)管理。
最后,塊對(duì)于容災(zāi)和數(shù)據(jù)獲取都表現(xiàn)不錯(cuò)。為了應(yīng)對(duì)塊,硬盤或計(jì)算機(jī)損塊,每一個(gè)塊中的數(shù)據(jù)都會(huì)被復(fù)制到獨(dú)立的幾個(gè)物理計(jì)算機(jī)中(通常是3個(gè))。如果某一塊中的數(shù)據(jù)不能獲取,可以以某一種方法從另外一個(gè)位置讀取塊的復(fù)本。這些操作對(duì)客戶來說是透明的。而且如果一個(gè)塊數(shù)據(jù)不能讀取,Hadoop就會(huì)從其它替代位置讀取塊內(nèi)容到另外一個(gè)正在運(yùn)行的計(jì)算機(jī)中以便讓復(fù)制參數(shù)回到正常水平(可以看"數(shù)據(jù)健壯性"那一章節(jié)了解更多應(yīng)對(duì)數(shù)據(jù)損多方法)。類似的,許多應(yīng)用對(duì)于經(jīng)常使用的文件選擇設(shè)置高的復(fù)制參數(shù),以便在集群中更多地方可以讀取到。
像文件系統(tǒng)中的fsck一樣,hadoop的fsck命令也能操作塊,例如運(yùn)行下面命令:
% hdfs fsck / -files -blocks
就會(huì)列出文件系統(tǒng)中包含文件數(shù)據(jù)的所有塊(可以參看"文件系統(tǒng)檢查(fsck)"章節(jié))。
名稱節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)
一個(gè)HDFS集群有兩種節(jié)點(diǎn)類型。它們以主-從形式工作。一個(gè)名稱節(jié)點(diǎn)(主)和多個(gè)數(shù)據(jù)節(jié)點(diǎn)(從)。名稱節(jié)點(diǎn)管理文件系統(tǒng)命名空間。維護(hù)文件系統(tǒng)樹和樹中所有文件和目錄的元信息。這些信息以命名空間鏡像和更改日志兩種形式永久存儲(chǔ)在本地硬盤中。從名稱節(jié)點(diǎn)可以查到數(shù)據(jù)節(jié)點(diǎn)。這些數(shù)據(jù)節(jié)點(diǎn)存儲(chǔ)著文件的塊數(shù)據(jù)。然而這些塊并不會(huì)永久存儲(chǔ)。因?yàn)楫?dāng)系統(tǒng)啟動(dòng)時(shí),這些塊會(huì)重新在數(shù)據(jù)節(jié)點(diǎn)中建立。
代表用戶的客戶端通過與名稱節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)溝通操作文件系統(tǒng)??蛻舳藭?huì)提供類似可移植操作系統(tǒng)接口(Portable Operating System Interface POSIX)的文件系統(tǒng)接口。所以用戶開發(fā)的時(shí)候不需要知道怎么操作名稱節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)。
數(shù)據(jù)節(jié)點(diǎn)是文件系統(tǒng)中苦力勞作者。他們存儲(chǔ)塊數(shù)據(jù),并按照客戶端或名稱節(jié)點(diǎn)的要求返回塊數(shù)據(jù)。他們會(huì)定期地向名稱節(jié)點(diǎn)返回他們存儲(chǔ)的塊列表。
沒有名稱節(jié)點(diǎn),文件系統(tǒng)無法使用。事實(shí)上,如果運(yùn)行名稱節(jié)點(diǎn)的計(jì)算機(jī)徹底損毀了,所有文件將會(huì)丟失。因?yàn)楦緵]法知道怎么樣根據(jù)數(shù)據(jù)節(jié)點(diǎn)中的塊重新生成文件。由于這個(gè)原因,能夠在當(dāng)名稱節(jié)點(diǎn)損壞后恢復(fù)顯得非常重要。Hadoop提供了兩種機(jī)制達(dá)到這個(gè)目的。
第一種方法是備份存儲(chǔ)著文件元信息的文件。能夠配置Hadoop使名稱節(jié)點(diǎn)中的數(shù)據(jù)能夠自動(dòng)地同步地寫入多個(gè)文件系統(tǒng)。通常的配置是一份存儲(chǔ)在本地系統(tǒng),另外一份存儲(chǔ)在遠(yuǎn)程的NFS系統(tǒng)中。
第二種方法是運(yùn)行另外一個(gè)名稱節(jié)點(diǎn),盡管它叫做名稱節(jié)點(diǎn),但它和名稱節(jié)點(diǎn)的作用不一樣。它的作用主要是根據(jù)更改日志合并名稱節(jié)點(diǎn)鏡像文件,以免更改日志過大。第二名稱節(jié)點(diǎn)通常在單獨(dú)的一個(gè)物理機(jī)中運(yùn)行,因?yàn)樗枰罅空加肅PU,并且需要與名稱節(jié)點(diǎn)一樣多的內(nèi)存空間以便執(zhí)行合并。它還保持著合并后名稱節(jié)點(diǎn)的復(fù)本,以便當(dāng)名稱節(jié)點(diǎn)失敗后能夠使用。然而,由于第二名稱節(jié)點(diǎn)的狀態(tài)更新比主計(jì)算機(jī)慢,所以當(dāng)主計(jì)算機(jī)完全損壞時(shí),數(shù)據(jù)幾乎肯定會(huì)丟失。這種情況發(fā)生時(shí),通常的做法是從遠(yuǎn)程N(yùn)FS復(fù)制一份名稱節(jié)點(diǎn)元數(shù)據(jù)文件到第二節(jié)點(diǎn),并把第二節(jié)點(diǎn)所在的計(jì)算機(jī)做為主計(jì)算機(jī)運(yùn)行(注意:可以運(yùn)行一個(gè)熱備用名稱節(jié)點(diǎn)而不使用第二節(jié)點(diǎn),如“HDFS高可用性”中所討論的那樣),可以參看"文件系統(tǒng)鏡像和更改日志"章節(jié)了解更詳細(xì)信息。
塊緩存
正常情況下,數(shù)據(jù)節(jié)點(diǎn)會(huì)從硬盤中讀取塊數(shù)據(jù)。但是對(duì)于需要頻繁讀取的文件,這些塊數(shù)據(jù)可以被緩存在非堆棧的數(shù)據(jù)節(jié)點(diǎn)內(nèi)存中。雖然在以文件為基礎(chǔ)的系統(tǒng)中,可以配置一個(gè)塊數(shù)據(jù)緩存在幾個(gè)數(shù)據(jù)節(jié)點(diǎn)中,但默認(rèn)情況下,一個(gè)塊數(shù)據(jù)僅僅緩存在一個(gè)數(shù)據(jù)節(jié)點(diǎn)內(nèi)存中。作業(yè)調(diào)試器(例如:MapReduce,Spark或者其它框架)在緩存了塊數(shù)據(jù)的數(shù)據(jù)節(jié)點(diǎn)上運(yùn)行任務(wù)時(shí)能夠利用這些緩存的塊數(shù)據(jù)以提高讀取性能。例如,一個(gè)小的用于連接查詢的表就比較適合于緩存。
用戶或應(yīng)用通過向緩存池中發(fā)送一個(gè)緩存命令告訴名稱節(jié)點(diǎn)那些文件需要緩存,緩存多久。緩存池是一個(gè)管理型組織,管理著緩存權(quán)和資源使用權(quán)。
HDFS聯(lián)盟
名稱節(jié)點(diǎn)會(huì)在內(nèi)存中保存文件系統(tǒng)中所有文件和塊的引用。也就是說在有著非常多的文件的大集群中,內(nèi)存的大小存為了集群擴(kuò)充的限制(看"一個(gè)名稱節(jié)點(diǎn)需要多大內(nèi)存?"章節(jié))。HDFS聯(lián)盟是Hadoop 2.x系統(tǒng)介紹的一種解決方法。它允許集群可以通過增加名稱節(jié)點(diǎn)擴(kuò)充。每一個(gè)名稱節(jié)點(diǎn)管理著文件系統(tǒng)的一部分。例如:一個(gè)名稱節(jié)點(diǎn)管理著/user目錄下所有文件,另一個(gè)名稱節(jié)點(diǎn)管理著/share目錄下所有文件。
在聯(lián)盟形式下,每一個(gè)名稱空間管理一個(gè)命名空間卷和一個(gè)塊池。命名空間卷由命名空間的元信息組成。塊池則包括命名空間下所有文件的所有塊數(shù)據(jù)。命名空間卷相互獨(dú)立,意味著名稱節(jié)點(diǎn)相互獨(dú)立,更進(jìn)一步地講,某一個(gè)名稱節(jié)點(diǎn)毀壞了不會(huì)影響到被其它名稱節(jié)點(diǎn)管理的命名空間的數(shù)據(jù)獲取。然而,塊池不是分區(qū)的,所以集群中數(shù)據(jù)節(jié)點(diǎn)可以被注冊(cè)在任意一個(gè)名稱節(jié)點(diǎn)中,并且可以存儲(chǔ)來自多個(gè)塊池中的塊。
為了配置一個(gè)HDFS聯(lián)盟的集群,客戶端需使用存放在客戶端的表來把文件路徑映射每一個(gè)名稱節(jié)點(diǎn)??梢酝ㄟ^ViewFileSystem和viewfs://URIs進(jìn)行配置。
HDFS高可用性
將名稱節(jié)點(diǎn)中保存在多個(gè)文件系統(tǒng)中和使用第二名稱節(jié)點(diǎn)創(chuàng)建檢查點(diǎn),這兩者的目的都是為了防止數(shù)據(jù)丟失,然后它并不能保證文件系統(tǒng)的高可用性。名稱節(jié)點(diǎn)仍然會(huì)有單點(diǎn)故障。如果出現(xiàn)故障,所有的客戶端包括MapReduce作業(yè)等將將不能讀取,寫入數(shù)據(jù)或者顯示文件。因?yàn)槊Q節(jié)點(diǎn)是元數(shù)據(jù)和文件與塊對(duì)應(yīng)關(guān)系存儲(chǔ)的唯一倉庫。如果出現(xiàn)如此情況,整個(gè)Hadoop系統(tǒng)將很快中斷服務(wù),直到一個(gè)新的名稱節(jié)點(diǎn)啟用。
在名稱節(jié)點(diǎn)失敗后,為了恢復(fù),管理者必須從所有文件系統(tǒng)元數(shù)據(jù)備份中選擇一個(gè)備份作為主名稱節(jié)點(diǎn)啟用,并配置數(shù)據(jù)節(jié)點(diǎn)和客戶端使用這個(gè)新的名稱節(jié)點(diǎn)。啟用后這個(gè)新節(jié)點(diǎn)并不能立即投入使用。直到(1)節(jié)點(diǎn)中命名空間鏡像載入內(nèi)存;(2)重新根據(jù)更改日志執(zhí)行一遍失敗的操作;(3)收到足夠多的來自數(shù)據(jù)節(jié)點(diǎn)中塊的報(bào)告表明其已離開安全模式,這三步完成后才會(huì)啟用。在有大量文件和塊的集群中,冷啟動(dòng)一個(gè)名稱節(jié)點(diǎn)需要花費(fèi)30分鐘或更多。
長(zhǎng)的恢復(fù)時(shí)間對(duì)于運(yùn)維來說是一個(gè)問題。事實(shí)上,名稱節(jié)點(diǎn)不可預(yù)料的失敗發(fā)生的情況少之又少,而計(jì)劃的停機(jī)事件在實(shí)際中顯得更重要。
Hadoop2通過提供HDFS高可用性(HA)改善了這種狀況。實(shí)現(xiàn)上是有兩個(gè)名稱節(jié)點(diǎn),一個(gè)激活狀態(tài),一個(gè)備用狀態(tài)。當(dāng)激活狀態(tài)的名稱節(jié)點(diǎn)失敗之后,備用名稱節(jié)點(diǎn)立即會(huì)接替它的任務(wù),服務(wù)客戶端的請(qǐng)求。客戶端不會(huì)感覺到明顯的中斷。要想實(shí)現(xiàn)HA,需要做一些結(jié)構(gòu)上的改變。
- 兩個(gè)名稱節(jié)點(diǎn)必須能夠使用高速訪問的存儲(chǔ)空間共享更改日志。當(dāng)備用的名稱節(jié)點(diǎn)運(yùn)行的時(shí)候,它會(huì)讀取更改日志所有內(nèi)容,并同步狀態(tài),然后當(dāng)激活名稱節(jié)點(diǎn)寫入新內(nèi)容時(shí),再讀取新的狀態(tài)同步。
- 數(shù)據(jù)節(jié)點(diǎn)必須將塊報(bào)告發(fā)送給這兩個(gè)名稱節(jié)點(diǎn),因?yàn)閴K之間的映射關(guān)系存儲(chǔ)在名稱節(jié)點(diǎn)內(nèi)存中,而不是在硬盤中。
- 使用一種對(duì)用戶透明的機(jī)制,客戶端必須要被設(shè)置成能夠處理名稱節(jié)點(diǎn)的失敗后的備援。
- 這個(gè)備用的名稱節(jié)點(diǎn)包含了第二節(jié)點(diǎn)的角色,會(huì)對(duì)激活的名稱節(jié)點(diǎn)中的命名空間進(jìn)行定期檢查。
對(duì)于高訪問的共享存儲(chǔ)有兩種選擇:NFS和QJM(a quorum journal manager)。QJM是專門為HDFS實(shí)現(xiàn)的,設(shè)計(jì)的唯一目的就是能夠快速訪問更改日志,它是大部分HDFS安裝說明推薦的選擇。QJM以日志節(jié)點(diǎn)組形式運(yùn)行,每一次更改都會(huì)被寫入大量的日志節(jié)點(diǎn)中。通常會(huì)有三個(gè)日志節(jié)點(diǎn),所以系統(tǒng)能夠容忍它們其中的一個(gè)損壞。這樣的方式與ZooKeeper工作方式類似,但是QJM的實(shí)現(xiàn)并沒有使用ZooKeeper。然后,需要注意的是,HDFS HA確實(shí)使用了ZooKeeper來選擇激活的名稱節(jié)點(diǎn),下一部分會(huì)講到。
如果激活的名稱節(jié)點(diǎn)失敗了,備用名稱節(jié)點(diǎn)一般會(huì)在幾十秒之內(nèi)替代失敗的節(jié)點(diǎn)。因?yàn)檫€需要獲取最新的更改日志和更新的塊映射關(guān)系。實(shí)際觀察到的替代時(shí)間將會(huì)更長(zhǎng),一般在一分鐘左右,因?yàn)橄到y(tǒng)需要確定激活的名稱節(jié)點(diǎn)確實(shí)失敗了。
還有一種不太可能發(fā)生的情況,當(dāng)激活的名稱節(jié)點(diǎn)失敗后,備用的也停止了工作,管理員仍然能夠冷啟動(dòng)備用名稱節(jié)點(diǎn)。這也比沒有HA的情況要好。從可操作性角度來看,這是一個(gè)進(jìn)步,因?yàn)檫@個(gè)過程是一個(gè)內(nèi)嵌在Hadoop中的標(biāo)準(zhǔn)操作過程。
失敗備援(Failover)和筑圍(Fencing)
從激活的名稱節(jié)點(diǎn)切換到備用節(jié)點(diǎn)由系統(tǒng)中"失敗備援控制器"管理。有各種各樣的失敗備援控制器,但是默認(rèn)是使用ZooKeeper確保只有一個(gè)名稱節(jié)點(diǎn)是激活的。每一個(gè)名稱節(jié)點(diǎn)都對(duì)應(yīng)運(yùn)行一個(gè)輕量級(jí)的失敗備援控制器進(jìn)程,這些控制器進(jìn)程的作用是通過簡(jiǎn)單心跳的機(jī)制監(jiān)視名稱節(jié)點(diǎn),看它是否失敗,并激活備用節(jié)點(diǎn)。
失敗備援也能夠由管理員發(fā)起,例如在日常維護(hù)中。這被稱為"優(yōu)雅的失敗備援"。因?yàn)榭刂破鲿?huì)在在這兩個(gè)名稱節(jié)點(diǎn)間進(jìn)行有序地過渡以交換角色。然而在不優(yōu)雅地失敗備援情況下,不可能確定失敗的名稱節(jié)點(diǎn)已經(jīng)停止運(yùn)行了。例如,緩慢的網(wǎng)絡(luò)或網(wǎng)絡(luò)不通都能觸發(fā)失敗備援切換。被切換掉的前一個(gè)激活的名稱節(jié)點(diǎn)仍然在運(yùn)行,仍然認(rèn)為它自己是激活的節(jié)點(diǎn)。HA的實(shí)例會(huì)使用叫做"筑圍(Fencing)"的方法盡全力確保先前的激活節(jié)點(diǎn)不能夠?qū)ο到y(tǒng)造成任何損害或引起系統(tǒng)癱瘓。
QJM僅僅允許同一時(shí)間有一個(gè)名稱節(jié)點(diǎn)編輯更改日志。然而先前激活的名稱節(jié)點(diǎn)仍然可能會(huì)響應(yīng)切換前來自客戶端的請(qǐng)求。所以好的辦法是啟動(dòng)一個(gè)SSH筑圍命令殺死這個(gè)名稱節(jié)點(diǎn)的進(jìn)程。當(dāng)使用NFS做為更改日志存儲(chǔ)的時(shí)候,需要更強(qiáng)大的筑圍,因?yàn)榇藭r(shí)不可能保證同一時(shí)間只有一個(gè)名稱節(jié)點(diǎn)編輯更改日志(這也是推薦使用QJM的原因)。這種更強(qiáng)大的筑圍機(jī)制的作用包括撤消名稱節(jié)點(diǎn)訪問共享存儲(chǔ)目錄權(quán)限(通常情況下使用供應(yīng)商提供的NFS命令)和通過遠(yuǎn)程管理命令關(guān)閉它的網(wǎng)絡(luò)端口。還有最后一種方法,使用被大眾所熟知的“STONITH”技術(shù)(shoot the other node in the head),它會(huì)通過專業(yè)的電源分配單元強(qiáng)制關(guān)閉主機(jī)電源。
失敗備援由客戶端庫透明處理,最簡(jiǎn)單的實(shí)現(xiàn)方法是配置客戶端的配置文件。在配置文件中,HDFS URI使用一個(gè)邏輯主機(jī)名,并把它映射到兩個(gè)名稱節(jié)點(diǎn)地址。客戶端庫會(huì)嘗試每一個(gè)名稱節(jié)點(diǎn)地址直到操作成功完成。
命令行接口
我們將以命令行的方式來看一看怎么樣與HDFS交互。有很多其它針對(duì)HDFS的接口,但是命令行是最簡(jiǎn)單的方式之一,也是許多開發(fā)者歡迎的方式。
我們首先在一臺(tái)服務(wù)器上運(yùn)行HDFS,按照附錄A中的說明搭建一臺(tái)偽分布式的Hadoop服務(wù)器。稍后,我們將在集群中運(yùn)行HDFS,讓它具備可擴(kuò)展和容錯(cuò)性。
配置偽分布的系統(tǒng),需要配置兩個(gè)屬性。第一個(gè)是屬性是fs.defaultFS,設(shè)置成hdfs://localhost/,這個(gè)屬性用于設(shè)置HDFS默認(rèn)的文件系統(tǒng)。文件系統(tǒng)通過URI來指定,這里我們配置了hdfs URI,讓Hadoop默認(rèn)使用HDFS。HDFS將根據(jù)這個(gè)屬性得到主機(jī)名和端口,給HDFS名稱節(jié)點(diǎn)使用。HDFS將會(huì)在localhost,默認(rèn)8020端口上運(yùn)行??蛻舳艘材芨鶕?jù)這個(gè)屬性知道名稱節(jié)點(diǎn)在哪里運(yùn)行,以便客戶端能連接到名稱節(jié)點(diǎn)。
第二個(gè)屬性dfs.replication設(shè)置成1,這樣HDFS不會(huì)按照默認(rèn)值3復(fù)制文件系統(tǒng)塊。當(dāng)在單個(gè)數(shù)據(jù)節(jié)點(diǎn)上運(yùn)行時(shí),HDFS不能夠?qū)?shù)據(jù)塊復(fù)制到3個(gè)數(shù)據(jù)節(jié)點(diǎn)中時(shí),它將會(huì)一直警告塊需要復(fù)制。配置成1就解決了這個(gè)問題。
基本的文件系統(tǒng)操作
當(dāng)文件系統(tǒng)準(zhǔn)備好的時(shí)候,我們就能夠進(jìn)行一些常規(guī)的文件操作了。例如讀取文件,創(chuàng)建目錄,移動(dòng)文件,刪除數(shù)據(jù),列出文件目錄等操作。你可以在每一個(gè)命令后鍵入hadoop fs -help得到命令詳細(xì)幫助信息。
將本地硬盤上的一個(gè)文件復(fù)制到HDFS中:
% hadoop fs -copyFromLocal input/docs/quangle.txt \
hdfs://localhost/user/tom/quangel.txt
這條命令使用了Hadoop文件系統(tǒng)Shell命令fs。這個(gè)命令包含一些子命令。我們剛才用-copyFromLocal 來表示將quangle.txt復(fù)制到HDFS中的/user/tom/quangle.txt中。事實(shí)上,我們可以隱去URI中的協(xié)議和主機(jī)名,hadoop會(huì)默認(rèn)去core-site.xml中去取hdfs://localhost.
% hadoop fs -copyFromLocal input/docs/quangle.txt /user/tom/quangle.txt
我們也可以使用相對(duì)路徑,將文件復(fù)制到HDFS的根目錄中。我們這個(gè)例子中根目錄是/user/tom:
% hadoop fs -copyFromLocal input/docs/quangle.txt quangle.txt
讓我們?cè)侔盐募腍DFS中復(fù)制回本地文件系統(tǒng),并檢查一下他們是否一樣。
% 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碼是一樣的,表明這個(gè)文件成功復(fù)制到HDFS后,仍然完好無損地復(fù)制回來了。
最后,讓我們看一下一個(gè)列舉HDFS文件的命令。我們首先創(chuàng)建了一個(gè)目錄,然后看看怎么列舉文件:
% 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返回的信息很相似。但有一些小的區(qū)別。第一列顯示文件權(quán)限模式,第二列顯示文件的復(fù)制參數(shù)(這是傳統(tǒng)的Unix文件系統(tǒng)沒有的)。還記得我們?cè)谡军c(diǎn)范圍的配置文件中配置的默認(rèn)復(fù)制參數(shù)是1吧,這就是為什么我們能在這里看見了相同的值。這個(gè)值對(duì)于目錄來說是空的,因?yàn)閺?fù)制不會(huì)應(yīng)用到目錄,目錄屬于元數(shù)據(jù),它們被存儲(chǔ)在名稱節(jié)點(diǎn)中,不是數(shù)據(jù)節(jié)點(diǎn)。第三和第四列分別顯示這個(gè)文件的所有者和所屬的組。第五列以字節(jié)形式顯示這個(gè)文件的大小,目錄大小為0。第6和第7列顯示文件或目錄最后被編輯的日期和時(shí)間。最后,第8列顯示文件或目錄的名字。
HDFS中文件的權(quán)限
HDFS對(duì)于文件和目錄有一種權(quán)限控制模式,就像POSIX一樣。有三種權(quán)限:讀權(quán)限(r),寫權(quán)限(w),,執(zhí)行權(quán)限(x)。讀權(quán)限可以用于讀取文件或列舉目錄下的所有文件內(nèi)容。寫權(quán)限可以用于編輯文件,對(duì)于目錄來說,可以創(chuàng)建或刪除目錄中的文件或目錄。HDFS中的文件沒有執(zhí)行權(quán)限,因?yàn)镠DFS不允許執(zhí)行文件,這與POSIX不一樣,至于目錄,執(zhí)行權(quán)限可以用于獲取子目錄。
每一個(gè)文件或目錄都有一個(gè)所有者,一個(gè)組和一個(gè)模型。這個(gè)模型由三部分用戶地權(quán)限組成,一部分是所有者權(quán)限,一部分是組中成員權(quán)限,還有一部分是既不是所有者也不是組成員的用戶權(quán)限。
默認(rèn)情況下,Hadoop沒有開啟安全驗(yàn)證功能,這就意味著客戶的身份不會(huì)被驗(yàn)證。因?yàn)榭蛻羰沁h(yuǎn)程的,客戶就可以簡(jiǎn)單地通過創(chuàng)建賬號(hào)變成任意一個(gè)用戶。如果開啟了安全驗(yàn)證功能,這就不可能發(fā)生,詳細(xì)信息見"安全性"章節(jié)。還有另外一個(gè)值得開啟安全驗(yàn)證的原因,那就是為了避免文件系統(tǒng)的重要部分遭到意外的修改或刪除,不管是被用戶或者自動(dòng)修改的工具或程序修改。
權(quán)限驗(yàn)證開啟后,如果客戶端的用戶是所有者,則使用所有者權(quán)限驗(yàn)證,如果客戶端用戶是組中的一個(gè)成員,則使用組權(quán)限驗(yàn)證,如果都不是,則使用其它設(shè)定的權(quán)限驗(yàn)證。
Hadoop文件系統(tǒng)
Hadoop的文件系統(tǒng)是一個(gè)抽象概念。HDFS僅僅是其中一個(gè)實(shí)現(xiàn)。org.apache.hadoop.fs.FileSystem這個(gè)Java抽象類定義了客戶訪問Hadoop文件的一系統(tǒng)接口。有很多具體的文件系統(tǒng),表3-1列舉出了幾個(gè)適用于Hadoop的文件系統(tǒng)。
| 文件系統(tǒng) | URI協(xié)議 | Java的實(shí)現(xiàn)(所有類在包org.apache.hadoop下) | 描述 |
|---|---|---|---|
| Local | file | fs.LocalFileSystem | 一個(gè)用于本地的具體客戶端校驗(yàn)硬盤的文件系統(tǒng)。對(duì)于沒有校驗(yàn)的硬盤使用RawLocal FileSystem。見"本地文件系統(tǒng)" |
| HDFS | hdfs | hdfs.DistributedFileSystem | Hadoop的分布式文件系統(tǒng)。HDFS被設(shè)計(jì)用于和MapReduce連接進(jìn)行高效地工作 |
| WebHDFS | webhdfs | hdfs.web.WebHdfsFileSystem | 提供對(duì)基于HTTP讀寫HDFS進(jìn)行權(quán)限驗(yàn)證的文件系統(tǒng),見"HTTP" |
| 安全的WebHDFS | swebhdfs | hdfs.web.SWebHdfsFileSystem | WebHDFS的HTTPS版本 |
| HAR | har | fs.HarFileSystem | 在另一個(gè)文件系統(tǒng)之上的一個(gè)文件系統(tǒng),用于歸檔文件。Hadoop歸檔用于將HDFS中的文件打包歸檔進(jìn)一個(gè)文件中,以減少名稱節(jié)點(diǎn)所占的內(nèi)存。使用hadoop archive命令創(chuàng)建HAR文件 |
| View | viewfs | viewfs.ViewFileSystem | 一個(gè)客戶端掛載表,作用于另外一個(gè)Hadoop文件系統(tǒng),通常用于對(duì)聯(lián)盟名稱節(jié)點(diǎn)創(chuàng)建掛載點(diǎn)。見"HDFS聯(lián)盟" |
| FTP | ftp | fs.ftp.FTPFileSystem | 基于FTP服務(wù)的文件系統(tǒng) |
| S3 | s3a | fs.s3a.S3AFileSystem | 基于Amazon S3的文件系統(tǒng),代替舊的s3n(S3 native) |
| Azure | wasb | fs.azure.NativeAzureFileSystem | 基于微軟Azure的文件系統(tǒng) |
| Swift | swift | fs.swift.snative.SwiftNativeFile | 基于OpenStack Swift 的文件系統(tǒng) |
Hadoop提供了很多接口用于操作文件系統(tǒng),通常使用URI協(xié)議來選擇正確的文件系統(tǒng)實(shí)例進(jìn)行交互。我們之前所使用的文件系統(tǒng)shell適用于hadoop所有文件系統(tǒng)。例如為了列舉本地硬盤根目錄下所有文件,使用如下命令:
% hadoop fs -ls file:///
雖然可以運(yùn)行MapReduce程序從以上任意一個(gè)文件系統(tǒng)獲取數(shù)據(jù),有時(shí)甚至非常方便。但當(dāng)我們處理非常大批量的數(shù)據(jù)時(shí),我們應(yīng)該選擇能夠進(jìn)行數(shù)據(jù)本地優(yōu)化的分布式文件系統(tǒng),尤其是HDFS(見"擴(kuò)展"內(nèi)容)。
接口
Hadoop是用JAVA開發(fā)的,所以大多數(shù)的Hadoop文件系統(tǒng)交互都是以JAVA API作為中間溝通的橋梁。例如文件系統(tǒng)shell就是一個(gè)JAVA應(yīng)用程序,這個(gè)應(yīng)用程序使用JAVA類FileSystem來操作文件。其它的文件系統(tǒng)接口也會(huì)在這一塊簡(jiǎn)單地討論。這些接口大多數(shù)通常在HDFS中使用,因?yàn)镠DFS中一般都有現(xiàn)存的訪問底層文件系統(tǒng)的接口,例如FTP客戶端訪問FTP,S3工具使用S3等等。但是他們中的一些適用于任意的hadoop文件系統(tǒng)。
HTTP
Hadoop系統(tǒng)的文件系統(tǒng)接口是用Java開發(fā)的,這就使用非JAVA應(yīng)用很難與HDFS交互。當(dāng)其它語言需要與HDFS交互時(shí),我們可以使用WebHDFS提供的HTTP REST API接口,這將會(huì)容易許多。但是要注意的是HTTP接口會(huì)比原生的JAVA客戶端慢,所以如果可以的話,應(yīng)盡量避免進(jìn)行大數(shù)據(jù)量傳輸。
通過HTTP協(xié)議,有兩種與HDFS交互的方式。一種是直接通過HTTP與HDFS交互,還有一種是通過代理方式??蛻舳嗽L問代理,代理再代表客戶,通常使用DistributedFileSystem API訪問HDFS。圖3-1說明了這兩種方式,這兩種方式都是使用了WebHDFS協(xié)議.
使用第一種方法時(shí),內(nèi)嵌在名稱節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)中的webservice作為WebHDFS協(xié)議的終結(jié)點(diǎn)(WebHDFS默認(rèn)是啟動(dòng)的,因?yàn)閐fs.webhdfs.enabled設(shè)置成了true)。文件元數(shù)據(jù)由名稱節(jié)點(diǎn)處理,文件的讀或?qū)懖僮髡?qǐng)求會(huì)首先傳給名稱節(jié)點(diǎn),然后名稱節(jié)點(diǎn)會(huì)向客戶端返回一個(gè)HTTP重啟向鏈接,指向數(shù)據(jù)節(jié)點(diǎn),以便進(jìn)行文件的流式操作。
使用第二種方法時(shí),通過使用一個(gè)或多個(gè)獨(dú)立的代理服務(wù)基于HTTP訪問HDFS。這些代理是無狀態(tài)的,所以它們能夠在標(biāo)準(zhǔn)的負(fù)載均衡器之后。所以傳向集錄的請(qǐng)求必須經(jīng)過代理,所以客戶端不會(huì)直接與名稱節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)交互。我們可以在代理層加入更嚴(yán)格的防火墻和帶寬限制策略。通常Hadoop集群分布在不同的數(shù)據(jù)中心時(shí)或者需要訪問外部網(wǎng)絡(luò)云中的集群時(shí),使用代理來傳輸數(shù)據(jù)。
HTTPFS代理暴露了與WebHDFS一樣的HTTP(HTTPS)接口。所以客戶端能夠通過webhdfs(swebhdfs) URIs訪問二者。HTTPFS使用httpfs.sh.script啟動(dòng),并獨(dú)立于名稱節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)服務(wù)器,默認(rèn)使用一個(gè)不同的端口監(jiān)聽,一般是14000端口。
C
Hadoop提供了一個(gè)叫做libhdfs的C函數(shù)庫,與Java FileSystem接口功能相同。盡管它是一個(gè)訪問HDFS的C函數(shù)庫,但卻能被用于訪問任何任意的hadoop文件系統(tǒng)。它通過使用JNI調(diào)用Java文件系統(tǒng)接口。與上面講解的WebHDFS接口類似,對(duì)應(yīng)地有一個(gè)libwebhdfs庫。
C API與JAVA很像,但它不如JAVA API。因?yàn)橐恍┬碌奶匦圆恢С帧D憧梢栽陬^文件hdfs.h中看到。這個(gè)頭文件位于Apache Hadoop二進(jìn)制文件分布目錄中。
Apache Hadoop二進(jìn)制文件中已經(jīng)有為64位LInux系統(tǒng)預(yù)先構(gòu)建好的libhdfs二進(jìn)制文件。但對(duì)其它系統(tǒng),你需要自己構(gòu)建,可以按照原始樹目錄頂層的BUILDING.txt說明來構(gòu)建。
NFS
通過使用Hadoop的NFSv3網(wǎng)關(guān)可以將HDFS掛載到本地的文件系統(tǒng)。然后就可以使用Unix工具,(例如ls和cat)來與文件系統(tǒng)交互,上傳文件。通常還可以使用任意編程語言調(diào)用POSIX函數(shù)庫與文件系統(tǒng)交互。可以向文件中追加內(nèi)容,但不能隨機(jī)修改文件,因?yàn)镠DFS僅僅可以在文件末尾寫入內(nèi)容。
可以看Hadoop文檔了解如何配置運(yùn)行NFS網(wǎng)關(guān)以及怎么樣從客戶端連接它。
FUSE
用戶空間文件系統(tǒng)(FileSystem in userspace)允許用戶空間中實(shí)現(xiàn)的因?yàn)橛腥讼到y(tǒng)可以被集成進(jìn)Unix文件系統(tǒng)。Hadoop的Fuse-DFS模塊可以使HDFS或任意其它文件系統(tǒng)掛載成一個(gè)標(biāo)準(zhǔn)的本地文件系統(tǒng)。Fuse-DFS使用C語言實(shí)現(xiàn),通過libhdfs與HDFS交互。當(dāng)需要寫數(shù)據(jù)的時(shí)間,Hadoop NFS網(wǎng)關(guān)仍然是掛載HDFS更有效的解決方案,所以應(yīng)該優(yōu)先于Fuse-DFS考慮。
JAVA接口
這部分,我們將會(huì)深入了解與Hadoop文件系統(tǒng)交互的Hadoop FileSystem類。雖然我們一般主要關(guān)注對(duì)于HDFS的實(shí)現(xiàn)即DistributedFileSystem,但是通常來說,你應(yīng)該基于FileSystem抽象類實(shí)現(xiàn)你自己的代碼,能夠盡可能地跨文件系統(tǒng)。例如當(dāng)測(cè)試程序的時(shí)候,這顯示非常有用。因?yàn)槟隳軌蚩焖俚販y(cè)試在本地文件系統(tǒng)的數(shù)據(jù)。
從Hadoop URL讀取數(shù)據(jù)
從hadoop文件系統(tǒng)讀取文件最簡(jiǎn)單的方法之一是使用java.net.URL類,這個(gè)類會(huì)打開文件的流用于讀取。一般的寫法是:
InputStream in = null;
try {
in = new URL("hdfs://host/path").openStream();
// process in
} finally {
IOUtils.closeStream(in);
}
我們還需要做一些工作讓Java能夠識(shí)別Hadoop的hdfs的URL。調(diào)用URL的setURLStreamHandlerFactory()方法,傳遞一個(gè)FsUrlStreamHandlerFactory類的實(shí)例。一個(gè)JVM只允許調(diào)用一次這個(gè)方法。所以它通常在靜態(tài)塊中執(zhí)行。這個(gè)限制意味著如果你的程序中某一部分,也許是不在你控制范圍內(nèi)的第三方組件設(shè)置了URLStreamHandlerFactory,你就不能通過這種途徑從Hadoop中讀取數(shù)據(jù),下一部分將討論另一種方法。
示例3-1顯示了從Hadoop文件系統(tǒng)中讀取文件并顯示在標(biāo)準(zhǔn)輸出中,就像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提供的現(xiàn)成的IOUtils類用于在finally語句中關(guān)閉流,也能用于從輸入流中復(fù)制字節(jié)并輸出到指定的輸出流中(示例中是System.out)。copyBytes最后面兩個(gè)參數(shù)是字節(jié)數(shù)大小和當(dāng)復(fù)制完成后是否關(guān)閉輸入流。我們自己手工關(guān)閉輸入流,System.out不需要關(guān)閉。
看一下示例的調(diào)用:
% 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讀取數(shù)據(jù)
正如上一部分所講的那樣,有時(shí)我們不能夠使用SetURLStreamHandlerFactory。這時(shí)候,我們就需要使用文件系統(tǒng)的API打開一個(gè)文件的輸入流。
Hadoop文件系統(tǒng)中的文件由一個(gè)Hadoop路徑對(duì)象表示(不是java.io.File對(duì)象,雖然它的語義與本地文件系統(tǒng)很接近)。你可以把一個(gè)路徑想象成Hadoop文件系統(tǒng)URI,例如:hdfs://localhost/user/tom/quangle.txt。
FileSystem是常用的文件系統(tǒng)API。所以第一步獲取一個(gè)FileSystem實(shí)例。本例中,需要獲取操作HDFS的FileSystem實(shí)例。有幾個(gè)靜態(tài)方法可以獲取FileSystem實(shí)例。
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對(duì)象封裝了客戶端或服務(wù)器的配置。這些配置來自于classpath指定路徑下的配置文件,例如:etc/hadoop/core-site.xml。第一個(gè)方法返回默認(rèn)的filesystem對(duì)象(core-site指定的對(duì)象,如果沒指定,則默認(rèn)是本地的filesystem對(duì)象)。第二個(gè)方法使用給定的URI協(xié)議和權(quán)限決定使用的filesystem,如果URI中沒有指定協(xié)議,則按照配置獲取filesystem。第三個(gè)方法獲取指定用戶的filesystem,這對(duì)于上下文的安全性很重要。可以參看"安全"章節(jié)。
在某些情況下,你需要獲取一個(gè)本地文件系統(tǒng)的實(shí)例對(duì)象,這時(shí),你可以方便地使用getLocal()方法即可。
public static LocalFileSystem getLocal(Configuration conf) throws IOException
獲得了filesystem實(shí)例對(duì)象后,我們可以使用open()方法獲取一個(gè)文件的輸入流。
public FSDataInputStream open(Path f) throws IOException
public abstract FSDataInputStream open(Path f, int bufferSize) throws IOException
第一個(gè)方法使用默認(rèn)的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);
}
}
}
程序的運(yùn)行結(jié)果如下:
% 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()方法實(shí)際返回了一個(gè)FSDataInputStream對(duì)象而不是標(biāo)準(zhǔn)的java.io.class對(duì)象。這個(gè)類繼承了java.io.DataInputStream,支持隨機(jī)訪問,所以你可以從文件流任意部分讀取。
package org.apache.hadoop.fs;
public class FSDataInputStream extends DataInputStream
implements Seekable, PositionedReadable {
// 實(shí)現(xiàn)部分省略
}
Seekable接口允許定位到文件中的某個(gè)位置并且提供了一個(gè)方法查詢當(dāng)前位置距離文件開始位置的位移。
public interface Seekable {
void seek(long pos) throws IOException;
long getPos() throws IOException;
}
如果調(diào)用seek()方法傳入了一個(gè)比文件長(zhǎng)度長(zhǎng)的值,則會(huì)拋出IOException異常。Java.io.InputStream中方法skip()方法也可以傳入一個(gè)位置,但這個(gè)位置必須在當(dāng)前位置之后,而seek()能夠移動(dòng)到文件任意一個(gè)位置。
簡(jiǎn)單對(duì)示例3-2修改一下,見示例3-3.將文件中內(nèi)容兩次寫入標(biāo)準(zhǔn)輸出。在第一次寫入后,跳回到文件起始位置,再寫一次。
示例: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);
}
}
}
本次運(yùn)行結(jié)果如下:
% 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也實(shí)現(xiàn)了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長(zhǎng)度的字節(jié)放到給定的offset位移開始的buffer中。返回值是實(shí)際讀取的字節(jié)數(shù)。調(diào)用者應(yīng)該檢查這個(gè)值,因?yàn)樗苍S比length小。
readFully()方法讀取length長(zhǎng)度的字節(jié)放入buffer中,對(duì)于是字節(jié)數(shù)組的buffer,讀取buffer.length長(zhǎng)度字節(jié)數(shù)到buffer中。如果到文件末尾,就中斷操作,拋出一個(gè)EOFException異常。
所有這些方法都能保持文件當(dāng)前位移的占有,是線程安全的。所以它們?cè)谧x取文件內(nèi)容的時(shí)候,還提供了一個(gè)獲取文件文件元信息的方法。但FSDataInputStream設(shè)計(jì)時(shí)不是線程安全的,因此最好還是創(chuàng)建多個(gè)實(shí)例。
最后,記住調(diào)用sekk()方法是一個(gè)相當(dāng)耗時(shí)的操作,所以應(yīng)該盡量少調(diào)用。你應(yīng)該將你的應(yīng)用中訪問文件的模式結(jié)構(gòu)化,使用流數(shù)據(jù)的形式,例如使用MapReduce,而不是執(zhí)行大量的seek。
寫數(shù)據(jù)
FileSystem有許多創(chuàng)建文件的方法。最簡(jiǎn)單的方法是傳入一個(gè)文件路徑,返回文件輸出流,然后向輸出流中寫入數(shù)據(jù)。
public FSDataOutputStream create(Path f) throws IOException
這個(gè)方法還有一些重載的方法,可以讓你指定是否強(qiáng)制覆蓋存在的文件,文件的復(fù)制參數(shù)(復(fù)制到幾個(gè)節(jié)點(diǎn)),向文件寫數(shù)據(jù)時(shí)buffer的大小,文件所用塊的大小以及文件權(quán)限。
如果文件所在的父路徑中目錄不存在,create()方法將會(huì)創(chuàng)建它。雖然這很方便,但是這種形為也許是不希望發(fā)生的。你希望如果父目錄不存在,就不寫入數(shù)據(jù),那么就應(yīng)該在調(diào)用這個(gè)方法之前,先調(diào)用exists()方法檢查一下父目錄是否存在。另一種方法,你可以使用FileContext類,它可以讓你控制父目錄不存在時(shí),創(chuàng)建還是不創(chuàng)建目錄。
仍然有一個(gè)重載方法,接收一個(gè)回調(diào)接口,Progressable。實(shí)現(xiàn)此接口后,當(dāng)數(shù)據(jù)寫入數(shù)據(jù)節(jié)點(diǎn)時(shí),你可以知道數(shù)據(jù)寫入的進(jìn)度。
package org.apache.hadoop.util;
public interface Progressable {
public void progress();
}
再介紹另外一個(gè)創(chuàng)建文件的方法,可以使用append()方法向已經(jīng)存在的文件中添加內(nèi)容。當(dāng)然這個(gè)方法也有許多重載方法。
public FSDataOutputStream append(Path f) throws IOException
這個(gè)append操作允許一個(gè)writer操作一個(gè)已經(jīng)存在的文件,打開并從文件末尾處開始寫入數(shù)據(jù)。使用這個(gè)方法,那些能夠生成沒有大小限制的文件(例如日志文件)的應(yīng)用可以在關(guān)閉文件后仍然能寫入數(shù)據(jù)。append操作是可選的,并不是所有的hadoop文件系統(tǒng)都實(shí)現(xiàn)了它,例如HDFS實(shí)現(xiàn)了,而S3文件系統(tǒng)沒有實(shí)現(xiàn)。
示例3-4顯示了怎么樣將本地的一個(gè)文件復(fù)制到Hadoop文件系統(tǒng)中。當(dāng)Hadoop每次調(diào)用progress方法的時(shí)候,我們通過打印輸出句號(hào)顯示進(jìn)度(當(dāng)每一次有64KB數(shù)據(jù)寫入數(shù)據(jù)節(jié)點(diǎn)通道后,hadoop就會(huì)調(diào)用progress方法)。注意這種特殊的行為并不是create()方法要求的,它僅僅是想讓你知道有事情正在發(fā)生,這在下一個(gè)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);
}
}
調(diào)用示范:
% hadoop FileCopyWithProgress input/docs/1400-8.txt
hdfs://localhost/user/tom/1400-8.txt
.................
目前除了HDFS,沒有其它Hadoop文件系統(tǒng)會(huì)在寫數(shù)據(jù)的過程中調(diào)用progress()。進(jìn)度在MapReduce應(yīng)用中是重要的,你將在接下來的章節(jié)中還會(huì)看到。
FSDataOutputStream
FileSystem的create()方法返回一個(gè)FSDataOutputStream實(shí)例,和FSDataInputStream類似,它有一個(gè)查詢文件當(dāng)前位置的方法。
package org.apache.hadoop.fs;
public class FSDataOutputStream extends DataOutputStream implements Syncable {
public long getPos() throws IOException {
// implementation elided
}
// implementation elided
}
然而,和FSDataInputStream不一樣的是,它不允許尋址(seeking)。因?yàn)镠DFS僅僅允許連續(xù)地向打開的文件中寫入內(nèi)容或者向一個(gè)可寫入內(nèi)容的文件中追加內(nèi)容。換句話說,不允許隨意地要任意位置寫入內(nèi)容,只能在文件末尾寫入。所以寫入的時(shí)候?qū)ぶ窙]有意義。
目錄
FileSystem提供了一個(gè)創(chuàng)建目錄的方法
public boolean mkdirs(Path f) throws IOException
這個(gè)方法會(huì)創(chuàng)建所有必要的父目錄,如果它們不存在的話,就像java.io.File的mkdirs()方法一樣。當(dāng)目錄(或所有的父目錄)創(chuàng)建成功后,返回true。
一般,你不需要顯式地創(chuàng)建一個(gè)目錄,因?yàn)楫?dāng)調(diào)用create()方法創(chuàng)建一個(gè)文件時(shí)會(huì)自動(dòng)地創(chuàng)建任何父目錄。
文件系統(tǒng)查詢
文件元數(shù)據(jù):文件狀態(tài)
任何文件系統(tǒng)都有一個(gè)重要的特性。那就是能夠進(jìn)行目錄結(jié)構(gòu)導(dǎo)航和獲取它所存儲(chǔ)的文件或目錄的信息。FileStatus類封裝了文件和目錄的元信息,包括文件長(zhǎng)度,塊大小,復(fù)制參數(shù),修改時(shí)間,所有者和權(quán)限信息。
FileSystem中的getFileStatus()方法提供了一個(gè)獲取某個(gè)文件或目錄狀態(tài)的FileStatus對(duì)象的方法。示例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"));
}
}
如果文件或目錄不存在,則會(huì)拋出一個(gè)FileNotFoundException。然而,如果你僅僅關(guān)注文件或目錄是否存在,F(xiàn)ileSystem的exists()方法會(huì)更方便。
public boolean exists(Path f) throws IOException
列舉文件
查詢單個(gè)文件或目錄的信息是有用的,但你也經(jīng)常需要列舉目錄下的內(nèi)容,那就是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
當(dāng)參數(shù)是單個(gè)文件的時(shí)候,最簡(jiǎn)單變量的那個(gè)方法返回一個(gè)FileStatus對(duì)象數(shù)組,長(zhǎng)度為1.當(dāng)參數(shù)是一個(gè)目錄的時(shí)候,返回零或多個(gè)FileStatus對(duì)象,代表該目錄下的所有文件或目錄。
重載的方法中,允許傳入一個(gè)PathFileter對(duì)象,限制匹配的文件或目錄。你將在“PathFilter”部分看到一個(gè)示例。最后,如果你傳入一個(gè)路徑數(shù)組,相當(dāng)于對(duì)每個(gè)路徑都調(diào)用listStatus()方法,然后將每個(gè)路徑返回的FileStatus對(duì)象合并到一個(gè)數(shù)組中。這將非常有用,當(dāng)Input文件夾中的文件來自文件系統(tǒng)中不同路徑時(shí)。示例3-6就是這方面簡(jiǎn)單的應(yīng)用示例。注意其中使用了Hadoop的FileUtil類中的stat2Paths()方法將FileStatus數(shù)組轉(zhuǎn)換成Path對(duì)象數(shù)組。
示例:3-6 顯示來自Hadoop文件系統(tǒng)中多個(gè)路徑的文件狀態(tài)使用示例
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);
}
}
}
運(yùn)行結(jié)果如下:
% hadoop ListStatus hdfs://localhost/ hdfs://localhost/user/tom
hdfs://localhost/user
hdfs://localhost/user/tom/books
hdfs://localhost/user/tom/quangle.txt
文件模式
我們經(jīng)常需要同時(shí)操作大量文件。例如,一個(gè)處理日志的MapReduce作業(yè)也許需要分析一個(gè)月的日志文件,這些文件存放在多個(gè)目錄中。通常我們可以很方便地使用一個(gè)通配符表達(dá)式匹配多個(gè)文件,而不是遍歷每一個(gè)目錄下的每一個(gè)文件。Hadoop提供了兩個(gè)使用通配符表達(dá)式的方法。
public FileStatus[] globStatus(Path pathPattern) throws IOException
public FileStatus[] globStatus(Path pathPattern, PathFilter filter)
throws IOException
globStatus()方法會(huì)返回符合通配符表達(dá)式的FileStatus對(duì)象數(shù)組,并按路徑排序??蛇x的PathFileter參數(shù)能夠進(jìn)一步限制匹配的路徑。
Hadoop支持與Unix Shell一樣的通配符集合,見表3-2
| 通配符 | 名稱 | 匹配項(xiàng) |
|---|---|---|
| * | 星號(hào) | 匹配零或多個(gè)字符 |
| ? | 問號(hào) | 匹配單個(gè)字符 |
| [ab] | 字符集 | 匹配在集合{a,b}中的某個(gè)字符 |
| [^ab] | 排除字符集 | 匹配不在集合{a,b}中單個(gè)的字符 |
| [a-b] | 字符范圍 | 匹配在范圍[a,b]內(nèi)的單個(gè)字符,a要小于或等于b |
| [^a-b] | 排除字符范圍 | 匹配不在范圍[a,b]內(nèi)的單個(gè)字符,a要小于等于b |
| {a,b} | 二選一 | 匹配表達(dá)式a或b中一個(gè) |
| \c | 轉(zhuǎn)義字符 | 當(dāng)c是特殊字符時(shí),使用\c匹配c字符 |
假設(shè)日志文件按照日期以層級(jí)結(jié)構(gòu)形式存儲(chǔ)在目錄下。例如:2007最后一天的日志文件存儲(chǔ)在目錄2007/12/31下。假設(shè)完整的文件列表如下:

下面是一些文件通配符和它們的匹配結(jié)果:
| 通配符 | 匹配結(jié)果 |
|---|---|
| /* | /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()方法都可以接受一個(gè)可選的PathFilter參數(shù),允許通過編程控制能夠匹配的文件。
package org.apache.hadoop.fs;
public interface PathFilter {
boolean accept(Path path);
}
PathFilter和java.io.FileFilter類對(duì)于Path對(duì)象的操作功能一樣,而與File類不一樣。示例3-7排除符合正則表達(dá)式的路徑
示例: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);
}
}
這個(gè)過濾器僅僅允許不符合正則表達(dá)式的文件通過。globStatus()方法接收一個(gè)初始化的文件集合后,使用filter過濾出符合條件的結(jié)果。
fs.globStatus(new Path("/2007/*/*"), new RegexExcludeFilter("^.*/2007/12/31$"))
將得到結(jié)果:/2007/12/30
過濾器僅僅作用于以路徑表示的文件名,不能使用文件的屬性例如創(chuàng)建時(shí)間作為過濾條件。然而,他們可以匹配通配符和正則表達(dá)式都不能夠匹配的文件,例如如果你將文件存儲(chǔ)在按照日期分類的目錄下,你就可以使用PathFileter篩選出某個(gè)日期范圍之間的文件。
刪除數(shù)據(jù)
使用FileSystem的delete()方法可以永久地刪除文件或目錄。
public boolean delete(Path f, boolean recursive) throws IOException
如果f是一個(gè)文件或一個(gè)空目錄,則recursive值被忽略。如果recursive值為true,則一個(gè)非空目錄連同目錄下的內(nèi)容都會(huì)被刪除,否則,拋出IOException異常。
由于本章節(jié)內(nèi)容較多,達(dá)到了簡(jiǎn)書單頁最大長(zhǎng)度限制,本章其它內(nèi)容將另起一篇書寫,見Hadoop分布式文件系統(tǒng)(2)。
本文是筆者翻譯自《OReilly.Hadoop.The.Definitive.Guide.4th.Edition》第一部分第3章,后續(xù)將繼續(xù)翻譯其它章節(jié)。雖盡力翻譯,但奈何水平有限,錯(cuò)誤再所難免,如果有問題,請(qǐng)不吝指出!希望本文對(duì)你有所幫助。