Working with State

weixin_33912445發表於2018-01-22

原文連結


Keyed State and Operator State

在Flink中有兩種基本型別的狀態:Keyed State and Operator State。

Keyed State

Keyed State總是和keys相關,並且只能用於KeyedStream上的函式和操作。
你可以將Keyed State認為是已經被分段或分割槽的Operator State,每個key都有且僅有一個state-partition。每個keyed-state邏輯上繫結到一個唯一的<parallel-operator-instance, key>組合上,並且由於每個key“屬於”keyed operator的一個並行例項,所以我們可以簡單的認為是<operator,key>。
Keyed State進一步被組織到所謂的Key Groups中。Key Groups是Flink能夠重新分配keyed State的原子單元。Key Groups的數量等於定義的最大並行度。在一個keyed operator的並行例項執行期間,它與一個或多個Key Groups配合工作。

Operator State

對於Operator State(或者non-keyed state),每個operator state繫結到一個並行operator例項上。在Flink中,Kafka Connector是一個使用Operator State的很好的例子。每個並行Kafka消費者例項維護一個主題分割槽和偏移的map作為它的Operator State。
當並行度被修改時,Operator State介面支援在並行operator例項上重新分配狀態。進行這種重新分配可以有不同的方案。

Raw and Managed State

Keyed StateOperator State 有兩種形式: managedraw
Managed State表示資料結構由Flink runtime控制,例如內部雜湊表,或者RocksDB。例如,“ValueState”,“ListState”等等。Flink的runtime層編碼State並將其寫入checkpoint中。
Raw State是operator儲存在它的資料結構中的state。當進行checkpoint時,它只寫入位元組序列到checkpoint中。Flink並不知道狀態的資料結構,並且只能看到raw位元組。
所有的資料流函式都可以使用managed state,但是raw state介面只可以在操作符的實現類中使用。推薦使用managed state(而不是raw state),因為使用managed state,當並行度變化時,Flink可以自動的重新分佈狀態,並且可以做更好的記憶體管理。
注意 如果你的managed state需要自定義序列化邏輯,請參見managed state的自定義序列化以確保未來的相容性。Flink預設的序列化不需要特殊處理。

使用Managed Keyed State

managed keyed state介面提供了對當前輸入元素的key的不同型別的狀態的訪問。這意味著這種型別的狀態只能在KeyedStream中使用,它可以通過stream.keyBy(...)建立。
現在,我們首先看下不同型別的狀態,然後展示如何在程式中使用它們。可用的狀態原語是:

  • ValueState<T>:它會儲存一個可以被更新和查詢的值(限於上面提到的輸入元素的key,因此操作看到的每個key可能都是同一個值)。可是使用update(T) 和 T value() 更新和查詢值。
  • ListState<T>: 它儲存了一個元素列表。你可以新增元素和檢索Iterable來獲取所有當前儲存的元素。新增元素使用add(T)方法,獲取Iterable使用Iterable<T> get()方法。
  • ReducingState<T>: 它儲存了一個聚合了所有新增到這個狀態的值的結果。介面和ListState相同,但是使用add(T)方法本質是使用指定ReduceFunction的聚合行為。
  • AggregatingState<IN, OUT>: 它儲存了一個聚合了所有新增到這個狀態的值的結果。與ReducingState想反,聚合型別可能不同於新增到狀態的元素的型別。介面和ListState相同,但是使用add(IN)新增的元素通過使用指定的AggregateFunction進行聚合。
  • FoldingState<T, ACC>:它儲存了一個聚合了所有新增到這個狀態的值的結果。與ReducingState想反,聚合型別可能不同於新增到狀態的元素的型別。介面和ListState相同,但是使用add(IN)新增的元素通過使用指定的FoldFunction摺疊進行聚合。
  • MapState<UK, UV>:它儲存了一個對映列表。你可以將key-value對放入狀態中,並通過Iterable檢索所有當前儲存的對映關係。使用put(UK, UV) 或 putAll(Map<UK, UV>)新增對映關係。使用get(UK)獲取key相關的value。分別使用entries(), keys() 和 values() 獲取對映關係,key和value的檢視。
    所有型別的狀態都有一個clear()方法,它清除當前活躍key(即輸入元素的key)的狀態。
    注意 FoldingState 和 FoldingStateDescriptor在Flink1.4中已經被廢棄,並且可能在將來完全刪除。請使用AggregatingState和 AggregatingStateDescriptor替代。
    首先需要記住的是這些狀態物件只能用來與狀態進行互動。狀態不一定儲存在記憶體中,但是可能儲存在磁碟或者其他地方。第二個需要記住的是,從狀態獲取的值依賴於輸入元素的key。因此如果包含不同的key,那麼在你的使用者函式中的一個呼叫獲得的值和另一個呼叫獲得值可能不同。
    為了獲得狀態控制程式碼,必須建立一個StateDescriptor。它維護了狀態的名稱(稍後將看到,你可以建立多個狀態,並且他們必須有唯一的名稱,以便你可以引用它們),狀態維護的值的型別,和可能使用者指定的function,例如ReduceFunction。根據你想要查詢的狀態的型別,你可以建立ValueStateDescriptor,ListStateDescriptor,ReducingStateDescriptor,FoldingStateDescriptor或MapStateDescriptor。
    使用RuntimeContext訪問狀態,因此它只有在rich function中才可以使用。rich function的相關資訊請看這裡,但是我們也很快會看到一個示例。RichFunction中,RuntimeContext有這些訪問狀態的方法:
  • ValueState<T> getState(ValueStateDescriptor<T>)
  • ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
  • ListState<T> getListState(ListStateDescriptor<T>)
  • AggregatingState<IN, OUT> getAggregatingState(AggregatingState<IN, OUT>)
  • FoldingState<T, ACC> getFoldingState(FoldingStateDescriptor<T, ACC>)
  • MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)

