第3章:Hadoop分佈式文件系統(1)

當數據量增大到超出了單個物理計算機存儲容量時,有必要把它分開存儲在多個不一樣的計算機中。那些管理存儲在多個網絡互連的計算機中的文件系統被稱爲「分佈式文件系統」。因爲這些計算機是基於網絡鏈接的,因此網絡編程的那些複雜性都會涉及,這也形成了分佈式文件系統比通常的磁盤存儲文件系統更復雜。例如,其中最大的一個難題是如何使文件系統因其中一個節點失敗而不形成數據丟失。html

Hadoop使用的分佈式文件系統稱爲HDFS,即Hadoop Distributed Filesystem。在非正式或早期文檔或配置文件中見到DFS也指的是HDFS。HDFS是Hadoop最重要的文件系統,是這章節要講的核心。可是Hadoop實際上具備通用文件系統抽象層,因此咱們也順便看一下Hadoop如何與其它存儲系統集成,例如本地文件系統和Amazon S3。java

HDFS的設計

設計HDFS的目的是爲了可以存儲很是大致積的文件,這些文件可以以流的方式訪問,並可以運行於通常平常用的硬件設備集羣中。讓咱們更詳細地說明一下這句話的意思。node

很是大致積的文件

這裏的"很是大"意思是文件的大小是幾百M,幾百G或者幾百T。今天的運行的Hadoop集羣可以存儲P級數據。web

流式數據訪問

HDFS認爲一次寫入,屢次讀取的模式是最高效的處理模式,它圍繞着這個模式創建。數據集一般都是自生成或從源數據複製,而後隨着時間推移會進行屢次分析。每次分析可能不是全部數據,但也會是佔數據集很大比例的數據,因此讀取整個數據集所花的時間比讀取第一條數據延遲的時間更重要。正則表達式

平常用的硬件設備

Hadoop不要求昂貴的,高可靠的硬件。它被設計可以運行於通常經常使用的硬件之上。經常使用的硬件指的是可以從多個供應商購買到的通常的可用的硬件。這種便件集羣中節點失敗的可能性更大,至少對於大的集羣來講是這樣。HDFS被設計的目的就是當節點失敗時可以繼續運行,而不會讓用戶察覺到明顯的中斷。shell

並非全部應用領域HDFS都有出色的表現,雖然這可能在未來改變。有以下領域HDFS不太適合:apache

  • 低延遲數據讀取
    那些要求數據讀取幾十毫秒延遲的應用不適合使用HDFS。記住,HDFS對於傳輸高吞吐量數據進行了優化,這也許以延遲爲代價。HBase當前是低延遲比較好的選擇,見第20章節。
  • 大量的小文件
    因爲文件元數據存儲在內存中的名稱結點中,因此內存中名稱節點大小決定了可以存儲文件的數量。根據經驗,每個文件名,目錄或塊名佔用150字節,因此若是你有一百萬個文件,每個文件佔一塊,你將至少須要300M內存空間。雖然存儲幾百萬個文件是沒問題的,但存儲十億個文件就超出了當前硬件可以容納的數量。
  • 多個寫入器,文件任意修改
    HDFS的文件只有一個寫入器,而且只在文件結束時以追加的方式寫入。不支持多個寫入器或者能在文件當中任意一個位置修改。這也許在未來被支持,但它們極可能相對低效一些。

Hadoop概念

硬盤中的塊指一次讀取或寫入的最小單位數量。基於此的單塊硬盤的文件系統處理多塊中的數據,處理的塊大小是單塊大小的整數倍。文件系統塊大小一般是幾M大小,而硬盤中的塊大小正常是512字節。這對於操做文件系統的用戶來講是透明的。這些用戶僅僅須要讀取或寫入任意長度的文件便可,不用關心塊大小。而後,有一些工具能夠維護文件系統,例如df和fsck.這些工具直接在塊級別操做。編程

HDFS也有塊的概念。但它的塊單元要大小一些,通常默認是128MB。和單塊硬盤的文件系統同樣,HDFS中的文件也會按照塊大小被拆分獨立存儲,而不一樣的是,比一塊大小小的數據不會佔用一塊的空間,例如塊大小是128M,而文件大小是1M,則此文件只使用了1M空間,而不是128M。沒有特殊說明,本書中的"塊"指的是HDFS中的塊。swift

爲何HDFS中的塊默認這麼大?
HDFS塊跟比硬盤中塊要大的多,目的是爲了減小查找的開銷。若是塊足夠大,將數據從硬盤中讀取出來的時間將會比尋找塊起始地址所花的時候要多的多。所以傳輸一個由多個塊組成的大文件時,傳輸時間主要取決於硬盤的傳輸速率。
讓咱們簡單計算一下,若是尋址時間是10ms,傳輸速率是100M/s。爲了使尋址時間佔傳輸時間的1%,咱們設置塊大小爲100M。默認塊大小時128M,而一些HDFS安裝說明建議設置更大的塊。隨着新一代的硬盤驅動安裝,傳輸速率增長,這個值會愈來愈大。然而這個值不能設置的太大,由於MapReduce中的map任務一般一次在一塊上執行,若是你有不少map任務,比集羣中的節點還多,這時候,做業運行的時候比它應該運行的時間要慢一些。api

對分佈系統來講,有抽象的塊有幾個優點。第一個優點也是最明顯的:一個文件可能比互聯網中任意一個單塊硬盤容量都要大,也沒有理由要求文件的塊都存儲在一塊硬盤中。因此能夠利用集羣中全部的硬盤。事實上,一個文件的塊能夠佔滿集羣中全部硬盤的空間,雖然這不常發生。

第二,將塊作爲抽象單元而不是文件簡化了存儲子系統。簡單是全部系統都努力追求的。這對於分佈式系統來講尤爲重要,由於分佈式系統失敗的狀況各類各樣。存儲子系統操做塊僅僅是簡單地管理存儲,由於塊的大小固定,因此很容易計算出指定的硬盤中可以有多少塊。而且存儲子系統不須要關心元數據。由於塊僅存儲數據,文件的元數據,例如權限信息不須要存儲在塊中,有另一個系統單獨管理。

最後,塊對於容災和數據獲取都表現不錯。爲了應對塊,硬盤或計算機損塊,每個塊中的數據都會被複制到獨立的幾個物理計算機中(一般是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協議.圖3-1 直接經過HTTP或經過HDFS代理訪問HDFS

使用第一種方法時,內嵌在名稱節點和數據節點中的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章,後續將繼續翻譯其它章節。雖盡力翻譯,但奈何水平有限,錯誤再所不免,若是有問題,請不吝指出!但願本文對你有所幫助。
本文轉自個人簡書博客

相關文章
相關標籤/搜索