HDFS(Hadoop Distributed File System)是一個分布式文件存儲系統,幾乎是離線存儲領域的標准解決方案(有能力自研的大廠列外),業內應用非常廣泛。近段抽時間,看一下 HDFS 的架構設計,雖然研究生也學習過相關內容,但是現在基本忘得差不多了,今天抽空對這塊做了一個簡單的總結,也算是再溫習了一下這塊的內容,這樣后續再看 HDFS 方面的文章時,不至於處於懵逼狀態。
HDFS 1.0 架構
HDFS 采用的是 Master/Slave 架構,一個 HDFS 集群包含一個單獨的 NameNode 和多個 DataNode 節點,如下圖所示(這個圖是 HDFS1.0的架構圖,經典的架構圖):
NameNode
NameNode 負責管理整個分布式系統的元數據,主要包括:
- 目錄樹結構;
- 文件到數據庫 Block 的映射關系;
- Block 副本及其存儲位置等管理數據;
- DataNode 的狀態監控,兩者通過段時間間隔的心跳來傳遞管理信息和數據信息,通過這種方式的信息傳遞,NameNode 可以獲知每個 DataNode 保存的 Block 信息、DataNode 的健康狀況、命令 DataNode 啟動停止等(如果發現某個 DataNode 節點故障,NameNode 會將其負責的 block 在其他 DataNode 上進行備份)。
這些數據保存在內存中,同時在磁盤保存兩個元數據管理文件:fsimage 和 editlog。
- fsimage:是內存命名空間元數據在外存的鏡像文件;
- editlog:則是各種元數據操作的 write-ahead-log 文件,在體現到內存數據變化前首先會將操作記入 editlog 中,以防止數據丟失。
這兩個文件相結合可以構造完整的內存數據。
Secondary NameNode
Secondary NameNode 並不是 NameNode 的熱備機,而是定期從 NameNode 拉取 fsimage 和 editlog 文件,並對兩個文件進行合並,形成新的 fsimage 文件並傳回 NameNode,這樣做的目的是減輕 NameNod 的工作壓力,本質上 SNN 是一個提供檢查點功能服務的服務點。
DataNode
負責數據塊的實際存儲和讀寫工作,Block 默認是64MB(HDFS2.0改成了128MB),當客戶端上傳一個大文件時,HDFS 會自動將其切割成固定大小的 Block,為了保證數據可用性,每個 Block 會以多備份的形式存儲,默認是3份。
文件寫入過程
Client 向 HDFS 文件寫入的過程可以參考HDFS寫文件過程分析,整體過程如下圖(這個圖比較經典,最開始來自《Hadoop:The Definitive Guide》)所示:
具體過程如下:
- Client 調用 DistributedFileSystem 對象的
create
方法,創建一個文件輸出流(FSDataOutputStream)對象; - 通過 DistributedFileSystem 對象與集群的 NameNode 進行一次 RPC 遠程調用,在 HDFS 的 Namespace 中創建一個文件條目(Entry),此時該條目沒有任何的 Block,NameNode 會返回該數據每個塊需要拷貝的 DataNode 地址信息;
- 通過 FSDataOutputStream 對象,開始向 DataNode 寫入數據,數據首先被寫入 FSDataOutputStream 對象內部的數據隊列中,數據隊列由 DataStreamer 使用,它通過選擇合適的 DataNode 列表來存儲副本,從而要求 NameNode 分配新的 block;
- DataStreamer 將數據包以流式傳輸的方式傳輸到分配的第一個 DataNode 中,該數據流將數據包存儲到第一個 DataNode 中並將其轉發到第二個 DataNode 中,接着第二個 DataNode 節點會將數據包轉發到第三個 DataNode 節點;
- DataNode 確認數據傳輸完成,最后由第一個 DataNode 通知 client 數據寫入成功;
- 完成向文件寫入數據,Client 在文件輸出流(FSDataOutputStream)對象上調用
close
方法,完成文件寫入; - 調用 DistributedFileSystem 對象的 complete 方法,通知 NameNode 文件寫入成功,NameNode 會將相關結果記錄到 editlog 中。
文件讀取過程
相對於文件寫入,文件的讀取就簡單一些,流程如下圖所示:
其具體過程總結如下(簡單總結一下):
- Client 通過 DistributedFileSystem 對象與集群的 NameNode 進行一次 RPC 遠程調用,獲取文件 block 位置信息;
- NameNode 返回存儲的每個塊的 DataNode 列表;
- Client 將連接到列表中最近的 DataNode;
- Client 開始從 DataNode 並行讀取數據;
- 一旦 Client 獲得了所有必須的 block,它就會將這些 block 組合起來形成一個文件。
在處理 Client 的讀取請求時,HDFS 會利用機架感知選舉最接近 Client 位置的副本,這將會減少讀取延遲和帶寬消耗。
HDFS 1.0 的問題
在前面的介紹中,關於 HDFS1.0 的架構,首先都會看到 NameNode 的單點問題,這個在生產環境中是非常要命的問題,早期的 HDFS 由於規模較小,有些問題就被隱藏了,但自從進入了移動互聯網時代,很多公司都開始進入了 PB 級的大數據時代,HDFS 1.0的設計缺陷已經無法滿足生產的需求,最致命的問題有以下兩點:
- NameNode 的單點問題,如果 NameNode 掛掉了,數據讀寫都會受到影響,HDFS 整體將變得不可用,這在生產環境中是不可接受的;
- 水平擴展問題,隨着集群規模的擴大,1.0 時集群規模達到3000時,會導致整個集群管理的文件數目達到上限(因為 NameNode 要管理整個集群 block 元信息、數據目錄信息等)。
為了解決上面的兩個問題,Hadoop2.0 提供一套統一的解決方案:
- HA(High Availability 高可用方案):這個是為了解決 NameNode 單點問題;
- NameNode Federation:是用來解決 HDFS 集群的線性擴展能力。
HDFS 2.0 的 HA 實現
關於 HDFS 高可用方案,非常推薦這篇文章:Hadoop NameNode 高可用 (High Availability) 實現解析,IBM 博客的質量確實很高,這部分我這里也是主要根據這篇文章做一個總結,這里會從問題的原因、如何解決的角度去總結,並不會深入源碼的實現細節,想有更深入了解還是推薦上面文章。
這里先看下 HDFS 高可用解決方案的架構設計,如下圖(下圖來自上面的文章)所示:
這里與前面 1.0 的架構已經有很大變化,簡單介紹一下上面的組件:
- Active NameNode 和 Standby NameNode:兩台 NameNode 形成互備,一台處於 Active 狀態,為主 NameNode,另外一台處於 Standby 狀態,為備 NameNode,只有主 NameNode 才能對外提供讀寫服務;
- ZKFailoverController(主備切換控制器,FC):ZKFailoverController 作為獨立的進程運行,對 NameNode 的主備切換進行總體控制。ZKFailoverController 能及時檢測到 NameNode 的健康狀況,在主 NameNode 故障時借助 Zookeeper 實現自動的主備選舉和切換(當然 NameNode 目前也支持不依賴於 Zookeeper 的手動主備切換);
- Zookeeper 集群:為主備切換控制器提供主備選舉支持;
- 共享存儲系統:共享存儲系統是實現 NameNode 的高可用最為關鍵的部分,共享存儲系統保存了 NameNode 在運行過程中所產生的 HDFS 的元數據。主 NameNode 和備 NameNode 通過共享存儲系統實現元數據同步。在進行主備切換的時候,新的主 NameNode 在確認元數據完全同步之后才能繼續對外提供服務。
- DataNode 節點:因為主 NameNode 和備 NameNode 需要共享 HDFS 的數據塊和 DataNode 之間的映射關系,為了使故障切換能夠快速進行,DataNode 會同時向主 NameNode 和備 NameNode 上報數據塊的位置信息。
FailoverController
FC 最初的目的是為了實現 SNN 和 ANN 之間故障自動切換,FC 是獨立與 NN 之外的故障切換控制器,ZKFC 作為 NameNode 機器上一個獨立的進程啟動 ,它啟動的時候會創建 HealthMonitor 和 ActiveStandbyElector 這兩個主要的內部組件,其中:
- HealthMonitor:主要負責檢測 NameNode 的健康狀態,如果檢測到 NameNode 的狀態發生變化,會回調 ZKFailoverController 的相應方法進行自動的主備選舉;
- ActiveStandbyElector:主要負責完成自動的主備選舉,內部封裝了 Zookeeper 的處理邏輯,一旦 Zookeeper 主備選舉完成,會回調 ZKFailoverController 的相應方法來進行 NameNode 的主備狀態切換。
自動觸發主備選舉
NameNode 在選舉成功后,會在 zk 上創建了一個 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock
節點,而沒有選舉成功的備 NameNode 會監控這個節點,通過 Watcher 來監聽這個節點的狀態變化事件,ZKFC 的 ActiveStandbyElector 主要關注這個節點的 NodeDeleted 事件(這部分實現跟 Kafka 中 Controller 的選舉一樣)。
如果 Active NameNode 對應的 HealthMonitor 檢測到 NameNode 的狀態異常時, ZKFailoverController 會主動刪除當前在 Zookeeper 上建立的臨時節點 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock,這樣處於 Standby 狀態的 NameNode 的 ActiveStandbyElector 注冊的監聽器就會收到這個節點的 NodeDeleted 事件。收到這個事件之后,會馬上再次進入到創建 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 節點的流程,如果創建成功,這個本來處於 Standby 狀態的 NameNode 就選舉為主 NameNode 並隨后開始切換為 Active 狀態。
當然,如果是 Active 狀態的 NameNode 所在的機器整個宕掉的話,那么根據 Zookeeper 的臨時節點特性,/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 節點會自動被刪除,從而也會自動進行一次主備切換。
HDFS 腦裂問題
在實際中,NameNode 可能會出現這種情況,NameNode 在垃圾回收(GC)時,可能會在長時間內整個系統無響應,因此,也就無法向 zk 寫入心跳信息,這樣的話可能會導致臨時節點掉線,備 NameNode 會切換到 Active 狀態,這種情況,可能會導致整個集群會有同時有兩個 NameNode,這就是腦裂問題。
腦裂問題的解決方案是隔離(Fencing),主要是在以下三處采用隔離措施:
- 第三方共享存儲:任一時刻,只有一個 NN 可以寫入;
- DataNode:需要保證只有一個 NN 發出與管理數據副本有關的刪除命令;
- Client:需要保證同一時刻只有一個 NN 能夠對 Client 的請求發出正確的響應。
關於這個問題目前解決方案的實現如下:
- ActiveStandbyElector 為了實現 fencing,會在成功創建 Zookeeper 節點 hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 從而成為 Active NameNode 之后,創建另外一個路徑為 /hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 的持久節點,這個節點里面保存了這個 Active NameNode 的地址信息;
- Active NameNode 的 ActiveStandbyElector 在正常的狀態下關閉 Zookeeper Session 的時候,會一起刪除這個持久節點;
- 但如果 ActiveStandbyElector 在異常的狀態下 Zookeeper Session 關閉 (比如前述的 Zookeeper 假死),那么由於 /hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 是持久節點,會一直保留下來,后面當另一個 NameNode 選主成功之后,會注意到上一個 Active NameNode 遺留下來的這個節點,從而會回調 ZKFailoverController 的方法對舊的 Active NameNode 進行 fencing。
在進行 fencing 的時候,會執行以下的操作:
- 首先嘗試調用這個舊 Active NameNode 的 HAServiceProtocol RPC 接口的
transitionToStandby
方法,看能不能把它轉換為 Standby 狀態; - 如果
transitionToStandby
方法調用失敗,那么就執行 Hadoop 配置文件之中預定義的隔離措施。
Hadoop 目前主要提供兩種隔離措施,通常會選擇第一種:
- sshfence:通過 SSH 登錄到目標機器上,執行命令 fuser 將對應的進程殺死;
- shellfence:執行一個用戶自定義的 shell 腳本來將對應的進程隔離。
只有在成功地執行完成 fencing 之后,選主成功的 ActiveStandbyElector 才會回調 ZKFailoverController 的 becomeActive
方法將對應的 NameNode 轉換為 Active 狀態,開始對外提供服務。
NameNode 選舉的實現機制與 Kafka 的 Controller 類似,那么 Kafka 是如何避免腦裂問題的呢?
- Controller 給 Broker 發送的請求中,都會攜帶 controller epoch 信息,如果 broker 發現當前請求的 epoch 小於緩存中的值,那么就證明這是來自舊 Controller 的請求,就會決絕這個請求,正常情況下是沒什么問題的;
- 但是異常情況下呢?如果 Broker 先收到異常 Controller 的請求進行處理呢?現在看 Kafka 在這一部分並沒有適合的方案;
- 正常情況下,Kafka 新的 Controller 選舉出來之后,Controller 會向全局所有 broker 發送一個 metadata 請求,這樣全局所有 Broker 都可以知道當前最新的 controller epoch,但是並不能保證可以完全避免上面這個問題,還是有出現這個問題的幾率的,只不過非常小,而且即使出現了由於 Kafka 的高可靠架構,影響也非常有限,至少從目前看,這個問題並不是嚴重的問題。
第三方存儲(共享存儲)
上述 HA 方案還有一個明顯缺點,那就是第三方存儲節點有可能失效,之前有很多共享存儲的實現方案,目前社區已經把由 Clouderea 公司實現的基於 QJM 的方案合並到 HDFS 的 trunk 之中並且作為默認的共享存儲實現,本部分只針對基於 QJM 的共享存儲方案的內部實現原理進行分析。
QJM(Quorum Journal Manager)本質上是利用 Paxos 協議來實現的,QJM 在 2F+1
個 JournalNode 上存儲 NN 的 editlog,每次寫入操作都通過 Paxos 保證寫入的一致性,它最多可以允許有 F 個 JournalNode 節點同時故障,其實現如下(圖片來自:Hadoop NameNode 高可用 (High Availability) 實現解析 ):
Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再從 JournalNode 集群定時同步 EditLog。
還有一點需要注意的是,在 2.0 中不再有 SNN 這個角色了,NameNode 在啟動后,會先加載 FSImage 文件和共享目錄上的 EditLog Segment 文件,之后 NameNode 會啟動 EditLogTailer 線程和 StandbyCheckpointer 線程,正式進入 Standby 模式,其中:
- EditLogTailer 線程的作用是定時從 JournalNode 集群上同步 EditLog;
- StandbyCheckpointer 線程的作用其實是為了替代 Hadoop 1.x 版本之中的 Secondary NameNode 的功能,StandbyCheckpointer 線程會在 Standby NameNode 節點上定期進行 Checkpoint,將 Checkpoint 之后的 FSImage 文件上傳到 Active NameNode 節點。
HDFS 2.0 Federation 實現
在 1.0 中,HDFS 的架構設計有以下缺點:
- namespace 擴展性差:在單一的 NN 情況下,因為所有 namespace 數據都需要加載到內存,所以物理機內存的大小限制了整個 HDFS 能夠容納文件的最大個數(namespace 指的是 HDFS 中樹形目錄和文件結構以及文件對應的 block 信息);
- 性能可擴展性差:由於所有請求都需要經過 NN,單一 NN 導致所有請求都由一台機器進行處理,很容易達到單台機器的吞吐;
- 隔離性差:多租戶的情況下,單一 NN 的架構無法在租戶間進行隔離,會造成不可避免的相互影響。
而 Federation 的設計就是為了解決這些問題,采用 Federation 的最主要原因是設計實現簡單,而且還能解決問題。
Federation 架構
Federation 的架構設計如下圖所示(圖片來自 HDFS Federation):
Federation 的核心設計思想
Federation 的核心思想是將一個大的 namespace 划分多個子 namespace,並且每個 namespace 分別由單獨的 NameNode 負責,這些 NameNode 之間互相獨立,不會影響,不需要做任何協調工作(其實跟拆集群有一些相似),集群的所有 DataNode 會被多個 NameNode 共享。
其中,每個子 namespace 和 DataNode 之間會由數據塊管理層作為中介建立映射關系,數據塊管理層由若干數據塊池(Pool)構成,每個數據塊只會唯一屬於某個固定的數據塊池,而一個子 namespace 可以對應多個數據塊池。每個 DataNode 需要向集群中所有的 NameNode 注冊,且周期性地向所有 NameNode 發送心跳和塊報告,並執行來自所有 NameNode 的命令。
- 一個 block pool 由屬於同一個 namespace 的數據塊組成,每個 DataNode 可能會存儲集群中所有 block pool 的數據塊;
- 每個 block pool 內部自治,也就是說各自管理各自的 block,不會與其他 block pool 交流,如果一個 NameNode 掛掉了,不會影響其他 NameNode;
- 某個 NameNode 上的 namespace 和它對應的 block pool 一起被稱為 namespace volume,它是管理的基本單位。當一個 NameNode/namespace 被刪除后,其所有 DataNode 上對應的 block pool 也會被刪除,當集群升級時,每個 namespace volume 可以作為一個基本單元進行升級。
到這里,基本對 HDFS 這部分總結完了,雖然文章的內容基本都來自下面的參考資料,但是自己在總結的過程中,也對 HDFS 的基本架構有一定的了解,后續結合公司 HDFS 團隊的 CaseStudy 深入學習這部分的內容,工作中,也慢慢感覺到分布式系統,很多的設計實現與問題解決方案都很類似,只不過因為面對業務場景的不同而采用了不同的實現。