大資料系列1:一文初識Hdfs

lillcol發表於2021-01-25

最近有位同事經常問一些Hadoop的東西,特別是Hdfs的一些細節,有些記得不清楚,所以趁機整理一波。

會按下面的大綱進行整理:

  1. 簡單介紹Hdfs
  2. 簡單介紹Hdfs讀寫流程
  3. 介紹Hdfs HA實現方式
  4. 介紹Yarn統一資源管理器
  5. 追一下Hdfs讀寫的原始碼

同時也有其他方面的整理,有興趣可以看看:
演算法系列-動態規劃(4):買賣股票的最佳時機
資料庫倉庫系列(一)什麼是資料倉儲為什麼要資料倉儲


羅拉的好奇

對話記錄

羅拉

八哥,最近我們不是在建立資料倉儲嘛
有個叫做Hdfs 的東西,好像很厲害,你給我講講這是啥玩意唄

八哥

額,你先說說你對Hdfs瞭解多少?

羅拉

我只是聽說這個使用分散式儲存的框架,可以儲存海量的資料,在大資料領域很常用

八哥

就這?那就有點尷尬,看來我得從頭開始給你介紹了,所以今晚的碗你洗

羅拉

如果你說的我都懂了,那沒問題

八哥

行,成交


查個戶口

要想了解Hdfs,就得先查一下他的戶口。
Hdfs全名叫做Hadoop分散式檔案系統(Hadoop Distributed File System)這貨跟Hadoop還有關係。
那沒辦法,先去看看Hadoop又是啥玩意。

Hadoop是一個由Apache基金會所開發的分散式系統基礎架構
使用者可以在不瞭解分散式底層細節的情況下,開發分散式程式。充分利用叢集的優勢進行高速運算和儲存。

目前的Hadoop有一個強大的生態系統,如下:

Hadoop生態

其中有幾個為核心的元件,如下:

  1. Hdfs(Hadoop Distributed File System):可提供高吞吐量的分散式檔案系統
  2. Yarn:用於任務排程和叢集資源管理的框架(這玩意是2.0後的重大突破)
  3. MapReduce:基於Yarn上,用於大資料叢集並行處理的系統。(現在更多的被當作思想來看,或者高手才手擼這玩意)

核心元件隨著Hadoop更新,在1.x2.x有顯著的區別,如下:

從上面兩圖中我們可以發現,Hadoop1.xHadoop2.x的主要區別就是2.x引入了Yarn
之所以說這是一個大的突破主要是因為以下幾點:

  1. 1.xMapReduce不僅負責資料的計算,還負責叢集作業的排程和資源(記憶體,CPU)管理(自己即是也是工人,全能型人才),擴充性差,應用場景單一。
  2. 2.x中,引入了Yarn,負責叢集的資源的統一管理和排程。 MapReduce則執行在Yarn之上,只負責資料的計算。分工更加明確。
  3. 由於Yarn具有通用性,可以作為其他的計算框架的資源管理系統, 比如(Spark,Strom,SparkStreaming等)。

Yarn作為統一的資源管理和排程,帶來了三個顯著的效益:

  1. 提高資源利用率:通過統一資源管理和排程,各個不同的元件可以共享叢集資源,提高資源的利用率,避免各自為戰出現資源利用不充分甚至資源緊張的情況。
  2. 降低運維成本:只需對叢集進行統一的管理,降低工作量。
  3. 資料共享:共享叢集通過共享叢集之間的資料和資源,有效提高資料移動的效率和降低時間成本

共享叢集資源架構圖:

以後會專門寫一篇Yarn的文章,此處不再詳細展開。

有了上面的介紹,我們就看看Hdfs是到底做了什麼。


Hdfs

Hdfs的設計目標

在大資料我們經常會通過分散式計算對海量資料進行儲存和管理。
Hdfs就是在這樣的需求下應運而生,它基於流資料模式訪問和能夠處理超大的檔案。
並且可以在在廉價的機器上執行並提供資料容錯機制,給大資料的處理帶來很大便利。

Hdfs設計之初,就有幾個目標:

目標 實現方式或原因
硬體故障 硬體故障是常態
需要有故障檢測,並且快速自動的從故障中恢復的機制
流式訪問 Hdfs強調的是資料訪問高吞吐量,而不是訪問的低延遲性
大型資料集 支援大型資料集的檔案系統
為具有數百甚至更多階段的叢集提供資料的儲存與計算
簡單一致性 一次寫入,多次讀取
一旦檔案建立,寫入,關閉就不能從任意的位置進行改變
ps:在2.x後可以在檔案末尾追加內容
移動計算比移動資料容易 利用資料的本地性,提高計算的效率
平臺可移植性 使用Java語言構建
任何支援Java的計算機都可以執行Hdfs

