Hadoop 分布式文件系統

HDFS的設計目標

Hadoop分布式文件系統(HDFS)被設計成適合運行在通用硬件(commodity hardware)上的分布式文件系統。它和現有的分布式文件系統有很多共同點。但同時,它和其他的分布式文件系統的區別也是很明顯的。HDFS是一個高度容錯性的系統,適合部署在廉價的機器上。HDFS能提供高吞吐量的數據訪問,非常適合大規模數據集上的應用。HDFS放寬了一部分POSIX約束,來實現流式讀取文件系統數據的目的。HDFS在最開始是作為Apache Nutch搜索引擎項目的基礎架構而開發的。

超大文件

這里非常大指的是幾百M、G、或者TB級別。實際應用中已有很多集群存儲的數據達到PB級別。

流式數據訪問

HDFS構建思路是這樣的:最有效的數據處理模式是一次寫入、多次讀取。數據集經常從數據源生成或者從數據源復制而來,接著長時間在此數據集上進行各種分析。然后每次分析工作經常讀取其中的大部分數據甚至是全部。 因此讀取整個數據集所需時間比讀取第一條記錄的延時更重要。

商用硬件

hadoop設計運行在商用硬件的集群上的。因此至少對于龐大的集群來說節點故障的幾率還是非常高的。

有些場景不適合采用hdfs存儲數據

低時間延遲的數據訪問

hdfs不適合要求低時間延遲的數據訪問應用,如幾十毫秒。記住,HDFS是未該數據吞吐量應用優化的,這可能會以提高時間延遲為代價。

大量的小文件

文件的元數據(如目錄結構,文件block的節點列表,block-node mapping)保存在NameNode的內存中, 整個文件系統的文件數量會受限于NameNode的內存大小。 經驗而言,一個文件/目錄/文件塊一般占有150字節的元數據內存空間。如果有100萬個文件,每個文件占用1個文件塊,則需要大約300M的內存。因此十億級別的文件數量在現有商用機器上難以支持。

多用戶寫入,任意修改文件

HDFS中的文件寫入只支持單個寫入,并且寫操作也總是以追加的方式。

HDFS的概念

數據塊

HDFS的數據塊大小為128Mb。HDFS上的文件被劃分為塊大小的多個分塊,作為獨立的存儲單元。比Block小的文件不會占用整個Block,只會占據實際大小。

HDFS的Block為什么這么大??是為了最小化查找(seek)時間,控制定位文件與傳輸文件所用的時間比例。

Block抽象的好處

1、block的拆分使得單個文件大小可以大于整個磁盤的容量,構成文件的Block可以分布在整個集群, 理論上,單個文件可以占據集群中所有機器的磁盤。

2、使用抽象塊而非整個文件作為存儲單元,簡化了存儲系統的設計。對于Block,無需關注其權限,所有者等內容(這些內容都在文件級別上進行控制)。

3、Block非常適合用于數據備份進而提供數據容錯能力和挺高可用性。

Namenode & Datanode

HDFS具有主/從架構。HDFS集群由單個NameNode,一個管理文件系統命名空間的主服務器和管理客戶端對文件的訪問組成。此外,還有許多DataNode,通常是群集中每個節點一個,用于管理連接到它們運行的??節點的存儲。HDFS公開文件系統命名空間,并允許用戶數據存儲在文件中。在內部,文件被分成一個或多個塊,這些塊存儲在一組DataNode中。NameNode執行文件系統命名空間操作,如打開,關閉和重命名文件和目錄。它還確定了塊到DataNode的映射。DataNode負責提供來自文件系統客戶端的讀寫請求。DataNode還根據NameNode的指令執行塊創建,刪除和復制。


塊緩存

DataNode通常直接從磁盤讀取數據,但是對于訪問頻繁的文件,其對應的塊可能被顯式地緩存在datanode的內存中,以堆外塊緩存的形式存在。默認情況下,一個Block只有一個數據節點會緩存。但是可以針對每個文件可以個性化配置。作業調度器可以利用緩存提升性能,例如MapReduce可以把任務運行在有Block緩存的節點上。用戶或者應用可以向NameNode發送緩存指令(緩存哪個文件,緩存多久), 緩存池的概念用于管理一組緩存的權限和資源。

聯邦HDFS

我們知道NameNode的內存會制約文件數量,HDFS Federation提供了一種橫向擴展NameNode的方式。在Federation模式中,每個NameNode管理命名空間的一部分,例如一個NameNode管理/user目錄下的文件, 另一個NameNode管理/share目錄下的文件。

