Apache Flink 零基礎入門(六):狀態管理及容錯機制

本文Apache Flink零基礎入門系列文章第六篇,重點爲大家介紹Flink的狀態管理和容錯機制,主要內容包括:狀態管理的基本概念;狀態的類型與使用示例;容錯機制與故障恢復。

一.狀態管理的基本概念

1.什麼是狀態

首先舉一個無狀態計算的例子:消費延遲計算。假設現在有一個消息隊列,消息隊列中有一個生產者持續往消費隊列寫入消息,多個消費者分別從消息隊列中讀取消息。從圖上可以看出,生產者已經寫入 16 條消息,Offset 停留在 15 ;有 3 個消費者,有的消費快,而有的消費慢。消費快的已經消費了 13 條數據,消費者慢的才消費了 7、8 條數據。

如何實時統計每個消費者落後多少條數據,如圖給出了輸入輸出的示例。可以瞭解到輸入的時間點有一個時間戳,生產者將消息寫到了某個時間點的位置,每個消費者同一時間點分別讀到了什麼位置。剛纔也提到了生產者寫入了 15 條,消費者分別讀取了 10、7、12 條。那麼問題來了,怎麼將生產者、消費者的進度轉換爲右側示意圖信息呢?

consumer 0 落後了 5 條,consumer 1 落後了 8 條,consumer 2 落後了 3 條,根據 Flink 的原理,此處需進行 Map 操作。Map 首先把消息讀取進來,然後分別相減,即可知道每個 consumer 分別落後了幾條。Map 一直往下發,則會得出最終結果。

大家會發現,在這種模式的計算中,無論這條輸入進來多少次,輸出的結果都是一樣的,因爲單條輸入中已經包含了所需的所有信息。消費落後等於生產者減去消費者。生產者的消費在單條數據中可以得到,消費者的數據也可以在單條數據中得到,所以相同輸入可以得到相同輸出,這就是一個無狀態的計算。

相應的什麼是有狀態的計算?

以訪問日誌統計量的例子進行說明,比如當前拿到一個 Nginx 訪問日誌,一條日誌表示一個請求,記錄該請求從哪裏來,訪問的哪個地址,需要實時統計每個地址總共被訪問了多少次,也即每個 API 被調用了多少次。可以看到下面簡化的輸入和輸出,輸入第一條是在某個時間點請求 GET 了 /api/a;第二條日誌記錄了某個時間點 Post /api/b ;第三條是在某個時間點 GET了一個 /api/a,總共有 3 個 Nginx 日誌。從這 3 條 Nginx 日誌可以看出,第一條進來輸出 /api/a 被訪問了一次,第二條進來輸出 /api/b 被訪問了一次,緊接着又進來一條訪問 api/a,所以 api/a 被訪問了 2 次。不同的是,兩條 /api/a 的 Nginx 日誌進來的數據是一樣的,但輸出的時候結果可能不同,第一次輸出 count=1 ,第二次輸出 count=2,說明相同輸入可能得到不同輸出。輸出的結果取決於當前請求的 API 地址之前累計被訪問過多少次。第一條過來累計是 0 次,count = 1,第二條過來 API 的訪問已經有一次了,所以 /api/a 訪問累計次數 count=2。單條數據其實僅包含當前這次訪問的信息,而不包含所有的信息。要得到這個結果,還需要依賴 API 累計訪問的量,即狀態。

這個計算模式是將數據輸入算子中,用來進行各種複雜的計算並輸出數據。這個過程中算子會去訪問之前存儲在裏面的狀態。另外一方面,它還會把現在的數據對狀態的影響實時更新,如果輸入 200 條數據,最後輸出就是 200 條結果。

