讀Flink源碼談設(shè)計:Exactly Once

本文首發(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。這里會對算子進行blockedunblocked操作,如果一個算子是blocked,它會把從上游通道接收到的所有數(shù)據(jù)緩存起來,直接收到unblocked的信號才發(fā)送。
  • Task可以對它們的通道進行以下操作:block, unblock, send messages, broading messages。
  • 對于Source節(jié)點來說,會被抽象成Nil輸入通道。

3. Checkpoint的實現(xiàn)

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

  1. 可行性檢查
  2. JobMaster通知Task觸發(fā)檢查點
  3. TaskExecutor執(zhí)行檢查點
  4. JobMaster確認檢查點

接下來,讓我們跟著源碼來看一下里面的具體實現(xiàn)。

3.1 可行性檢查

參考代碼:CheckpointCoordinator#startTriggeringCheckpoint。

  1. 確保作業(yè)不是處于關(guān)閉中或未啟動的狀態(tài)(見CheckpointPlanCalculator#calculateCheckpointPlan)。
  2. 生成新的CheckpointingID,并創(chuàng)建一個PendingCheckpoint——當(dāng)所有Task都完成了Checkpoint,則會轉(zhuǎn)換成一個CompletedCheckpoint。同時也會注冊一個線程去關(guān)注是否有超時的情況,如果超時則會Abort當(dāng)前的Checkpoint(見CheckpointPlanCalculator#createPendingCheckpoint)。
  3. 觸發(fā)MasterHook。部分外部系統(tǒng)在觸發(fā)檢查點之前,需要做一些擴展邏輯,通過該實現(xiàn)MasterHook可以實現(xiàn)通知機制(見CheckpointPlanCalculator#snapshotMasterState)。
  4. 重復(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來做:

  1. 做一些檢查,比如Task是否是Running狀態(tài)
  2. 觸發(fā)Checkpoint:調(diào)用CheckpointableTask#triggerCheckpointAsync
  3. 執(zhí)行檢查點:CheckpointableTask#triggerCheckpointAsync。以StreamTask實現(xiàn)為例,這里會考慮上游已經(jīng)Finish時如何觸發(fā)下游Checkpoint的情況——通過塞入CheckpointBarrier來觸發(fā);如果任務(wù)沒有結(jié)束,則調(diào)用StreamTask#triggerCheckpointAsyncInMailbox。最終都會走入SubtaskCheckpointCoordinator#checkpointState來觸發(fā)Checkpoint。
  4. 算子保存快照:調(diào)用OperatorChain#broadcastEvent:保存OperatorState與KeyedState。
  5. 調(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è)計模式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容