FLINK 高可用服務概覽與改造

分佈式系統總要面對天然的失敗場景,這可能是網絡分區、節點故障或者線程死亡等等五花八門的問題。在失敗場景下保證服務整體對外的可用性(Availability)是分佈式系統質量的一個重要衡量標準。

FLINK 使用的高可用服務提供了在 Master 失敗的情況下已提交的作業會重新運行,並且重新運行的進度不早於最後一次 checkpoint 的保證。

本文從拆解 FLINK 使用的高可用服務的框架和職責入手,先介紹 FLINK 高可用服務的整體結構,接着指出社區現有實現的若干問題,進而介紹我在 FLINK-10333 提出的對高可用服務的改造的提議以及它如何解決這些問題。

FLINK 高可用服務的框架和職責

FLINK 的高可用服務大體可以分爲兩塊,一塊是 LeaderElectionService 和 LeaderRetrievalService 組成的選舉和名稱服務,另一塊是由 JobGraphStore、CheckpointStore 和 JobRegistry 組成的作業管理服務。這兩塊服務均可在 Master 失敗的情況下保持整體對外的可用性。

選舉與名稱服務

FLINK 針對選舉和名稱服務提供了三種不同的實現,其中真正高可用的是基於 ZooKeeper 的實現,Standalone 實現是簡單的預配置信息,Embedded 實現是在內存中模擬 ZooKeeper 實現的選舉過程。選舉的算法和執行過程在下文介紹對高可用服務的改造時會細講,這裏僅介紹其抽象上地使用方式。

LeaderElectionService 通過選舉算法在 Master 實例中選出 Leader 後,Leader 將發佈自己的地址,隨後 LeaderRetrivalService 從 Leader 發佈的位置上發現新任 Leader 的地址並連接(Standalone 的情況下,地址是預配置的,也就是說 LeaderRetrievalService 天生知道 Leader 在哪)。例如,TaskManager 上有一個發現 JobManager Leader 的 LeaderRetrivevalService,在 JobManager 選出 Leader 後即可發現其地址,並連上 JobManager Leader 開始通信和工作。當 Leader 失敗掛掉時,新選出的 Leader 發佈自己的地址,LeaderRetrievalService 發現新的 Leader,斷開丟掉 Leadership 的 JobManager 的鏈接並和新的 Leader 建立連接。

作業管理服務

在 Master 失敗並重啓之後,FLINK 保證已提交的作業會重新運行,這依賴於 JobGraphStore。當作業成功提交之後,作業對應的 JobGraph 會被持久化到 JobGraphStore 上。當 Master 失敗並重啓之後,它會通過配置重新構建 JobGraphStore 並獲取此前持久化的 JobGraph 並從中恢復作業。

JobRegistry 是用於協助作業管理的元信息,它保存了作業是否處於 RUNNING 或者 DONE 的狀態。一旦作業被某個 JobManager 執行,這就意味着作業已經進入 RUNNING 狀態;而如果有 JobManager 已經完成了作業,這個作業在 JobRegistry 上的狀態就會變成 DONE。RUNNING 狀態主要用於使 JobManager 失敗並重啓時有嘗試直接恢復現有作業的能力,具體細節將在下文提及;DONE 狀態主要用於避免作業重複提交和 standby 的 JobManager 在獲得 Leadership 後執行已完成的任務。

CheckpointStore 持久化了已完成的 checkpoint 的元數據,當 Master 失敗並重啓之後,可以恢復任務完成過的 checkpoint,並從 checkpoint 的位置重新開始任務,避免了任務完全從頭開始。

社區版本 FLINK 高可用服務的問題

上面提到的種種功能均是 FLINK 高可用服務理想情況下應該工作的樣子,然而,由於實現上的問題,實際上社區不僅缺少了一些功能,甚至已有功能的實現也是有缺陷的。危言聳聽地說,社區版本 FLINK 所提供的高可用服務是個假的高可用服務,在可以預見的邊界情況下無法保證正確性。

Master Failover 時無法接管現有任務

我們知道,在 Spark 等其他計算引擎中,發生 Master failover 時,由於掛掉的是 Master,可能 Task 還是正常的在運行。在 Master 恢復之後應該首先嚐試接管現有任務,如果所有 Task 都正常運行,就直接將作業狀態同步後進行下一步調度。

但是,在社區版本的 FLINK 實現中,發生 Master failover 時,TaskManager 發現 Master Leader 丟失,將立刻 fail 其上所有隸屬於這個 JobManager 的 Task,也就是說,由於 Master 掛掉,所有的 Task 也被迫掛掉。這顯然是不可接受的。

FLIP-6 的設計中是包含 Master failover 時接管現有任務的邏輯的。總的來說,在 Master 失敗並重啓之後,如果它看到 JobRegistry 上記錄的作業狀態是 RUNNING 的話,它就認爲這個作業已經有運行的實例,並開始嘗試接管現有任務。在 TaskManager 端,它會發現新發布的 Leader 的信息,連上新的 Leader 後如果自己有正在運行的屬於這個 Job 的 Task,就會主動向 Master 彙報。Master 從 TaskManager 的彙報中恢復出現有的作業,如果作業狀態能夠完全成功恢復,則接管現有任務繼續調度和運行。如果彙報上來的狀態有失敗的或者超時前沒有接收到 TaskManager 的彙報,則 Master 認爲接管失敗,將所有的 Task fail 之後重新調度作業。

