Hadoop NameNode 高可用 (High Availability) 實現解析[轉]

NameNode 高可用總體架構概述

在 Hadoop 1.0 時代,Hadoop 的兩大核心組件 HDFS NameNode 和 JobTracker 都存在着單點問題,這其中以 NameNode 的單點問題尤其嚴重。由於 NameNode 保存了整個 HDFS 的元數據信息,一旦 NameNode 掛掉,整個 HDFS 就沒法訪問,同時 Hadoop 生態系統中依賴於 HDFS 的各個組件,包括 MapReduce、Hive、Pig 以及 HBase 等也都沒法正常工做,而且從新啓動 NameNode 和進行數據恢復的過程也會比較耗時。這些問題在給 Hadoop 的使用者帶來困擾的同時,也極大地限制了 Hadoop 的使用場景,使得 Hadoop 在很長的時間內僅能用做離線存儲和離線計算,沒法應用到對可用性和數據一致性要求很高的在線應用場景中。html

所幸的是,在 Hadoop2.0 中,HDFS NameNode 和 YARN ResourceManger(JobTracker 在 2.0 中已經被整合到 YARN ResourceManger 之中) 的單點問題都獲得瞭解決,通過多個版本的迭代和發展,目前已經能用於生產環境。HDFS NameNode 和 YARN ResourceManger 的高可用 (High Availability,HA) 方案基本相似,二者也複用了部分代碼,可是因爲 HDFS NameNode 對於數據存儲和數據一致性的要求比 YARN ResourceManger 高得多,因此 HDFS NameNode 的高可用實現更爲複雜一些,本文從內部實現的角度對 HDFS NameNode 的高可用機制進行詳細的分析。node

HDFS NameNode 的高可用總體架構如圖 1 所示 (圖片來源於參考文獻 [1]):算法

圖 1.HDFS NameNode 高可用總體架構

從上圖中,咱們能夠看出 NameNode 的高可用架構主要分爲下面幾個部分:shell

Active NameNode 和 Standby NameNode:兩臺 NameNode 造成互備,一臺處於 Active 狀態,爲主 NameNode,另一臺處於 Standby 狀態,爲備 NameNode,只有主 NameNode 才能對外提供讀寫服務。bootstrap

主備切換控制器 ZKFailoverController:ZKFailoverController 做爲獨立的進程運行,對 NameNode 的主備切換進行整體控制。ZKFailoverController 能及時檢測到 NameNode 的健康情況,在主 NameNode 故障時藉助 Zookeeper 實現自動的主備選舉和切換,固然 NameNode 目前也支持不依賴於 Zookeeper 的手動主備切換。安全

Zookeeper 集羣:爲主備切換控制器提供主備選舉支持。網絡

共享存儲系統:共享存儲系統是實現 NameNode 的高可用最爲關鍵的部分,共享存儲系統保存了 NameNode 在運行過程當中所產生的 HDFS 的元數據。主 NameNode 和session

NameNode 經過共享存儲系統實現元數據同步。在進行主備切換的時候,新的主 NameNode 在確認元數據徹底同步以後才能繼續對外提供服務。架構

DataNode 節點:除了經過共享存儲系統共享 HDFS 的元數據信息以外,主 NameNode 和備 NameNode 還須要共享 HDFS 的數據塊和 DataNode 之間的映射關係。DataNode 會同時向主 NameNode 和備 NameNode 上報數據塊的位置信息。框架

下面開始分別介紹 NameNode 的主備切換實現和共享存儲系統的實現,在文章的最後會結合筆者的實踐介紹一下在 NameNode 的高可用運維中的一些注意事項。

NameNode 的主備切換實現

NameNode 主備切換主要由 ZKFailoverController、HealthMonitor 和 ActiveStandbyElector 這 3 個組件來協同實現:

ZKFailoverController 做爲 NameNode 機器上一個獨立的進程啓動 (在 hdfs 啓動腳本之中的進程名爲 zkfc),啓動的時候會建立 HealthMonitor 和 ActiveStandbyElector 這兩個主要的內部組件,ZKFailoverController 在建立 HealthMonitor 和 ActiveStandbyElector 的同時,也會向 HealthMonitor 和 ActiveStandbyElector 註冊相應的回調方法。

HealthMonitor 主要負責檢測 NameNode 的健康狀態,若是檢測到 NameNode 的狀態發生變化,會回調 ZKFailoverController 的相應方法進行自動的主備選舉。

ActiveStandbyElector 主要負責完成自動的主備選舉,內部封裝了 Zookeeper 的處理邏輯,一旦 Zookeeper 主備選舉完成,會回調 ZKFailoverController 的相應方法來進行 NameNode 的主備狀態切換。

NameNode 實現主備切換的流程如圖 2 所示,有如下幾步:

  1. HealthMonitor 初始化完成以後會啓動內部的線程來定時調用對應 NameNode 的 HAServiceProtocol RPC 接口的方法,對 NameNode 的健康狀態進行檢測。
  2. HealthMonitor 若是檢測到 NameNode 的健康狀態發生變化,會回調 ZKFailoverController 註冊的相應方法進行處理。
  3. 若是 ZKFailoverController 判斷須要進行主備切換,會首先使用 ActiveStandbyElector 來進行自動的主備選舉。
  4. ActiveStandbyElector 與 Zookeeper 進行交互完成自動的主備選舉。
  5. ActiveStandbyElector 在主備選舉完成後,會回調 ZKFailoverController 的相應方法來通知當前的 NameNode 成爲主 NameNode 或備 NameNode。
  6. ZKFailoverController 調用對應 NameNode 的 HAServiceProtocol RPC 接口的方法將 NameNode 轉換爲 Active 狀態或 Standby 狀態。