這是一個顯示了所有部分如何組合在一起的FlatMapFunction示例:

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

    /**
     * The ValueState handle. The first field is the count, the second field a running sum.
     */
    private transient ValueState<Tuple2<Long, Long>> sum;

    @Override
    public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {

        // access the state value
        Tuple2<Long, Long> currentSum = sum.value();

        // update the count
        currentSum.f0 += 1;

        // add the second field of the input value
        currentSum.f1 += input.f1;

        // update the state
        sum.update(currentSum);

        // if the count reaches 2, emit the average and clear the state
        if (currentSum.f0 >= 2) {
            out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
            sum.clear();
        }
    }

    @Override
    public void open(Configuration config) {
        ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
                new ValueStateDescriptor<>(
                        "average", // the state name
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {}), // type information
                        Tuple2.of(0L, 0L)); // default value of the state, if nothing was set
        sum = getRuntimeContext().getState(descriptor);
    }
}

// this can be used in a streaming program like this (assuming we have a StreamExecutionEnvironment env)
env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 4L), Tuple2.of(1L, 2L))
    .keyBy(0)
    .flatMap(new CountWindowAverage())
    .print();

// the printed output will be (1,4) and (1,5)

這個例子實現了一個計數視窗。我們以元組的第一個屬性為key(在示例中都有相同的key 1)。該函式儲存計數器和一個累加和到ValueState中。一旦計數器達到2,它會發出平均值並且清空狀態以便重新從0開始。注意,如果我們在元組的第一個屬性中有不同的值,那麼將為每個不同的輸入key保留不同的狀態值。

State in the Scala DataStream API

除了上面描述的介面,Scala API在KeyedStream上為使用單個ValueState的有狀態的map() 或 flatMap() 函式提供了快捷方式。使用者函式在Option中獲取ValueState的當前值,並且必須返回一個更新後的值,該值將用於更新狀態。

val stream: DataStream[(String, Int)] = ...

val counts: DataStream[(String, Int)] = stream
  .keyBy(_._1)
  .mapWithState((in: (String, Int), count: Option[Int]) =>
    count match {
      case Some(c) => ( (in._1, c), Some(c + in._2) )
      case None => ( (in._1, 0), Some(in._2) )
    })

Using Managed Operator State

為了使用managed operator state,有狀態的函式可以實現更通用的CheckpointedFunction介面,或者ListCheckpointed<T extends Serializable>介面。

CheckpointedFunction

CheckpointedFunction介面提供了訪問具備不同的重新分配策略的非keyed狀態。它需要方式的實現:

void snapshotState(FunctionSnapshotContext context) throws Exception;

void initializeState(FunctionInitializationContext context) throws Exception;

