1. 程式人生 > >原來大資料 Hadoop 是這樣儲存資料的

原來大資料 Hadoop 是這樣儲存資料的

## HDFS概述 ### 產生背景 隨著資料量越來越大,在一個作業系統中存不下所有的資料。需要將這些資料分配到更多的作業系統中,帶來的問題是多作業系統不方便管理和維護。需要**一種系統來管理多臺機器上的檔案**,這就是分散式檔案管理系統。**HDFS是分散式檔案管理系統中的一種** ### 定義 HDFS(Hadoop Distributed File System)它是一個檔案系統,用於儲存檔案,通過目錄樹來定位檔案。其次,他是分散式的,由很多伺服器聯合起來實現其功能,叢集中的伺服器有各自的角色 HDFS 的使用場景:適合一次寫,多次讀的場景,且不支援檔案的修改。適合用來做資料分析 ### 優缺點 #### 優點 - 高容錯 - 資料自動儲存多個副本,通過增加副本的形式,提高容錯性 - 某一個副本丟失以後,可以自動恢復 - 適合處理大資料 - 資料規模:達到 GB、TB、甚至 PB 級別的資料 - 檔案規模:能夠處理百萬規模以上的檔案數量 - 可以構建在廉價機器上 #### 缺點 - 不適合低延時資料訪問。比如毫秒級的儲存資料,做不到 - 無法高效的對大量小檔案進行儲存 - namenode 對每個檔案至少需要消耗 150 位元組去儲存目錄和塊資訊,小檔案相比大檔案更消耗 namenode 伺服器記憶體 - 小檔案定址時間會超過讀取時間,違背 HDFS 設計初衷 - 不支援併發寫入、檔案隨機修改 - 一個檔案只能有一個寫,不允許多執行緒同時寫 - 僅支援資料追加,不支援修改 ### 組成架構 ![image-20201221211014820](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233008483-1523232254.png) ![image-20201221211229108](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233007659-1878299835.png) ### HDFS 檔案塊大小 HDFS 中的檔案在物理上是分塊儲存(Block),塊的大小可以手動配置引數 `dfs.blocksize` 來修改(Hadoop 2.x 是 128m,之前是 64m) #### 為什麼取 128m 呢? 普遍認為,定址時間(即查詢目標 block 的時間)為 10ms,而定址時間為傳輸時間的 1% 時,為 HDFS 執行的理想最佳狀態。此時傳輸時間為 10ms / 1% = 1000ms = 1s,而目前硬碟的傳輸速度普遍為 100m/s ,因此 block 的大小取 1s*100m/s = 100m。離它最近的 2 的次冪就是 128 了。這塊可以看出,影響 block 大小的主要因素就是硬碟的讀取速度。因此當採用固態硬碟的時候完全可以把數值調整到 256 m 甚至更多。 塊太小的時候,會增加定址時間 但當塊變得很大時,就要想辦法避免熱點資料的頻繁讀取了。這一點在 Google 的論文中有提到,論文中給到的解決思路是客戶端快取,但是並沒有提具體實現 https://www.cnblogs.com/zzjhn/p/3834729.html ## HDFS 的 Shell 操作 ### 基本語法 bin/hadoop fs 具體命令 OR bin/hdfs dfs 具體命令 dfs是fs的實現類 ### 常用命令 | 命令 | 解釋 | 示例 | 備註 | | ----------------------- | ------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------ | | -ls | 顯示目錄資訊 | | | | -mkdir | 在HDFS上建立目錄 | hadoop fs -mkdir -p /user/keats/love | -p 建立多級目錄 | | -moveFromLocal | 從本地剪下貼上到HDFS | hadoop fs -moveFromLocal ./yaya.txt /user/keats/love/ | 前面是來源路徑 後面是目標路徑,下同 | | -appendToFile | 追加一個檔案到已經存在的檔案末尾 | | | | -cat | 顯示檔案內容 | | | | -chgrp 、-chmod、-chown | Linux檔案系統中的用法一樣,修改檔案所屬許可權 | | | | -copyFromLocal | 從本地檔案系統中拷貝檔案到HDFS路徑去 | | 同 -put | | -copyToLocal | 從HDFS拷貝到本地 | | 同 -get | | -getmerge | 合併下載多個檔案 | hadoop fs -getmerge /user/keats/love/* ./yaya.txt | | | -tail | 顯示一個檔案的末尾 | | | | -rm | 刪除檔案或資料夾 | | | | -rmdir | 刪除空目錄 | | | | -du | 統計資料夾的大小資訊 | | 可以理解為 disk use | | -setrep | 設定HDFS中檔案的副本數量 | | 裡設定的副本數只是記錄在NameNode的元資料中,是否真的會有這麼多副本,還得看DataNode的數量。因為目前只有3臺裝置,最多也就3個副本,只有節點數的增加到10臺時,副本數才能達到10 | ## HDFS 客戶端操作 ### 環境準備 1. 把之前下載的 hadoop 安裝包解壓,複製到一個不含中文路徑的目錄下(建議所有開發相關東西放在一個目錄下,方便管理) 2. 配置環境變數 HADOOP_HOME 和 Path ### 專案準備 專案地址 https://github.com/keatsCoder/HdfsClientDemo 建立 maven 專案,引入依賴 ```xml junit junit RELEASE org.apache.logging.log4j log4j-core 2.8.2 org.apache.hadoop hadoop-common 2.10.1 org.apache.hadoop
hadoop-client 2.10.1
org.apache.hadoop hadoop-hdfs 2.10.1 jdk.tools jdk.tools 1.8 system ${JAVA_HOME}/lib/tools.jar
``` 建立 Java 類,HdfsClient 主要進行了三步操作 1. 建立 FileSystem 物件 2. 通過 FileSystem 物件執行命令 3. 關閉 FileSystem ```java public class HdfsClient { static FileSystem fs; static { Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://linux102:9000"); try { fs = FileSystem.get(conf); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { // 執行操作 mkDir(); // 釋放資源 fs.close(); } private static void mkDir() throws IOException { fs.mkdirs(new Path("/john/keats")); } } ``` 嘗試執行,會得到第一個錯誤,大意是許可權被拒絕,這個時候就需要配置 JVM 引數 `-DHADOOP_USER_NAME=root` 來告訴叢集,使用 root 使用者進行操作 ![image-20201222213812660](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233006919-1844737090.png) 配置好之後再執行,會遇到第二個錯誤 ``` Could not locate Hadoop executable: D:\develop\hadoop\bin\winutils.exe -see https://wiki.apache.org/hadoop/WindowsProblems ``` 這是因為我們之前配置環境的時候,解壓的 hadoop 檔案 bin 目錄下**沒有 winutils.exe 這個檔案**,根據後面地址 wiki 百科的指示,可以下載該檔案放在 bin 目錄下。但是目前那個檔案的最新版本是 2.8.1,也許會存在某些方面不相容的問題,目前還暫時沒有發現。因此可以直接下載該版本使用 https://github.com/steveloughran/winutils/releases #### 簡化配置使用者名稱和訪問路徑 FileSystem.get() 有一個過載方法,三個引數,第一個是 hadoop namenode 地址,第二個是 conf 物件,第三個是使用者名稱。可以一次配好 測試方法詳見示例程式碼 HdfsClient2.java 類 ```java fs = FileSystem.get(new URI("hdfs://linux102:9000"), conf, "root"); ``` #### 上傳檔案 在專案 resource 目錄下建立檔案 zhangsan.txt 呼叫 copyFromLocalFile 方法上傳檔案 ```java private static void uploadFile() throws IOException { URL systemResource = ClassLoader.getSystemResource("zhangsan.txt"); String path = systemResource.getPath(); fs.copyFromLocalFile(new Path(path), new Path("/john/keats/love")); } ``` copyFromLocalFile 還有三個過載方法,分別提供以下功能 - 是否刪除原始檔 - 當目標檔案存在時,是否覆蓋目標檔案 - 多檔案批量上傳,但目標路徑必須是資料夾路徑 #### 配置檔案優先順序 之前我們在 hadoop 叢集配置的副本數量是 3 ,而 hadoop client 也支援兩種方式配置引數 1. conf 物件通過 key-value 形式配置 2. resources 目錄下放置 xml 配置檔案配置 加上預設的 default-xxxx.xml 一共四種配置的方式。他們的優先順序是 conf >
resources 下的配置檔案 > hadoop 叢集配置檔案 > default **ConfigFileTest.java** 類對此處的配置進行的說明與測試,讀者可以執行體驗 ![image-20201222223559044](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233006461-1394556333.png) #### 下載檔案 ```java fs.copyToLocalFile(new Path("/three.txt"), new Path("D://zhangsan.txt")); ``` copyToLocalFile 還有兩個過載方法,分別添加了。具體程式碼可參考 DownLoadFileTest.java ```java // 是否刪除原始檔 boolean delSrc ``` ```java // 是否使用RawLocalFileSystem作為本地檔案系統 // 預設是 false,目前比較直觀的就是 false 狀態下下載檔案會同時生成 .crc 格式的校驗檔案,設定為 true 時不會生成 boolean useRawLocalFileSystem ``` #### 刪除檔案 刪除檔案的API,第二個引數表示是否遞迴刪除。如果要刪除的 Path 對應的是資料夾,recursive 需要設定為 true ,否則會拋異常。其實就相當於 `rm -r` 中的引數 -r ```java public abstract boolean delete(Path f, boolean recursive) throws IOException; ``` ```java private static void deleteFile() throws IOException { // /john/keats 是資料夾目錄,遞迴設定為 false 會報錯 PathIsNotEmptyDirectoryException: ``/john/keats is non empty': Directory is not empty // fs.delete(new Path("/john/keats"), false); // 先上傳,再刪除 HdfsClient2.uploadFile(true); fs.delete(new Path("/john/keats/love/zhangsan.txt"), true); } ``` 這塊我在刪除之前添加了上傳操作,目的是為了防止檔案不存在。而上傳又存在一種可能就是目標檔案已存在。這是個薛定諤的檔案- -,因此我查看了 FileSystem 的上傳 API,他是提供 overwrite 開關的,預設是 true 即覆蓋目標檔案 #### 檔案重新命名 ```java private static void renameFile() throws IOException { String dstFileName = "wangwu.txt"; HdfsClient2.uploadFile(); deleteFile(dstFileName); // 目標檔案不存在,則更名成功 boolean rename = fs.rename(new Path("/john/keats/love/zhangsan.txt"), new Path("/john/keats/love/", dstFileName)); Assert.assertTrue(rename); // 目標檔案存在,則更名失敗 boolean renameButDstIsExist = fs.rename(new Path("/john/keats/love/zhangsan.txt"), new Path("/john/keats/love/", dstFileName)); Assert.assertFalse(renameButDstIsExist); } ``` #### 讀取檔案詳細資訊 ```java public static void listFiles() throws IOException { // 獲取檔案詳情 RemoteIterator listFiles = fs.listFiles(new Path("/"), true); while (listFiles.hasNext()) { LocatedFileStatus status = listFiles.next(); // 輸出詳情 // 檔名稱 System.out.println(status.getPath().getName()); // 長度 System.out.println(status.getLen()); // 許可權 System.out.println(status.getPermission()); // 分組 System.out.println(status.getGroup()); // 獲取儲存的塊資訊 BlockLocation[] blockLocations = status.getBlockLocations(); for (BlockLocation blockLocation : blockLocations) { // 獲取塊儲存的主機節點 String[] hosts = blockLocation.getHosts(); for (String host : hosts) { System.out.println(host); } } System.out.println("-----------分割線----------"); } } ``` 判斷某路徑下的內容是檔案還是資料夾 ```java public static void isFile() throws IOException { // FileStatus[] listStatus = fs.listStatus(new Path("/")); FileStatus[] listStatus = fs.listStatus(new Path("/three.txt")); for (FileStatus fileStatus : listStatus) { // 如果是檔案 if (fileStatus.isFile()) { System.out.println("f:"+fileStatus.getPath().getName()); }else { System.out.println("d:"+fileStatus.getPath().getName()); } } } ``` ### HDFS的I/O流操作 ```java // 從本地上傳到HDFS public static void copyFileFromDiskByIO() throws IOException { // 2 建立輸入流 FileInputStream fis = new FileInputStream(new File("D:/zhangsan.txt")); // 3 獲取輸出流 FSDataOutputStream fos = fs.create(new Path("/zhangsan.txt")); // 4 流對拷 IOUtils.copyBytes(fis, fos, conf); } // 從HDFS拷貝到本地 public static void copyFileFromHDFSByIO() throws IOException { FSDataInputStream fis = fs.open(new Path("/zhangsan.txt")); // 3 獲取輸出流 FileOutputStream fos = new FileOutputStream(new File("D:/zhangsan1.txt")); // 4 流的對拷 IOUtils.copyBytes(fis, fos, conf); } ``` #### 檔案的定位讀取 ```java /** * 從某個位置開始拷貝檔案,用於讀取某個完整檔案的部分內容 */ public static void copyFileSeek() throws Exception{ // 2 開啟輸入流 FSDataInputStream fis = fs.open(new Path("/hadoop-2.10.1.tar.gz")); // 3 定位輸入資料位置 fis.seek(1024*1024*128); // 4 建立輸出流 FileOutputStream fos = new FileOutputStream(new File("D:/hadoop-2.7.2.tar.gz.part2")); // 5 流的對拷 IOUtils.copyBytes(fis, fos, conf); } ``` ## HDFS 原理 ### HDFS的讀寫資料流程 #### 寫資料 ![image-20201229221340913](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233005937-1862741208.png) 1)客戶端通過Distributed FileSystem模組向NameNode請求上傳檔案,NameNode檢查目標檔案是否已存在,父目錄是否存在 2)NameNode返回是否可以上傳 3)客戶端請求第一個 Block上傳到哪幾個DataNode伺服器上(根據伺服器距離以及負載排序,取前副本數個伺服器返回) 4)NameNode返回3個DataNode節點,分別為dn1、dn2、dn3 5)客戶端通過FSDataOutputStream模組請求dn1上傳資料,dn1收到請求會繼續呼叫dn2,然後dn2呼叫dn3,將這個通訊管道建立完成 6)dn1、dn2、dn3逐級應答客戶端 7)客戶端開始往dn1上傳第一個Block(先從磁碟讀取資料放到一個本地記憶體快取),以Packet為單位,dn1收到一個Packet就會傳給dn2,dn2傳給dn3;dn1每傳一個packet會放入一個應答佇列等待應答 8)當一個Block傳輸完成之後,客戶端再次請求NameNode上傳第二個Block的伺服器。(重複執行3-7步) #### 讀資料 ![image-20210104210100229](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233005079-1265421473.png) 1)客戶端通過Distributed FileSystem向NameNode請求下載檔案,NameNode通過查詢元資料,找到檔案塊所在的DataNode地址 2)挑選一臺DataNode(就近原則,然後隨機)伺服器,請求讀取資料 3)DataNode開始傳輸資料給客戶端(從磁盤裡面讀取資料輸入流,以Packet為單位來做校驗) 4)客戶端以Packet為單位接收,先在本地快取,然後寫入目標檔案 ### 網路拓撲圖,節點距離計算 在HDFS寫資料的過程中,NameNode會選擇距離待上傳資料最近距離的DataNode接收資料。那麼這個最近距離怎麼計算呢? 節點距離:兩個節點到達最近的共同祖先的距離總和 ![image-20201229222511164](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233004466-839496650.png) ### 機架感知,副本儲存節點選擇 機架感知說明 http://hadoop.apache.org/docs/r2.10.1/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html#Data_Replication ``` For the common case, when the replication factor is three, HDFS’s placement policy is to put one replica on one node in the local rack, another on a different node in the local rack, and the last on a different node in a different rack. ``` ![image-20201229223021321](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233003834-211736912.png) 這樣佈置,第一考慮的是速度,也兼顧了容災的要求 ### NameNode和SecondaryNameNode #### NameNode中的元資料是儲存在哪裡的? 首先,我們做個假設,如果儲存在NameNode節點的磁碟中,因為經常需要進行隨機訪問,還有響應客戶請求,必然是效率過低。因此,元資料需要存放在記憶體中。但如果只存在記憶體中,一旦斷電,元資料丟失,整個叢集就無法工作了。因此產生在磁碟中備份元資料的**FsImage** 這樣又會帶來新的問題,當在記憶體中的元資料更新時,如果同時更新FsImage,就會導致效率過低,但如果不更新,就會發生一致性問題,一旦NameNode節點斷電,就會產生資料丟失。因此,**引入Edits檔案(只進行追加操作,效率很高)**。每當元資料有更新或者新增元資料時,修改記憶體中的元資料並追加到Edits中。這樣,一旦NameNode節點斷電,可以通過FsImage和Edits的合併,合成元資料。 但是,如果長時間新增資料到Edits中,會導致該檔案資料過大,效率降低,而且一旦斷電,恢復元資料需要的時間過長。因此,需要定期進行FsImage和Edits的合併,如果這個操作由NameNode節點完成,又會效率過低。因此,引入一個新的節點**SecondaryNamenode,專門用於FsImage和Edits的合併** 整體的操作機制和 Redis 差不多,FsImage 相當於 Redis 中的 RDB 快照,Edits 相當於 Redis 中的 AOF 日誌,兩者結合。而 Redis 合併兩個檔案是採用的 Fork 程序的方式 ![image-20210104211308789](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233003049-1955110365.png) 第一階段:NameNode啟動 (1)第一次啟動NameNode格式化後,建立Fsimage和Edits檔案。如果不是第一次啟動,直接載入編輯日誌和映象檔案到記憶體 (2)客戶端對元資料進行增刪改的請求 (3)NameNode記錄操作日誌,更新滾動日誌 (4)NameNode在記憶體中對資料進行增刪改 第二階段:Secondary NameNode工作 ​ (1)Secondary NameNode詢問NameNode是否需要CheckPoint。直接帶回NameNode是否檢查結果 ​ (2)Secondary NameNode請求執行CheckPoint ​ (3)NameNode滾動正在寫的Edits日誌 ​ (4)將滾動前的編輯日誌和映象檔案拷貝到Secondary NameNode ​ (5)Secondary NameNode載入編輯日誌和映象檔案到記憶體,併合並 ​ (6)生成新的映象檔案fsimage.chkpoint ​ (7)拷貝fsimage.chkpoint到NameNode ​ (8)NameNode將fsimage.chkpoint重新命名成fsimage ### DataNode 工作機制 ![image-20210104220225348](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104233001739-2097650147.png) 1)一個數據塊在DataNode上以檔案形式儲存在磁碟上,包括兩個檔案,一個是資料本身,一個是元資料包括資料塊的長度,塊資料的校驗和,以及時間戳 2)DataNode啟動後向NameNode註冊,通過後,週期性(1小時)的向NameNode上報所有的塊資訊 3)心跳是每3秒一次,心跳返回結果帶有NameNode給該DataNode的命令如複製塊資料到另一臺機器,或刪除某個資料塊。如果超過10分鐘沒有收到某個DataNode的心跳,則認為該節點不可用 4)叢集執行中可以安全加入和退出一些機器 #### DataNode 多目錄配置 DataNode 也可以配置成多個目錄,每個目錄儲存的資料不一樣。不同於 NameNode 多目錄配置,NameNode 多個目錄直接的資料是一樣的,僅做備份和容災用。我想是因為 DataNode 已經使用副本來做備份了,如果還繼續在本機複製多份,不是很有必要。而 NameNode 在未做高可用之前並沒有足夠的備份,因此產生了差異 hdfs-site.xml ```xml dfs.datanode.data.dir
file:///${hadoop.tmp.dir}/dfs/data1,file:///${hadoop.tmp.dir}/dfs/data2
``` ### 新增新資料節點 (1)在hadoop104主機上再克隆一臺hadoop105主機 (2)修改IP地址和主機名稱 (3)**刪除原來HDFS檔案系統留存的檔案(/opt/module/hadoop/data和log)** (4)source一下配置檔案 (5)直接啟動DataNode,即可關聯到叢集 如果資料不均衡,可以使用 ` ./start-balancer.sh` 命令實現叢集的再均衡 但是這樣存在一個問題:如果某些惡意分子知道了 NameNode 的地址,便可以連線叢集並克隆出叢集的資料,這樣是極不安全的 #### 新增白名單 只允許白名單內的地址連線 NameNode 在 NameNode 的 /opt/module/hadoop/etc/hadoop目錄下建立 dfs.hosts 檔案,並新增如下主機名稱 ``` linux102 linux103 linux104 ``` 在 NameNode 的 hdfs-site.xml 配置檔案中增加 dfs.hosts 屬性 ```xml dfs.hosts /opt/module/hadoop/etc/hadoop/dfs.hosts ``` 檔案分發 ```shell xsync hdfs-site.xml ``` 重新整理NameNode ``` hdfs dfsadmin -refreshNodes ``` 開啟 Web 頁面,可以看到不在白名單的 DataNode 會被下線 #### 黑名單退役 在黑名單上的節點會被強制退出 黑名單的配置 key 如下 ```xml dfs.hosts.exclude /opt/module/hadoop/etc/hadoop/dfs.hosts.exclude ``` 需要注意 1. 退役節點時,需要等待退役節點狀態為 decommissioned(所有塊已經複製完成),停止該節點及節點資源管理器 2. 如果副本數是3,服役的節點小於等於3,是不能退役成功的,需要修改副本數後才能退役 3. 不允許黑白名單同時出現一個主機名 ## HDFS 2.X 新特性 ### 叢集間資料拷貝 ```shell bin/hadoop distcp hdfs://linux102:9000/user/keats/hello.txt hdfs://linux103:9000/user/keats/hello.txt ``` ### 小檔案存檔 ![image-20210104231711569](https://img2020.cnblogs.com/blog/1654189/202101/1654189-20210104232958754-1538981754.png) ### 回收站 開啟回收站功能,可以將刪除的檔案在不超時的情況下,恢復原資料,起到防止誤刪除、備份等作用 ### 快照管理 快照相當於對目錄做一個備份,**並不會立刻複製所有檔案**。而是記錄檔案變化 ## 參考內容 [B站尚矽谷大資料課程](https://www.bilibili.com/video/BV1cW411r7