圖 2.NameNode 的主備切換流程

下面分別對 HealthMonitor、ActiveStandbyElector 和 ZKFailoverController 的實現細節進行分析:

HealthMonitor 實現分析

ZKFailoverController 在初始化的時候會建立 HealthMonitor,HealthMonitor 在內部會啓動一個線程來循環調用 NameNode 的 HAServiceProtocol RPC 接口的方法來檢測 NameNode 的狀態,並將狀態的變化經過回調的方式來通知 ZKFailoverController。

HealthMonitor 主要檢測 NameNode 的兩類狀態,分別是 HealthMonitor.State 和 HAServiceStatus。HealthMonitor.State 是經過 HAServiceProtocol RPC 接口的 monitorHealth 方法來獲取的,反映了 NameNode 節點的健康情況,主要是磁盤存儲資源是否充足。HealthMonitor.State 包括下面幾種狀態:

  • INITIALIZING:HealthMonitor 在初始化過程當中,尚未開始進行健康情況檢測;
  • SERVICE_HEALTHY:NameNode 狀態正常;
  • SERVICE_NOT_RESPONDING:調用 NameNode 的 monitorHealth 方法調用無響應或響應超時;
  • SERVICE_UNHEALTHY:NameNode 還在運行,可是 monitorHealth 方法返回狀態不正常,磁盤存儲資源不足;
  • HEALTH_MONITOR_FAILED:HealthMonitor 本身在運行過程當中發生了異常,不能繼續檢測 NameNode 的健康情況,會致使 ZKFailoverController 進程退出;

HealthMonitor.State 在狀態檢測之中起主要的做用,在 HealthMonitor.State 發生變化的時候,HealthMonitor 會回調 ZKFailoverController 的相應方法來進行處理,具體處理見後文 ZKFailoverController 部分所述。

而 HAServiceStatus 則是經過 HAServiceProtocol RPC 接口的 getServiceStatus 方法來獲取的,主要反映的是 NameNode 的 HA 狀態,包括:

  • INITIALIZING:NameNode 在初始化過程當中;
  • ACTIVE:當前 NameNode 爲主 NameNode;
  • STANDBY:當前 NameNode 爲備 NameNode;
  • STOPPING:當前 NameNode 已中止;

HAServiceStatus 在狀態檢測之中只是起輔助的做用,在 HAServiceStatus 發生變化時,HealthMonitor 也會回調 ZKFailoverController 的相應方法來進行處理,具體處理見後文 ZKFailoverController 部分所述。

ActiveStandbyElector 實現分析

Namenode(包括 YARN ResourceManager) 的主備選舉是經過 ActiveStandbyElector 來完成的,ActiveStandbyElector 主要是利用了 Zookeeper 的寫一致性和臨時節點機制,具體的主備選舉實現以下:

建立鎖節點

若是 HealthMonitor 檢測到對應的 NameNode 的狀態正常,那麼表示這個 NameNode 有資格參加 Zookeeper 的主備選舉。若是目前尚未進行過主備選舉的話,那麼相應的 ActiveStandbyElector 就會發起一次主備選舉,嘗試在 Zookeeper 上建立一個路徑爲/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 的臨時節點 (${dfs.nameservices} 爲 Hadoop 的配置參數 dfs.nameservices 的值,下同),Zookeeper 的寫一致性會保證最終只會有一個 ActiveStandbyElector 建立成功,那麼建立成功的 ActiveStandbyElector 對應的 NameNode 就會成爲主 NameNode,ActiveStandbyElector 會回調 ZKFailoverController 的方法進一步將對應的 NameNode 切換爲 Active 狀態。而建立失敗的 ActiveStandbyElector 對應的 NameNode 成爲備 NameNode,ActiveStandbyElector 會回調 ZKFailoverController 的方法進一步將對應的 NameNode 切換爲 Standby 狀態。

註冊 Watcher 監聽

無論建立/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 節點是否成功,ActiveStandbyElector 隨後都會向 Zookeeper 註冊一個 Watcher 來監聽這個節點的狀態變化事件,ActiveStandbyElector 主要關注這個節點的 NodeDeleted 事件。

自動觸發主備選舉

若是 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 節點會自動被刪除,從而也會自動進行一次主備切換。

防止腦裂

Zookeeper 在工程實踐的過程當中常常會發生的一個現象就是 Zookeeper 客戶端「假死」,所謂的「假死」是指若是 Zookeeper 客戶端機器負載太高或者正在進行 JVM Full GC,那麼可能會致使 Zookeeper 客戶端到 Zookeeper 服務端的心跳不能正常發出,一旦這個時間持續較長,超過了配置的 Zookeeper Session Timeout 參數的話,Zookeeper 服務端就會認爲客戶端的 session 已通過期從而將客戶端的 Session 關閉。「假死」有可能引發分佈式系統常說的雙主或腦裂 (brain-split) 現象。具體到本文所述的 NameNode,假設 NameNode1 當前爲 Active 狀態,NameNode2 當前爲 Standby 狀態。若是某一時刻 NameNode1 對應的 ZKFailoverController 進程發生了「假死」現象,那麼 Zookeeper 服務端會認爲 NameNode1 掛掉了,根據前面的主備切換邏輯,NameNode2 會替代 NameNode1 進入 Active 狀態。可是此時 NameNode1 可能仍然處於 Active 狀態正常運行,即便隨後 NameNode1 對應的 ZKFailoverController 由於負載降低或者 Full GC 結束而恢復了正常,感知到本身和 Zookeeper 的 Session 已經關閉,可是因爲網絡的延遲以及 CPU 線程調度的不肯定性,仍然有可能會在接下來的一段時間窗口內 NameNode1 認爲本身仍是處於 Active 狀態。這樣 NameNode1 和 NameNode2 都處於 Active 狀態,均可以對外提供服務。這種狀況對於 NameNode 這類對數據一致性要求很是高的系統來講是災難性的,數據會發生錯亂且沒法恢復。Zookeeper 社區對這種問題的解決方法叫作 fencing,中文翻譯爲隔離,也就是想辦法把舊的 Active NameNode 隔離起來,使它不能正常對外提供服務。