什麼場景會用到狀態呢?下面列舉了常見的 4 種:

  • 去重:比如上游的系統數據可能會有重複,落到下游系統時希望把重複的數據都去掉。去重需要先了解哪些數據來過,哪些數據還沒有來,也就是把所有的主鍵都記錄下來,當一條數據到來後,能夠看到在主鍵當中是否存在。
  • 窗口計算:比如統計每分鐘 Nginx 日誌 API 被訪問了多少次。窗口是一分鐘計算一次,在窗口觸發前,如 08:00 ~ 08:01 這個窗口,前59秒的數據來了需要先放入內存,即需要把這個窗口之內的數據先保留下來,等到 8:01 時一分鐘後,再將整個窗口內觸發的數據輸出。未觸發的窗口數據也是一種狀態。
  • 機器學習/深度學習:如訓練的模型以及當前模型的參數也是一種狀態,機器學習可能每次都用有一個數據集,需要在數據集上進行學習,對模型進行一個反饋。
  • 訪問歷史數據:比如與昨天的數據進行對比,需要訪問一些歷史數據。如果每次從外部去讀,對資源的消耗可能比較大,所以也希望把這些歷史數據也放入狀態中做對比。

2.爲什麼要管理狀態

管理狀態最直接的方式就是將數據都放到內存中,這也是很常見的做法。比如在做 WordCount 時,Word 作爲輸入,Count 作爲輸出。在計算的過程中把輸入不斷累加到 Count。

但對於流式作業有以下要求:

  • 7*24小時運行,高可靠;
  • 數據不丟不重,恰好計算一次;
  • 數據實時產出,不延遲;

基於以上要求,內存的管理就會出現一些問題。由於內存的容量是有限制的。如果要做 24 小時的窗口計算,將 24 小時的數據都放到內存,可能會出現內存不足;另外,作業是 7*24,需要保障高可用,機器若出現故障或者宕機,需要考慮如何備份及從備份中去恢復,保證運行的作業不受影響;此外,考慮橫向擴展,假如網站的訪問量不高,統計每個 API 訪問次數的程序可以用單線程去運行,但如果網站訪問量突然增加,單節點無法處理全部訪問數據,此時需要增加幾個節點進行橫向擴展,這時數據的狀態如何平均分配到新增加的節點也問題之一。因此,將數據都放到內存中,並不是最合適的一種狀態管理方式。

3.理想的狀態管理

最理想的狀態管理需要滿足易用、高效、可靠三點需求:

  • 易用,Flink 提供了豐富的數據結構、多樣的狀態組織形式以及簡潔的擴展接口,讓狀態管理更加易用;
  • 高效,實時作業一般需要更低的延遲,一旦出現故障,恢復速度也需要更快;當處理能力不夠時,可以橫向擴展,同時在處理備份時,不影響作業本身處理性能;
  • 可靠,Flink 提供了狀態持久化,包括不丟不重的語義以及具備自動的容錯能力,比如 HA,當節點掛掉後會自動拉起,不需要人工介入。

二.Flink 狀態的類型與使用示例

1.Managed State & Raw State

Managed State 是 Flink 自動管理的 State,而 Raw State 是原生態 State,兩者的區別如下:

  • 從狀態管理方式的方式來說,Managed State 由 Flink Runtime 管理,自動存儲,自動恢復,在內存管理上有優化;而 Raw State 需要用戶自己管理,需要自己序列化,Flink 不知道 State 中存入的數據是什麼結構,只有用戶自己知道,需要最終序列化爲可存儲的數據結構。
  • 從狀態數據結構來說,Managed State 支持已知的數據結構,如 Value、List、Map 等。而 Raw State只支持字節數組 ,所有狀態都要轉換爲二進制字節數組纔可以。
  • 從推薦使用場景來說,Managed State 大多數情況下均可使用,而 Raw State 是當 Managed State 不夠用時,比如需要自定義 Operator 時,推薦使用 Raw State。

2.Keyed State & Operator State

Managed State 分爲兩種,一種是 Keyed State;另外一種是 Operator State。在Flink Stream模型中,Datastream 經過 keyBy 的操作可以變爲 KeyedStream 。
每個 Key 對應一個 State,即一個 Operator 實例處理多個 Key,訪問相應的多個 State,並由此就衍生了 Keyed State。Keyed State 只能用在 KeyedStream 的算子中,即在整個程序中沒有 keyBy 的過程就沒有辦法使用 KeyedStream。

相比較而言,Operator State 可以用於所有算子,相對於數據源有一個更好的匹配方式,常用於 Source,例如 FlinkKafkaConsumer。相比 Keyed State,一個 Operator 實例對應一個 State,隨着併發的改變,Keyed State 中,State 隨着 Key 在實例間遷移,比如原來有 1 個併發,對應的 API 請求過來,/api/a 和 /api/b 都存放在這個實例當中;如果請求量變大,需要擴容,就會把 /api/a 的狀態和 /api/b 的狀態分別放在不同的節點。由於 Operator State 沒有 Key,併發改變時需要選擇狀態如何重新分配。其中內置了 2 種分配方式:一種是均勻分配,另外一種是將所有 State 合併爲全量 State 再分發給每個實例。

