本文首發(fā)于泊浮目的語雀:https://www.yuque.com/17sing
版本 | 日期 | 備注 |
---|---|---|
1.0 | 2022.2.2 | 文章首發(fā) |
1.0 | 2022.2.14 | 更新3.4部分,增強注釋部分 |
1.2 | 2022.2.27 | 更新3.6部分,刪除部分對于1.14版本不適的描述 |
1.3 | 2022.3.8 | fix typo |
本文基于Flink 1.14代碼進行分析。
0.前言
將Flink應(yīng)用至生產(chǎn)已有一段時間,剛上生產(chǎn)的時候有幸排查過因數(shù)據(jù)傾斜引起的Checkpoint超時問題——當(dāng)時簡單的了解了相關(guān)機制,最近正好在讀Flink源碼,不如趁這個機會搞清楚。
在這里,我們首先要搞清楚兩種Exactly-Once的區(qū)別:
- Exactly Once:在計算引擎內(nèi)部,數(shù)據(jù)不丟失不重復(fù)。本質(zhì)是通過Flink開啟檢查點進行Barrier對齊,即可做到。
- End to End Exactly Once:這意味著從數(shù)據(jù)讀取、引擎處理到寫入外部存儲的整個過程中,數(shù)據(jù)都是不丟失不重復(fù)的。這要求數(shù)據(jù)源可重放,寫入端支持事務(wù)的恢復(fù)和回滾或冪等。
1. 數(shù)據(jù)傾斜為什么會引起Checkpoint超時
做Checkpoint時算子會有一個barrier的對齊機制(為何一定要對齊后面會講到)。以下圖為例講解對齊過程:
當(dāng)兩條邊下發(fā)barrier時,barrier1比barrier2先到達了算子,那么算子會將一條邊輸入的元素緩存起來,直到barrier2到了做Checkpoint以后才會下發(fā)元素。
每個算子對齊barrier后,會進行異步狀態(tài)存儲,然后下發(fā)barrier。每個算子做完Checkpoint時,會通知CheckpointCoordinator
。當(dāng)CheckpointCoordinator
得知所有算子的Checkpoint都做完時,認為本次Checkpoint完成。
而在我們的應(yīng)用程序中,有一個map算子接受了大量數(shù)據(jù),導(dǎo)致barrier一直沒有下發(fā),最終整個Checkpoint超時。
2. Checkpoint的原理
其具體原理可以參考Flink團隊的論文:Lightweight Asynchronous Snapshots for Distributed Dataflow。簡單來說,早期流計算的容錯方案都是周期性做全局狀態(tài)的快照,但這有兩個缺點:
- 阻塞計算——做快照時是同步阻塞的。
- 會將當(dāng)前算子未處理以及正在處理的record一起做進快照,因此快照會變得特別大。
而Flink是基于Chandy-Lamport 算法來擴展的——該算法異步地執(zhí)行快照,同時要求數(shù)據(jù)源可重放,但仍然會存儲上游數(shù)據(jù)。而Flink的方案提出的方案在無環(huán)圖中并不會存儲數(shù)據(jù)。
在Flink中(無環(huán)有向圖),會周期性的插入Barrier這個標(biāo)記,告知下游算子開始做快照。這個算法基于以下前提:
- 網(wǎng)絡(luò)傳輸可靠,可以做到FIFO。這里會對算子進行
blocked
和unblocked
操作,如果一個算子是blocked
,它會把從上游通道接收到的所有數(shù)據(jù)緩存起來,直接收到unblocked
的信號才發(fā)送。 - Task可以對它們的通道進行以下操作:
block
,unblock
,send messages
,broading messages
。 - 對于Source節(jié)點來說,會被抽象成
Nil
輸入通道。
3. Checkpoint的實現(xiàn)
在Flink中,做Checkpoint大致由以下幾步組成:
- 可行性檢查
- JobMaster通知Task觸發(fā)檢查點
- TaskExecutor執(zhí)行檢查點
- JobMaster確認檢查點
接下來,讓我們跟著源碼來看一下里面的具體實現(xiàn)。
3.1 可行性檢查
參考代碼:CheckpointCoordinator#startTriggeringCheckpoint
。
- 確保作業(yè)不是處于關(guān)閉中或未啟動的狀態(tài)(見
CheckpointPlanCalculator#calculateCheckpointPlan
)。 - 生成新的CheckpointingID,并創(chuàng)建一個PendingCheckpoint——當(dāng)所有Task都完成了Checkpoint,則會轉(zhuǎn)換成一個CompletedCheckpoint。同時也會注冊一個線程去關(guān)注是否有超時的情況,如果超時則會Abort當(dāng)前的Checkpoint(見
CheckpointPlanCalculator#createPendingCheckpoint
)。 - 觸發(fā)MasterHook。部分外部系統(tǒng)在觸發(fā)檢查點之前,需要做一些擴展邏輯,通過該實現(xiàn)MasterHook可以實現(xiàn)通知機制(見
CheckpointPlanCalculator#snapshotMasterState
)。 - 重復(fù)步驟1,沒問題的話通知SourceStreamTask開始觸發(fā)檢查點(見
CheckpointPlanCalculator#triggerCheckpointRequest
)。
3.2 JobMaster通知Task觸發(fā)檢查點
在CheckpointPlanCalculator#triggerCheckpointRequest
中,會通過triggerTasks
方法調(diào)用到Execution#triggerCheckpoint
方法。Execution對應(yīng)了一個Task實例,因此JobMaster可以通過里面的Slot引用找到其TaskManagerGateway
,發(fā)送遠程請求觸發(fā)Checkpoint。
3.3 TaskManager執(zhí)行檢查點
TaskManager在代碼中的體現(xiàn)為TaskExecutor
。當(dāng)JobMaster觸發(fā)遠程請求至TaskExecutor時,handle的方法為TaskExecutor#triggerCheckpoint
,之后便會調(diào)用Task#triggerCheckpointBarrier
來做:
- 做一些檢查,比如Task是否是Running狀態(tài)
- 觸發(fā)Checkpoint:調(diào)用
CheckpointableTask#triggerCheckpointAsync
- 執(zhí)行檢查點:
CheckpointableTask#triggerCheckpointAsync
。以StreamTask
實現(xiàn)為例,這里會考慮上游已經(jīng)Finish時如何觸發(fā)下游Checkpoint的情況——通過塞入CheckpointBarrier
來觸發(fā);如果任務(wù)沒有結(jié)束,則調(diào)用StreamTask#triggerCheckpointAsyncInMailbox
。最終都會走入SubtaskCheckpointCoordinator#checkpointState
來觸發(fā)Checkpoint。 - 算子保存快照:調(diào)用
OperatorChain#broadcastEvent
:保存OperatorState與KeyedState。 - 調(diào)用
SubtaskCheckpointCoordinatorImpl#finishAndReportAsync
,:異步的匯報當(dāng)前快照已完成。
3.4 JobMaster確認檢查點
|-- RpcCheckpointResponder
\-- acknowledgeCheckpoint
|-- JobMaster
\-- acknowledgeCheckpoint
|-- SchedulerBase
\-- acknowledgeCheckpoint
|-- ExecutionGraphHandler
\-- acknowledgeCheckpoint
|-- CheckpointCoordinator
\-- receiveAcknowledgeMessage
在3.1中,我們提到過PendingCheckpoint。這里面維護了一些狀來確保Task全部Ack、Master全部Ack。當(dāng)確認完成后, CheckpointCoordinator
將會通知所有的Checkpoint已經(jīng)完成。
|-- CheckpointCoordinator
\-- receiveAcknowledgeMessage
\-- sendAcknowledgeMessages //通知下游Checkpoint已經(jīng)完成。如果Sink實現(xiàn)了TwoPhaseCommitSinkFunction,將會Commit;如果因為一些原因?qū)е翪ommit沒有成功,則會拋出一個FlinkRuntimeException,而pendingCommitTransactions中的將會繼續(xù)保存失敗的CheckpointId,當(dāng)檢查點恢復(fù)時將會重新執(zhí)行。
3.5 檢查點恢復(fù)
該部分代碼較為簡單,有興趣的同學(xué)可以根據(jù)相關(guān)調(diào)用棧自行閱讀代碼。
|-- Task
\-- run
\-- doRun
|-- StreamTask
\-- invoke
\-- restoreInternal
\-- restoreGates
|-- OperatorChain
\-- initializeStateAndOpenOperators
|-- StreamOperator
\-- initializeState
|-- StreamOperatorStateHandler
\-- initializeOperatorState
|-- AbstractStreamOperator
\-- initializeState
|-- StreamOperatorStateHandler
\-- initializeOperatorState
|-- CheckpointedStreamOperator
\-- initializeState #調(diào)用用戶代碼
3.6 End to End Exactly Once
端到端的精準(zhǔn)一次實現(xiàn)其實是比較困難的——考慮一個Source對N個Sink的場景。故此Flink設(shè)計了相應(yīng)的接口來保障端到端的精準(zhǔn)一次,分別是:
- TwoPhaseCommitSinkFunction:想做精準(zhǔn)一次的Sink必須實現(xiàn)此接口。
- CheckpointedFunction:Checkpoint被調(diào)用時的鉤子。
- CheckpointListener:顧名思義,當(dāng)Checkpoint完成或失敗時會通知此接口的實現(xiàn)者。
目前Source和Sink全部ExactlyOnce實現(xiàn)的只有Kafka——其上游支持?jǐn)帱c讀取,下游支持回滾or冪等。有興趣的同學(xué)可以閱讀該接口的相關(guān)實現(xiàn)。
4. 小結(jié)
本文以問題視角切入Checkpoint的原理與實現(xiàn),并對相關(guān)源碼做了簡單的跟蹤。其實代碼的線路是比較清晰的,但涉及大量的類——有心的同學(xué)可能已經(jīng)發(fā)現(xiàn),這是單一職責(zé)原則的體現(xiàn)。TwoPhaseCommitSinkFunction
中的實現(xiàn)也是典型的模版方法設(shè)計模式。