Master 修改持久化數據時不能保證自己是 Leader

發佈 Leader 的信息和修改 JobGraphStore/JobRegistry/CheckpointStore 的內容,這類操作都需要保證修改人是當前的 Leader,否則可能出現 Leader 信息發佈出來的是一個不是 Leader 的實例的信息,或者不是 Leader 的實例改動了 JobGraphStore 等持久化存儲的內容,使得作業管理時狀態不一致。

社區版本 FLINK 的實現並不能保證這一點,它採取的方案是在訪問對應的 ZK 節點時掛一個 ephemeral 類型的子節點,類似於分佈式鎖的功能,只有 ephemeral 節點的 owner 才能讀寫對應的存儲節點。然而,這個鎖節點的創建和修改本身是不受保證的。雖然代碼邏輯中只會在 Master 成爲 Leader 後纔去嘗試創建/修改這個節點,但是 Leader 選舉的狀態是以 ZK 上節點的狀態爲準的,可能在 Leader 丟失 Leadership 後收到通知前,新 Leader 收到通知並開始工作時,集羣中有兩個 Master 均認爲自己是 Leader,從而併發的操作同一份數據導致不一致。

爲了解決這個問題,保證只有 Leader 能夠修改用於高可用服務的持久化數據,我們提出了將檢查 LeaderShip 和執行修改動作原子化執行的方案,即不可能出現 Leader-1 檢查 LeaderShip 爲真,在執行修改前插入了 Leader-2 的修改的情況。具體方案和實現如下。

基於 LeaderStore 的高可用服務

社區對應的 issue 是 FLINK-10333,總的設計文檔在這裏,實現語義與 ZK 社區和 Curator 社區的討論郵件在這裏

總的來說,我們希望把 LeaderShip 的檢查和對數據的修改原子化的執行,保證在確認自己是 Leader 和執行修改之間不會插入其他對數據的修改。對於 ZooKeeper 的具體實現來說,我們可以使用其提供的 multi-op 機制,即在同一個 ZK Transaction 中執行復合動作(下面代碼示例使用 Curator 的流式接口)

client.inTransaction()
      .check().forPath(leaderLatchNode).and()
      .create().withMode(CreateMode.EPHEMERAL).forPath(path, data).and()
      .commit();

由於 Curator 提供的 LeaderLatch recipe 沒有暴露出內部競選邏輯可以用於檢驗 LeaderShip 的 leaderLatchPath,所以我們仿照 LeaderLatch recipe 實現了一個 FLINK scope 的基於 ZooKeeper 的選舉服務,從而可以訪問 leaderLatchPath。具體的算法可以參考 ZooKeeper 文檔的介紹

有了這個基礎的查詢保障之後,我們就有了原子化的檢查 LeaderShip 和修改的原語,我們複用這個原語構造出訪問和修改持久化數據的接口 LeaderStore,從而保證了只有 Leader 才能修改用於高可用服務的持久化數據。有了這個保證以後,社區複雜的 best effort(仍然可能出錯)的檢查 LeaderShip 的邏輯都可以被清理。

上面的實現是 ZooKeeper 特殊的實現,但是對於 etcd 等其他用於分佈式共識的組件,也可以實現類似的保證。如果不能保證執行修改動作的一定是 Leader,那也就破壞了高可用服務的正確性保證,是錯誤的實現。對於非高可用的情況,我們配置一個內存中的 LeaderStore,在 Master 故障後丟失數據,因此流程上是相似的,但是效果上是非高可用的。

後記

在實現了 LeaderStore 之後重寫原先的 JobGraphStore 等作業管理邏輯的過程中,發現了原先 Dispatcher 等組件的生命週期,Leader 任期週期和作業的生命週期管理都需要重新梳理。社區版本的 FLINK 沒有一個清楚的生命週期模型,只是針對具體的 BUG 打補丁式的補漏,我們在實現 LeaderStore 後討論作業狀態的管理詳細地描述了修改數據和不同生命週期事件的先後順序和各種失敗場景下的處理和假定。對於分佈式系統中的共識問題,究竟以哪個狀態爲準,如何維護 happens before 的關係,是值得仔細推敲和落實到具體順序圖的。

另外,使用 Curator 的 LeaderLatch recipe 來做 Leader 選舉的分佈式組件非常多,實際上它們都面臨了一樣的問題,例如 Spark。這個問題同時也記錄在了 Curator 的技術文檔中。Spark 中幾乎不可復現這一問題,是因爲這個問題只在網絡不穩定等情況下出現特定的執行順序纔會發生,此外,Spark 的寫頻率低,Master 在丟掉 Leader 後粗暴的 System.exit 而不是像 FLINK 一樣企圖恢復從而會有其他的後臺進程需要異步地終止,以及 Spark 持久化的是 App Worker 和 Driver,在 Worker/Driver 狀態錯誤的情況下也可以通過其他手段容忍,因此這個問題幾乎沒有造成什麼實際的影響。而在 FLINK 中,由於 Curator 版本和使用方式的原因,ConnectionLoss(ZK 客戶端與服務端 TCP 連接斷開,本來是 ZK 會自動重連的一個可恢復故障)即會導致 Leader 重新選舉,在 ZK 稍微不穩定的情況下就有概率發生問題,並且在 ZK 短時間內抖動頻繁的情況下併發地修改持久化數據,且一次持久化數據修改耗時長,發生問題的概率也就大大上升了。

編輯於 2019-10-31

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章