在訪問上,Keyed State 通過 RuntimeContext 訪問,這需要 Operator 是一個Rich Function。Operator State 需要自己實現 CheckpointedFunction 或 ListCheckpointed 接口。在數據結構上,Keyed State 支持的數據結構,比如 ValueState、ListState、ReducingState、AggregatingState 和 MapState;而 Operator State 支持的數據結構相對較少,如 ListState。

3.Keyed State 使用示例

Keyed State 有很多種,如圖爲幾種 Keyed State 之間的關係。首先 State 的子類中一級子類有 ValueState、MapState、AppendingState。AppendingState 又有一個子類 MergingState。MergingState 又分爲 3 個子類分別是ListState、ReducingState、AggregatingState。這個繼承關係使它們的訪問方式、數據結構也存在差異。

幾種 Keyed State 的差異具體體現在:

  • ValueState 存儲單個值,比如 Wordcount,用 Word 當 Key,State 就是它的 Count。這裏面的單個值可能是數值或者字符串,作爲單個值,訪問接口可能有兩種,get 和 set。在 State 上體現的是 update(T) / T value()。
  • MapState 的狀態數據類型是 Map,在 State 上有 put、remove等。需要注意的是在 MapState 中的 key 和 Keyed state 中的 key 不是同一個。
  • ListState 狀態數據類型是 List,訪問接口如 add、update 等。
  • ReducingState 和 AggregatingState 與 ListState 都是同一個父類,但狀態數據類型上是單個值,原因在於其中的 add 方法不是把當前的元素追加到列表中,而是把當前元素直接更新進了 Reducing 的結果中。
  • AggregatingState 的區別是在訪問接口,ReducingState 中 add(T)和 T get() 進去和出來的元素都是同一個類型,但在 AggregatingState 輸入的 IN,輸出的是 OUT。

下面以 ValueState 爲例,來闡述一下具體如何使用,以狀態機的案例來講解 。

源代碼地址

感興趣的同學可直接查看完整源代碼,在此截取部分。如圖爲 Flink 作業的主方法與主函數中的內容,前面的輸入、後面的輸出以及一些個性化的配置項都已去掉,僅保留了主幹。

首先 events 是一個 DataStream,通過 env.addSource 加載數據進來,接下來有一個 DataStream 叫 alerts,先 keyby 一個 sourceAddress,然後在 flatMap 一個StateMachineMapper。StateMachineMapper 就是一個狀態機,狀態機指有不同的狀態與狀態間有不同的轉換關係的結合,以買東西的過程簡單舉例。首先下訂單,訂單生成後狀態爲待付款,當再來一個事件狀態付款成功,則事件的狀態將會從待付款變爲已付款,待發貨。已付款,待發貨的狀態再來一個事件發貨,訂單狀態將會變爲配送中,配送中的狀態再來一個事件簽收,則該訂單的狀態就變爲已簽收。在整個過程中,隨時都可以來一個事件,取消訂單,無論哪個狀態,一旦觸發了取消訂單事件最終就會將狀態轉移到已取消,至此狀態就結束了。

Flink 寫狀態機是如何實現的?首先這是一個 RichFlatMapFunction,要用 Keyed State getRuntimeContext,getRuntimeContext 的過程中需要 RichFunction,所以需要在 open 方法中獲取 currentState ,然後 getState,currentState 保存的是當前狀態機上的狀態。

如果剛下訂單,那麼 currentState 就是待付款狀態,初始化後,currentState 就代表訂單完成。訂單來了後,就會走 flatMap 這個方法,在 flatMap 方法中,首先定義一個 State,從 currentState 取出,即 Value,Value 取值後先判斷值是否爲空,如果 sourceAddress state 是空,則說明沒有被使用過,那麼此狀態應該爲剛創建訂單的初始狀態,即待付款。然後賦值 state = State.Initial,注意此處的 State 是本地的變量,而不是 Flink 中管理的狀態,將它的值從狀態中取出。接下來在本地又會來一個變量,然後 transition,將事件對它的影響加上,剛纔待付款的訂單收到付款成功的事件,就會變成已付款,待發貨,然後 nextState 即可算出。此外,還需要判斷 State 是否合法,比如一個已簽收的訂單,又來一個狀態叫取消訂單,會發現已簽收訂單不能被取消,此時這個狀態就會下發,訂單狀態爲非法狀態。