ActiveStandbyElector 爲了實現 fencing,會在成功建立 Zookeeper 節點 hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 從而成爲 Active NameNode 以後,建立另一個路徑爲/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 的持久節點,這個節點裏面保存了這個 Active NameNode 的地址信息。Active NameNode 的 ActiveStandbyElector 在正常的狀態下關閉 Zookeeper Session 的時候 (注意因爲/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 是臨時節點,也會隨之刪除),會一塊兒刪除節點/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb。可是若是 ActiveStandbyElector 在異常的狀態下 Zookeeper Session 關閉 (好比前述的 Zookeeper 假死),那麼因爲/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 是持久節點,會一直保留下來。後面當另外一個 NameNode 選主成功以後,會注意到上一個 Active NameNode 遺留下來的這個節點,從而會回調 ZKFailoverController 的方法對舊的 Active NameNode 進行 fencing,具體處理見後文 ZKFailoverController 部分所述。

ZKFailoverController 實現分析

ZKFailoverController 在建立 HealthMonitor 和 ActiveStandbyElector 的同時,會向 HealthMonitor 和 ActiveStandbyElector 註冊相應的回調函數,ZKFailoverController 的處理邏輯主要靠 HealthMonitor 和 ActiveStandbyElector 的回調函數來驅動。

對 HealthMonitor 狀態變化的處理

如前所述,HealthMonitor 會檢測 NameNode 的兩類狀態,HealthMonitor.State 在狀態檢測之中起主要的做用,ZKFailoverController 註冊到 HealthMonitor 上的處理 HealthMonitor.State 狀態變化的回調函數主要關注 SERVICE_HEALTHY、SERVICE_NOT_RESPONDING 和 SERVICE_UNHEALTHY 這 3 種狀態:

  • 若是檢測到狀態爲 SERVICE_HEALTHY,表示當前的 NameNode 有資格參加 Zookeeper 的主備選舉,若是目前尚未進行過主備選舉的話,ZKFailoverController 會調用 ActiveStandbyElector 的 joinElection 方法發起一次主備選舉。
  • 若是檢測到狀態爲 SERVICE_NOT_RESPONDING 或者是 SERVICE_UNHEALTHY,就表示當前的 NameNode 出現問題了,ZKFailoverController 會調用 ActiveStandbyElector 的 quitElection 方法刪除當前已經在 Zookeeper 上創建的臨時節點退出主備選舉,這樣其它的 NameNode 就有機會成爲主 NameNode。

而 HAServiceStatus 在狀態檢測之中僅起輔助的做用,在 HAServiceStatus 發生變化時,ZKFailoverController 註冊到 HealthMonitor 上的處理 HAServiceStatus 狀態變化的回調函數會判斷 NameNode 返回的 HAServiceStatus 和 ZKFailoverController 所指望的是否一致,若是不一致的話,ZKFailoverController 也會調用 ActiveStandbyElector 的 quitElection 方法刪除當前已經在 Zookeeper 上創建的臨時節點退出主備選舉。

對 ActiveStandbyElector 主備選舉狀態變化的處理

在 ActiveStandbyElector 的主備選舉狀態發生變化時,會回調 ZKFailoverController 註冊的回調函數來進行相應的處理:

  • 若是 ActiveStandbyElector 選主成功,那麼 ActiveStandbyElector 對應的 NameNode 成爲主 NameNode,ActiveStandbyElector 會回調 ZKFailoverController 的 becomeActive 方法,這個方法經過調用對應的 NameNode 的 HAServiceProtocol RPC 接口的 transitionToActive 方法,將 NameNode 轉換爲 Active 狀態。
  • 若是 ActiveStandbyElector 選主失敗,那麼 ActiveStandbyElector 對應的 NameNode 成爲備 NameNode,ActiveStandbyElector 會回調 ZKFailoverController 的 becomeStandby 方法,這個方法經過調用對應的 NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,將 NameNode 轉換爲 Standby 狀態。
  • 若是 ActiveStandbyElector 選主成功以後,發現了上一個 Active NameNode 遺留下來的/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 節點 (見「ActiveStandbyElector 實現分析」一節「防止腦裂」部分所述),那麼 ActiveStandbyElector 會首先回調 ZKFailoverController 註冊的 fenceOldActive 方法,嘗試對舊的 Active NameNode 進行 fencing,在進行 fencing 的時候,會執行如下的操做:
  1. 首先嚐試調用這個舊 Active NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,看能不能把它轉換爲 Standby 狀態。
  2. 若是 transitionToStandby 方法調用失敗,那麼就執行 Hadoop 配置文件之中預約義的隔離措施,Hadoop 目前主要提供兩種隔離措施,一般會選擇 sshfence:
  • sshfence:經過 SSH 登陸到目標機器上,執行命令 fuser 將對應的進程殺死;
  • shellfence:執行一個用戶自定義的 shell 腳原本將對應的進程隔離;

只有在成功地執行完成 fencing 以後,選主成功的 ActiveStandbyElector 纔會回調 ZKFailoverController 的 becomeActive 方法將對應的 NameNode 轉換爲 Active 狀態,開始對外提供服務。

NameNode 的共享存儲實現

