一文搞懂Flink內部的Exactly Once和At Least Once

  • 看完本文,你能get到以下知識
    • 介紹CheckPoint如何保障Flink任務的高可用
    • CheckPoint中的狀態簡介
    • 如何實現全域一致的分佈式快照?
    • 什麼是barrier?什麼是barrier對齊?
    • 證明了:爲什麼barrier對齊就是Exactly Once?爲什麼barrier不對齊就是 At Least Once?

Flink簡介

  • Apache Flink® - Stateful Computations over Data Streams

Apache Flink® - 數據流上的有狀態計算

Flink 1.8 Document

State & Fault Tolerance

  • 有狀態函數和運算符在各個元素/事件的處理中存儲數據(狀態數據可以修改和查詢,可以自己維護,根據自己的業務場景,保存歷史數據或者中間結果到狀態中)

  • 例如:

    • 當應用程序搜索某些事件模式時,狀態將存儲到目前爲止遇到的事件序列。
    • 在每分鐘/小時/天聚合事件時,狀態保存待處理的聚合。
    • 當在數據點流上訓練機器學習模型時,狀態保持模型參數的當前版本。
    • 當需要管理歷史數據時,狀態允許有效訪問過去發生的事件。
  • 什麼是狀態?

    • 無狀態計算的例子
      • 比如:我們只是進行一個字符串拼接,輸入 a,輸出 a_666,輸入b,輸出 b_666
      • 輸出的結果跟之前的狀態沒關係,符合冪等性。
        • 冪等性:就是用戶對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因爲多次點擊而產生了副作用
    • 有狀態計算的例子
      • 計算pv、uv
      • 輸出的結果跟之前的狀態有關係,不符合冪等性,訪問多次,pv會增加

Flink的CheckPoint功能簡介

  • Flink CheckPoint 的存在就是爲了解決flink任務failover掉之後,能夠正常恢復任務。那CheckPoint具體做了哪些功能,爲什麼任務掛掉之後,通過CheckPoint能使得任務恢復呢?

  • CheckPoint是通過給程序快照的方式使得將歷史某些時刻的狀態保存下來,當任務掛掉之後,默認從最近一次保存的完整快照處進行恢復任務。問題來了,快照是什麼鬼?能喫嗎?

  • SnapShot翻譯爲快照,指將程序中某些信息存一份,後期可以用來恢復。對於一個Flink任務來講,快照裏面到底保存着什麼信息呢?

  • 晦澀難懂的概念怎麼辦?當然用案例來代替咯,用案例讓大家理解快照裏面到底存什麼信息。選一個大家都比較清楚的指標,app的pv,flink該怎麼統計呢?

    • 我們從Kafka讀取到一條條的日誌,從日誌中解析出app_id,然後將統計的結果放到內存中一個Map集合,app_id做爲key,對應的pv做爲value,每次只需要將相應app_id 的pv值+1後put到Map中即可