每個NameNode管理一個namespace volumn,所有volumn構成文件系統的元數據。每個NameNode同時維護一個Block Pool,保存Block的節點映射等信息。各NameNode之間是獨立的,一個節點的失敗不會導致其他節點管理的文件不可用。

客戶端使用mount table將文件路徑映射到NameNode。mount table是在Namenode群組之上封裝了一層,這一層也是一個Hadoop文件系統的實現,通過viewfs:協議訪問。

HDFS的高可用性

在HDFS集群中,NameNode依然是單點故障(SPOF)。元數據同時寫到多個文件系統以及Second NameNode定期checkpoint有利于保護數據丟失,但是并不能提高可用性。

HDFS的解決方式是采用HA的HDFS集群配置兩個NameNode,分別處于Active和Standby狀態。當Active NameNode故障之后,Standby接過責任繼續提供服務,用戶沒有明顯的中斷感覺。一般耗時在幾十秒到數分鐘。

HA涉及到的主要實現邏輯有

1) 主備需共享edit log存儲。

主NameNode和待命的NameNode共享一份edit log,當主備切換時,Standby通過回放edit log同步數據。

共享存儲通常有2種選擇:

NFS:傳統的網絡文件系統

QJM:quorum journal manager

QJM是專門為HDFS的HA實現而設計的,用來提供高可用的edit log。QJM運行一組journal node,edit log必須寫到大部分的journal nodes。通常使用3個節點,因此允許一個節點失敗,類似ZooKeeper。注意QJM沒有使用ZK,雖然HDFS HA的確使用了ZK來選舉主Namenode。一般推薦使用QJM。

2)DataNode需要同時往主備發送Block Report

因為Block映射數據存儲在內存中(不是在磁盤上),為了在Active NameNode掛掉之后,新的NameNode能夠快速啟動,不需要等待來自Datanode的Block Report,DataNode需要同時向主備兩個NameNode發送Block Report。

3)客戶端需要配置failover模式(對用戶透明)

Namenode的切換對客戶端來說是無感知的,通過客戶端庫來實現??蛻舳嗽谂渲梦募惺褂玫腍DFS URI是邏輯路徑,映射到一對Namenode地址??蛻舳藭粩鄧L試每一個Namenode地址直到成功。

4)Standby替代Secondary NameNode

如果沒有啟用HA,HDFS獨立運行一個守護進程作為Secondary Namenode。定期checkpoint,合并鏡像文件和edit日志。

命令行接口

HDFS提供了各種交互方式,例如通過Java API、HTTP、shell命令行的。命令行的交互主要通過hadoop fs來操作。例如:

1、hadoop fs -copyFromLocal // 從本地復制文件到HDFS

2、hadoop fs mkdir // 創建目錄

3、hadoop fs -ls? // 列出文件列表

Hadoop中,文件和目錄的權限類似于POSIX模型,包括讀、寫、執行3種權限:

讀權限(r):用于讀取文件或者列出目錄中的內容

寫權限(w):對于文件,就是文件的寫權限。目錄的寫權限指在該目錄下創建或者刪除文件(目錄)的權限。

執行權限(x):文件沒有所謂的執行權限,被忽略。對于目錄,執行權限用于訪問器目錄下的內容。

每個文件或目錄都有owner,group,mode三個屬性,owner指文件的所有者,group為權限組。mode

由所有者權限、文件所屬的組中組員的權限、非所有者非組員的權限組成。下圖表示其所有者root擁有讀寫權限,supergroup組的組員有讀權限,其他人有讀權限。

文件權限是否開啟通過dfs.permissions.enabled屬性來控制,這個屬性默認為false,沒有打開安全限制,因此不會對客戶端做授權校驗,如果開啟安全限制,會對操作文件的用戶做權限校驗。特殊用戶superuser是Namenode進程的標識,不會針對該用戶做權限校驗。

Hadoop文件系統

Hadoop有一個抽象的文件系統概念,HDFS只是其中的一種實現。Hadoop提供的實現如下圖:

hadoop文件系統

接口

Hadoop對于文件系統提供了許多的接口,主要為HTTP、C語言、NFS 和 java。

HTTP

WebHDFS和SWebHDFS協議將文件系統暴露HTTP操作,這種交互方式比原生的Jav客戶端慢,不適合操作大文件。通過HTTP,有2種訪問方式,直接訪問和通過代理訪問。

直接訪問