過去幾年中 Hadoop 社區涌現過不少的 NameNode 共享存儲方案,好比 shared NAS+NFS、BookKeeper、BackupNode 和 QJM(Quorum Journal Manager) 等等。目前社區已經把由 Clouderea 公司實現的基於 QJM 的方案合併到 HDFS 的 trunk 之中而且做爲默認的共享存儲實現,本部分只針對基於 QJM 的共享存儲方案的內部實現原理進行分析。爲了理解 QJM 的設計和實現,首先要對 NameNode 的元數據存儲結構有所瞭解。

NameNode 的元數據存儲概述

一個典型的 NameNode 的元數據存儲目錄結構如圖 3 所示 (圖片來源於參考文獻 [4]),這裏主要關注其中的 EditLog 文件和 FSImage 文件:

圖 3 .NameNode 的元數據存儲目錄結構

NameNode 在執行 HDFS 客戶端提交的建立文件或者移動文件這樣的寫操做的時候,會首先把這些操做記錄在 EditLog 文件之中,而後再更新內存中的文件系統鏡像。內存中的文件系統鏡像用於 NameNode 向客戶端提供讀服務,而 EditLog 僅僅只是在數據恢復的時候起做用。記錄在 EditLog 之中的每個操做又稱爲一個事務,每一個事務有一個整數形式的事務 id 做爲編號。EditLog 會被切割爲不少段,每一段稱爲一個 Segment。正在寫入的 EditLog Segment 處於 in-progress 狀態,其文件名形如 edits_inprogress_${start_txid},其中${start_txid} 表示這個 segment 的起始事務 id,例如上圖中的 edits_inprogress_0000000000000000020。而已經寫入完成的 EditLog Segment 處於 finalized 狀態,其文件名形如 edits_${start_txid}-${end_txid},其中${start_txid} 表示這個 segment 的起始事務 id,${end_txid} 表示這個 segment 的結束事務 id,例如上圖中的 edits_0000000000000000001-0000000000000000019。

NameNode 會按期對內存中的文件系統鏡像進行 checkpoint 操做,在磁盤上生成 FSImage 文件,FSImage 文件的文件名形如 fsimage_${end_txid},其中${end_txid} 表示這個 fsimage 文件的結束事務 id,例如上圖中的 fsimage_0000000000000000020。在 NameNode 啓動的時候會進行數據恢復,首先把 FSImage 文件加載到內存中造成文件系統鏡像,而後再把 EditLog 之中 FsImage 的結束事務 id 以後的 EditLog 回放到這個文件系統鏡像上。

基於 QJM 的共享存儲系統的整體架構

基於 QJM 的共享存儲系統主要用於保存 EditLog,並不保存 FSImage 文件。FSImage 文件仍是在 NameNode 的本地磁盤上。QJM 共享存儲的基本思想來自於 Paxos 算法 (參見參考文獻 [3]),採用多個稱爲 JournalNode 的節點組成的 JournalNode 集羣來存儲 EditLog。每一個 JournalNode 保存一樣的 EditLog 副本。每次 NameNode 寫 EditLog 的時候,除了向本地磁盤寫入 EditLog 以外,也會並行地向 JournalNode 集羣之中的每個 JournalNode 發送寫請求,只要大多數 (majority) 的 JournalNode 節點返回成功就認爲向 JournalNode 集羣寫入 EditLog 成功。若是有 2N+1 臺 JournalNode,那麼根據大多數的原則,最多能夠容忍有 N 臺 JournalNode 節點掛掉。

基於 QJM 的共享存儲系統的內部實現架構圖如圖 4 所示,主要包含下面幾個主要的組件:

圖 4 . 基於 QJM 的共享存儲系統的內部實現架構圖

FSEditLog:這個類封裝了對 EditLog 的全部操做,是 NameNode 對 EditLog 的全部操做的入口。

JournalSet: 這個類封裝了對本地磁盤和 JournalNode 集羣上的 EditLog 的操做,內部包含了兩類 JournalManager,一類爲 FileJournalManager,用於實現對本地磁盤上 EditLog 的操做。一類爲 QuorumJournalManager,用於實現對 JournalNode 集羣上共享目錄的 EditLog 的操做。FSEditLog 只會調用 JournalSet 的相關方法,而不會直接使用 FileJournalManager 和 QuorumJournalManager。

FileJournalManager:封裝了對本地磁盤上的 EditLog 文件的操做,不只 NameNode 在向本地磁盤上寫入 EditLog 的時候使用 FileJournalManager,JournalNode 在向本地磁盤寫入 EditLog 的時候也複用了 FileJournalManager 的代碼和邏輯。

QuorumJournalManager:封裝了對 JournalNode 集羣上的 EditLog 的操做,它會根據 JournalNode 集羣的 URI 建立負責與 JournalNode 集羣通訊的類 AsyncLoggerSet, QuorumJournalManager 經過 AsyncLoggerSet 來實現對 JournalNode 集羣上的 EditLog 的寫操做,對於讀操做,QuorumJournalManager 則是經過 Http 接口從 JournalNode 上的 JournalNodeHttpServer 讀取 EditLog 的數據。

AsyncLoggerSet:內部包含了與 JournalNode 集羣進行通訊的 AsyncLogger 列表,每個 AsyncLogger 對應於一個 JournalNode 節點,另外 AsyncLoggerSet 也包含了用於等待大多數 JournalNode 返回結果的工具類方法給 QuorumJournalManager 使用。

AsyncLogger:具體的實現類是 IPCLoggerChannel,IPCLoggerChannel 在執行方法調用的時候,會把調用提交到一個單線程的線程池之中,由線程池線程來負責向對應的 JournalNode 的 JournalNodeRpcServer 發送 RPC 請求。