flink任務task圖.png

  • flink的Source task記錄了當前消費到kafka test topic的所有partition的offset,爲了方便理解CheckPoint的作用,這裏先用一個partition進行講解,假設名爲 “test”的 topic只有一個partition0

    • 例:(0,1000)
      • 表示0號partition目前消費到offset爲1000的數據
  • flink的pv task記錄了當前計算的各app的pv值,爲了方便講解,我這裏有兩個app:app1、app2

    • 例:(app1,50000)(app2,10000)
      • 表示app1當前pv值爲50000
      • 表示app2當前pv值爲10000
    • 每來一條數據,只需要確定相應app_id,將相應的value值+1後put到map中即可
  • 該案例中,CheckPoint到底記錄了什麼信息呢?

    • 記錄的其實就是第n次CheckPoint消費的offset信息和各app的pv值信息,記錄一下發生CheckPoint當前的狀態信息,並將該狀態信息保存到相應的狀態後端。(注:狀態後端是保存狀態的地方,決定狀態如何保存,如何保障狀態高可用,我們只需要知道,我們能從狀態後端拿到offset信息和pv信息即可。狀態後端必須是高可用的,否則我們的狀態後端經常出現故障,會導致無法通過checkpoint來恢復我們的應用程序)
      • chk-100
        • offset:(0,1000)
        • pv:(app1,50000)(app2,10000)
    • 該狀態信息表示第100次CheckPoint的時候, partition 0 offset消費到了1000,pv統計結果爲(app1,50000)(app2,10000)
  • 任務掛了,如何恢復?

    • 假如我們設置了三分鐘進行一次CheckPoint,保存了上述所說的 chk-100 的CheckPoint狀態後,過了十秒鐘,offset已經消費到 (0,1100),pv統計結果變成了(app1,50080)(app2,10020),但是突然任務掛了,怎麼辦?
    • 莫慌,其實很簡單,flink只需要從最近一次成功的CheckPoint保存的offset(0,1000)處接着消費即可,當然pv值也要按照狀態裏的pv值(app1,50000)(app2,10000)進行累加,不能從(app1,50080)(app2,10020)處進行累加,因爲 partition 0 offset消費到 1000時,pv統計結果爲(app1,50000)(app2,10000)
      • 當然如果你想從offset (0,1100)pv(app1,50080)(app2,10020)這個狀態恢復,也是做不到的,因爲那個時刻程序突然掛了,這個狀態根本沒有保存下來。我們能做的最高效方式就是從最近一次成功的CheckPoint處恢復,也就是我一直所說的chk-100
    • 以上講解,基本就是CheckPoint承擔的工作,描述的場景比較簡單
  • 疑問,計算pv的task在一直運行,它怎麼知道什麼時候去做這個快照?或者說計算pv的task怎麼保障它自己計算的pv值(app1,50000)(app2,10000)就是offset(0,1000)那一刻的統計結果呢?

    • flink是在數據中加了一個叫做barrier的東西(barrier中文翻譯:柵欄),下圖中紅圈處就是兩個barrier

    barrier.png

    • barrier從Source Task處生成,一直流到Sink Task,期間所有的Task只要碰到barrier,就會觸發自身進行快照

      • CheckPoint barrier n-1處做的快照就是指Job從開始處理到 barrier n-1所有的狀態數據
      • barrier n 處做的快照就是指從Job開始到處理到 barrier n所有的狀態數據
    • 對應到pv案例中就是,Source Task接收到JobManager的編號爲chk-100的CheckPoint觸發請求後,發現自己恰好接收到kafka offset(0,1000)處的數據,所以會往offset(0,1000)數據之後offset(0,1001)數據之前安插一個barrier,然後自己開始做快照,也就是將offset(0,1000)保存到狀態後端chk-100中。然後barrier接着往下游發送,當統計pv的task接收到barrier後,也會暫停處理數據,將自己內存中保存的pv信息(app1,50000)(app2,10000)保存到狀態後端chk-100中。OK,flink大概就是通過這個原理來保存快照的

      • 統計pv的task接收到barrier,就意味着barrier之前的數據都處理了,所以說,不會出現丟數據的情況
    • barrier的作用就是爲了把數據區分開,CheckPoint過程中有一個同步做快照的環節不能處理barrier之後的數據,爲什麼呢?

      • 如果做快照的同時,也在處理數據,那麼處理的數據可能會修改快照內容,所以先暫停處理數據,把內存中快照保存好後,再處理數據
      • 結合案例來講就是,統計pv的task想對(app1,50000)(app2,10000)做快照,但是如果數據還在處理,可能快照還沒保存下來,狀態已經變成了(app1,50001)(app2,10001),快照就不準確了,就不能保障Exactly Once了
  • 總結

    • 流式計算中狀態交互

