讀Flink原始碼談設計:Exactly Once

泊浮目發表於2022-02-02
本文首發於泊浮目的語雀:https://www.yuque.com/17sing
版本日期備註
1.02022.2.2文章首發

0.前言

將Flink應用至生產已有一段時間,剛上生產的時候有幸排查過因資料傾斜引起的Checkpoint超時問題——當時簡單的瞭解了相關機制,最近正好在讀Flink原始碼,不如趁這個機會搞清楚。

在這裡,我們首先要搞清楚兩種Exactly-Once的區別:

  • Exactly Once:在計算引擎內部,資料不丟失不重複。本質是通過Flink開啟檢查點進行Barrier對齊,即可做到。
  • End to End Exactly Once:這意味著從資料讀取、引擎處理到寫入外部儲存的整個過程中,資料都是不丟失不重複的。這要求資料來源可重放,寫入端支援事務的恢復和回滾或冪等。

1. 資料傾斜為什麼會引起Checkpoint超時

做Checkpoint時運算元會有一個barrier的對齊機制(為何一定要對齊後面會講到)。以下圖為例講解對齊過程:

當兩條邊下發barrier時,barrier1比barrier2先到達了運算元,那麼運算元會將一條邊輸入的元素快取起來,直到barrier2到了做Checkpoint以後才會下發元素。

每個運算元對齊barrier後,會進行非同步狀態儲存,然後下發barrier。每個運算元做完Checkpoint時,會通知CheckpointCoordinator。當CheckpointCoordinator得知所有運算元的Checkpoint都做完時,認為本次Checkpoint完成。

而在我們的應用程式中,有一個map運算元接受了大量資料,導致barrier一直沒有下發,最終整個Checkpoint超時。

2. Checkpoint的原理

其具體原理可以參考Flink團隊的論文:Lightweight Asynchronous Snapshots for Distributed Dataflow。簡單來說,早期流計算的容錯方案都是週期性做全域性狀態的快照,但這有兩個缺點:

  • 阻塞計算——做快照時是同步阻塞的。
  • 會將當前運算元未處理以及正在處理的record一起做進快照,因此快照會變得特別大。

而Flink是基於Chandy-Lamport 演算法來擴充套件的——該演算法非同步地執行快照,同時要求資料來源可重放,但仍然會儲存上游資料。而Flink的方案提出的方案在無環圖中並不會儲存資料。

在Flink中(無環有向圖),會週期性的插入Barrier這個標記,告知下游運算元開始做快照。這個演算法基於以下前提:

  • 網路傳輸可靠,可以做到FIFO。這裡會對運算元進行blockedunblocked操作,如果一個運算元是blocked,它會把從上游通道接收到的所有資料快取起來,直接收到unblocked的訊號才傳送。
  • Task可以對它們的通道進行以下操作:block, unblock, send messages, broading messages
  • 對於Source節點來說,會被抽象成Nil輸入通道。

3. Checkpoint的實現

在Flink中,做Checkpoint大致由以下幾步組成:

  1. 可行性檢查
  2. JobMaster通知Task觸發檢查點
  3. TaskExecutor執行檢查點
  4. JobMaster確認檢查點

接下來,讓我們跟著原始碼來看一下里面的具體實現。

3.1 可行性檢查