JournalNodeRpcServer:運行在 JournalNode 節點進程中的 RPC 服務,接收 NameNode 端的 AsyncLogger 的 RPC 請求。

JournalNodeHttpServer:運行在 JournalNode 節點進程中的 Http 服務,用於接收處於 Standby 狀態的 NameNode 和其它 JournalNode 的同步 EditLog 文件流的請求。

下面對基於 QJM 的共享存儲系統的兩個關鍵性問題同步數據和恢復數據進行詳細分析。

基於 QJM 的共享存儲系統的數據同步機制分析

Active NameNode 和 StandbyNameNode 使用 JouranlNode 集羣來進行數據同步的過程如圖 5 所示,Active NameNode 首先把 EditLog 提交到 JournalNode 集羣,而後 Standby NameNode 再從 JournalNode 集羣定時同步 EditLog:

圖 5 . 基於 QJM 的共享存儲的數據同步機制

Active NameNode 提交 EditLog 到 JournalNode 集羣

當處於 Active 狀態的 NameNode 調用 FSEditLog 類的 logSync 方法來提交 EditLog 的時候,會經過 JouranlSet 同時向本地磁盤目錄和 JournalNode 集羣上的共享存儲目錄寫入 EditLog。寫入 JournalNode 集羣是經過並行調用每個 JournalNode 的 QJournalProtocol RPC 接口的 journal 方法實現的,若是對大多數 JournalNode 的 journal 方法調用成功,那麼就認爲提交 EditLog 成功,不然 NameNode 就會認爲此次提交 EditLog 失敗。提交 EditLog 失敗會致使 Active NameNode 關閉 JournalSet 以後退出進程,留待處於 Standby 狀態的 NameNode 接管以後進行數據恢復。

從上面的敘述能夠看出,Active NameNode 提交 EditLog 到 JournalNode 集羣的過程其實是同步阻塞的,可是並不須要全部的 JournalNode 都調用成功,只要大多數 JournalNode 調用成功就能夠了。若是沒法造成大多數,那麼就認爲提交 EditLog 失敗,NameNode 中止服務退出進程。若是對應到分佈式系統的 CAP 理論的話,雖然採用了 Paxos 的「大多數」思想對 C(consistency,一致性) 和 A(availability,可用性) 進行了折衷,但仍是能夠認爲 NameNode 選擇了 C 而放棄了 A,這也符合 NameNode 對數據一致性的要求。

Standby NameNode 從 JournalNode 集羣同步 EditLog

當 NameNode 進入 Standby 狀態以後,會啓動一個 EditLogTailer 線程。這個線程會按期調用 EditLogTailer 類的 doTailEdits 方法從 JournalNode 集羣上同步 EditLog,而後把同步的 EditLog 回放到內存之中的文件系統鏡像上 (並不會同時把 EditLog 寫入到本地磁盤上)。

這裏須要關注的是:從 JournalNode 集羣上同步的 EditLog 都是處於 finalized 狀態的 EditLog Segment。「NameNode 的元數據存儲概述」一節說過 EditLog Segment 實際上有兩種狀態,處於 in-progress 狀態的 Edit Log 當前正在被寫入,被認爲是處於不穩定的中間態,有可能會在後續的過程之中發生修改,好比被截斷。Active NameNode 在完成一個 EditLog Segment 的寫入以後,就會向 JournalNode 集羣發送 finalizeLogSegment RPC 請求,將完成寫入的 EditLog Segment finalized,而後開始下一個新的 EditLog Segment。一旦 finalizeLogSegment 方法在大多數的 JournalNode 上調用成功,代表這個 EditLog Segment 已經在大多數的 JournalNode 上達成一致。一個 EditLog Segment 處於 finalized 狀態以後,能夠保證它不再會變化。

從上面描述的過程能夠看出,雖然 Active NameNode 向 JournalNode 集羣提交 EditLog 是同步的,但 Standby NameNode 採用的是定時從 JournalNode 集羣上同步 EditLog 的方式,那麼 Standby NameNode 內存中文件系統鏡像有很大的多是落後於 Active NameNode 的,因此 Standby NameNode 在轉換爲 Active NameNode 的時候須要把落後的 EditLog 補上來。

基於 QJM 的共享存儲系統的數據恢復機制分析

處於 Standby 狀態的 NameNode 轉換爲 Active 狀態的時候,有可能上一個 Active NameNode 發生了異常退出,那麼 JournalNode 集羣中各個 JournalNode 上的 EditLog 就可能會處於不一致的狀態,因此首先要作的事情就是讓 JournalNode 集羣中各個節點上的 EditLog 恢復爲一致。另外如前所述,當前處於 Standby 狀態的 NameNode 的內存中的文件系統鏡像有很大的多是落後於舊的 Active NameNode 的,因此在 JournalNode 集羣中各個節點上的 EditLog 達成一致以後,接下來要作的事情就是從 JournalNode 集羣上補齊落後的 EditLog。只有在這兩步完成以後,當前新的 Active NameNode 才能安全地對外提供服務。

補齊落後的 EditLog 的過程複用了前面描述的 Standby NameNode 從 JournalNode 集羣同步 EditLog 的邏輯和代碼,最終調用 EditLogTailer 類的 doTailEdits 方法來完成 EditLog 的補齊。使 JournalNode 集羣上的 EditLog 達成一致的過程是一致性算法 Paxos 的典型應用場景,QJM 對這部分的處理能夠看作是 Single Instance Paxos(參見參考文獻 [3]) 算法的一個實現,在達成一致的過程當中,Active NameNode 和 JournalNode 集羣之間的交互流程如圖 6 所示,具體描述以下:

圖 6.Active NameNode 和 JournalNode 集羣的交互流程圖

生成一個新的 Epoch

Epoch 是一個單調遞增的整數,用來標識每一次 Active NameNode 的生命週期,每發生一次 NameNode 的主備切換,Epoch 就會加 1。這其實是一種 fencing 機制,爲何須要 fencing 已經在前面「ActiveStandbyElector 實現分析」一節的「防止腦裂」部分進行了說明。產生新 Epoch 的流程與 Zookeeper 的 ZAB(Zookeeper Atomic Broadcast) 協議在進行數據恢復以前產生新 Epoch 的過程徹底相似:

  1. Active NameNode 首先向 JournalNode 集羣發送 getJournalState RPC 請求,每一個 JournalNode 會返回本身保存的最近的那個 Epoch(代碼中叫 lastPromisedEpoch)。

  2. NameNode 收到大多數的 JournalNode 返回的 Epoch 以後,在其中選擇最大的一個加 1 做爲當前的新 Epoch,而後向各個 JournalNode 發送 newEpoch RPC 請求,把這個新的 Epoch 發給各個 JournalNode。

  3. 每個 JournalNode 在收到新的 Epoch 以後,首先檢查這個新的 Epoch 是否比它本地保存的 lastPromisedEpoch 大,若是大的話就把 lastPromisedEpoch 更新爲這個新的 Epoch,而且向 NameNode 返回它本身的本地磁盤上最新的一個 EditLogSegment 的起始事務 id,爲後面的數據恢復過程作好準備。若是小於或等於的話就向 NameNode 返回錯誤。

  4. NameNode 收到大多數 JournalNode 對 newEpoch 的成功響應以後,就會認爲生成新的 Epoch 成功。

在生成新的 Epoch 以後,每次 NameNode 在向 JournalNode 集羣提交 EditLog 的時候,都會把這個 Epoch 做爲參數傳遞過去。每一個 JournalNode 會比較傳過來的 Epoch 和它本身保存的 lastPromisedEpoch 的大小,若是傳過來的 epoch 的值比它本身保存的 lastPromisedEpoch 小的話,那麼此次寫相關操做會被拒絕。一旦大多數 JournalNode 都拒絕了此次寫操做,那麼此次寫操做就失敗了。若是原來的 Active NameNode 恢復正常以後再向 JournalNode 寫 EditLog,那麼由於它的 Epoch 確定比新生成的 Epoch 小,而且大多數的 JournalNode 都接受了這個新生成的 Epoch,因此拒絕寫入的 JournalNode 數目至少是大多數,這樣原來的 Active NameNode 寫 EditLog 就確定會失敗,失敗以後這個 NameNode 進程會直接退出,這樣就實現了對原來的 Active NameNode 的隔離了。

選擇須要數據恢復的 EditLog Segment 的 id

須要恢復的 Edit Log 只多是各個 JournalNode 上的最後一個 Edit Log Segment,如前所述,JournalNode 在處理完 newEpoch RPC 請求以後,會向 NameNode 返回它本身的本地磁盤上最新的一個 EditLog Segment 的起始事務 id,這個起始事務 id 實際上也做爲這個 EditLog Segment 的 id。NameNode 會在全部這些 id 之中選擇一個最大的 id 做爲要進行數據恢復的 EditLog Segment 的 id。

向 JournalNode 集羣發送 prepareRecovery RPC 請求

NameNode 接下來向 JournalNode 集羣發送 prepareRecovery RPC 請求,請求的參數就是選出的 EditLog Segment 的 id。JournalNode 收到請求後返回本地磁盤上這個 Segment 的起始事務 id、結束事務 id 和狀態 (in-progress 或 finalized)。

這一步對應於 Paxos 算法的 Phase 1a 和 Phase 1b(參見參考文獻 [3]) 兩步。Paxos 算法的 Phase1 是 prepare 階段,這也與方法名 prepareRecovery 相對應。而且這裏之前面產生的新的 Epoch 做爲 Paxos 算法中的提案編號 (proposal number)。只要大多數的 JournalNode 的 prepareRecovery RPC 調用成功返回,NameNode 就認爲成功。

選擇進行同步的基準數據源,向 JournalNode 集羣發送 acceptRecovery RPC 請求 NameNode 根據 prepareRecovery 的返回結果,選擇一個 JournalNode 上的 EditLog Segment 做爲同步的基準數據源。選擇基準數據源的原則大體是:在 in-progress 狀態和 finalized 狀態的 Segment 之間優先選擇 finalized 狀態的 Segment。若是都是 in-progress 狀態的話,那麼優先選擇 Epoch 比較高的 Segment(也就是優先選擇更新的),若是 Epoch 也同樣,那麼優先選擇包含的事務數更多的 Segment。

在選定了同步的基準數據源以後,NameNode 向 JournalNode 集羣發送 acceptRecovery RPC 請求,將選定的基準數據源做爲參數。JournalNode 接收到 acceptRecovery RPC 請求以後,從基準數據源 JournalNode 的 JournalNodeHttpServer 上下載 EditLog Segment,將本地的 EditLog Segment 替換爲下載的 EditLog Segment。

這一步對應於 Paxos 算法的 Phase 2a 和 Phase 2b(參見參考文獻 [3]) 兩步。Paxos 算法的 Phase2 是 accept 階段,這也與方法名 acceptRecovery 相對應。只要大多數 JournalNode 的 acceptRecovery RPC 調用成功返回,NameNode 就認爲成功。

向 JournalNode 集羣發送 finalizeLogSegment RPC 請求,數據恢復完成

