端到端Exactly-Once是分佈式系統最大挑戰?Flink是如何解決?

故障恢復與一致性保障

某條數據投遞到某個流處理系統後,該系統對這條數據只處理一次,提供Exactly-Once的保障是一種理想的情況。如果系統不出任何故障,那簡直堪稱完美。然而現實世界中,系統經常受到各類意外因素的影響而發生故障,比如流量激增、網絡抖動、雲服務資源分配出現問題等。如果發生了故障,Flink重啟作業,讀取Checkpoint中的數據,恢復狀態,重新執行計算。

Flink的State和Checkpoint機制:

  • 在Flink中進行有狀態的計算
  • Flink Checkpoint機制
端到端Exactly-Once是分佈式系統最大挑戰?Flink是如何解決?

Checkpoint和故障恢復過程可以保證內部狀態的一致性,但有數據重發的問題,如下圖所示。假設系統最近一次Checkpoint時間戳是3,系統在時間戳10處發生故障,在Checkpoint之後和故障之前的3到10期間,系統已經處理了一些數據(圖中時間戳為5和8的數據)。從上帝視角來看,我們假設系統在時間戳10處發生的故障,但實際場景中,我們是無法預知故障發生的時間,只能是故障發生後,收到報警信息,並知道最近一次的Checkpoint時間戳是3。重啟後,我們可以從最近一次的Checkpoint數據中恢復,整個作業的狀態被初始化到時間戳3處。為了保證一致性,時間戳3以後的數據需要重新處理一遍,在這個例子中時間戳為5和8的數據被重新處理。Flink的Checkpoint過程保證了一個作業內部的數據一致性,主要因為Flink將兩類數據做了備份:

  1. 作業中每個算子的狀態
  2. 輸入數據的偏移量Offset
端到端Exactly-Once是分佈式系統最大挑戰?Flink是如何解決?

數據重發的過程就像觀看實時直播的比賽,即使錯過了一些精彩瞬間,我們可以從錄像中再次觀看重播,英文單詞Replay能非常形象地描述這個場景。但是這引發了一個問題,那就是時間戳3至10之間的數據被重發了。故障之前,這部分數據已經被一些算子處理了,甚至可能已經發送到外部系統了,重啟後,這些數據又重新發送一次。一條數據不是隻被處理一次,而是有可能被處理了多次(At-Least-Once)。從結果的準確性角度來說,我們期望一條數據隻影響一次最終的結果。如果一個系統能保證一條數據隻影響一次最終結果,我們稱這個系統提供端到端的Exactly-Once保證。

端到端的Exactly-Once問題是分佈式系統領域最具挑戰性的問題之一,很多框架都在試圖攻克這個難題。在這個問題上,Flink內部狀態的一致性主要依賴Checkpoint機制,外部交互的一致性主要依賴Source和Sink提供的一些功能。Source需要支持重發功能,Sink需要採用一定的數據寫入技術,比如冪等寫或事務寫。

對於Source重發功能,如上圖所示,只要我們記錄了輸入的偏移量Offset,故障重啟後數據發送方從該Offset重新開始發送數據即可。Kafka的Producer除了發送數據,還會將數據持久化寫到日誌文件中。如果下游應用重啟,Producer根據下游提供的Offset,從持久化的文件中定位到數據,可以重新開始向下遊發送數據。

Source的重發會導致一條數據被處理多次,為了保證只對下游系統產生一次影響,還需要依賴Sink的冪等寫或事務寫。下面重點介紹這這兩個概念。

冪等寫

冪等寫(Idempotent Write)操作是指,任意多次向一個系統寫入數據,只對目標系統產生一次結果影響。例如,重複向一個HashMap裡插入同一個Key-Value二元對,第一次插入時這個HashMap發生變化,後續的插入操作不會改變HashMap的結果,這就是一個冪等寫操作。重複地對一個整數執行加法操作就不是冪等寫,因為多次操作後,這個整數會變大。