如果不是非法的狀態,還要看該狀態是否已經無法轉換,比如這個狀態變爲已取消時,就不會在有其他的狀態再發生了,此時就會從 state 中 clear。clear 是所有的 Flink 管理 keyed state 都有的公共方法,意味着將信息刪除,如果既不是一個非法狀態也不是一個結束狀態,後面可能還會有更多的轉換,此時需要將訂單的當前狀態 update ,這樣就完成了 ValueState 的初始化、取值、更新以及清零,在整個過程中狀態機的作用就是將非法的狀態進行下發,方便下游進行處理。其他的狀態也是類似的使用方式。

三.容錯機制與故障恢復

1.狀態如何保存及恢復

Flink 狀態保存主要依靠 Checkpoint 機制,Checkpoint 會定時製作分佈式快照,對程序中的狀態進行備份。分佈式快照是如何實現的可以參考【第二課時】的內容,這裏就不在闡述分佈式快照具體是如何實現的。分佈式快照 Checkpoint 完成後,當作業發生故障瞭如何去恢復?假如作業分佈跑在 3 臺機器上,其中一臺掛了。這個時候需要把進程或者線程移到 active 的 2 臺機器上,此時還需要將整個作業的所有 Task 都回滾到最後一次成功 Checkpoint 中的狀態,然後從該點開始繼續處理。

如果要從 Checkpoint 恢復,必要條件是數據源需要支持數據重新發送。Checkpoint恢復後, Flink 提供兩種一致性語義,一種是恰好一次,一種是至少一次。在做 Checkpoint時,可根據 Barries 對齊來判斷是恰好一次還是至少一次,如果對齊,則爲恰好一次,否則沒有對齊即爲至少一次。如果作業是單線程處理,也就是說 Barries 是不需要對齊的;如果只有一個 Checkpoint 在做,不管什麼時候從 Checkpoint 恢復,都會恢復到剛纔的狀態;如果有多個節點,假如一個數據的 Barries 到了,另一個 Barries 還沒有來,內存中的狀態如果已經存儲。那麼這 2 個流是不對齊的,恢復的時候其中一個流可能會有重複。

Checkpoint 通過代碼的實現方法如下:

  • 首先從作業的運行環境 env.enableCheckpointing 傳入 1000,意思是做 2 個 Checkpoint 的事件間隔爲 1 秒。Checkpoint 做的越頻繁,恢復時追數據就會相對減少,同時 Checkpoint 相應的也會有一些 IO 消耗。
  • 接下來是設置 Checkpoint 的 model,即設置了 Exactly_Once 語義,並且需要 Barries 對齊,這樣可以保證消息不會丟失也不會重複。
  • setMinPauseBetweenCheckpoints 是 2 個 Checkpoint 之間最少是要等 500ms,也就是剛做完一個 Checkpoint。比如某個 Checkpoint 做了700ms,按照原則過 300ms 應該是做下一個 Checkpoint,因爲設置了 1000ms 做一次 Checkpoint 的,但是中間的等待時間比較短,不足 500ms 了,需要多等 200ms,因此以這樣的方式防止 Checkpoint 太過於頻繁而導致業務處理的速度下降。
  • setCheckpointTimeout 表示做 Checkpoint 多久超時,如果 Checkpoint 在 1min 之內尚未完成,說明 Checkpoint 超時失敗。
    setMaxConcurrentCheckpoints 表示同時有多少個 Checkpoint 在做快照,這個可以根據具體需求去做設置。
  • enableExternalizedCheckpoints 表示下 Cancel 時是否需要保留當前的 Checkpoint,默認 Checkpoint 會在整個作業 Cancel 時被刪除。Checkpoint 是作業級別的保存點。

上面講過,除了故障恢復之外,還需要可以手動去調整併發重新分配這些狀態。手動調整併發,必須要重啓作業並會提示 Checkpoint 已經不存在,那麼作業如何恢復數據?

