Hadoop分布式文件系統HDFS

HDFS的設計

HDFS以流式數據訪問模式來存儲超大文件,運行于商用硬件集群上。

  • 超大文件:指的是幾百MB、幾百GB甚至幾百TB大小的文件。

  • 流式數據訪問:HDFS構建思路:一次寫入、多次讀取時做高效的訪問模式

  • 商用硬件:Hadoop是設計運行在通用商用硬件的集群上的,因此對于龐大的集群而言,節點發生故障的幾率非常高。HDFS遇到故障時,被設計成能夠繼續運行且不讓用戶察覺到明顯的中斷。

  • 低時延的數據訪問:HDFS是為高數據吞吐量應用優化的,這可能會以提高時延為代價,對低時延的訪問需求,HBase是更好的選擇。

  • 大量的小文件:namenode將文件系統的元數據存儲在內存中,因此該文件系統所能存儲的文件總數受限于namenode的內存容量。

  • 多用戶寫入,任意修改文件:HDFS中的文件寫入支持單個寫入者,而且寫操作總是以“只添加”方式在文件末尾寫數據,不支持多個寫入者的操作。

HDFS的組成

數據塊

HDFS上的文件被劃分為塊大小的多個分塊(chunk),作為獨立的存儲單元,默認大小為120MB,HDFS的塊比磁盤塊大的目的是為了最小化尋址開銷,如果塊足夠大,從磁盤傳輸數據的時間會明顯大于定位這個塊開始位置所需的時間。HDFS中小于一個塊大小的文件不會占據整個塊空間。對分布式文件系統的塊進行抽象的好處如下:

  • 一個文件的大小可以大于網絡中任意一個磁盤的容量。文件的所有塊不需要存儲在同一個磁盤上,因此它們可以利用集群上的任意一個磁盤進行存儲。

  • 使用抽象塊而非整個文件作為存儲單元,大大簡化了存儲子系統的設計。因為塊的大小時固定的,因此計算單個磁盤能存儲多少個塊就相對容易。

namenode和datanode

  • namenode為管理節點,用于管理文件系統的命名空間,維護著文件系統樹及整棵樹內所有的文件和目錄。這些信息以命名空間鏡像文件和編輯日志文件的形式永久保存在本地磁盤上。為實現namenode的高可用,可以備份那些組成文件系統元數據持久狀態的文件或者運行一個輔助namenode。

  • datanode是文件系統的工作節點,根據需要存儲并檢索數據塊,定期向namenode發送它們所存儲的塊的列表。

塊緩存

對于訪問頻繁的文件,其對應的塊會被顯示地緩存在DataNode的內存中,以堆外緩存的形式存在。作業調度器通過緩存塊的datanode上運行任務,可以實現高性能讀操作。

聯邦HDFS

聯邦HDFS允許系統通過添加namenode實現擴展,其中每個namenode負責管理文件系統命名空間中的一部分。

HDFS的高可用

Hadoop2對HDFS提供了高可用支持,通過配置一對活動-備用namenode,當活動namenode失效,備用namenode就好接管它的任務并開始服務于來自客戶端的請求,不會有任何明顯中斷。架構需要作如下調整。

  • namenode之間需要通過高可用共享存儲實現編輯日志的共享。

  • datanode需要同時向兩個namenode發送數據塊處理報告,因為數據塊的映射信息存儲在namenode的內存中而非硬盤。

  • 客戶端需要使用特定的機制來處理namenode的實效問題,且該機制是透明的。

  • 實現高可用共享存儲有:NFS過濾器或者群體日志管理器QJM

故障切換

系統中存在一個故障轉移控制器,管理著將活動namenode轉換為備用namenode的轉換過程。每個namenode運行著一個輕量級的故障轉移控制器,其工作是通過一個簡單的心跳機制來監視宿主namenode是否失效,并在namenode失效時進行故障轉移。

規避

確保先前活動的namenode不會執行危害系統并導致系統崩潰的操作,該方式即為規避。

HDFS的Java 接口實現

從Hadoop URL讀取數據

通過URLStreamHandler實例以標準輸出方式顯示Hadoop文件系統的文件:

public class URLCat{
 static{
   URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
 }

 public static void main(String[] args){
   InputStream in = null;
   try{
     in = new URL("hdfs://host/path").openStream();
     IOUtils.copyBytes(in,System.out,4096,false);
   }finally{
     IOUtils.closeStream(in);
     }
   }
}

通過FileSystem API讀取數據

Hadoop文件系統通過Hadoop Path對象(而非java.io.File對象)來代表文件,可以將路徑視為一個Hadoop文件系統URI,如:hdfs://localhost/user/tom/quangle.txt

使用FileSystem以標準輸出格式顯示Hadoop文件系統中的文件

public class FileSystemCat{
   public static void main(String[] args){
     String uri = "hdfs://localhost/user/tom/abc.txt";
     Configuration conf = new Configuration();
     FileSystem fs = FileSystem.get(URI.create(uri),conf);
     InputSteam in = null;
     try{
         in = fs.open(new Path(uri));
         IOUtils.copyBytes(in,System.out,4096,false);
     }finally{
         IOUtils.closeStream(in);
       }
   }
}