每當要執行checkpoint時,都會呼叫snapshotState()方法。對應的 initializeState()在每次使用者定義的函式初始化時呼叫,即函式第一次初始化或者函式從較早的checkpoint恢復時。因此initializeState()不僅是不同型別的狀態初始化的地方,也是包含恢復邏輯的地方。
目前,支援列表風格的managed操作符狀態。狀態期望是一個可序列化物件的列表,每個元素都是獨立的,因此可以在彈性擴容時重新分配。換句話說,這些物件是非keyed狀態可重新分配的最佳粒度。根據狀態訪問方法,定義了下屬重新分配方案:

  • Even-split redistribution: 每個操作符返回一個狀態元素列表。完整的狀態邏輯上是所有列表的連線。在恢復/重新分配時,列表被均勻的分成操作符並行度數量相同的子列表。每個操作符獲得一個子列表,它可以是空的,或者包含一個或多個元素。例如,如果操作符的並行度為1,checkpoint包含元素element1和element2,當並行度增加到2時,element1可能分配到操作符例項0中,而element2分配到操作符例項1中。

  • Union redistribution:每個操作符返回一個狀態元素列表。完整的狀態邏輯上是所有列表的連線。在恢復/重新分配時,每個操作符獲得狀態元素的完整列表。
    下面有一個有狀態的SinkFunction示例,它使用CheckpointedFunction來快取將傳送到外部世界的元素。它展示了基本的均勻在分配列表狀態:

    public class BufferingSink
          implements SinkFunction<Tuple2<String, Integer>>,
                 CheckpointedFunction {
    
        private final int threshold;
    
        private transient ListState<Tuple2<String, Integer>> checkpointedState;
    
        private List<Tuple2<String, Integer>> bufferedElements;
    
        public BufferingSink(int threshold) {
            this.threshold = threshold;
            this.bufferedElements = new ArrayList<>();
        }
    
        @Override
        public void invoke(Tuple2<String, Integer> value) throws Exception {
            bufferedElements.add(value);
            if (bufferedElements.size() == threshold) {
                for (Tuple2<String, Integer> element: bufferedElements) {
                    // send it to the sink
                }
                bufferedElements.clear();
            }
        }
    
        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {
            checkpointedState.clear();
            for (Tuple2<String, Integer> element : bufferedElements) {
                checkpointedState.add(element);
            }
        }
    
        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            ListStateDescriptor<Tuple2<String, Integer>> descriptor =
                new ListStateDescriptor<>(
                    "buffered-elements",
                    TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {}));
    
            checkpointedState = context.getOperatorStateStore().getListState(descriptor);
    
            if (context.isRestored()) {
                for (Tuple2<String, Integer> element : checkpointedState.get()) {
                    bufferedElements.add(element);
                }
            }
        }
    }
    

initializeState方法接受FunctionInitializationContext作為引數。它用來初始化非keyed狀態“容器”。上面是ListState型別的容器,當進行checkpoint時非keyed狀態的物件儲存在ListState中。
注意狀態是如何初始化的,類似於keyed狀態,有一個包含狀態的名稱和狀態所持有的狀態的資訊的StateDescriptor:

ListStateDescriptor<Tuple2<String, Integer>> descriptor =
    new ListStateDescriptor<>(
        "buffered-elements",
        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {}));

checkpointedState = context.getOperatorStateStore().getListState(descriptor);

狀態訪問方法的命名約定包含它的狀態結構的重新分配的模式。例如,在恢復時使用Union redistribution方案的list state,通過使用getUnionListState(descriptor)方法訪問狀態。如果方法名不包含重新分配模式,例如getListState(descriptor),它意味著重新分配方案使用基本的even-split redistribution。
初始化容器後,我們使用context的isRestored()方法來檢查我們是否正在從故障中恢復。如果是true,也就是正在恢復中,則應用恢復邏輯。
就像BufferingSink程式碼中所示,在狀態初始化時恢復的ListState儲存在一個類變數中,以便snapshotState()中使用。ListState清除所有前一個checkpoint包含的所有物件,然後填充我們想要checkpoint的新物件。
另外,keyed狀態也能在 initializeState() 方法中初始化。這通過使用提供的FunctionInitializationContext實現。

ListCheckpointed

ListCheckpointed介面是CheckpointedFunction的限制更嚴的變體,它只支援恢復時使用even-split redistribution方案的列表風格的狀態。它也要求實現兩個方法:

List<T> snapshotState(long checkpointId, long timestamp) throws Exception;

void restoreState(List<T> state) throws Exception;

在snapshotState()上操作符應該返回一個checkpoint的物件列表,並且恢復時restoreState必須處理這樣一個列表。如果狀態是不可分割的,你可以在snapshotState()上總是返回Collections.singletonList(MY_STATE)。

Stateful Source Functions

有狀態的Source相比其它操作符需要關注多一點。為了保證狀態和輸出集合的更新是原子的(精確一次語義在故障/恢復時要求),使用者要求從Source的context中獲取鎖。

public static class CounterSource
        extends RichParallelSourceFunction<Long>
        implements ListCheckpointed<Long> {

    /**  current offset for exactly once semantics */
    private Long offset;

    /** flag for job cancellation */
    private volatile boolean isRunning = true;

    @Override
    public void run(SourceContext<Long> ctx) {
        final Object lock = ctx.getCheckpointLock();

        while (isRunning) {
            // output and state update are atomic
            synchronized (lock) {
                ctx.collect(offset);
                offset += 1;
            }
        }
    }

    @Override
    public void cancel() {
        isRunning = false;
    }

    @Override
    public List<Long> snapshotState(long checkpointId, long checkpointTimestamp) {
        return Collections.singletonList(offset);
    }

    @Override
    public void restoreState(List<Long> state) {
        for (Long s : state)
            offset = s;
    }
}

一些操作符當一個checkpoint被Flink完全確認時可能需要與外部世界通訊。在這種情況下見org.apache.flink.runtime.state.CheckpointListener介面。

相關文章