上一步執行完成以後,NameNode 確認大多數 JournalNode 上的 EditLog Segment 已經從基準數據源進行了同步。接下來,NameNode 向 JournalNode 集羣發送 finalizeLogSegment RPC 請求,JournalNode 接收到請求以後,將對應的 EditLog Segment 從 in-progress 狀態轉換爲 finalized 狀態,實際上就是將文件名從 edits_inprogress_${startTxid} 重命名爲 edits_${startTxid}-${endTxid},見「NameNode 的元數據存儲概述」一節的描述。

只要大多數 JournalNode 的 finalizeLogSegment RPC 調用成功返回,NameNode 就認爲成功。此時能夠保證 JournalNode 集羣的大多數節點上的 EditLog 已經處於一致的狀態,這樣 NameNode 才能安全地從 JournalNode 集羣上補齊落後的 EditLog 數據。

須要注意的是,儘管基於 QJM 的共享存儲方案看起來理論完備,設計精巧,可是仍然沒法保證數據的絕對強一致,下面選取參考文獻 [2] 中的一個例子來講明:

假設有 3 個 JournalNode:JN一、JN2 和 JN3,Active NameNode 發送了事務 id 爲 15一、152 和 153 的 3 個事務到 JournalNode 集羣,這 3 個事務成功地寫入了 JN2,可是在還沒能寫入 JN1 和 JN3 以前,Active NameNode 就宕機了。同時,JN3 在整個寫入的過程當中延遲較大,落後於 JN1 和 JN2。最終成功寫入 JN1 的事務 id 爲 150,成功寫入 JN2 的事務 id 爲 153,而寫入到 JN3 的事務 id 僅爲 125,如圖 7 所示 (圖片來源於參考文獻 [2])。按照前面描述的只有成功地寫入了大多數的 JournalNode 才認爲寫入成功的原則,顯然事務 id 爲 15一、152 和 153 的這 3 個事務只能算做寫入失敗。在進行數據恢復的過程當中,會發生下面兩種狀況:

圖 7.JournalNode 集羣寫入的事務 id 狀況

  • 若是隨後的 Active NameNode 進行數據恢復時在 prepareRecovery 階段收到了 JN2 的回覆,那麼確定會以 JN2 對應的 EditLog Segment 爲基準來進行數據恢復,這樣最後在多數 JournalNode 上的 EditLog Segment 會恢復到事務 153。從恢復的結果來看,實際上能夠認爲前面宕機的 Active NameNode 對事務 id 爲 15一、152 和 153 的這 3 個事務的寫入成功了。可是若是從 NameNode 自身的角度來看,這顯然就發生了數據不一致的狀況。
  • 若是隨後的 Active NameNode 進行數據恢復時在 prepareRecovery 階段沒有收到 JN2 的回覆,那麼確定會以 JN1 對應的 EditLog Segment 爲基準來進行數據恢復,這樣最後在多數 JournalNode 上的 EditLog Segment 會恢復到事務 150。在這種狀況下,若是從 NameNode 自身的角度來看的話,數據就是一致的了。

事實上不光本文描述的基於 QJM 的共享存儲方案沒法保證數據的絕對一致,你們一般認爲的一致性程度很是高的 Zookeeper 也會發生相似的狀況,這也從側面說明了要實現一個數據絕對一致的分佈式存儲系統的確很是困難。

NameNode 在進行狀態轉換時對共享存儲的處理

下面對 NameNode 在進行狀態轉換的過程當中對共享存儲的處理進行描述,使得你們對基於 QJM 的共享存儲方案有一個完整的瞭解,同時也做爲本部分的總結。

NameNode 初始化啓動,進入 Standby 狀態

在 NameNode 以 HA 模式啓動的時候,NameNode 會認爲本身處於 Standby 模式,在 NameNode 的構造函數中會加載 FSImage 文件和 EditLog Segment 文件來恢復本身的內存文件系統鏡像。在加載 EditLog Segment 的時候,調用 FSEditLog 類的 initSharedJournalsForRead 方法來建立只包含了在 JournalNode 集羣上的共享目錄的 JournalSet,也就是說,這個時候只會從 JournalNode 集羣之中加載 EditLog,而不會加載本地磁盤上的 EditLog。另外值得注意的是,加載的 EditLog Segment 只是處於 finalized 狀態的 EditLog Segment,而處於 in-progress 狀態的 Segment 須要後續在切換爲 Active 狀態的時候,進行一次數據恢復過程,將 in-progress 狀態的 Segment 轉換爲 finalized 狀態的 Segment 以後再進行讀取。

加載完 FSImage 文件和共享目錄上的 EditLog Segment 文件以後,NameNode 會啓動 EditLogTailer 線程和 StandbyCheckpointer 線程,正式進入 Standby 模式。如前所述,EditLogTailer 線程的做用是定時從 JournalNode 集羣上同步 EditLog。而 StandbyCheckpointer 線程的做用實際上是爲了替代 Hadoop 1.x 版本之中的 Secondary NameNode 的功能,StandbyCheckpointer 線程會在 Standby NameNode 節點上按期進行 Checkpoint,將 Checkpoint 以後的 FSImage 文件上傳到 Active NameNode 節點。

NameNode 從 Standby 狀態切換爲 Active 狀態