FSDataInputStream對象

FileSystem對象中的open()方法返回的是FSDataInputStream對象,而不是標準的java.io對象。該對象支持隨機訪問,可以從流的任意位置讀取數據。其源碼如下:

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 IOExceptiom;
}

調用seek()來定位大于文件長度的位置會引發IOException異常

FSDataInputStream類也實現了PositionedReadable接口,從一個指定偏移量處讀取文件的一部分:

public interface PositionedReadable{
 //read()方法從文件的指定position處讀取至少length字節的數據
 //并存入buffer緩沖區的指定偏移量offset處
 //返回值是實際讀到的字節數
 public int read(long position,byte[] buffer,int offset,int length) throws IOException;

 //readFully()方法將指定length長度的字節數據讀取到buffer中
 public void readFully(long position,byte[] buffer,int offset,int length)throws IOException;
 public void readFully(long position,byte[] buffer) throws IOException;
}

以上方法會保留文件當前偏移量,并且是線程安全的。但seek()方法是一個相對高開銷的操作,需要慎用。

寫入數據

將本地文件復制到hdfs:重要方法為create()方法

public class FileCopyWithProgress{
     public static void main(String[] args)throws Exception{
       String localSrc = "c://tmp/abc.txt";
       String dst = "hdfs://localhost/usr/tmp/abc.txt";
       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);
   }
}

其中,FileSystem實例的create()方法返回FSDataOutputStream對象,該對象與FSDataInputStream類似,實現如下:

package org.apache.hadoop.fs;
public class FSDataOutputStream extends DataOutputStream implements Syncable{
 public long getPos()throws IOException{
 //...
 }
 ....
}

FSDataOutputStream類不允許在文件中定位,這是因為hdfs只允許對一個已打開的文件順序寫入,或在現有文件的末尾追加數據(append方法)

創建目錄

FileSystem實例提供了創建目錄的方法:

public boolean mkdir(Path f)throws IOException;

通常不需要顯示創建目錄,調用create()方法寫入文件時會自動創建父目錄。

查詢文件系統

1、文件元數據:FileStatus類封裝了文件系統中文件的目錄和元數據,包括長度、塊大小、復本、修改時間、所有者以及權限信息,FileSystem的getFileStatus()方法用于獲取文件或者目錄的FileStatus對象。檢查文件或者目錄是否存在調用exists()方法

public boolean exists(Path f)throws IOException

2、列出文件:FileSystem類提供的listStatus()方法來實現列出目錄中的內容功能

public FileStatus[] listStatus(Paht f)throws IOException
public FileStatus[] listStatus(Paht f,PathFilter filter)throws IOException
public FileStatus[] listStatus(Paht[] files)throws IOException
public FileStatus[] listStatus(Paht[] files,PathFilter filter)throws IOException

3、文件模式:Hadoop為執行通配提供了兩個FileSystem方法:

public FileStatus[] globStatus(Path pathPattern)throws IOException
public FileStatus[] globStatus(Path pathPattern,PahtFilter filter)throws IOException

4、PathFilter對象:PathFilter與java.io.FileFilter一樣,是Path對象而不是file對象

PathFilter用于排除匹配正則表達式的路徑

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);
 }
}

自定義過濾器,結合3中的globStatus()方法,可以實現大多數的文件名匹配。

刪除數據

使用FileSystem的delete()方法可以永久性刪除文件或目錄

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

如果f是一個文件或者空目錄,那么recursive的值則會別忽略,只有在recrusive的值為true時,非空目錄及其內容才會被刪除,否則拋出IOException異常。

數據流

文件讀取剖析

HDFS文件讀取流.png

1.客戶端通過調用FileSystem對象的open()方法打開希望讀取的文件,對于HDFS而言,該對像是一個DistributedFileSystem的一個實例。

2.DistributedFileSystem通過使用RPC(遠程過程調用)來調用namenode,以確定文件起始塊的位置。對于每一個塊,namenode返回存有該塊副本的datanode地址。DistributedFileSystem類返回一個FSDataInputStream對象給客戶端以便讀取數據,FSDataInputStream類封裝DFSInputStream對象,該對象管理著datanode和namenode的IO。

3.客戶端對FSDataInputStream輸入流調用read()方法。存儲著文件起始幾個塊的datanode地址的DFSInputStream隨機連接距離最近的文件中第一個塊所在的datanode。

4.對數據流反復調用read()方法,將數據從datanode傳輸到客戶端。

5.到達塊的末端時,DFSInputStream關閉與該datanode的連接,然后尋找下一個塊的最佳datanode。所有這些對于客戶端都是透明的,在客戶看來它一直在讀取一個連續的流。

6.客戶端從流中讀取數據時,塊是按照打開DFSInputStream與datanode新建連接的順序讀取的,也會根據需要詢問namenode來檢索下一批數據塊的datanode的位置,一旦完成讀取,就對FSDataInputStream調用close()方法。