流式計算中狀態交互.png

  • 簡易場景精確一次的容錯方法

    • 週期性地對消費offset和統計的狀態信息或統計結果進行快照

    checkpoint簡介圖1.png

    • 消費到X位置的時候,將X對應的狀態保存下來

    checkpoint簡介圖2.png

    • 消費到Y位置的時候,將Y對應的狀態保存下來

    checkpoint簡介圖3.png

多並行度、多Operator情況下,CheckPoint過程

  • 分佈式狀態容錯面臨的問題與挑戰

    • 如何確保狀態擁有精確一次的容錯保證?
    • 如何在分佈式場景下替多個擁有本地狀態的算子產生一個全域一致的快照
    • 如何在不中斷運算的前提下產生快照?
  • 多並行度、多Operator實例的情況下,如何做全域一致的快照

    • 所有的Operator運行過程中遇到barrier後,都對自身的狀態進行一次快照,保存到相應狀態後端

      • 對應到pv案例:有的Operator計算的app1的pv,有的Operator計算的app2的pv,當他們碰到barrier時,都需要將目前統計的pv信息快照到狀態後端

      多並行度CheckPoint快照簡圖.png

  • 多Operator狀態恢復

    多並行度CheckPoint恢復簡圖.png

  • 具體怎麼做這個快照呢?
  • 利用之前所有的barrier策略

    barrier.png

  • JobManager向Source Task發送CheckPointTrigger,Source Task會在數據流中安插CheckPoint barrier

多並行度快照詳圖0.png

  • Source Task自身做快照,並保存到狀態後端

多並行度快照詳圖1.png

  • Source Task將barrier跟數據流一塊往下游發送

多並行度快照詳圖2.png

  • 當下遊的Operator實例接收到CheckPoint barrier後,對自身做快照

多並行度快照詳圖3.png