Hdfs框架構設計

接下來我們看看Hdfs叢集的框架

從上圖可以明顯的看出,Hdfs是一個典型的主/從架構。

我們看看它有什麼元件


NameNode

Master由一個NameNode組成,是一個主伺服器,主要功能如下:

  1. 負責管理檔案系統的名稱空間,儲存後設資料(檔名稱、大小、儲存位置等)
  2. 協調客戶端對檔案的訪問

NameNode會將所有的檔案和資料夾儲存在一個檔案系統的目錄樹中,並且記錄任何後設資料的變化。

我們知道Hdfs會將檔案拆分為多個資料塊儲存,其中檔案與檔案塊的對應關係也儲存在檔案系統的目錄樹中,由NameNode維護。

除了檔案與資料塊的對映資訊,還有一個資料塊與DataNode 的對映資訊,
因為資料塊最終是儲存到DataNode中。我們需要知道一個檔案資料塊存在那些DataNode中,
或者說DataNode中有哪些資料塊。這些資訊也記錄在NameNode中。

從上圖中可以看到,NameNodeDataNode之間還有心跳。
NameNode會週期性的接收叢集中DataNode的“心跳”和“塊報告”。
通過“心跳”檢測DataNode的狀態(是否當機),決定是否需要作出相關的調整策略。
“塊報告”包含DataNode上所有資料塊列表的資訊


DataNode

DataNodeHdfs的從節點,一般會有多個DataNode(一般一個節點一個DataNode)。
主要功能如下:

  1. 管理它們所執行節點的資料儲存
  2. 週期性向NameNode上報自身儲存的“塊資訊”

這裡的管理指的是在ClientHdfs進行資料讀寫操作的時候,會接收來自NameNode的指令,執行資料塊的建立、刪除、複製等操作。

DataNode中的資料儲存在本地磁碟。


Blcok

從上圖可以看出,在內部,一個檔案會被切割為多個塊(Blocks),這些塊儲存在一組Datanodes中,同時還有對Block進行備份(Replication)。

Hdfs檔案以Block的形式儲存,預設一個Block大小為128MB1.x64Mb

簡單來說就是一個檔案會被切割為多個128MB的小檔案進行儲存,如果檔案小於128MB則不切割,按照實際的大小儲存,不會佔用整個資料塊的大小。

關於Hdfs讀寫後續會寫一個原始碼追蹤的文章,到時候就知道如何實現按照實際大小儲存了。

預設Block之所以是128M,主要是降低定址開銷和獲得較佳的執行效率。
因為Block越小,那麼切割的檔案就越多,定址耗費的時間也會越多。
但如果Block太大,雖然切割的檔案比較少,定址快,但是單個檔案過大,執行時間過長,發揮不了平行計算的優勢。

每個Block的後設資料也記錄在NameNode中,可以說Block的大小一定程度也會影響整個叢集的儲存能力

同時為了容錯,一般會有三個副本,副本的存放策略一般為:

備份編號 位置
1 Standalone模式:上傳檔案的節點
Cluster模式:隨機選一臺記憶體充足的機器
2 與1號備份同機架的不同節點
3 不同機架的節點

備份除了容錯,也是資料本地性(移動計算)的一個強有力支撐。


Secondary NameNode

有一說一,
Secondary NameNode 取了一個標題黨的的名字,這個讓人感覺這就是第二個NameNode
實際上不是,在介紹 Secondary NameNode 之前,我們得先了解NameNode是怎麼儲存後設資料的。

我們之前說的Hdfs的後設資料資訊主要存在兩個檔案中:fsimageedits

  1. fsimage:檔案系統的對映檔案,儲存檔案的後設資料資訊,包括檔案系統所有的目錄、檔案資訊以及資料塊的索引。
  2. editsHdfs操作日誌檔案,記錄Hdfs對檔案系統的修改日誌。

NameNode 對這兩個檔案的操作如下圖:

從這張圖中,可以知道,在NameNode啟動的時候,會從fsimage中讀取Hdfs的狀態,
同時會合並fsimageedits獲得完整的後設資料資訊,並將新的Hdfs狀態寫入fsimage
並使用一個空的edits檔案開始正常操作。

