可能是以下原因,未測試:
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 的高可用機制進行詳細的分析。
這裡寫圖檔描述

圖 1.HDFS NameNode 高可用整體架構
從上圖中,我們可以看出 NameNode 的高可用架構主要分為下面幾個部分:
Active NameNode 和 Standby NameNode:兩台 NameNode 形成互備,一台處于 Active 狀态,為主 NameNode。
另外一台處于 Standby 狀态,為備 NameNode。隻有主 NameNode 才能對外提供讀寫服務。備 NameNode 隻可以同步主 NameNode 的Editlog與接收 DataNode 的資料塊彙報。
主備切換控制器 ZKFailoverController:ZKFailoverController 作為獨立的程序運作(運作在 ActiveNameNode 和 StandByNameNode 上),對 NameNode 的主備切換進行總體控制。ZKFailoverController 能及時檢測到 NameNode 的健康狀況,在主 NameNode 故障時借助 Zookeeper 實作自動的主備選舉和切換,當然 NameNode 目前也支援不依賴于 Zookeeper 的手動主備切換(但是在應用中傾向于依賴 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 注冊相應的回調方法。
zkfc進行名啟動記錄:
這裡寫圖檔描述
NameNode線程狀态:
HealthMonitor 主要負責檢測 NameNode 的健康狀态(檢測 ActiveNameNode 是否可以提供服務,檢測StandByNameNode 是否可以參加主節點選舉),如果檢測到 NameNode 的狀态發生變化,會回調 ZKFailoverController 的相應方法進行自動的主備選舉。
ActiveStandbyElector 主要負責完成自動的主備選舉,内部封裝了 Zookeeper 的處理邏輯,一旦 Zookeeper 主備選舉完成,會回調 ZKFailoverController 的相應方法來進行 NameNode 的主備狀态切換。
圖 2.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 狀态。
下面分别對 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 的相應方法來進行處理,
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進而成為ActiveNameNode之後,建立另外一個路徑為/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 狀态,開始對外提供服務。
共享存儲系統
過去幾年中 Hadoop 社群湧現過很多的 NameNode 共享存儲方案,比如 shared NAS+NFS、BookKeeper、BackupNode 和 QJM(Quorum Journal Manager) 等等。目前社群已經把由 Clouderea 公司實作的基于 QJM 的方案合并到 HDFS 的 trunk 之中并且作為預設的共享存儲實作,本部分隻針對基于 QJM 的共享存儲方案的内部實作原理進行分析。為了了解 QJM 的設計和實作,首先要對 NameNode 的中繼資料存儲結構有所了解。
NameNode 的中繼資料存儲概述
一個典型的 NameNode 的中繼資料存儲目錄結構如圖 3 所示 ,這裡主要關注其中的 EditLog 檔案和 FSImage 檔案:
NameNode 在執行 HDFS 用戶端送出的建立檔案或者移動檔案這樣的寫操作的時候,會首先把這些操作記錄在 EditLog 檔案之中,
然後再更新記憶體中的檔案系統鏡像。記憶體中的檔案系統鏡像用于 NameNode 向用戶端提供讀服務,
而 EditLog 僅僅隻是在資料恢複的時候起作用。記錄在 EditLog 之中的每一個操作又稱為一個事務,每個事務有一個整數形式的事務 id 作為編号。
EditLog 會被切割為很多段,每一段稱為一個 Segment。正在寫入的 EditLog Segment 處于 in-progress 狀态,其檔案名形如 edits_inprogress_starttxid,其中{start_txid} 表示這個 segment 的起始事務 id,
例如上圖中的 edits_inprogress_0000000000000000020。而已經寫入完成的 EditLog Segment 處于 finalized 狀态,其檔案名形如 edits_starttxid−{end_txid},其中starttxid表示這個segment的起始事務id,{end_txid} 表示這個 segment 的結束事務 id,例如上圖中的 edits_0000000000000000001-0000000000000000019。
NameNode 會定期對記憶體中的檔案系統鏡像進行 checkpoint 操作,在磁盤上生成 FSImage 檔案,FSImage 檔案的檔案名形如 fsimage_endtxid,其中{end_txid} 表示這個 fsimage 檔案的結束事務 id,例如上圖中的 fsimage_0000000000000000020。在 NameNode 啟動的時候會進行資料恢複,首先把 FSImage 檔案加載到記憶體中形成檔案系統鏡像,然後再把 EditLog 之中 FsImage 的結束事務 id 之後的 EditLog 回放到這個檔案系統鏡像上。
基于 QJM 的共享存儲系統的總體架構
基于 QJM 的共享存儲系統主要用于儲存 EditLog,并不儲存 FSImage 檔案。FSImage 檔案還是在 NameNode 的本地磁盤上。QJM 共享存儲的基本思想來自于 Paxos 算法,采用多個稱為 JournalNode 的節點組成的 JournalNode 叢集來存儲 EditLog。每個 JournalNode 儲存同樣的 EditLog 副本。每次 NameNode 寫 EditLog 的時候,除了向本地磁盤寫入 EditLog 之外,也會并行地向 JournalNode 叢集之中的每一個 JournalNode 發送寫請求,隻要大多數 (majority) 的 JournalNode 節點傳回成功就認為向 JournalNode 叢集寫入 EditLog 成功。如果有 2N+1 台 JournalNode,那麼根據大多數的原則,最多可以容忍有 N 台 JournalNode 節點挂掉。
基于 QJM 的共享存儲系統的内部實作架構圖如圖 4 所示,主要包含下面幾個主要的元件:
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 的過程完全類似:
Active NameNode 首先向 JournalNode 叢集發送 getJournalState RPC 請求,每個 JournalNode 會傳回自己儲存的最近的那個 Epoch(代碼中叫 lastPromisedEpoch)。
NameNode 收到大多數的 JournalNode 傳回的 Epoch 之後,在其中選擇最大的一個加 1 作為目前的新 Epoch,然後向各個 JournalNode 發送 newEpoch RPC 請求,把這個新的 Epoch 發給各個 JournalNode。
每一個 JournalNode 在收到新的 Epoch 之後,首先檢查這個新的 Epoch 是否比它本地儲存的 lastPromisedEpoch 大,如果大的話就把 lastPromisedEpoch 更新為這個新的 Epoch,并且向 NameNode 傳回它自己的本地磁盤上最新的一個 EditLogSegment 的起始事務 id,為後面的資料恢複過程做好準備。如果小于或等于的話就向 NameNode 傳回錯誤。
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兩步。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:JN1、JN2 和 JN3,Active NameNode 發送了事務 id 為 151、152 和 153 的 3 個事務到 JournalNode 叢集,這 3 個事務成功地寫入了 JN2,但是在還沒能寫入 JN1 和 JN3 之前,Active NameNode 就當機了。同時,JN3 在整個寫入的過程中延遲較大,落後于 JN1 和 JN2。最終成功寫入 JN1 的事務 id 為 150,成功寫入 JN2 的事務 id 為 153,而寫入到 JN3 的事務 id 僅為 125,如圖 7 所示。按照前面描述的隻有成功地寫入了大多數的 JournalNode 才認為寫入成功的原則,顯然事務 id 為 151、152 和 153 的這 3 個事務隻能算作寫入失敗。在進行資料恢複的過程中,會發生下面兩種情況:
圖 7.JournalNode 叢集寫入的事務 id 情況:
如果随後的 Active NameNode 進行資料恢複時在 prepareRecovery 階段收到了 JN2 的回複,那麼肯定會以 JN2 對應的 EditLog Segment 為基準來進行資料恢複,這樣最後在多數 JournalNode 上的 EditLog Segment 會恢複到事務 153。從恢複的結果來看,實際上可以認為前面當機的 Active NameNode 對事務 id 為 151、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 的高可用架構的話,就要特别注意在更新的時候需要按照以下的步驟進行操作:
對 Zookeeper 進行初始化,建立 Zookeeper 上的/hadoop-ha/${dfs.nameservices} 節點。建立節點是為随後通過 Zookeeper 進行主備選舉做好準備,在進行主備選舉的時候會在這個節點下面建立子節點 (具體可參照“ActiveStandbyElector 實作分析”一節的叙述)。這一步通過在原有的 NameNode 上執行指令 hdfs zkfc -formatZK 來完成。
啟動所有的 JournalNode,這通過腳本指令 hadoop-daemon.sh start journalnode 來完成。
對 JouranlNode 叢集的共享存儲目錄進行格式化,并且将原有的 NameNode 本地磁盤上最近一次 checkpoint 操作生成 FSImage 檔案 (具體可參照“NameNode 的中繼資料存儲概述”一節的叙述) 之後的 EditLog 拷貝到 JournalNode 叢集上的共享目錄之中,這通過在原有的 NameNode 上執行指令 hdfs namenode -initializeSharedEdits 來完成。
啟動原有的 NameNode 節點,這通過腳本指令 hadoop-daemon.sh start namenode 完成。
對新增的 NameNode 節點進行初始化,将原有的 NameNode 本地磁盤上最近一次 checkpoint 操作生成 FSImage 檔案拷貝到這個新增的 NameNode 的本地磁盤上,同時需要驗證 JournalNode 叢集的共享存儲目錄上已經具有了這個 FSImage 檔案之後的 EditLog(已經在第 3 步完成了)。這一步通過在新增的 NameNode 上執行指令 hdfs namenode -bootstrapStandby 來完成。
啟動新增的 NameNode 節點,這通過腳本指令 hadoop-daemon.sh start namenode 完成。
在這兩個 NameNode 上啟動 zkfc(ZKFailoverController) 程序,誰通過 Zookeeper 選主成功,誰就是主 NameNode,另一個為備 NameNode。這通過腳本指令 hadoop-daemon.sh start zkfc 完成。
日常維護
筆者在日常的維護之中主要遇到過下面兩種問題:
Zookeeper 過于敏感:Hadoop 的配置項中 Zookeeper 的 session timeout 的配置參數 ha.zookeeper.session-timeout.ms 的預設值為 5000,
也就是 5s,此參數在hdfs的core-site.xml檔案中配置,這個值比較小,會導緻 Zookeeper 比較敏感,可以把這個值盡量設定得大一些,避免因為網絡抖動等原因引起 NameNode 進行無謂的主備切換。
單台 JouranlNode 故障時會導緻主備無法切換:在理論上,如果有 3 台或者更多的 JournalNode,那麼挂掉一台 JouranlNode 應該仍然可以進行正常的主備切換
。但是筆者在某次 NameNode 重新開機的時候,正好趕上一台 JournalNode 挂掉當機了,這個時候雖然某一台 NameNode 通過 Zookeeper 選主成功,
但是這台被選為主的 NameNode 無法成功地從 Standby 狀态切換為 Active 狀态。事後追查原因發現,被選為主的 NameNode 卡在退出 Standby 狀态的最後一步,
這個時候它需要等待到 JournalNode 的請求全部完成之後才能退出。但是由于有一台 JouranlNode 當機,
到這台 JournalNode 的請求都積壓在一起并且在不斷地進行重試,同時在 Hadoop 的配置項中重試次數的預設值非常大,
是以就會導緻被選為主的 NameNode 無法及時退出 Standby 狀态。這個問題主要是 Hadoop 内部的 RPC 通信架構的設計缺陷引起的
本文轉自 yntmdr 51CTO部落格,原文連結:http://blog.51cto.com/yntmdr/2069692,如需轉載請自行聯系原作者