第09講:Flink 狀態與容錯

Flink系列文章

  1. 第01講:Flink 的應用場景和架構模型
  2. 第02講:Flink 入門程序 WordCount 和 SQL 實現
  3. 第03講:Flink 的編程模型與其他框架比較
  4. 第04講:Flink 常用的 DataSet 和 DataStream API
  5. 第05講:Flink SQL & Table 編程和案例
  6. 第06講:Flink 集羣安裝部署和 HA 配置
  7. 第07講:Flink 常見核心概念分析
  8. 第08講:Flink 窗口、時間和水印
  9. 第09講:Flink 狀態與容錯

這一課時我們主要講解 Flink 的狀態和容錯。

在 Flink 的框架中,進行有狀態的計算是 Flink 最重要的特性之一。所謂的狀態,其實指的是 Flink 程序的中間計算結果。Flink 支持了不同類型的狀態,並且針對狀態的持久化還提供了專門的機制和狀態管理器。

狀態

我們在 Flink 的官方博客中找到這樣一段話,可以認爲這是對狀態的定義:

When working with state, it might also be useful to read about Flink’s state backends. Flink provides different state backends that specify how and where state is stored. State can be located on Java’s heap or off-heap. Depending on your state backend, Flink can also manage the state for the application, meaning Flink deals with the memory management (possibly spilling to disk if necessary) to allow applications to hold very large state. State backends can be configured without changing your application logic.

這段話告訴我們,所謂的狀態指的是,在流處理過程中那些需要記住的數據,而這些數據既可以包括業務數據,也可以包括元數據。Flink 本身提供了不同的狀態管理器來管理狀態,並且這個狀態可以非常大。

Flink 的狀態數據可以存在 JVM 的堆內存或者堆外內存中,當然也可以藉助第三方存儲,例如 Flink 已經實現的對 RocksDB 支持。Flink 的官網同樣給出了適用於狀態計算的幾種情況:

  • When an application searches for certain event patterns, the state will store the sequence of events encountered so far
  • When aggregating events per minute/hour/day, the state holds the pending aggregates
  • When training a machine learning model over a stream of data points, the state holds the current version of the model parameters
  • When historic data needs to be managed, the state allows efficient access to events that occurred in the past

以上四種情況分別是:複雜事件處理獲取符合某一特定時間規則的事件、聚合計算、機器學習的模型訓練、使用歷史的數據進行計算。

我們在之前的課時中提到過 KeyedStream 的概念,並且介紹過 KeyBy 這個算子的使用。在 Flink 中,根據數據集是否按照某一個 Key 進行分區,將狀態分爲 Keyed StateOperator State(Non-Keyed State)兩種類型。

image (4).png

如上圖所示,Keyed State 是經過分區後的流上狀態,每個 Key 都有自己的狀態,圖中的八邊形、圓形和三角形分別管理各自的狀態,並且只有指定的 key 才能訪問和更新自己對應的狀態。

與 Keyed State 不同的是,Operator State 可以用在所有算子上,每個算子子任務或者說每個算子實例共享一個狀態,流入這個算子子任務的數據可以訪問和更新這個狀態。每個算子子任務上的數據共享自己的狀態。

但是有一點需要說明的是,無論是 Keyed State 還是 Operator State,Flink 的狀態都是基於本地的,即每個算子子任務維護着這個算子子任務對應的狀態存儲,算子子任務之間的狀態不能相互訪問。

image (5).png

我們可以看一下 State 的類圖,對於 Keyed State,Flink 提供了幾種現成的數據結構供我們使用,State 主要有四種實現,分別爲 ValueState、MapState、AppendingState 和 ReadOnlyBrodcastState ,其中 AppendingState 又可以細分爲ReducingState、AggregatingState 和 ListState。

那麼我們怎麼訪問這些狀態呢?Flink 提供了 StateDesciptor 方法專門用來訪問不同的 state,類圖如下:

image (6).png

下面演示一下如何使用 StateDesciptor 和 ValueState,代碼如下:

public static void main(String[] args) throws Exception {

   final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

   env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 5L), Tuple2.of(1L, 2L))
         .keyBy(0)
         .flatMap(new CountWindowAverage())
         .printToErr();

       env.execute("submit job");

}
 public static class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {

       private transient ValueState<Tuple2<Long, Long>> sum;
       public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {

           Tuple2<Long, Long> currentSum;
           // 訪問ValueState
           if(sum.value()==null){
               currentSum = Tuple2.of(0L, 0L);
           }else {
               currentSum = sum.value();
           }
           // 更新
           currentSum.f0 += 1;
           // 第二個元素加1
           currentSum.f1 += input.f1;
           // 更新state
           sum.update(currentSum);

           // 如果count的值大於等於2,求知道並清空state
           if (currentSum.f0 >= 2) {
               out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
               sum.clear();
           }
   }

   public void open(Configuration config) {
       ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
               new ValueStateDescriptor<>(
                       "average", // state的名字
                       TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})
                       ); // 設置默認值

       StateTtlConfig ttlConfig = StateTtlConfig
               .newBuilder(Time.seconds(10))
               .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
               .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
               .build();

       descriptor.enableTimeToLive(ttlConfig);

       sum = getRuntimeContext().getState(descriptor);
       }
}

