NameNode 高可用整體架構概述
在 Hadoop 1.0 時代,Hadoop 的兩大核心組件 HDFS NameNode 和 JobTracker 都存在着單點問題,這其中以 NameNode 的單點問題尤為嚴重。
因為 NameNode 保存了整個 HDFS 的元數據信息,一旦 NameNode 掛掉,整個 HDFS 就無法訪問,同時 Hadoop 生態系統中依賴於 HDFS 的各個組件,
包括 MapReduce、Hive、Pig 以及 HBase 等也都無法正常工作,並且重新啟動 NameNode 和進行數據恢復的過程也會比較耗時。
這些問題在給 Hadoop 的使用者帶來困擾的同時,也極大地限制了 Hadoop 的使用場景,使得 Hadoop 在很長的時間內僅能用作離線存儲和離線計算,
無法應用到對可用性和數據一致性要求很高的在線應用場景中。
所幸的是,在 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 的高可用機制進行詳細的分析。
HDFS NameNode 的高可用整體架構如圖 1 所示 (圖片來源於參考文獻 [1]):
圖 1.HDFS NameNode 高可用整體架構
從上圖中,我們可以看出 NameNode 的高可用架構主要分為下面幾個部分:
Active NameNode 和 Standby NameNode:兩台 NameNode 形成互備,一台處於 Active 狀態,為主 NameNode,
另外一台處於 Standby 狀態,為備 NameNode,只有主 NameNode 才能對外提供讀寫服務。
主備切換控制器 ZKFailoverController:ZKFailoverController 作為獨立的進程運行,對 NameNode 的主備切換進行總體控制。ZKFailoverController 能及時檢測到 NameNode 的健康狀況,在主 NameNode 故障時借助 Zookeeper 實現自動的主備選舉和切換,當然 NameNode 目前也支持不依賴於 Zookeeper 的手動主備切換。
Zookeeper 集群:為主備切換控制器提供主備選舉支持。
共享存儲系統:共享存儲系統是實現 NameNode 的高可用最為關鍵的部分,共享存儲系統保存了 NameNode 在運行過程中所產生的 HDFS 的元數據。主 NameNode 和
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 所示,有以下幾步:
- HealthMonitor 初始化完成之后會啟動內部的線程來定時調用對應 NameNode 的 HAServiceProtocol RPC 接口的方法,對 NameNode 的健康狀態進行檢測。
- HealthMonitor 如果檢測到 NameNode 的健康狀態發生變化,會回調 ZKFailoverController 注冊的相應方法進行處理。
- 如果 ZKFailoverController 判斷需要進行主備切換,會首先使用 ActiveStandbyElector 來進行自動的主備選舉。
- ActiveStandbyElector 與 Zookeeper 進行交互完成自動的主備選舉。
- ActiveStandbyElector 在主備選舉完成后,會回調 ZKFailoverController 的相應方法來通知當前的 NameNode 成為主 NameNode 或備 NameNode。
- 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 的時候,會執行以下的操作:
- 首先嘗試調用這個舊 Active NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,看能不能把它轉換為 Standby 狀態。
- 如果 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 的元數據存儲結構有所了解。