直接訪問的示意圖如下:

Namenode和Datanode默認打開了嵌入式web server,即dfs.webhdfs.enabled默認為true。webhdfs通過這些服務器來交互。元數據的操作通過namenode完成,文件的讀寫首先發到namenode,然后重定向到datanode讀?。▽懭耄嶋H的數據流。

通過HDFS代理

采用代理的示意圖如上所示。 使用代理的好處是可以通過代理實現負載均衡或者對帶寬進行限制,或者防火墻設置。代理通過HTTP或者HTTPS暴露為WebHDFS,對應為webhdfs和swebhdfs URL Schema。

代理作為獨立的守護進程,獨立于namenode和datanode,使用httpfs.sh腳本,默認運行在14000端口

C語言

Hadoop提供了一個名為libhdfs的C語言庫,改語言庫時java FileSystem接口類的一個鏡像。它使用java的原生接口調用java文件系統客戶端。它與java的api非常相似,但是滯后于java API。

NFS

使用Hadoop的NFSv3網關將HDFS掛載為本地客戶端的文件系統是可行的。然后你可以使用Unix實用程序如(ls 和 cat)與改文件系統交互,上傳文件,通過任意一種編程語言調用POSIX庫來訪問文件系統。

java 接口

實際的應用中,對HDFS的大多數操作還是通過FileSystem來操作,這部分重點介紹一下相關的接口,主要關注HDFS的實現類DistributedFileSystem及相關類。

?讀操作

可以使用URL來讀取數據,或者而直接使用FileSystem操作。

從Hadoop URL讀取數據

java.net.URL類提供了資源定位的統一抽象,任何人都可以自己定義一種URL Schema,并提供相應的處理類來進行實際的操作。hdfs schema便是這樣的一種實現。

InputStream in = null;

try {

????in = new URL("hdfs://master/user/hadoop").openStream();

}finally{

????IOUtils.closeStream(in);

}

為了使用自定義的Schema,需要設置URLStreamHandlerFactory,這個操作一個JVM只能進行一次,多次操作會導致不可用,通常在靜態塊中完成。下面的截圖是一個使用示例:


運行結果

使用FileSystem API讀取數據

1) 首先獲取FileSystem實例,一般使用靜態get工廠方法

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


如果是本地文件,通過getLocal獲取本地文件系統對象:

public static LocalFileSystem getLocal(COnfiguration conf) thrown IOException

2)調用FileSystem的open方法獲取一個輸入流:

public FSDataInputStream open(Path f) throws IOException

public abstarct FSDataInputStream open(Path f , int bufferSize) throws IOException

默認情況下,open使用4KB的Buffer,可以根據需要自行設置。

3)使用FSDataInputStream進行數據操作

FSDataInputStream是java.io.DataInputStream的特殊實現,在其基礎上增加了隨機讀取、部分讀取的能力

public class FSDataInputStream extends DataInputStream

? ? implements Seekable, PositionedReadable,

? ? ? ByteBufferReadable, HasFileDescriptor, CanSetDropBehind, CanSetReadahead,

? ? ? HasEnhancedByteBufferAccess

隨機讀取操作通過Seekable接口定義:

public interface Seekable {

? ? void seek(long pos) throws IOException;

? ? long getPos() throws IOException;

}

seek操作開銷昂貴,慎用。

部分讀取通過PositionedReadable接口定義:

public interface PositionedReadable{

? ? public int read(long pistion ,byte[] buffer,int offser , int length) throws IOException;

? ? public int readFully(long pistion ,byte[] buffer,int offser , int length) throws IOException;

? ? public int readFully(long pistion ,byte[] buffer) throws IOException;

}

?寫數據

在HDFS中,文件使用FileSystem類的create方法及其重載形式來創建,create方法返回一個輸出流FSDataOutputStream,可以調用返回輸出流的getPos方法查看當前文件的位移,但是不能進行seek操作,HDFS僅支持追加操作。

創建時,可以傳遞一個回調接口Peofressable,獲取進度信息

append(Path f)方法用于追加內容到已有文件,但是并不是所有的實現都提供該方法,例如Amazon的文件實現就沒有提供追加功能。

下面是一個例子:

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 vid progress(){

? ? ? ? System.out.print(.);

? ? }

});

IOUtils.copyBytes(in , out, 4096,true);

目錄操作

使用mkdirs()方法,會自動創建沒有的上級目錄