在上述例子中,我們通過繼承 RichFlatMapFunction 來訪問 State,通過 getRuntimeContext().getState(descriptor) 來獲取狀態的句柄。而真正的訪問和更新狀態則在 Map 函數中實現。

我們這裏的輸出條件爲,每當第一個元素的和達到二,就把第二個元素的和與第一個元素的和相除,最後輸出。我們直接運行,在控制檯可以看到結果:

image (7).png

Operator State 的實際應用場景不如 Keyed State 多,一般來說它會被用在 Source 或 Sink 等算子上,用來保存流入數據的偏移量或對輸出數據做緩存,以保證 Flink 應用的 Exactly-Once 語義。

同樣,我們對於任何狀態數據還可以設置它們的過期時間。如果一個狀態設置了 TTL,並且已經過期,那麼我們之前保存的值就會被清理。

想要使用 TTL,我們需要首先構建一個 StateTtlConfig 配置對象;然後,可以通過傳遞配置在任何狀態描述符中啓用 TTL 功能。

複製代碼

StateTtlConfig ttlConfig = StateTtlConfig
        .newBuilder(Time.seconds(10))
        .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
        .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
        .build();
descriptor.enableTimeToLive(ttlConfig);

image (8).png

StateTtlConfig 這個類中有一些配置需要我們注意:

image (9).png

UpdateType 表明了過期時間什麼時候更新,而對於那些過期的狀態,是否還能被訪問則取決於 StateVisibility 的配置。

狀態後端種類和配置

我們在上面的內容中講到了 Flink 的狀態數據可以存在 JVM 的堆內存或者堆外內存中,當然也可以藉助第三方存儲。默認情況下,Flink 的狀態會保存在 taskmanager 的內存中,Flink 提供了三種可用的狀態後端用於在不同情況下進行狀態後端的保存。

  • MemoryStateBackend
  • FsStateBackend
  • RocksDBStateBackend

MemoryStateBackend

顧名思義,MemoryStateBackend 將 state 數據存儲在內存中,一般用來進行本地調試用,我們在使用 MemoryStateBackend 時需要注意的一些點包括:

每個獨立的狀態(state)默認限制大小爲 5MB,可以通過構造函數增加容量
狀態的大小不能超過 akka 的 Framesize 大小
聚合後的狀態必須能夠放進 JobManager 的內存中

MemoryStateBackend 可以通過在代碼中顯示指定:

複製代碼

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

env.setStateBackend(new MemoryStateBackend(DEFAULT_MAX_STATE_SIZE,false));

其中,new MemoryStateBackend(DEFAULT_MAX_STATE_SIZE,false) 中的 false 代表關閉異步快照機制。關於快照,我們在後面的課時中有單獨介紹。

很明顯 MemoryStateBackend 適用於我們本地調試使用,來記錄一些狀態很小的 Job 狀態信息。

FsStateBackend

FsStateBackend 會把狀態數據保存在 TaskManager 的內存中。CheckPoint 時,將狀態快照寫入到配置的文件系統目錄中,少量的元數據信息存儲到 JobManager 的內存中。

使用 FsStateBackend 需要我們指定一個文件路徑,一般來說是 HDFS 的路徑,例如,hdfs://namenode:40010/flink/checkpoints。

我們同樣可以在代碼中顯示指定:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints", false));

FsStateBackend 因爲將狀態存儲在了外部系統如 HDFS 中,所以它適用於大作業、狀態較大、全局高可用的那些任務。

RocksDBStateBackend

RocksDBStateBackend 和 FsStateBackend 有一些類似,首先它們都需要一個外部文件存儲路徑,比如 HDFS 的 hdfs://namenode:40010/flink/checkpoints,此外也適用於大作業、狀態較大、全局高可用的那些任務。

但是與 FsStateBackend 不同的是,RocksDBStateBackend 將正在運行中的狀態數據保存在 RocksDB 數據庫中,RocksDB 數據庫默認將數據存儲在 TaskManager 運行節點的數據目錄下。

這意味着,RocksDBStateBackend 可以存儲遠超過 FsStateBackend 的狀態,可以避免向 FsStateBackend 那樣一旦出現狀態暴增會導致 OOM,但是因爲將狀態數據保存在 RocksDB 數據庫中,吞吐量會有所下降。

此外,需要注意的是,RocksDBStateBackend 是唯一支持增量快照的狀態後端

總結

我們在這一課時中講解了 Flink 中的狀態分類和使用,並且用實際案例演示了用法;此外介紹了 Flink 狀態的保存方式和優缺點。

關注公衆號:大數據技術派,回覆資料,領取1024G資料。

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