Hadoop學習(二)之 HDFS
HDFS(Hadoop Distributed File System) 是一個 Apache Software Foundation 項目,是 Apache Hadoop 項目的一個子項目.。Hadoop 非常適於存儲大型數據 (比如 TB 和 PB), 其就是使用 HDFS 作爲存儲系統.。HDFS 使用多臺計算機存儲文件, 並且提供統一的訪問接口,像是訪問一個普通文件系統一樣使用分佈式文件系統。 HDFS 對數據文件的訪問通過流的方式進行處理,這意味着通過命令和 MapReduce 程序的方式可以直接使用 HDFS。HDFS 是容錯的,且提供對大數據集的高吞吐量訪問。
HDFS 的一個非常重要的特點就是一次寫入、多次讀取, 該模型降低了對併發控制的要求, 簡化了數據聚合性, 支持高吞吐量訪問. 而吞吐量是大數據系統的一個非常重要的指標, 吞吐量高意味着能處理的數據量就大.
1 設計目標
- 分佈式:通過跨多個廉價計算機集羣分佈數據和處理來節約成本
- 副本機制:通過自動維護多個數據副本和在故障發生時來實現可靠性
- 易擴展:它們爲存儲和處理超大規模數據提供所需的擴展能力。
2 HDFS 的歷史
- Doug Cutting 在做 Lucene 的時候, 需要編寫一個爬蟲服務, 這個爬蟲寫的並不順利, 遇到了一些問題, 諸如: 如何存儲大規模的數據, 如何保證集羣的可伸縮性, 如何動態容錯等
- 2013年的時候, Google 發佈了三篇論文, 被稱作爲三駕馬車, 其中有一篇叫做 GFS, 是描述了 Google 內部的一個叫做 GFS 的分佈式大規模文件系統, 具有強大的可伸縮性和容錯性
- Doug Cutting 後來根據 GFS 的論文, 創造了一個新的文件系統, 叫做 HDFS
3 HDFS 的架構
- NameNode 是一箇中心服務器, 單一節點(簡化系統的設計和實現), 負責管理文件系統的名字空間(NameSpace)以及客戶端對文件的訪問
- 文件操作, NameNode 是負責文件元數據的操作, DataNode 負責處理文件內容的讀寫請求, 跟文件內容相關的數據流不經過 NameNode, 只詢問它跟哪個 DataNode聯繫, 否則 NameNode 會成爲系統的瓶頸
- 副本存放在哪些 DataNode 上由 NameNode 來控制, 根據全局情況作出塊放置決定, 讀取文件時 NameNode 儘量讓用戶先讀取最近的副本, 降低讀取網絡開銷和讀取延時
- NameNode 全權管理數據庫的複製, 它週期性的從集羣中的每個 DataNode 接收心跳信合和狀態報告, 接收到心跳信號意味着 DataNode 節點工作正常, 塊狀態報告包含了一個該 DataNode 上所有的數據列表
NameNode | DataNode |
---|---|
存儲元數據 | 存儲文件內容 |
元數據保存在內存中 | 文件內容保存在磁盤 |
保存文件, block, DataNode 之間的關係 | 維護了 block id 到 DataNode 文件之間的關係 |
4 HDFS 文件副本和 Block 塊存儲
所有的文件都是以 block 塊的方式存放在 HDFS 文件系統當中, 在 Hadoop1 當中, 文件的 block 塊默認大小是 64M, hadoop2 當中, 文件的 block 塊大小默認是 128M, block 塊的大小可以通過 hdfs-site.xml 當中的配置文件進行指定
<property>
<name>dfs.block.size</name>
<value>塊大小 以字節爲單位</value>
</property>
4.1 引入塊機制的好處
- 一個文件有可能大於集羣中任意一個磁盤
- 使用塊抽象而不是文件可以簡化存儲子系統
- 塊非常適合用於數據備份進而提供數據容錯能力和可用性
4.2 塊緩存
通常 DataNode 從磁盤中讀取塊, 但對於訪問頻繁的文件, 其對應的塊可能被顯式的緩存在 DataNode 的內存中, 以堆外塊緩存的形式存在. 默認情況下,一個塊僅緩存在一個 DataNode 的內存中,當然可以針對每個文件配置 DataNode 的數量. 作業調度器通過在緩存塊的 DataNode 上運行任務, 可以利用塊緩存的優勢提高讀操作的性能.
例如:連接(join) 操作中使用的一個小的查詢表就是塊緩存的一個很好的候選
用戶或應用通過在緩存池中增加一個 Cache Directive 來告訴 NameNode 需要緩存哪些文件及存多久. 緩存池(Cache Pool) 是一個擁有管理緩存權限和資源使用的管理性分組.
例如:一個文件 130M, 會被切分成 2 個 block 塊, 保存在兩個 block 塊裏面, 實際佔用磁盤 130M 空間, 而不是佔用256M的磁盤空間
4.3 HDFS 文件權限驗證
HDFS 的文件權限機制與 Linux 系統的文件權限機制類似
r:read w:write x:execute
權限 x
對於文件表示忽略, 對於文件夾表示是否有權限訪問其內容 如果 Linux 系統用戶 zhangsan 使用 Hadoop 命令創建一個文件, 那麼這個文件在 HDFS 當中的 Owner 就是 zhangsan HDFS 文件權限的目的, 防止好人做錯事, 而不是阻止壞人做壞事. HDFS相信你告訴我你是誰, 你就是誰
5 HDFS 的元信息和 SecondaryNameNode
當 Hadoop 的集羣當中, 只有一個 NameNode 的時候, 所有的元數據信息都保存在了 FsImage 與 Eidts 文件當中, 這兩個文件就記錄了所有的數據的元數據信息, 元數據信息的保存目錄配置在了 hdfs-site.xml
當中
<property>
<name>dfs.namenode.name.dir</name>
<value>file:///export/services/hadoop-3.1.1/datas/namenode/namenodedatas</value>
</property>
<property>
<name>dfs.namenode.edits.dir</name>
<value>file:///export/services/hadoop-3.1.1/datas/dfs/nn/edits</value>
</property>
5.1 FsImage 和 Edits 詳解
edits
edits
存放了客戶端最近一段時間的操作日誌- 客戶端對 HDFS 進行寫文件時會首先被記錄在
edits
文件中 edits
修改時元數據也會更新- 每次 HDFS 更新時
edits
先更新後客戶端纔會看到最新信息
fsimage
- NameNode 中關於元數據的鏡像, 一般稱爲檢查點,
fsimage
存放了一份比較完整的元數據信息 - 因爲
fsimage
是 NameNode 的完整的鏡像, 如果每次都加載到內存生成樹狀拓撲結構,這是非常耗內存和CPU, 所以一般開始時對 NameNode 的操作都放在 edits 中 fsimage
內容包含了 NameNode 管理下的所有 DataNode 文件及文件 block 及 block 所在的 DataNode 的元數據信息.- 隨着
edits
內容增大, 就需要在一定時間點和fsimage
合併
- NameNode 中關於元數據的鏡像, 一般稱爲檢查點,
5.2 fsimage 中的文件信息查看
使用命令 hdfs oiv
cd /export/services/hadoop-3.1.1/datas/namenode/namenodedatas
hdfs oiv -i fsimage_0000000000000000864 -p XML -o hello.xml
5.3 edits 中的文件信息查看
使用命令 hdfs oev
cd /export/services/hadoop-3.1.1/datas/dfs/nn/edits
hdfs oev -i edits_0000000000000000865-0000000000000000866 -o myedit.xml -p XML
5.4 SecondaryNameNode 如何輔助管理 fsimage 與 edits 文件?
-
SecondaryNameNode 定期合併 fsimage 和 edits, 把 edits 控制在一個範圍內
-
配置 SecondaryNameNode
-
SecondaryNameNode 在
conf/masters
中指定 -
在 masters 指定的機器上, 修改
hdfs-site.xml
<property> <name>dfs.http.address</name> <value>host:50070</value> </property>
-
修改
core-site.xml
, 這一步不做配置保持默認也可以<!-- 多久記錄一次 HDFS 鏡像, 默認 1小時 --> <property> <name>fs.checkpoint.period</name> <value>3600</value> </property> <!-- 一次記錄多大, 默認 64M --> <property> <name>fs.checkpoint.size</name> <value>67108864</value> </property>
-
- SecondaryNameNode 通知 NameNode 切換 editlog
- SecondaryNameNode 從 NameNode 中獲得 fsimage 和 editlog(通過http方式)
- SecondaryNameNode 將 fsimage 載入內存, 然後開始合併 editlog, 合併之後成爲新的 fsimage
- SecondaryNameNode 將新的 fsimage 發回給 NameNode
- NameNode 用新的 fsimage 替換舊的 fsimage
特點
- 完成合並的是 SecondaryNameNode, 會請求 NameNode 停止使用 edits, 暫時將新寫操作放入一個新的文件中
edits.new
- SecondaryNameNode 從 NameNode 中通過 Http GET 獲得 edits, 因爲要和 fsimage 合併, 所以也是通過 Http Get 的方式把 fsimage 加載到內存, 然後逐一執行具體對文件系統的操作, 與 fsimage 合併, 生成新的 fsimage, 然後通過 Http POST 的方式把 fsimage 發送給 NameNode. NameNode 從 SecondaryNameNode 獲得了 fsimage 後會把原有的 fsimage 替換爲新的 fsimage, 把 edits.new 變成 edits. 同時會更新 fstime
- Hadoop 進入安全模式時需要管理員使用 dfsadmin 的 save namespace 來創建新的檢查點
- SecondaryNameNode 在合併 edits 和 fsimage 時需要消耗的內存和 NameNode 差不多, 所以一般把 NameNode 和 SecondaryNameNode 放在不同的機器上
6 HDFS 文件寫入過程
- Client 發起文件上傳請求,通過 RPC 與 NameNode 建立通訊,NameNode 檢查目標文件是否已存在, 父目錄是否存在, 返回是否可以上傳
- Client 請求第一個 block 該傳輸到哪些 DataNode 服務器上
- NameNode 根據配置文件中指定的備份數量及機架感知原理進行文件分配, 返回可用的 DataNode 的地址如: A, B, C
- Hadoop 在設計時考慮到數據的安全與高效, 數據文件默認在 HDFS 上存放三份, 存儲策略爲本地一份, 同機架內其它某一節點上一份, 不同機架的某一節點上一份。
- Client 請求 3 臺 DataNode 中的一臺 A 上傳數據(本質上是一個 RPC 調用,建立 pipeline ), A 收到請求會繼續調用 B, 然後 B 調用 C, 將整個 pipeline 建立完成, 後逐級返回 client
- Client 開始往 A 上傳第一個 block(先從磁盤讀取數據放到一個本地內存緩存), 以 packet 爲單位(默認64K), A 收到一個 packet 就會傳給 B, B 傳給 C. A 每傳一個 packet 會放入一個應答隊列等待應答
- 數據被分割成一個個 packet 數據包在 pipeline 上依次傳輸, 在 pipeline 反方向上, 逐個發送 ack(命令正確應答), 最終由 pipeline 中第一個 DataNode 節點 A 將 pipelineack 發送給 Client
- 當一個 block 傳輸完成之後, Client 再次請求 NameNode 上傳第二個 block 到服務 1
7 HDFS 文件讀取過程
- Client向NameNode發起RPC請求,來確定請求文件block所在的位置;
- NameNode會視情況返回文件的部分或者全部block列表,對於每個block,NameNode 都會返回含有該 block 副本的 DataNode 地址; 這些返回的 DN 地址,會按照集羣拓撲結構得出 DataNode 與客戶端的距離,然後進行排序,排序兩個規則:網絡拓撲結構中距離 Client 近的排靠前;心跳機制中超時彙報的 DN 狀態爲 STALE,這樣的排靠後;
- Client 選取排序靠前的 DataNode 來讀取 block,如果客戶端本身就是DataNode,那麼將從本地直接獲取數據(短路讀取特性);
- 底層上本質是建立 Socket Stream(FSDataInputStream),重複的調用父類 DataInputStream 的 read 方法,直到這個塊上的數據讀取完畢;
- 當讀完列表的 block 後,若文件讀取還沒有結束,客戶端會繼續向NameNode 獲取下一批的 block 列表;
- 讀取完一個 block 都會進行 checksum 驗證,如果讀取 DataNode 時出現錯誤,客戶端會通知 NameNode,然後再從下一個擁有該 block 副本的DataNode 繼續讀。
- read 方法是並行的讀取 block 信息,不是一塊一塊的讀取;NameNode 只是返回Client請求包含塊的DataNode地址,並不是返回請求塊的數據;
- 最終讀取來所有的 block 會合併成一個完整的最終文件。
8 HDFS的命令行使用
語法:hdfs dfs 參數
hdfs 常用的操作命令
hdfs dfs -ls / // 查看根路徑下的文件或文件夾
hdfs dfs -mkdir -p /xx/xxx //在hdfs上遞歸創建文件夾
hdfs dfs -moveFromLocal sourceDir(本地磁盤的文件或文件夾路徑) destDir(hdfs的路徑)
hdfs dfs -mv hdfsSourceDir hdfsDestDir
hdfs dfs -put localDir hdfsDir //將本地文件系統的文件或文件夾放到hdfs上去
hdfs dfs -appendToFile a.txt b.txt /hello.txt
hdfs dfs -cat hdfsDir //查看hdfs的文件內容
hdfs dfs -cp hdfsSourceDir hdfsDestDir //拷貝文件或者文件夾
hdfs dfs -rm [-r] //(遞歸)刪除文件或者文件夾
hdfs的權限管理兩個命令:
hdfs dfs -chmod -R 777 /xxx
hdfs dfs -chown -R hadoop:hadoop /xxx
9 HDFS 的 API 操作
在windows系統配置hadoop運行環境:
第一步:將apache-hadoop-3.1.1文件拷貝到一個沒有中文沒有空格的路徑下面
第二步:在windows上面配置hadoop的環境變量:HADOOP_HOME;以及將其bin目錄加入path中
第三步:將bin目錄中的hadoop.dll文件放到系統盤 C:\Windows\System32
第四步:重啓Windows
9.1 導入 Maven 依賴
<repositories>
<repository>
<id>cloudera</id>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs-client</artifactId>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
9.2 概述
在 Java 中操作 HDFS, 主要涉及以下 Class:
-
Configuration
- 該類的對象封轉了客戶端或者服務器的配置
-
FileSystem
-
該類的對象是一個文件系統對象, 可以用該對象的一些方法來對文件進行操作, 通過 FileSystem 的靜態方法 get 獲得該對象
FileSystem fs = FileSystem.get(conf)
get
方法從conf
中的一個參數fs.defaultFS
的配置值判斷具體是什麼類型的文件系統- 如果我們的代碼中沒有指定
fs.defaultFS
, 並且工程 ClassPath 下也沒有給定相應的配置,conf
中的默認值就來自於 Hadoop 的 Jar 包中的core-default.xml
- 默認值爲
file:///
, 則獲取的不是一個 DistributedFileSystem 的實例, 而是一個本地文件系統的客戶端對象
-
9.3 獲取 FileSystem 的幾種方式
備註:要在下面代碼中直接使用node**代替192.168.188.***需要設置的Window中System32中的hosts文件進行映射,否則直接使用192.168.188.***代替即可。
- 第一種方式
@Test
public void getFileSystem() throws IOException {
// 獲取Configuration對象
Configuration configuration = new Configuration();
// 設置Configuration對象,設置的目的是來指定要操作的文件系統
configuration.set("fs.defaultFS", "hdfs://node01:8020");
// 獲取指定的文件系統,獲取FileSystem就相當於獲取了主節點中所有的元數據信息
FileSystem fileSystem = FileSystem.get(configuration);
System.out.println("fileSystem: " + fileSystem.toString());
}
輸出結果:
- 第二種方式
@Test
public void getFileSystem2() throws IOException, URISyntaxException {
FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
System.out.println(fileSystem);
}
執行結果:
- 第三種方式
@Test
public void getFileSystem3() throws IOException, URISyntaxException {
Configuration configuration = new Configuration();
configuration.set("fs.defaultFS", "hdfs://node01:8020");
FileSystem fileSystem = FileSystem.newInstance(configuration);
System.out.println("fileSystem: " + fileSystem.toString());
}
執行結果:
- 第四種方式
@Test
public void getFileSystem4() throws IOException, URISyntaxException {
FileSystem fileSystem = FileSystem.newInstance(new URI("hdfs://node01:8020"), new Configuration());
System.out.println(fileSystem);
}
9.4 遍歷 HDFS 中所有文件
- 遞歸遍歷
@Test
public void listFile() throws Exception{
// 1. 獲取FileSystem
FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
// 2. 獲取指定目錄下的所有信息,第一個參數指定遍歷的路徑,第二個參數表示是否要
RemoteIterator<LocatedFileStatus> iterator = fileSystem.listFiles(new Path("/"), true);
// 3. 遍歷迭代器
while (iterator.hasNext()){
// 獲取每一個文件的詳細信息
LocatedFileStatus fileStatus = iterator.next();
// 獲取每一個文件的存儲路徑
System.out.println(fileStatus.getPath()+"---"+fileStatus.getPath().getName());
// 獲取文件的block存儲信息
BlockLocation[] blockLocations = fileStatus.getBlockLocations();
// 打印每一個文件的block數
System.out.println(blockLocations.length);
// 打印每一個block副本的儲存位置
for (BlockLocation blockLocation : blockLocations) {
String[] hosts = blockLocation.getHosts();
for (String host : hosts) {
System.out.print(host+" <-> ");
}
System.out.println();
}
}
}
執行結果:
9.5 下載文件到本地
@Test
public void downLoadFileTest() throws URISyntaxException, IOException {
// 1. 獲取FileSystem對象
FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
// 2. 獲取Hdfs文件輸入流
FSDataInputStream inputStream = fileSystem.open(new Path("/aaa/zookeeper.out"));
// 3. 獲取本地文件的輸出流
FileOutputStream outputStream = new FileOutputStream(new File("F://zookeeper.out"));
// 4. 實現文件的複製
IOUtils.copy(inputStream, outputStream);
// 5. 關閉流
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
fileSystem.close();
}
更簡單的下載方式:
@Test
public void downLoadFileTest2() throws URISyntaxException, IOException {
// 1. 獲取FileSystem對象
FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
// 2. 調用方法直接實現文件的下載
fileSystem.copyToLocalFile(new Path("/aaa/zookeeper.out"), new Path("E://zookeeper.out"));
fileSystem.close();
}
9.6 HDFS 上創建文件夾和文件
@Test
public void mkdirsTest() throws URISyntaxException, IOException {
// 獲取
FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
// 創建文件夾
fileSystem.mkdirs(new Path("/app/text/hello"));
// 創建文件
fileSystem.create(new Path("/a.txt"));
}
查詢執行的結果:
9.7 HDFS 文件上傳
@Test
public void uploadFileTest() throws IOException, URISyntaxException {
// 1. 獲取FileSystem對象
FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
// 2. 調用方法實現文件的上傳
fileSystem.copyFromLocalFile(new Path("F://hello.txt"), new Path("/hello.txt"));
fileSystem.close();
}
9.8 僞造用戶
- 停止hdfs集羣,在node01機器上執行以下命令
/export/services/hadoop-3.1.1/sbin/stop-yarn.sh
/export/services/hadoop-3.1.1/sbin/stop-dfs.sh
- 修改node01機器上的hdfs-site.xml當中的配置文件
cd /export/services/hadoop-3.1.1/etc/hadoop
vim hdfs-site.xml
<property>
<name>dfs.permissions.enabled</name>
<value>true</value>
</property>
- 修改完成之後配置文件發送到其他機器上面去
scp hdfs-site.xml node02:$PWD
scp hdfs-site.xml node03:$PWD
- 重啓hdfs集羣
cd /export/services/hadoop-3.1.1
sbin/start-dfs.sh
- 隨意上傳一些文件到我們hadoop集羣當中準備測試使用
cd /export/services/hadoop-3.1.1/etc/hadoop
hdfs dfs -mkdir /config
hdfs dfs -put *.xml /config
hdfs dfs -chmod 600 /config/core-site.xml
- 使用代碼準備下載文件
@Test
public void getConfig()throws Exception{
FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.188.100:8020"), new Configuration(),"root");
fileSystem.copyToLocalFile(new Path("/config/core-site.xml"),new Path("file:///c:/core-site.xml"));
fileSystem.close();
}
9.9 小文件合併
由於 Hadoop 擅長存儲大文件,因爲大文件的元數據信息比較少,如果 Hadoop 集羣當中有大量的小文件,那麼每個小文件都需要維護一份元數據信息,會大大的增加集羣管理元數據的內存壓力,所以在實際工作當中,如果有必要一定要將小文件合併成大文件進行一起處理
在我們的 HDFS 的 Shell 命令模式下,可以通過命令行將很多的 hdfs 文件合併成一個大文件下載到本地
cd /export/servers
hdfs dfs -getmerge /config/*.xml ./hello.xml
既然可以在下載的時候將這些小文件合併成一個大文件一起下載,那麼肯定就可以在上傳的時候將小文件合併到一個大文件裏面去
@Test
public void mergeFile() throws Exception{
//獲取分佈式文件系統
FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration(),"hadoop");
FSDataOutputStream outputStream = fileSystem.create(new Path("/bigfile.xml"));
//獲取本地文件系統
LocalFileSystem local = FileSystem.getLocal(new Configuration());
//通過本地文件系統獲取文件列表,爲一個集合
FileStatus[] fileStatuses = local.listStatus(new Path("F:\\input"));
for (FileStatus fileStatus : fileStatuses) {
FSDataInputStream inputStream = local.open(fileStatus.getPath());
IOUtils.copy(inputStream,outputStream);
IOUtils.closeQuietly(inputStream);
}
IOUtils.closeQuietly(outputStream);
local.close();
fileSystem.close();
}
未完待續…