第09講:Flink 狀態與容錯

大資料技術派發表於2022-02-03

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資料。

相關文章