參考程式碼:CheckpointingCoordinator#startTriggeringCheckpoint

  1. 確保作業不是處於關閉中或未啟動的狀態(見CheckpointPlanCalculator#calculateCheckpointPlan)。
  2. 生成新的CheckpointingID,並建立一個PendingCheckpoint——當所有Task都完成了Checkpoint,則會轉換成一個CompletedCheckpoint。同時也會註冊一個執行緒去關注是否有超時的情況,如果超時則會Abort當前的Checkpoint(見CheckpointPlanCalculator#createPendingCheckpoint)。
  3. 觸發MasterHook。部分外部系統在觸發檢查點之前,需要做一些擴充套件邏輯,通過該實現MasterHook可以實現通知機制(見CheckpointPlanCalculator#snapshotMasterState)。
  4. 重複步驟1,沒問題的話通知SourceStreamTask開始觸發檢查點(見CheckpointPlanCalculator#triggerCheckpointRequest)。

3.2 JobMaster通知Task觸發檢查點

CheckpointPlanCalculator#triggerCheckpointRequest中,會通過triggerTasks方法呼叫到Execution#triggerCheckpoint方法。Execution對應了一個Task例項,因此JobMaster可以通過裡面的Slot引用找到其TaskManagerGateway,傳送遠端請求觸發Checkpoint。

3.3 TaskManager執行檢查點

TaskManager在程式碼中的體現為TaskExecutor。當JobMaster觸發遠端請求至TaskExecutor時,handle的方法為TaskExecutor#triggerCheckpoint,之後便會呼叫Task#triggerCheckpointBarrier來做:

  1. 做一些檢查,比如Task是否是Running狀態
  2. 觸發Checkpoint:呼叫CheckpointableTask#triggerCheckpointAsync
  3. 執行檢查點:CheckpointableTask#triggerCheckpointAsync。以StreamTask實現為例,這裡會考慮上游已經Finish時如何觸發下游Checkpoint的情況——通過塞入CheckpointBarrier來觸發;如果任務沒有結束,則呼叫StreamTask#triggerCheckpointAsyncInMailbox。最終都會走入SubtaskCheckpointCoordinator#checkpointState來觸發Checkpoint。
  4. 運算元儲存快照:呼叫OperatorChain#broadcastEvent:儲存OperatorState與KeyedState。
  5. 呼叫SubtaskCheckpointCoordinatorImpl#finishAndReportAsync,:非同步的彙報當前快照已完成。

3.4 JobMaster確認檢查點

|-- RpcCheckpointResponder
  \-- acknowledgeCheckpoint
|-- JobMaster
  \-- acknowledgeCheckpoint
|-- SchedulerBase
  \-- acknowledgeCheckpoint
|-- ExecutionGraphHandler
  \-- acknowledgeCheckpoint
|-- CheckpointCoordinator
  \-- receiveAcknowledgeMessage

在3.1中,我們提到過PendingCheckpoint。這裡面維護了一些狀來確保Task全部Ack、Master全部Ack。當確認完成後, CheckpointCoordinator將會通知所有的Checkpoint已經完成。

|-- CheckpointCoordinator
  \-- receiveAcknowledgeMessage
  \-- sendAcknowledgeMessages

3.5 檢查點恢復

該部分程式碼較為簡單,有興趣的同學可以根據相關呼叫棧自行閱讀程式碼。

|-- Task
  \-- run
  \-- doRun
|-- StreamTask
  \-- invoke
  \-- restoreInternal
  \-- restoreGates
|-- OperatorChain
  \-- initializeStateAndOpenOperators
|-- StreamOperator
  \-- initializeState
|-- StreamOperatorStateHandler
  \-- initializeOperatorState
|-- AbstractStreamOperator
  \-- initializeState
|-- StreamOperatorStateHandler
  \-- initializeOperatorState
|-- CheckpointedStreamOperator
  \-- initializeState #呼叫使用者程式碼

3.6 End to End Exactly Once

端到端的精準一次實現其實是比較困難的——考慮一個Source對N個Sink的場景。故此Flink設計了相應的介面來保障端到端的精準一次,分別是:

  • TwoPhaseCommitSinkFunction:想做精準一次的Sink必須實現此介面。
  • CheckpointedFunction:Checkpoint被呼叫時的鉤子。
  • CheckpointListener:顧名思義,當Checkpoint完成或失敗時會通知此介面的實現者。

目前Source和Sink全部ExactlyOnce實現的只有Kafka——其上游支援斷點讀取,下游支援回滾or冪等。有興趣的同學可以閱讀該介面的相關實現。

可能有同學會好奇為什麼JDBC Sink沒有實現ExactlyOnce。本質和這個介面的執行方式無法相容JDBC的事務使用方式——當一個運算元意味退出時,是無法再對之前的事務進行操作的。因此TwoPhaseCommitSinkFunction中的retryCommit以及retryRollback是無法進行的——見https://github.com/apache/fli...。JDBC的Sink是基於XA實現的,儘可能保證一致性。這裡可能又有同學會問了為什麼不用Upset類的語句,因為這個方式並不通用——對於Upset需要一個唯一鍵,不然效能極差。

4. 小結

本文以問題視角切入Checkpoint的原理與實現,並對相關原始碼做了簡單的跟蹤。其實程式碼的線路是比較清晰的,但涉及大量的類——有心的同學可能已經發現,這是單一職責原則的體現。TwoPhaseCommitSinkFunction中的實現也是典型的模版方法設計模式。

相關文章