關於 Flink 狀態與容錯機制

Flink 作爲新一代基於事件流的、真正意義上的流批一體的大數據處理引擎,正在逐漸得到廣大開發者們的青睞。就從我自身的視角看,最近也是在數據團隊把一些原本由 Flume、SparkStreaming、Storm 編寫的流式作業往 Flink 遷移,它們之間的優劣對比本篇暫不討論。

近期會總結一些 Flink 的使用經驗和原理的理解,本篇先談談 Flink 中的狀態和容錯機制,這也是 Flink 核心能力之一,它支撐着 Flink Failover,甚至在較新的版本中,Flink 的 Queryable State 可以把內部狀態提供到外部系統進行查詢進而爲一些 BI 大屏等數據場景提供直接的支持。

關於有狀態計算

先說說什麼是有狀態計算,「狀態」的概念比較寬泛,它既可以是 Flink 在運行過程中不斷產生的一些聚合指標,例如『每分鐘活躍用戶量』、『每小時系統成交額』等等之類被實時不斷聚合的變量。也可以是 Flink 窗口計算中未達到觸發條件前的數據集、也可以是 Kafka、Pulsar 等隊列的消費位移。

狀態分類

Flink 中的狀態從管理方式上來說,分爲 Raw State 和 Managed State。其中,Raw State 是完全由用戶管理的,用戶需要實現狀態的序列化和反序列化且支持的數據類型有限制,一般很少會用到,除非在一些需要自定義算子實現的場景下,Flink 自帶的一些狀態無法派上用場並且需要使用狀態的場景下才會使用。

Managed State 根據數據流是否經過 「keyBy」算子,分爲 Keyed State 和 Operate State。其實這倆的區別不是太大,Keyed State 只是一種特殊情況下的 Operate State,本質上他們還是使用 Flink 預定義好的一些狀態類型。


官網的解釋已經很清楚了,這裏直接複製過來,作一些補充解釋。其中

  • ValueState 就是可以存儲一個值,可以理解爲一個普通變量;
  • ListState 是由一個 List 實現的列表,可以存儲一個狀態集合;
  • ReducingState 保存一個單值,並且需要你提供 ReducingFunction,它會在裏往裏面添加元素的同時調用你的函數自動聚合結果,但要求類型統一,你不能兩次 add 元素類型是不同的;
  • AggregatingState 允許你輸入和輸出的數據類型不一樣,也就是我 add(float) 得到 int 是被允許的,具體邏輯怎麼轉換取決於你的 AggregateFunction。

那麼,再來說說 Keyed State 和 Operate State 的區別,數據流 「keyBy」之後產生 KeyedStream,下游算子收到的數據元素具有相同的 key,那麼對於這些算子中使用的狀態就叫 Keyed State,它會自動綁定 key,一個 key 對應一個 State 存儲,也就是不同 key 的 State 是分開的。

而 Operate State 並不是基於 KeyedStream,所以在這些算子裏使用狀態,其實綁定的是當前算子實例上,需要注意的是,綁定的是算子實例,也就是和你的並行度是有關係的。下文我會說狀態的存儲,其實狀態是存儲在 TaskManager 節點本地的。

狀態後端

顧名思義,狀態後端其實指的就是狀態的存儲方式以及位置。Flink1.13 以前把普通狀態和 job checkpoint(快照文件) 的後端存儲配置是在一起的。分爲 MemoryStateBackend、FsStateBackend、RocksDBStateBackend,分別是基於內存、文件系統以及 RocksDb(一種KV類型的本地存儲DB)。

而 Flink1.13 以後將普通數據狀態和 checkpoint 的狀態存儲後端分離了,HashMapStateBackend、EmbeddedRocksDBStateBackend 是普通狀態的兩個後端,分別是基於內存 HashMap 和 基於 RocksDb 兩種後端。checkpoint 的配置也分爲內存和文件系統(file、hdfs、rocksDb等)。也就是你可能有多種組合,數據狀態存儲在內存而 checkpoint 卻存儲在文件系統等。