當 NameNode 從 Standby 狀態切換爲 Active 狀態的時候,首先須要作的就是中止它在 Standby 狀態的時候啓動的線程和相關的服務,包括上面提到的 EditLogTailer 線程和 StandbyCheckpointer 線程,而後關閉用於讀取 JournalNode 集羣的共享目錄上的 EditLog 的 JournalSet,接下來會調用 FSEditLog 的 initJournalSetForWrite 方法從新打開 JournalSet。不一樣的是,這個 JournalSet 內部同時包含了本地磁盤目錄和 JournalNode 集羣上的共享目錄。這些工做完成以後,就開始執行「基於 QJM 的共享存儲系統的數據恢復機制分析」一節所描述的流程,調用 FSEditLog 類的 recoverUnclosedStreams 方法讓 JournalNode 集羣中各個節點上的 EditLog 達成一致。而後調用 EditLogTailer 類的 catchupDuringFailover 方法從 JournalNode 集羣上補齊落後的 EditLog。最後打開一個新的 EditLog Segment 用於新寫入數據,同時啓動 Active NameNode 所須要的線程和服務。

NameNode 從 Active 狀態切換爲 Standby 狀態

當 NameNode 從 Active 狀態切換爲 Standby 狀態的時候,首先須要作的就是中止它在 Active 狀態的時候啓動的線程和服務,而後關閉用於讀取本地磁盤目錄和 JournalNode 集羣上的共享目錄的 EditLog 的 JournalSet。接下來會調用 FSEditLog 的 initSharedJournalsForRead 方法從新打開用於讀取 JournalNode 集羣上的共享目錄的 JournalSet。這些工做完成以後,就會啓動 EditLogTailer 線程和 StandbyCheckpointer 線程,EditLogTailer 線程會定時從 JournalNode 集羣上同步 Edit Log。

NameNode 高可用運維中的注意事項

本節結合筆者的實踐,從初始化部署和平常運維兩個方面介紹一些在 NameNode 高可用運維中的注意事項。

初始化部署

若是在開始部署 Hadoop 集羣的時候就啓用 NameNode 的高可用的話,那麼相對會比較容易。可是若是在採用傳統的單 NameNode 的架構運行了一段時間以後,升級爲 NameNode 的高可用架構的話,就要特別注意在升級的時候須要按照如下的步驟進行操做:

  1. 對 Zookeeper 進行初始化,建立 Zookeeper 上的/hadoop-ha/${dfs.nameservices} 節點。建立節點是爲隨後經過 Zookeeper 進行主備選舉作好準備,在進行主備選舉的時候會在這個節點下面建立子節點 (具體可參照「ActiveStandbyElector 實現分析」一節的敘述)。這一步經過在原有的 NameNode 上執行命令 hdfs zkfc -formatZK 來完成。
  2. 啓動全部的 JournalNode,這經過腳本命令 hadoop-daemon.sh start journalnode 來完成。
  3. 對 JouranlNode 集羣的共享存儲目錄進行格式化,而且將原有的 NameNode 本地磁盤上最近一次 checkpoint 操做生成 FSImage 文件 (具體可參照「NameNode 的元數據存儲概述」一節的敘述) 以後的 EditLog 拷貝到 JournalNode 集羣上的共享目錄之中,這經過在原有的 NameNode 上執行命令 hdfs namenode -initializeSharedEdits 來完成。
  4. 啓動原有的 NameNode 節點,這經過腳本命令 hadoop-daemon.sh start namenode 完成。
  5. 對新增的 NameNode 節點進行初始化,將原有的 NameNode 本地磁盤上最近一次 checkpoint 操做生成 FSImage 文件拷貝到這個新增的 NameNode 的本地磁盤上,同時須要驗證 JournalNode 集羣的共享存儲目錄上已經具備了這個 FSImage 文件以後的 EditLog(已經在第 3 步完成了)。這一步經過在新增的 NameNode 上執行命令 hdfs namenode -bootstrapStandby 來完成。
  6. 啓動新增的 NameNode 節點,這經過腳本命令 hadoop-daemon.sh start namenode 完成。
  7. 在這兩個 NameNode 上啓動 zkfc(ZKFailoverController) 進程,誰經過 Zookeeper 選主成功,誰就是主 NameNode,另外一個爲備 NameNode。這經過腳本命令 hadoop-daemon.sh start zkfc 完成。

平常維護

筆者在平常的維護之中主要遇到過下面兩種問題:

Zookeeper 過於敏感:Hadoop 的配置項中 Zookeeper 的 session timeout 的配置參數 ha.zookeeper.session-timeout.ms 的默認值爲 5000,也就是 5s,這個值比較小,會致使 Zookeeper 比較敏感,能夠把這個值儘可能設置得大一些,避免由於網絡抖動等緣由引發 NameNode 進行無謂的主備切換。

單臺 JouranlNode 故障時會致使主備沒法切換:在理論上,若是有 3 臺或者更多的 JournalNode,那麼掛掉一臺 JouranlNode 應該仍然能夠進行正常的主備切換。可是筆者在某次 NameNode 重啓的時候,正好遇上一臺 JournalNode 掛掉宕機了,這個時候雖然某一臺 NameNode 經過 Zookeeper 選主成功,可是這臺被選爲主的 NameNode 沒法成功地從 Standby 狀態切換爲 Active 狀態。過後追查緣由發現,被選爲主的 NameNode 卡在退出 Standby 狀態的最後一步,這個時候它須要等待到 JournalNode 的請求所有完成以後才能退出。可是因爲有一臺 JouranlNode 宕機,到這臺 JournalNode 的請求都積壓在一塊兒而且在不斷地進行重試,同時在 Hadoop 的配置項中重試次數的默認值很是大,因此就會致使被選爲主的 NameNode 沒法及時退出 Standby 狀態。這個問題主要是 Hadoop 內部的 RPC 通訊框架的設計缺陷引發的,Hadoop HA 的源代碼 IPCLoggerChannel 類中有關於這個問題的 TODO,可是截止到社區發佈的 2.7.1 版本這個問題仍然存在。

來源:https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-name-node/index.html

相關文章
相關標籤/搜索