HDFS中元數據封裝在FileStatus類中,包括長度、block size,replicaions,修改時間、所有者、權限等信息。使用FileSystem提供的getFileStatus方法獲取FileStatus。exists()方法判斷文件或者目錄是否存在;

列出文件(list),則使用listStatus方法,可以查看文件或者目錄的信息

? public abstract FileStatus[] listStatus(Path f) throws FileNotFoundException,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IOException;

Path是個文件的時候,返回長度為1的數組。FileUtil提供的stat2Paths方法用于將FileStatus轉化為Path對象。

globStatus則使用通配符對文件路徑進行匹配:

public FileStatus[] globStatus(Path pathPattern) throws IOException

PathFilter用于自定義文件名過濾,不能根據文件屬性進行過濾,類似于java.io.FileFilter。例如下面這個例子排除到給定正則表達式的文件:

public interfacePathFilter{

? ? boolean accept(Path path);

}

?刪除數據

使用FileSystem的delete()方法

public boolean delete(Path f , boolean recursive) throws IOException;

recursive參數在f是個文件的時候被忽略。如果f是文件并且recursice為true,則刪除整個目錄,否則拋出異常。

數據流

接下來詳細介紹HDFS讀寫數據的流程,以及一致性模型相關的一些概念。

讀文件

大致讀文件的流程如下:


客戶端讀取HDFS中的數據

1)客戶端傳遞一個文件Path給FileSystem的open方法

2)DFS采用RPC遠程獲取文件最開始的幾個block的datanode地址。Namenode會根據網絡拓撲結構決定返回哪些節點(前提是節點有block副本),如果客戶端本身是Datanode并且節點上剛好有block副本,直接從本地讀取。

3)客戶端使用open方法返回的FSDataInputStream對象讀取數據(調用read方法)

4)DFSInputStream(FSDataInputStream實現了改類)連接持有第一個block的、最近的節點,反復調用read方法讀取數據

5)第一個block讀取完畢之后,尋找下一個block的最佳datanode,讀取數據。如果有必要,DFSInputStream會聯系Namenode獲取下一批Block 的節點信息(存放于內存,不持久化),這些尋址過程對客戶端都是不可見的。

6)數據讀取完畢,客戶端調用close方法關閉流對象

在讀數據過程中,如果與Datanode的通信發生錯誤,DFSInputStream對象會嘗試從下一個最佳節點讀取數據,并且記住該失敗節點, 后續Block的讀取不會再連接該節點

讀取一個Block之后,DFSInputStram會進行檢驗和驗證,如果Block損壞,嘗試從其他節點讀取數據,并且將損壞的block匯報給Namenode。

客戶端連接哪個datanode獲取數據,是由namenode來指導的,這樣可以支持大量并發的客戶端請求,namenode盡可能將流量均勻分布到整個集群。

Block的位置信息是存儲在namenode的內存中,因此相應位置請求非常高效,不會成為瓶頸。

?寫文件


客戶端將數據寫入HDFS

步驟分解

1)客戶端調用DistributedFileSystem的create方法

2)DistributedFileSystem遠程RPC調用Namenode在文件系統的命名空間中創建一個新文件,此時該文件沒有關聯到任何block。 這個過程中,Namenode會做很多校驗工作,例如是否已經存在同名文件,是否有權限,如果驗證通過,返回一個FSDataOutputStream對象。 如果驗證不通過,拋出異常到客戶端。

3)客戶端寫入數據的時候,DFSOutputStream分解為packets,并寫入到一個數據隊列中,該隊列由DataStreamer消費。

4)DateStreamer負責請求Namenode分配新的block存放的數據節點。這些節點存放同一個Block的副本,構成一個管道。 DataStreamer將packer寫入到管道的第一個節點,第一個節點存放好packer之后,轉發給下一個節點,下一個節點存放 之后繼續往下傳遞。

5)DFSOutputStream同時維護一個ack queue隊列,等待來自datanode確認消息。當管道上的所有datanode都確認之后,packer從ack隊列中移除。

6)數據寫入完畢,客戶端close輸出流。將所有的packet刷新到管道中,然后安心等待來自datanode的確認消息。全部得到確認之后告知Namenode文件是完整的。 Namenode此時已經知道文件的所有Block信息(因為DataStreamer是請求Namenode分配block的),只需等待達到最小副本數要求,然后返回成功信息給客戶端。

Namenode如何決定副本存在哪個Datanode?

HDFS的副本的存放策略是可靠性、寫帶寬、讀帶寬之間的權衡。默認策略如下:

第一個副本放在客戶端相同的機器上,如果機器在集群之外,隨機選擇一個(但是會盡可能選擇容量不是太慢或者當前操作太繁忙的)

第二個副本隨機放在不同于第一個副本的機架上。

第三個副本放在跟第二個副本同一機架上,但是不同的節點上,滿足條件的節點中隨機選擇。

更多的副本在整個集群上隨機選擇,雖然會盡量便面太多副本在同一機架上。

副本的位置確定之后,在建立寫入管道的時候,會考慮網絡拓撲結構。下面是可能的一個存放策略:

一個典型的復本管線

這樣選擇很好滴平衡了可靠性、讀寫性能

可靠性:Block分布在兩個機架上

寫帶寬:寫入管道的過程只需要跨越一個交換機

讀帶寬:可以從兩個機架中任選一個讀取

?一致性模型

一致性模型描述文件系統中讀寫操縱的可見性。HDFS中,文件一旦創建之后,在文件系統的命名空間中可見:

Path p = new Path("p");

fs.create(p);

assertTaht(fs.exists(p),is(true));

但是任何被寫入到文件的內容不保證可見,即使對象流已經被刷新。

Path p = new Path(“p”);

OutputStream out = fs.create(p);

out.write(“content”.getBytes(“UTF-8”));

out.flush();

assertTaht(fs.getFileStatus(p).getLen,0L); // 為0,即使調用了flush

如果需要強制刷新數據到Datanode,使用FSDataOutputStream的hflush方法強制將緩沖刷到datanode

hflush之后,HDFS保證到這個時間點為止寫入到文件的數據都到達所有的數據節點。

Path p = new Path("p");

OutputStream out = fs.create(p);

out.write("content".getBytes("UTF-8"));

out.flush();

assertTaht(fs.getFileStatus(p).getLen,is(((long,"content".length())));

關閉對象流時,內部會調用hflush方法,但是hflush不保證datanode數據已經寫入到磁盤,只是保證寫入到datanode的內存, 因此在機器斷電的時候可能導致數據丟失,如果要保證寫入磁盤,使用hsync方法,hsync類型與fsync()的系統調用,fsync提交某個文件句柄的緩沖數據。

FileOutputStreamout = new FileOutPutStream(localFile);

out.write("content".getBytes("UTF-8"));

out.flush();

out.getFD().sync();

assertTaht(localFile.getLen,is(((long,"content".length())));

使用hflush或hsync會導致吞吐量下降,因此設計應用時,需要在吞吐量以及數據的健壯性之間做權衡。

另外,文件寫入過程中,當前正在寫入的Block對其他Reader不可見。

Hadoop節點距離

在讀取和寫入的過程中,namenode在分配Datanode的時候,會考慮節點之間的距離。HDFS中,距離沒有采用帶寬來衡量,因為實際中很難準確度量兩臺機器之間的帶寬。

Hadoop把機器之間的拓撲結構組織成樹結構,并且用到達公共父節點所需跳轉數之和作為距離。事實上這是一個距離矩陣的例子。下面的例子簡明地說明了距離的計算:

網格距離

Hadoop集群的拓撲結構需要手動配置,如果沒配置,Hadoop默認所有節點位于同一個數據中心的同一機架上。

通過distcp并行復制

前面的關注點都在于單線程的訪問,如果需要并行處理文件,需要自己編寫應用。Hadoop提供的distcp工具用于并行導入數據到Hadoop或者從Hadoop導出。一些例子:

hadoop distcp file1 file2? //可以作為fs -cp命令的高效替代

hadoop distcp dir1 dir2

hadoop distcp -update dir1 dir2 #update參數表示只同步被更新的文件,其他保持不變

distcp是底層使用MapReduce實現,只有map實現,沒有reduce。在map中并行復制文件。 distcp盡可能在map之間平均分配文件。map的數量可以通過-m參數指定:

hadoop distcp -update -delete -p hdfs://master1:9000/foo hdfs://master2/foo

這樣的操作常用于在兩個集群之間復制數據,update參數表示只同步被更新過的數據,delete會刪除目標目錄中存在,但是源目錄不存在的文件。p參數表示保留文件的全校、block大小、副本數量等屬性。

如果兩個集群的Hadoop版本不兼容,可以使用webhdfs協議:

hadoop distcp webhdfs: //namenode1: 50070/foo webhdfs: //namenode2: 50070/foo



原文鏈接:https://blog.csdn.net/bingduanlbd/article/details/51914550

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容