//設置內存狀態後端
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend());
//設置RocksDb狀態後端
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new EmbeddedRocksDBStateBackend());

//設置checkpoint內存存儲
env.getCheckpointConfig().setCheckpointStorage(new JobManagerCheckpointStorage());
//設置checkpoint文件存儲
env.getCheckpointConfig().setCheckpointStorage("file:///checkpoint-dir");
env.getCheckpointConfig().setCheckpointStorage("hdfs://namenode:40010/flink/checkpoints");

關於 Checkpoint & Savepoint

上文也多次提到了 Checkpoint,其實它是 Flink Failover 的基礎,在 Flink 中叫做檢查點,簡單來說就是它把 job 運行過程中各個算子中的狀態快照存儲到狀態後端,當 job 發生異常即可從最近的 Checkpoint 文件恢復故障前各個算子中數據處理現場。

Savepoint 和 Checkpoint 本質上是一個東西,只不過 Checkpoint 由 Flink 管理觸發和存儲,而 Savepoint 一般是用戶主動通過命令去觸發並指定文件輸出路徑。Checkpoint 是用於故障恢復,Savepoint 一般用於程序升級。

實現原理

Aligned Checkpoints(對齊)

每個 Jobmanager 都有一個組件 checkpointCoordinator 負責整個 job 的 Checkpoint 觸發,它會根據用戶配置的生成 Checkpoint 間隔時間,定時往 source 數據流中插入特殊數據(barrier),然後 barrier 數據就像普通數據一樣流向下游算子,下游算子在收到 barrier 數據之後會停止處理數據等待「對齊」。

這個「對齊」操作一直是性能瓶頸,它指的是某個算子只有等到所有上游實例的 barrier 事件之後纔會開始做 Checkpoint,一個簡單 union 例子:A、B 兩股數據流合併到 C,那麼 C 只有收到 A 和 B 兩條流的 barrier 事件之後纔會做 Checkpoint。

其實也比較容易理解,假如 A 做完 Checkpoint 並將自己處理到的數據偏移量記錄到快照中,向 C 傳播 barrier 事件,B 負載比較高還沒開始做,那麼如果當 C 只收到 A 的 barrier 事件後就開始做 Checkpoint 並剛好在它做完之後發生 job 故障並開始恢復,那麼 B 其實是沒有做完 Checkpoint 的,只能恢復到上一次的,這就直接導致上次以來所有的數據處理需要重複處理。這是比較大的問題,所以有個「對齊」操作。

以上只是基於沒有「對齊」操作的前提下做的假設,回到正常的處理流程上來。每個算子在自己做完 Checkpoint 後就會通知 checkpointCoordinator 並告知快照文件存儲位置,當最後一個算子完成了 Checkpoint,那麼整個 Checkpoint 流程 Completed。

UnAligned Checkpoints(非對齊)

上文其實也提到了,對齊的 Checkpoint 存在比較大的性能瓶頸,一方面會阻塞數據流正常處理,另一方面可能會導致 Flink 反壓進而導致 Checkpoint 超時 job 失敗並積壓更多的數據待處理,反壓的問題待會兒說,先看下非對齊特性。

Flink1.11 以後加入了 UnAligned Checkpoints,但仍不是默認配置,需要顯式配置,原因是非對齊的方式會產生比較大的 State 用於緩存一些數據,仍然只適用於一些容易高反壓且複雜難以優化的 job。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// enables the unaligned checkpoints
env.getCheckpointConfig().enableUnalignedCheckpoints();

Chandy-Lamport 算法的狀態變化如下:

對於非對齊的 Checkpoint 來說,任意一條流的 barrier 事件到來都將直接觸發當前算子的 Checkpoint。以上圖來說,上面的流稱爲 A 流,下面的流稱爲 B 流,虛線是 barrier 事件,我們假設這是一個 equals-join 操作。

  • 當 A 流中的數據「2」流過 Operator 並且和 B 流中的數據「2」join 成功,Operator 算子向下遊輸出數據「2」
  • 然後收到 A 流的 barrier 事件,Operator 算子當即開啓本算子的 Checkpoint 並向下遊輸出 barrier,此時這個 Checkpoint 已經是一個 Running 的狀態
  • 這時 B 流過來的每一條數據都會被緩存在狀態中,直到收到 B 流的 barrier 事件,這期間 A 流和 B 流是正常 join 處理的,完全無阻塞的
  • 當收到來自 B 流的 barrier,停止對 B 流數據的緩存,完成當前算子的異步快照(快照中會包含所有緩存的B流數據)

這樣,其實不論哪個時間點出現 job 的故障恢復,從 Checkpoint 恢復出來算子對齊的狀態+緩存(會被恢復到輸出channel)的數據即可保證數據處理現場都是正確的。但是缺點比較明顯,就是需要保存大容量的狀態,Checkpoint 文件也是很大,job 恢復的速度也會比較慢。

關於 Flink 反壓

反壓就是指 Flink 中上下游算子數據處理能力不匹配,下游算子處理太慢,上游算子發送區數據溢出。反壓造成的最常見的影響就是造成 Checkpoint 超時,進而的 job 故障恢復。

Credit-Based 反壓機制

反壓其實主要就分爲兩個部分,一個是算子與算子之間,下游算子要通過反壓限制上游算子的發送速率,另一個是每個算子內部,寫操作要反壓限制讀操作的讀取速率。

TaskManager 間反壓機制

這張圖展示了 Flink 算子跨節點通信的基本流程,NetWorkBufferPool 在每個 TaskManager 管理着網絡通信相關的緩衝區內存申請釋放; LocalBufferPool 是每個算子內部的緩衝池,從 NetWorkBufferPool 申請而來;ResultSubpartition 是寫出緩衝區,從 LocalBufferPool 申請而來;InputChannel 是讀緩衝區,從 LocalBufferPool 申請而來。

整體的流程就是,Writer 寫數據到 ResultSubpartition,再往下傳到 Netty,最終通過 Socket 發到其他節點,其他節點通過 Reader 讀取數據寫入 InputChannel。

Credit 也叫授信機制,每次從寫緩衝區往下游節點寫數據的時候會通過「backlog」告訴下游的 Reader 自己還積壓多少數據未發送。而下游 Reader 接收數據的同時會去檢查自己是否還有足夠的空間放下未來即將到來的數據,通過「credit」反應出來,如果沒有足夠的空間且向 LocalBufferPool 申請無果就會返回「credit=0」。

Writer 得到「credit=0」後會阻塞往 Netty 寫數據的操作,進而緩解了下游算子的壓力(有探活機制,一旦檢測到下游可寫會恢復寫操作的)

TaskManager 內部反壓機制

上面談到下游反饋回來的「credit=0」會阻塞自己對外的輸出操作,那麼它也應該傳播反壓到當前節點的讀操作。其實 Flink 裏面是把 Reader 和 Writer 放在一個線程裏的,那麼如果寫被阻塞了,讀就自然被阻塞住。

這樣上游算子就會迅速填滿 InputChannel,自動觸發反壓,向上一級級傳播,完成整個反壓的全局調整。

到這裏其實反壓就介紹完了,上文說道反壓會影響到 Checkpoint,就是說一級級反壓的結果就是整個 job 中數據流動緩慢,以至於 Checkpoint barrier 在一定時間內沒有完成對齊進而會導致 Checkpoint 超時失敗,任務重啓,然後由於重啓回退又有更嚴重的數據積壓,形成惡性循環。(也就是非對齊 Checkpoint 要解決的問題)

歡迎交流~

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