像Cassandra、HBase和Redis這樣的KV數據庫一般經常用來作為Sink,用以實現端到端的Exactly-Once。需要注意的是,並不是說一個KV數據庫就百分百支持冪等寫。冪等寫對KV對有要求,那就是Key-Value必須是可確定性(Deterministic)計算的。假如我們設計的Key是:name + curTimestamp,每次執行數據重發時,生成的Key都不相同,會產生多次結果,整個操作不是冪等的。因此,為了追求端到端的Exactly-Once,我們設計業務邏輯時要儘量使用確定性的計算邏輯和數據模型。

KV數據庫作為Sink還可能遇到時間閃回的現象。我們仍以剛才的數據重發為例,假設時間戳5的數據經過計算產生一個(a, t=5)的KV對,時間戳8的數據經過計算產生一個(a, t=8)的結果,不同的元素對同一個Key產生了影響。重啟前,(a, t=5)與(a, t=8)先後提交給了數據庫,兩行數據都基於同一個Key,當(a, t=8)被提交到數據庫時,數據庫一般認為當前這次提交是最新的,它會將(a, t=5)這行老數據覆蓋,這時數據庫中應該保存(a, t=8)這條結果。不幸的是,後來發生了故障重啟,重啟後的最初那段時間,(a, t=5)再次提交給數據庫,數據庫此時錯誤地認為這次又是最新的操作,它會再次更新這個Key,但實際上又回退到了時間戳5。只有當後續所有數據都重發一遍後,所有應該被覆蓋的Key都被最新數據覆蓋後,整個系統才達到數據的一致狀態。所以,從這個角度來講,在重啟過程中KV數據庫裡的數據很有可能是不一致的,當數據重發完成後,數據才恢復一致性,這時它才可以提供端到端的Exatcly-Once保障。

事務寫

事務(Transaction)是數據庫系統所要解決的最核心問題。Flink借鑑了數據庫中的事務處理技術,同時結合自身的Checkpoint機制來保證Sink只對外部輸出產生一次影響。

簡單概括來說,Flink的事務寫(Transaction Write)是指,Flink先將待輸出的數據保存下來暫時不向外部系統提交,等待Checkpoint結束的時刻,Flink上下游所有算子的數據都是一致時,將之前保存的數據全部提交(Commit)到外部系統。換句話說,只有經過Checkpoint確認的數據才向外部系統寫入。那麼數據重發的例子中,入下圖所示,如果使用事務寫,那隻把時間戳3之前的輸出提交到外部系統,時間戳3以後的數據(例如時間戳5和8生成的數據)暫時保存下來,等待下次Checkpoint時一起寫入到外部系統。這就避免了時間戳5這個數據產生多次結果,多次寫入到外部系統。

端到端Exactly-Once是分佈式系統最大挑戰?Flink是如何解決?

在事務寫的具體實現上,Flink目前提供了兩種方式:預寫日誌(Write-Ahead-Log,WAL)和兩階段提交(Two-Phase-Commit,2PC)。這兩種方式也是很多數據庫和分佈式系統實現事務時經常採用的協議,Flink根據自身的場景對這兩種協議做了適應性調整。這兩種方式主要區別在於:WAL方式通用性更強,適合幾乎所有外部系統,但也不能提供百分百端到端的Exactly-Once;如果外部系統自身就支持事務(比如Kafka),可以使用2PC方式,提供百分百端到端的Exactly-Once。我們將在接下來的文章中詳細介紹這兩種方式。

事務寫的方式能提供端到端的Exactly-Once一致性,它的代價也是非常明顯的,就是犧牲了延遲。輸出數據不再是實時寫入到外部系統,而是分批次地提交。目前來說,沒有完美的故障恢復和Exactly-Once保障機制,對於開發者來說,需要在不同需求之間權衡。


分享到:


相關文章: