本文首發於泊浮目的語雀:https://www.yuque.com/17sing
版本 | 日期 | 備註 |
---|---|---|
1.0 | 2022.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。這裡會對運算元進行
blocked
和unblocked
操作,如果一個運算元是blocked
,它會把從上游通道接收到的所有資料快取起來,直接收到unblocked
的訊號才傳送。 - Task可以對它們的通道進行以下操作:
block
,unblock
,send messages
,broading messages
。 - 對於Source節點來說,會被抽象成
Nil
輸入通道。
3. Checkpoint的實現
在Flink中,做Checkpoint大致由以下幾步組成:
- 可行性檢查
- JobMaster通知Task觸發檢查點
- TaskExecutor執行檢查點
- JobMaster確認檢查點
接下來,讓我們跟著原始碼來看一下里面的具體實現。
3.1 可行性檢查
參考程式碼:CheckpointingCoordinator#startTriggeringCheckpoint
。
- 確保作業不是處於關閉中或未啟動的狀態(見
CheckpointPlanCalculator#calculateCheckpointPlan
)。 - 生成新的CheckpointingID,並建立一個PendingCheckpoint——當所有Task都完成了Checkpoint,則會轉換成一個CompletedCheckpoint。同時也會註冊一個執行緒去關注是否有超時的情況,如果超時則會Abort當前的Checkpoint(見
CheckpointPlanCalculator#createPendingCheckpoint
)。 - 觸發MasterHook。部分外部系統在觸發檢查點之前,需要做一些擴充套件邏輯,通過該實現MasterHook可以實現通知機制(見
CheckpointPlanCalculator#snapshotMasterState
)。 - 重複步驟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
來做:
- 做一些檢查,比如Task是否是Running狀態
- 觸發Checkpoint:呼叫
CheckpointableTask#triggerCheckpointAsync
- 執行檢查點:
CheckpointableTask#triggerCheckpointAsync
。以StreamTask
實現為例,這裡會考慮上游已經Finish時如何觸發下游Checkpoint的情況——通過塞入CheckpointBarrier
來觸發;如果任務沒有結束,則呼叫StreamTask#triggerCheckpointAsyncInMailbox
。最終都會走入SubtaskCheckpointCoordinator#checkpointState
來觸發Checkpoint。 - 運算元儲存快照:呼叫
OperatorChain#broadcastEvent
:儲存OperatorState與KeyedState。 - 呼叫
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
中的實現也是典型的模版方法設計模式。