但是在產品化的叢集(如AmbariClouderManager)中NameNode是很少重啟的,在我的工作場景中,重啟基本就是掛了。
這也意味著當NameNode執行了很長時間後,edits檔案會變得很大。

在這種情況下就會兩個問題:

  1. edits檔案隨著操作增加會變的很大,怎麼去管理這個檔案是一個又是一個問題。
  2. NameNode的重啟會花費很長時間,因為經過長時間執行,Hdfs會有很多改動(edits)要合併到fsimage檔案上。

既然明白了痛點所在,那自然是需要對症下藥,核心問題就是edits會越來越大,導致重啟操作時間變長,只要解決這個問題就完事了。

我們只需要保證我們fsimage是最新的,而不是每次啟動的時候才合併出完整的fsimage就可以了,也就是更新快照。
這就是是我們需要介紹的Secondary NameNode的工作。

Secondary NameNode 用於幫助NameNode管理後設資料,從而使得NameNode可以快速、高效的工作。

簡單的說Secondary NameNode的工作就是定期合併fsimageedits日誌,將edits日誌檔案大小控制在一個限度下。

因為記憶體需求和NameNode在一個數量級上,所以通常secondary NameNodeNameNode 執行在不同的機器上。

下面看看這個過程是怎麼發生的

ps:圖中有個虛線,就是在傳輸edits的時候會不會傳輸fsimage?這個在最後面會有相關說明

這些步驟簡單的總結就是:
Secondary NameNode端:

  1. Secondary NameNode定期到NameNode更新edits
  2. 將更新到的edits與自身的fsimage或重新下載的fsimage合併獲得完整的fsiamge.ckpt
  3. fsimage.ckpt傳送給NameNode

NameNode端:

  1. Secondary NameNode發出合併訊號的時候,將更新日誌寫到一個新的new.edits中,停用舊的edits
  2. Secondary NameNode將新的fsimage.ckpt發過來後,將舊的fsimage用新的fsimage.ckpt替換,同時將久的editsnew.edits替換。

那麼合併的時機是什麼?主要有兩個引數可以配置:

  1. fs.checkpoint.period:指定連續兩次檢查點的最大時間間隔, 預設值是1小時。
  2. fs.checkpoint.size:定義了edits日誌檔案的最大值,一旦超過這個值會導致強制執行檢查點(即使沒到檢查點的最大時間間隔)。預設值是64MB

所以,Secondary NameNode 並不是第二個NameNode的意思,只是NameNode的一個助手。更準確的理解是它僅僅是NameNode的一個檢查點(CheckPoint)。

同時,我們所說的HA,也就是高可用,也不是隻Secondary NameNode,詳細的會有專門的文章介紹。


有個坑

Secondary NameNode 執行合併的時候,有一個步驟3,通過http Get的方式從NameNode獲取edits檔案。

在這一步驟中,到底需不需要把NameNode中的fsimage也獲取過來,目前我看了挺多資料,挺矛盾的。


在Hadoop權威指南中說明如下:

從這裡看,應該是同是獲得了fsimageedits


但是,在官網中有一個描述:

The secondary NameNode stores the latest checkpoint in a directory which is structured the same way as the primary NameNode’s directory.
So that the check pointed image is always ready to be read by the primary NameNode if necessary.

Secondary NameNode將最新的檢查點儲存在與主NameNode目錄結構相同的目錄中。
所以在NameNode需要的時候,會去讀取檢查點的映象image

並且在Secondary NameNode in Hadoop中關於Secondary NameNode有這樣的一個描述:

NameNode當前目錄的截圖如下:

fsimage 的當前版本號位165,從最後一個檢查點fsimage165正在進行的edits_inprogress日誌編號為166,
在下次namenode重啟時,它將與fsimage165合併,fsimage166將被建立。

Secondary NameNode 當前目錄的截圖如下:

注意,在namenode中沒有對應的實時編輯edits_inprogress_166版本。
此時有fsimage165,那麼在進行合併的時候不需要從NameNodefsimage也傳輸過來吧?

但是執行合併的時候輸出的日誌:

這看起來好像是需要下載fsimage

瞬間蒙圈了。

沒辦法只能去老老實實去瞄一下原始碼了:
下面只展示核心程式碼:

//org.apache.Hadoop.hdfs.server.namenode.SecondaryNameNode#doCheckpoint
 /**
   * 建立一個新的檢查點
   * @return image 是否從NameNode獲取
   */
  @VisibleForTesting
  @SuppressWarnings("deprecated")
  public boolean doCheckpoint() throws IOException {
    //告訴namenode在一個新的編輯檔案中開始記錄事務,將返回一個用於上傳合併後的image的token。
    CheckpointSignature sig = namenode.rollEditLog();
    //是否重新載入fsimage
    boolean loadImage = false;    
    //這裡需要reload fsImage有兩種情況
    //1. downloadCheckpointFiles中判斷fsiamge變化情況
    //2. 是否發生checkpointImage 和並錯誤
    loadImage |= downloadCheckpointFiles(
        fsName, checkpointImage, sig, manifest) |
        checkpointImage.hasMergeError(); 

      //執行合併操作    
      doMerge(sig, manifest, loadImage, checkpointImage, namesystem);

}

// org.apache.Hadoop.hdfs.server.namenode.SecondaryNameNode#downloadCheckpointFiles

  /**
   * 從name-node 下載 fsimage 和 edits
   * @return true if a new image has been downloaded and needs to be loaded
   * @throws IOException
   */
  static boolean downloadCheckpointFiles(...) throws IOException {
  	        //根據Image的變化情況決定是否download image
            if (sig.mostRecentCheckpointTxId ==
                dstImage.getStorage().getMostRecentCheckpointTxId()) {
              LOG.info("Image has not changed. Will not download image.");
            } else {
              LOG.info("Image has changed. Downloading updated image from NN.");
              MD5Hash downloadedHash = TransferFsImage.downloadImageToStorage(
                  nnHostPort, sig.mostRecentCheckpointTxId,
                  dstImage.getStorage(), true, false);
              dstImage.saveDigestAndRenameCheckpointImage(NameNodeFile.IMAGE,
                  sig.mostRecentCheckpointTxId, downloadedHash);
            }
        
            // download edits
            for (RemoteEditLog log : manifest.getLogs()) {
              TransferFsImage.downloadEditsToStorage(
                  nnHostPort, log, dstImage.getStorage());
            }
            // true if we haven't loaded all the transactions represented by the downloaded fsimage.
            return dstImage.getLastAppliedTxId() < sig.mostRecentCheckpointTxId;
  }            


  // org.apache.Hadoop.hdfs.server.namenode.SecondaryNameNode#doMerge
  void doMerge(...) throws IOException {   
  	  //如果需要load iamge 就reload image
      if (loadImage) dstImage.reloadFromImageFile(file, dstNamesystem);

    Checkpointer.rollForwardByApplyingLogs(manifest, dstImage, dstNamesystem);
    // 清除舊的fsimages 和edits 
    dstImage.saveFSImageInAllDirs(dstNamesystem, dstImage.getLastAppliedTxId());      
}

從上面的核心程式碼可以看到,進行合併的時候,是否需要從NameNode load fsiamge是要看情況的。

不過目前fsiamge 是否改變這點沒有深入看原始碼,
猜測大概是初次啟動NameNode時,合併出新的fsiamge(如上面的image_165edits_166 合併出來的image_166)。
與當前Secondary NameNode 中的image_165不一致了,所以需要重新拉取,
具體以後有時間再看看。

但是隻要記住有些場景下會把fsimage load下來,有些場景不會就可以了。


後續的內容

“怎麼樣,羅拉,這個簡單的介紹可以吧”?

“還行,但是就這麼簡單?”羅拉狐疑。

“簡單?這都是經過前人的努力才搞出來的,而且這是簡單的介紹,實際上我們現在再生產用的和這個其實都不太一樣了。”

“有多大差別?”

“我現在這裡沒有給你介紹Hdfs讀寫流程,還有現在NameNode其實還存在問題,統一的資源管理Yarn也沒說,早著呢。”
“還有,想真正掌握,還的去追下原始碼看看這個操作是怎麼實現的。就我現在說的這些,去面試都過不了。”八哥無限鄙視

“哦,那就是說你講的不完善,今晚的碗我不洗,等你講完了再說。”

“這麼賴皮的嘛?....”

後面有幾個點會單獨拿出來寫個文章,主要是以下幾個方面的內容:

  1. Hdfs讀寫流程
  2. Yarn統一資源管理
  3. Hdfs HA(高可用)
  4. Hdfs讀寫原始碼解析(會用3.1.3的原始碼)

本文為原創文莊,轉載請註明出處!!!
歡迎關注【兔八哥雜談】

相關文章