多並行度快照詳圖4.png

  • 上述圖中,有4個帶狀態的Operator實例,相應的狀態後端就可以想象成填4個格子。整個CheckPoint 的過程可以當做Operator實例填自己格子的過程,Operator實例將自身的狀態寫到狀態後端中相應的格子,當所有的格子填滿可以簡單的認爲一次完整的CheckPoint做完了
  • 上面只是快照的過程,整個CheckPoint執行過程如下

    • 1、JobManager端的 CheckPointCoordinator向 所有SourceTask發送CheckPointTrigger,Source Task會在數據流中安插CheckPoint barrier

    • 2、當task收到所有的barrier後,向自己的下游繼續傳遞barrier,然後自身執行快照,並將自己的狀態異步寫入到持久化存儲

      • 增量CheckPoint只是把最新的一部分更新寫入到 外部存儲
      • 爲了下游儘快做CheckPoint,所以會先發送barrier到下游,自身再同步進行快照
    • 3、當task完成備份後,會將備份數據的地址(state handle)通知給JobManager的CheckPointCoordinator

      • 如果CheckPoint的持續時長超過 了CheckPoint設定的超時時間,CheckPointCoordinator 還沒有收集完所有的 State Handle,CheckPointCoordinator就會認爲本次CheckPoint失敗,會把這次CheckPoint產生的所有 狀態數據全部刪除
    • 4、 最後 CheckPoint Coordinator 會把整個 StateHandle 封裝成 completed CheckPoint Meta,寫入到hdfs

  • barrier對齊

    • 什麼是barrier對齊?

      stream_aligning.png

      • 一旦Operator從輸入流接收到CheckPoint barrier n,它就不能處理來自該流的任何數據記錄,直到它從其他所有輸入接收到barrier n爲止。否則,它會混合屬於快照n的記錄和屬於快照n + 1的記錄
      • 接收到barrier n的流暫時被擱置。從這些流接收的記錄不會被處理,而是放入輸入緩衝區。
        • 上圖中第2個圖,雖然數字流對應的barrier已經到達了,但是barrier之後的1、2、3這些數據只能放到buffer中,等待字母流的barrier到達
      • 一旦最後所有輸入流都接收到barrier n,Operator就會把緩衝區中pending 的輸出數據發出去,然後把CheckPoint barrier n接着往下游發送
        • 這裏還會對自身進行快照
      • 之後,Operator將繼續處理來自所有輸入流的記錄,在處理來自流的記錄之前先處理來自輸入緩衝區的記錄
  • 什麼是barrier不對齊?
    • 上述圖2中,當還有其他輸入流的barrier還沒有到達時,會把已到達的barrier之後的數據1、2、3擱置在緩衝區,等待其他流的barrier到達後才能處理
    • barrier不對齊就是指當還有其他流的barrier還沒到達時,爲了不影響性能,也不用理會,直接處理barrier之後的數據。等到所有流的barrier的都到達後,就可以對該Operator做CheckPoint了
  • 爲什麼要進行barrier對齊?不對齊到底行不行?

    • 答:Exactly Once時必須barrier對齊,如果barrier不對齊就變成了At Least Once
      • 後面的部分主要證明這句話
    • CheckPoint的目的就是爲了保存快照,如果不對齊,那麼在chk-100快照之前,已經處理了一些chk-100 對應的offset之後的數據,當程序從chk-100恢復任務時,chk-100對應的offset之後的數據還會被處理一次,所以就出現了重複消費。如果聽不懂沒關係,後面有案例讓您懂
  • 結合pv案例來看,之前的案例爲了簡單,描述的kafka的topic只有1個partition,這裏爲了講述barrier對齊,所以topic有2個partittion

    flink消費kafka,計算pv詳圖.png

    • 結合業務,先介紹一下上述所有算子在業務中的功能
      • Source的kafka的Consumer,從kakfa中讀取數據到flink應用中
      • TaskA中的map將讀取到的一條kafka日誌轉換爲我們需要統計的app_id
      • keyBy 按照app_id進行keyBy,相同的app_id 會分到下游TaskB的同一個實例中
      • TaskB的map在狀態中查出該app_id 對應的pv值,然後+1,存儲到狀態中
      • 利用Sink將統計的pv值寫入到外部存儲介質中
    • 我們從kafka的兩個partition消費數據,TaskA和TaskB都有兩個並行度,所以總共flink有4個Operator實例,這裏我們稱之爲 TaskA0、TaskA1、TaskB0、TaskB1
    • 假設已經成功做了99次CheckPoint,這裏詳細解釋第100次CheckPoint過程
      • JobManager內部有個定時調度,假如現在10點00分00秒到了第100次CheckPoint的時間了,JobManager的CheckPointCoordinator進程會向所有的Source Task發送CheckPointTrigger,也就是向TaskA0、TaskA1發送CheckPointTrigger
      • TaskA0、TaskA1接收到CheckPointTrigger,會往數據流中安插barrier,將barrier發送到下游,在自己的狀態中記錄barrier安插的offset位置,然後自身做快照,將offset信息保存到狀態後端
        • 這裏假如TaskA0消費的partition0的offset爲10000,TaskA1消費的partition1的offset爲10005。那麼狀態中會保存 (0,10000)(1,10005),表示0號partition消費到了offset爲10000的位置,1號partition消費到了offset爲10005的位置
      • 然後TaskA的map和keyBy算子中並沒有狀態,所以不需要進行快照
      • 接着數據和barrier都向下游TaskB發送,相同的app_id 會發送到相同的TaskB實例上,這裏假設有兩個app:app0和app1,經過keyBy後,假設app0分到了TaskB0上,app1分到了TaskB1上。基於上面描述,TaskA0和TaskA1中的所有app0的數據都發送到TaskB0上,所有app1的數據都發送到TaskB1上
      • 現在我們假設TaskB0做CheckPoint的時候barrier對齊了,TaskB1做CheckPoint的時候barrier不對齊,當然不能這麼配置,我就是舉這麼個例子,帶大家分析一下barrier對不對齊到底對統計結果有什麼影響?
      • 上面說了chk-100的這次CheckPoint,offset位置爲(0,10000)(1,10005),TaskB0使用barrier對齊,也就是說TaskB0不會處理barrier之後的數據,所以TaskB0在chk-100快照的時候,狀態後端保存的app0的pv數據是從程序開始啓動到kafka offset位置爲(0,10000)(1,10005)的所有數據計算出來的pv值,一條不多(沒處理barrier之後,所以不會重複),一條不少(barrier之前的所有數據都處理了,所以不會丟失),假如保存的狀態信息爲(app0,8000)表示消費到(0,10000)(1,10005)offset的時候,app0的pv值爲8000
      • TaskB1使用的barrier不對齊,假如TaskA0由於服務器的CPU或者網絡等其他波動,導致TaskA0處理數據較慢,而TaskA1很穩定,所以處理數據比較快。導致的結果就是TaskB1先接收到了TaskA1的barrier,由於配置的barrier不對齊,所以TaskB1會接着處理TaskA1 barrier之後的數據,過了2秒後,TaskB1接收到了TaskA0的barrier,於是對狀態中存儲的app1的pv值開始做CheckPoint 快照,保存的狀態信息爲(app1,12050),但是我們知道這個(app1,12050)實際上多處理了2秒TaskA1發來的barrier之後的數據,也就是kafka topic對應的partition1 offset 10005之後的數據,app1真實的pv數據肯定要小於這個12050,partition1的offset保存的offset雖然是10005,但是我們實際上可能已經處理到了offset 10200的數據,假設就是處理到了10200
        • 雖然狀態保存的pv值偏高了,但是不能說明重複處理,因爲我的TaskA1並沒有再次去消費partition1的offset 10005~10200的數據,所以相當於也沒有重複消費,只是展示的結果更實時了
      • 分析到這裏,我們先梳理一下我們的狀態保存了什麼:
        • chk-100
          • offset:(0,10000)(1,10005)
          • pv:(app0,8000) (app1,12050)
      • 接着程序在繼續運行,過了10秒,由於某個服務器掛了,導致我們的四個Operator實例有一個Operator掛了,所以Flink會從最近一次的狀態恢復,也就是我們剛剛詳細講的chk-100處恢復,那具體是怎麼恢復的呢?
        • Flink 同樣會起四個Operator實例,我還稱他們是 TaskA0、TaskA1、TaskB0、TaskB1。四個Operator會從狀態後端讀取保存的狀態信息。
        • 從offset:(0,10000)(1,10005) 開始消費,並且基於 pv:(app0,8000) (app1,12050)值進行累加統計
        • 然後你就應該會發現這個app1的pv值12050實際上已經包含了partition1的offset 10005~10200的數據,所以partition1從offset 10005恢復任務時,partition1的offset 10005~10200的數據被消費了兩次
          • TaskB1設置的barrier不對齊,所以CheckPoint chk-100對應的狀態中多消費了barrier之後的一些數據(TaskA1發送),重啓後是從chk-100保存的offset恢復,這就是所說的At Least Once
        • 由於上面說TaskB0設置的barrier對齊,所以app0不會出現重複消費,因爲app0沒有消費offset:(0,10000)(1,10005) 之後的數據,也就是所謂的Exactly Once
  • 看到這裏你應該已經知道了哪種情況會出現重複消費了,也應該要掌握爲什麼barrier對齊就是Exactly Once,爲什麼barrier不對齊就是 At Least Once

  • 分析了這麼多,這裏我再補充一個問題,到底什麼時候會出現barrier對齊?

    • 首先設置了Flink的CheckPoint語義是:Exactly Once
    • Operator實例必須有多個輸入流纔會出現barrier對齊
      • 對齊,漢語詞彙,釋義爲使兩個以上事物配合或接觸得整齊。由漢語解釋可得對齊肯定需要兩個以上事物,所以,必須有多個流才叫對齊。barrier對齊其實也就是上游多個流配合使得數據對齊的過程
      • 言外之意:如果Operator實例只有一個輸入流,就根本不存在barrier對齊,自己跟自己默認永遠都是對齊的
  • 博客發出去後,感謝上海姜同學提問的幾個問題,最後跟姜同學語音了2個多小時,交流了很多Flink相關技術,最後提煉了以下三個問題,當然討論的很多FLink的其他技術並沒有放到該博客中
  1. 第一種場景計算PV,kafka只有一個partition,精確一次,至少一次就沒有區別?

    答:如果只有一個partition,對應flink任務的Source Task並行度只能是1,確實沒有區別,不會有至少一次的存在了,肯定是精確一次。因爲只有barrier不對齊纔會有可能重複處理,這裏並行度都已經爲1,默認就是對齊的,只有當上遊有多個並行度的時候,多個並行度發到下游的barrier才需要對齊,單並行度不會出現barrier不對齊,所以必然精確一次。其實還是要理解barrier對齊就是Exactly Once不會重複消費,barrier不對齊就是 At Least Once可能重複消費,這裏只有單個並行度根本不會存在barrier不對齊,所以不會存在至少一次語義

  2. 爲了下游儘快做CheckPoint,所以會先發送barrier到下游,自身再同步進行快照;這一步,如果向下發送barrier後,自己同步快照慢怎麼辦?下游已經同步好了,自己還沒?

    答: 可能會出現下游比上游快照還早的情況,但是這不影響快照結果,只是下游快照的更及時了,我只要保障下游把barrier之前的數據都處理了,並且不處理barrier之後的數據,然後做快照,那麼下游也同樣支持精確一次。這個問題你不要從全局思考,你單獨思考上游和下游的實例,你會發現上下游的狀態都是準確的,既沒有丟,也沒有重複計算。這裏需要注意一點,如果有一個Operator 的CheckPoint失敗了或者因爲CheckPoint超時也會導致失敗,那麼JobManager會認爲整個CheckPoint失敗。失敗的CheckPoint是不能用來恢復任務的,必須所有的算子的CheckPoint都成功,那麼這次CheckPoint才能認爲是成功的,才能用來恢復任務

  3. 我程序中Flink的CheckPoint語義設置了 Exactly Once,但是我的mysql中看到數據重複了?程序中設置了1分鐘1次CheckPoint,但是5秒向mysql寫一次數據,並commit

    答:Flink要求end to end的精確一次都必須實現TwoPhaseCommitSinkFunction。如果你的chk-100成功了,過了30秒,由於5秒commit一次,所以實際上已經寫入了6批數據進入mysql,但是突然程序掛了,從chk100處恢復,這樣的話,之前提交的6批數據就會重複寫入,所以出現了重複消費。Flink的精確一次有兩種情況,一個是Flink內部的精確一次,一個是端對端的精確一次,這個博客所描述的都是關於Flink內部去的精確一次,我後期再發一個博客詳細介紹一下Flink端對端的精確一次如何實現

參考鏈接:

Flink官網

An Overview of End-to-End Exactly-Once Processing in Apache Flink (with Apache Kafka, too!)

Managing Large State in Apache Flink: An Intro to Incremental Checkpointing

State & Fault Tolerance

Checkpoints

Savepoints

State Backends

Tuning Checkpoints and Large State

Data Streaming Fault Tolerance

flink-china系列課程

  • 1.2 Flink基本概念
  • 1.7 狀態管理與容錯機制
  • 2.3 Flink Checkpoint-輕量級分佈式快照
  • 2.11 Flink State 最佳實踐

談談流計算中的『Exactly Once』特性

Apache Flink結合Kafka構建端到端的Exactly-Once處理

  • 這篇文章有這麼一句話 TwoPhaseCommitSinkFunction已經把這種情況考慮在內了,並且在從checkpoint點恢復狀態時,會優先發出一個commit。,個人感覺只要把這句話理解了,知道爲什麼每次恢復狀態時,都需要優先發出一個commit,那就把flink的TwoPhaseCommitSinkFunction真正理解了


 

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