一方面 Flink 在 Cancel 時允許在外部介質保留 Checkpoint ;另一方面,Flink 還有另外一個機制是 SavePoint。

Savepoint 與 Checkpoint 類似,同樣是把狀態存儲到外部介質。當作業失敗時,可以從外部恢復。Savepoint 與 Checkpoint 有什麼區別呢?

  • 從觸發管理方式來講,Checkpoint 由 Flink 自動觸發並管理,而 Savepoint 由用戶手動觸發並人肉管理;
  • 從用途來講,Checkpoint 在 Task 發生異常時快速恢復,例如網絡抖動或超時異常,而 Savepoint 有計劃地進行備份,使作業能停止後再恢復,例如修改代碼、調整併發;
  • 最後從特點來講,Checkpoint 比較輕量級,作業出現問題會自動從故障中恢復,在作業停止後默認清除;而 Savepoint 比較持久,以標準格式存儲,允許代碼或配置發生改變,恢復需要啓動作業手動指定一個路徑恢復。

2.可選的狀態存儲方式

Checkpoint 的存儲,第一種是內存存儲,即 MemoryStateBackend,構造方法是設置最大的StateSize,選擇是否做異步快照,這種存儲狀態本身存儲在 TaskManager 節點也就是執行節點內存中的,因爲內存有容量限制,所以單個 State maxStateSize 默認 5 M,且需要注意 maxStateSize <= akka.framesize 默認 10 M。Checkpoint 存儲在 JobManager 內存中,因此總大小不超過 JobManager 的內存。推薦使用的場景爲:本地測試、幾乎無狀態的作業,比如 ETL、JobManager 不容易掛,或掛掉影響不大的情況。不推薦在生產場景使用。

另一種就是在文件系統上的 FsStateBackend ,構建方法是需要傳一個文件路徑和是否異步快照。State 依然在 TaskManager 內存中,但不會像 MemoryStateBackend 有 5 M 的設置上限,Checkpoint 存儲在外部文件系統(本地或 HDFS),打破了總大小 Jobmanager 內存的限制。容量限制上,單 TaskManager 上 State 總量不超過它的內存,總大小不超過配置的文件系統容量。推薦使用的場景、常規使用狀態的作業、例如分鐘級窗口聚合或 join、需要開啓HA的作業。

還有一種存儲爲 RocksDBStateBackend ,RocksDB 是一個 key/value 的內存存儲系統,和其他的 key/value 一樣,先將狀態放到內存中,如果內存快滿時,則寫入到磁盤中,但需要注意 RocksDB 不支持同步的 Checkpoint,構造方法中沒有同步快照這個選項。不過 RocksDB 支持增量的 Checkpoint,也是目前唯一增量 Checkpoint 的 Backend,意味着每次用戶不需要將所有狀態都寫進去,將增量的改變的狀態寫進去即可。它的 Checkpoint 存儲在外部文件系統(本地或HDFS),其容量限制只要單個 TaskManager 上 State 總量不超過它的內存+磁盤,單 Key最大 2G,總大小不超過配置的文件系統容量即可。推薦使用的場景爲:超大狀態的作業,例如天級窗口聚合、需要開啓 HA 的作業、最好是對狀態讀寫性能要求不高的作業。

四.總結

1.爲什麼要使用狀態?

前面提到有狀態的作業要有有狀態的邏輯,有狀態的邏輯是因爲數據之間存在關聯,單條數據是沒有辦法把所有的信息給表現出來。所以需要通過狀態來滿足業務邏輯。

2.爲什麼要管理狀態?

使用了狀態,爲什麼要管理狀態?因爲實時作業需要7*24不間斷的運行,需要應對不可靠的因素而帶來的影響。

3.如何選擇狀態的類型和存儲方式?

那如何選擇狀態的類型和存儲方式?結合前面的內容,可以看到,首先是要分析清楚業務場景;比如想要做什麼,狀態到底大不大。比較各個方案的利弊,選擇根據需求合適的狀態類型和存儲方式即可。

視頻回顧:https://www.bilibili.com/video/av49736102?from=search&seid=5739530486011132468

更多幹貨內容參見Apache Flink零基礎入門專題

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