HDFS是Hadoop Distribute File System 的簡稱,也就是Hadoop的一個分布式文件系統。它是hadoop生態的核心,自己在使用中也遇到了一些問題。
學習整理:
-
請簡述HDFS的HA.
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 主備切換主要由 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 的主備切換流程
步驟如下:
- 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 的相應方法來進行處理。
而 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 的共享存儲實現
NameNode 的元數據存儲概述
一個典型的 NameNode 的元數據存儲目錄結構如圖 3 所示這里主要關注其中的 EditLog 文件和 FSImage 文件:
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 算法,采用多個稱為 JournalNode 的節點組成的 JournalNode 集群來存儲 EditLog。
每個 JournalNode 保存同樣的 EditLog 副本。
每次 NameNode 寫 EditLog 的時候,除了向本地磁盤寫入 EditLog 之外,也會并行地向 JournalNode 集群之中的每一個 JournalNode 發送寫請求,只要大多數 (majority) 的 JournalNode 節點返回成功就認為向 JournalNode 集群寫入 EditLog 成功。
如果有 2N+1 臺 JournalNode,那么根據大多數的原則,最多可以容忍有 N 臺 JournalNode 節點掛掉。
基于 QJM 的共享存儲系統的內部實現架構圖如圖 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 的共享存儲系統的數據同步機制分析
Active NameNode 和 StandbyNameNode 使用 JouranlNode 集群來進行數據同步的過程如圖 5 所示,Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再從 JournalNode 集群定時同步 EditLog:
基于 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 算法的一個實現,在達成一致的過程中,Active NameNode 和 JournalNode 集群之間的交互流程如圖 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兩步。
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 數據。
NameNode 在進行狀態轉換時對共享存儲的處理
- 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。
參見:
Hadoop NameNode 高可用 (High Availability) 實現解析:https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-name-node/index.html
-
HDFS存儲策略?
講存儲策略之前,我想先補充下冷熱數據的概念
冷熱數據從訪問頻次來看可分為:
熱數據:是需要被計算節點頻繁訪問的在線類數據。
冷數據:是對于離線類不經常訪問的數據,比如企業備份數據、業務與操作日志數據、話單與統計數據。熱數據就近計算,冷數據集中存儲
從數據分析層面來看可分:
冷數據、溫數據和熱數據。
冷數據——性別、興趣、常住地、職業、年齡等數據畫像,表征“這是什么樣的人”;
溫數據——近期活躍應用、近期去過的地方等具有一定時效性的行為數據,表征“最近對什么感興趣”;
熱數據——當前地點、打開的應用等場景化明顯的、稍縱即逝的營銷機會,表征“正在哪里干什么”。
根據對數據的冷熱理解,我們能更好的規劃我們的hdfs存儲策略,那接下來主要講HDFS存儲策略的異構存儲和副本放置兩個方面。
- 異構存儲策略
Hadoop從2.6.0版本開始支持異構存儲功能。我們知道HDFS默認的存儲策略,對于每個數據塊,采用三個副本的存儲方式,保存在不同節點的磁盤上。異構存儲的作用在于利用服務器不同類型的存儲介質(包括HDD硬盤、SSD、內存等)提供更多的存儲策略(例如三個副本一個保存在SSD介質,剩下兩個仍然保存在HDD硬盤),從而使得HDFS的存儲能夠更靈活高效地應對各種應用場景。
HDFS中預定義支持的各種存儲包括:ARCHIVE:高存儲密度但耗電較少的存儲介質,例如磁帶,通常用來>存儲冷數據
DISK:磁盤介質,這是HDFS最早支持的存儲介質
SSD:固態硬盤,是一種新型存儲介質,目前被不少互聯網公司使用
RAM_DISK :數據被寫入內存中,同時會往該存儲介質中再(異步)寫一份異構存儲
HDFS中支持的存儲策略包括:
Lazy_persist:一個副本保存在內存RAM_DISK中,其余副本保存在磁盤中
ALL_SSD:所有副本都保存在SSD中
One_SSD:一個副本保存在SSD中,其余副本保存在磁盤中
Hot:所有副本保存在磁盤中,這也是默認的存儲策略
Warm:一個副本保存在磁盤上,其余副本保存在歸檔存儲上
Cold:所有副本都保存在歸檔存儲上一個存儲策略,包含以下部分
Policy ID --策略ID
Policy name --策略名稱
A list of storage types for block placement --塊存放的有關存儲類型(可以多個)
A list of fallback storage types for file creation--如果創建失敗的替代存儲類型(可以多個)
A list of fallback storage types for replication--如果復制失敗的替代存儲類型(可以多個)異構存儲當有足夠空間的時候,塊復制使用下表中第三列所列出的存儲類型。
如果第三列的空間不夠,則考慮用第四列的(創建的時候)或者第五列的(復制的時候)配置:
dfs.storage.policy.enabled - 啟用/關閉存儲策略特性。默認是true(開啟)
dfs.datanode.data.dir - 數據路徑,多個以逗號分隔,但必須在前面帶上存儲類型。例如:A datanode storage location /grid/dn/ssd0 on SSD can should configured with [SSD]file:///grid/dn/ssd0 A datanode storage location /grid/dn/archive0 on ARCHIVE should be configured with [ARCHIVE]file:///grid/dn/archive0 A datanode storage location /grid/dn/ram0 on RAM_DISK should be configured with [RAM_DISK]file:///grid/dn/ram0
如果沒有設定存儲類型,那么使用默認的([DISK])
總體上HDFS異構存儲的價值在于,根據數據熱度采用不同策略從而提升集群整體資源使用效率。對于頻繁訪問的數據,將其全部或部分保存在更高訪問性能的存儲介質(內存或SSD)上,提升其讀寫性能;對于幾乎不會訪問的數據,保存在歸檔存儲介質上,降低其存儲成本。但是HDFS異構存儲的配置需要用戶對目錄指定相應的策略,即用戶需要預先知道每個目錄下的文件的訪問熱度,在實際大數據平臺的應用中,這是比較困難的一點
補充一點,在hadoop3.0之后又引入了支持HDFS文件塊級別的糾刪碼,底層采用Reed-Solomon(k,m)算法。RS是一種常用的糾刪碼算法,通過矩陣運算,可以為k位數據生成m位校驗位,根據k和m的取值不同,可以實現不同程度的容錯能力,是一種比較靈活的糾刪碼算法。
Reed-Solomon(k,m)
HDFS糾刪碼技術能夠降低數據存儲的冗余度,以RS(3,2)為例,其數據冗余度為67%,相比Hadoop默認的200%大為減少。但是糾刪碼技術存儲數據和數據恢復都需要消耗cpu進行計算,實際上是一種以時間換空間的選擇,因此比較適用的場景是對冷數據的存儲。冷數據存儲的數據往往一次寫入之后長時間沒有訪問,這種情況下可以通過糾刪碼技術減少副本數。
- 副本放置策略
NameNode在挑選合適的DataNode去存儲Block的時候,不僅僅考慮了DataNode的存儲空間夠不夠,還會考慮這些DataNode在不在同一個機架上。
這就需要NameNode必須知道所有的DataNode分別位于哪個機架上(所以也稱為機架感知)。
當然,默認情況下NameNode是不會知道機架的存在的,也就是說,默認情況下,NameNode會認為所有的DataNode都在同一個機架上(/defaultRack)。
除非我們在hdfs-site.xml里面配置topology.script.file.name選項,這個選項的值是一個可執行文件的位置,而該只執行文件的作用是將輸入的DataNode的ip地址按照一定規則計算,然后輸出它所在的機架的名字,如/rack1, /rack2之類。借助這個文件,NameNode就具備了機架感知了。當它在挑選DataNode去存儲Block的時候,它會遵循以下原則:
- 1st replica. 如果寫請求方所在機器是其中一個個DataNode,則直接存放在本地,否則隨機在集群中選擇一個DataNode。
- 2nd replica. 第二個副本存放于不同第一個副本的所在的機架。
- 3rd replica. 第三個副本存放于第二個副本所在的機架,但是屬于不同的節點。
other replica. 如果還有更多的副本,則隨機放在節點中。
副本放置策略
參見:
冷數據、溫數據、熱數據,難道數據也是有溫度的?:
http://www.lxweimin.com/p/053ba529bf02
如何根據數據冷熱程度分層存儲,讓HDFS更高效?:http://dbaplus.cn/news-73-1615-1.html
Hadoop--HDFS之機架感知和副本策略:http://xiejm.com/Hadoop/Hadoop--HDFSreplication.html
HDFS的存儲策略:https://www.cnblogs.com/lzfhope/p/7068909.html
-
單獨說說Parquet列式存儲:
首先強調,paruqet是列式存儲(以前一直以為是行式存儲,踩了好多坑,全是淚)。一開始是由twitter和cloudera共同完成并開源,后來被apache孵化為頂級項目。語言無關,可以配合多種hadoop組件使用。
如上圖所示,parquet序列化和反序列化主要由三大部分組成:存儲格式(Parquet file format,定義了其內部數據類型和存儲格式)、對象模型轉換器(Conerters,完成內外部對象映射)和對象模型(object model,內存中的數據表示)組成。其列式存儲是將某一列的數據連續存儲,每一行中的不同列散列分布,這樣查詢的時候不用掃描全部數據,而且因為列是同構數據可以進行有效壓縮,所以能有效減少IO。
列式存儲還有兩個概念:映射下推、謂詞下推
映射下推(Project PushDown),是將以前關系型數據庫拿出所有數據后再篩選有用的字段,變為直接讀取需要的列。而且parquet還能會考慮列是否連續,可以一次性讀出多個列的數據到內存。
謂詞下推(Predicate PushDown),是將where或on中的過濾條件盡可能的交給底層執行,從而減少每層交互的數據量。parquet通過在每個列塊存儲的時候計算統計信息(最大最小值、空值個數),來判斷rowgroup是否需要掃描。
parquet數據模型如下所示:
message AddressBook {
required string owner;
repeated string ownerPhoneNumbers;
repeated group contacts {
required string name;
optional string phoneNumber;
}
}
根為message,message包含多個field,沒分field包含三個屬性:repetition、type、name,其中repetition的取值為required(必須1次)、optional(至多1次)或repeated(任意多次)。
parquet 沒有實現復雜的map、list、set,而是使用repeated field(實現set、list) 和 groups(key-value對groups來實現map)來實現。
parquet樹型結構如下:
系統在存儲的時候只存葉子節點,但會添加一些輔助字段來顯示結構。
如:
其存儲結構為:
其中r和d字段分別代表repetition level 和 definition level。
repetition level:只針對repeated field,指明其定義在哪一級(級數按repeated計算)重復,且第一次出現因為不存在重復,所以為0。
definition level:指明該列路徑上有多少可選(optional、repeated)field被定義。
最終converter通過stripping和assembly這些subfield實現序列化和反序列化。
parquet的文件格式如下:
parquet數據會按行組進行切分(官方建議調整行組大小和HDFS塊大小到1G以實現最優性能),每個行組包含擁有所有列數據的列塊,列塊中包含有分頁(官方建議8k),分頁為parquet壓縮和編碼的單元,不同的頁面允許使用不同的編碼方式,分頁對于數據模型透明。
性能方面,通過對比不同的存儲格式,parquet在數據壓縮率和查詢速度方面都有明顯優勢,相較于ORC可嵌套數據結構,但不支持數據修改和ACID,所以更適用于OLAP領域,是一種工業界廣泛認可的優化方案。
如果說HDFS是大數據時代文件系統的實際標準的話,parquet就是大數據時代存儲格式的實際標準。
參考http://www.lxweimin.com/p/47b39ae336d5。
TO BE CONTINUED ......