故障處理:在讀取數據的時候,如果DFSInputStream與datanode通信時遇到錯誤,會嘗試從這個塊的另外一個最鄰近datanode讀取數據,它也記住那個故障datanode,以保證以后不會反復讀取該節點上后續的塊。DFSInputStream也會通過校驗和確認從datanode發來的數據是否完整,如果發現有損壞塊,DFSInputStream會試圖從其他datanode讀取其復本,也會將被損壞的塊通知namenode。

設計重點:客戶端可以直接連接到datanode檢索數據,且namenode告知客戶端每個塊所在的最佳datanode。由于數據流分散在集群中的所有datanode,所以這種設計能使HDFS擴展到大量的并發客戶端。同時namenode只需要響應塊位置的請求(這些信息存在namenode的內存上,性能高),無需響應數據請求。

文件寫入剖析

HDFS文件寫入流.png

1.客戶端通過對DistributedFileSystem對象調用create()來新建文件。

2.DistributedFileSystem對namenode創建一個RPC調用,在文件系統的命名空間創建一個文件,此時該文件還沒有相應的數據塊。namenode執行各種不同的檢查以確保這個文件不存在以及客戶端有新建該文件的權限。檢查通過,namenode則為創建新文件立即一條記錄;否則創建失敗并向客戶端拋出一個IOException異常。

DistributedFileSystem向客戶端返回一個FSDataOutputStream對象,由此客戶端可以開始寫入數據。與讀取事件一樣,FSDataOutputStream封裝一個DFSoutputStream對象,該對象負責處理datanode和namenode之間的通信。

3.客戶端寫入數據,DFSOutputStream將它分成一個個數據包(大化小),并寫入內部隊列,成為數據隊列(data queue)DataStreamer處理數據隊列,它的責任是挑選出適合存儲數據復本的一組datanode,并根據此來要求datanode分配新的數據塊,這一組datanode構成一個管線。

4.管線內部節點將數據包流式傳輸,假設復本數為3,則管線中有3個節點,DataStreamer將數據包流式傳輸到管線中的第一個datanode,該datanode存儲數據包,并將它發送到管線中的第二個datanode,同樣,第二個存儲該數據包且發送到管線中的第三個datanode(最后一個)。

5.DFSOutputStream也維護著一個內部數據包隊列來等待datanode的收到確認回執,稱為確認隊列ack queue。收到管道中所有datanode確認信息后,該數據包才會從確認隊列中刪除。

6.客戶端完成數據寫入后,對數據流調用close()方法。該操作將剩余的所有數據包寫入datanode管線,并在聯系到namenode告知其文件寫入完成之前,等待確認(7)

故障處理:如果任何datanode在數據寫入期間發生故障,則執行以下操作:

  • 首先關閉管線,確認把隊列中的所有數據包都添加回數據隊列的最前端,以確保故障節點下游的datanode不會漏掉任何一個數據包

  • 為存儲在另一正常datanode的當前數據塊指定一個新的標識,并將該標識傳送給namenode,以便故障datanode在恢復后可以刪除存儲的部分數據塊。

  • 從管線中刪除故障datanode,基于兩個正常datanode構建一條新管線。

  • 余下的數據塊寫入管線中正常的datanode

  • namenode注意到塊復本量不足時,會在另一個節點創建一個新的復本。

復本布局策略:Hadoop的默認布局策略是運行在客戶端的節點上方第1個復本(如果客戶端運行在集群外,則隨機選一個節點,不過系統會避免挑選存儲過滿或太忙的節點);第2個復本放在與第1個復本不同且隨機另外挑選的機架中的節點(離架),第3個復本與第2個復本放在同一個機架上,且隨機選擇另一個節點。一旦選定復本的放置位置,就根據網絡拓撲創建一個管線。

復本布局策略.png

一致模型

文件系統的一致模型(coherency model)描述了文件讀寫的數據可見性。HDFS為性能犧牲了一些POSIX要求。新建一個文件后,它能在文件系統的命名空間中立即可見,但寫入文件的內容卻不保證能立即可見,為此,Hadoop提供一種強行將所有緩存刷新到datanode中的方法,即對FSDataOutputStream調用hflush()方法。當hflush()方法返回成功后,對所有新的reader而已,HDFS能保證文件中到目前為止寫入的數據均能到達所有datanode的寫入管道,并且對新的reader均可見。

Path p = new Path("p");
FSDataOutputStream out = fs.create(p);
out.write("content".getBytes("utf-8"));
out.hflush();
...

注意:hflush()不保證datanode已經將數據寫到磁盤上,進確保數據在datanode的內存中,為確保數據寫入磁盤,可以用hsync()替代。

在HDFS中關閉文件out.close()時,隱含執行hflush()方法。

并行復制

Hadoop自帶一個distcp程序,可以并行從Hadoop文件系統中復制出大量數據,也可以將大量數據復制到Hadoop中,distcp的一種用法是替代hadoop fs -cp

% hadoop distcp file1 file2
% hadoop distcp dir1 dir2</pre>

參考資料:

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

推薦閱讀更多精彩內容