過去幾年中 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 的過程完全類似:
-
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(參見參考文獻 [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: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 所示 (圖片來源於參考文獻 [2])。按照前面描述的只有成功地寫入了大多數的 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,這個值比較小,會導致 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 版本這個問題仍然存在。