Flink之状态之状态获取
1.什么是状态
对于任何一个操作,都可以被看成是一个函数,比如y=f(x),如果对于同一个x的任何一次输入,得到的y都是相同的,则可以认为这个函数是无状态,否则,这个函数就是有状态的。Flink的一大特点就在于对状态的支持。
2.Keyed State和Operator State
Keyed State
Keyed State正如其名,总是和具体的key相关联,也只能在keyedStream的function和operator上使用。
Keyed State可以被当做是Operator State的一种特例,但是被分区或者分片的,对于某个key在某个分区上有唯一的状态。逻辑上,key state总是对应一个 <parallel-operator-instance, key>二元组,某种程度上,由于某个具体的Key总是属于某个具体的并行实例,这种情况下,也可以被简化认为是 <operator, key>。
Keyed State会被组织成Key Group。Key Group是可以被Flink用来进行重分布的最小单元,所以有多少个并发,就会有多少个Key Group。在执行过程中,每个keyed operator的并发实例会处理来自不同key的不同的Key Group。
Operator State
对Operator State而言,每个operator state都对应一个并行实例。Kafka Connector就是一个很好的例子。每个Kafka consumer的并行实例都会持有一份topic partition 和offset的map,这个map就是它的Operator State。
Operator State可以在并行度发生变化的时候将状态在所有的并行实例中进行重分布,并且提供了多种方式来进行重分布。
3.托管状态和非托管状态
Keyed State和Operator State都有两种存在形式,即托管状态和非托管状态。
托管状态可以使用flink runtime提供数据结构来实现,比如internal hash table和RocksDB。具体有ValueState,ListState等。Flink runtime会对这些状态进行编码并写入到checkpoints。
非托管状态使用用户自己的数据结构来实现。当做checkpoints时,非托管状态会以字节流的形式被写入checkpoints。Flink对托管状态的数据结构一无所知,只认为他们是一堆字节数组。
datastream的所有function都可以使用托管状态,非托管状态只能在实现operator的时候使用。相对于非托管状态,推荐使用托管状态,因为如果使用托管状态,Flink可以自动帮你进行状态重分布,也可以更好的做内存管理。
注意:如果你的托管状态需要特殊的序列化,目前Flink还不支持。
4.使用托管Keyed State
有如下的状态可以使用:
ValueState<T>
:保持一个可以更新和获取的值(每个Key一个value),可以用来update(T)更新,用来T value()获取。
ListState<T>
: 保持一个值的列表,用add(T)
或者 addAll(List<T>)来添加,用Iterable<T> get()来获取。
ReducingState<T>
: 保持一个值,这个值是状态的很多值的聚合结果,接口和ListState类似,但是可以用相应的ReduceFunction来聚合。
AggregatingState<IN, OUT>
:保持很多值的聚合结果的单一值,与ReducingState相比,不同点在于聚合类型可以和元素类型不同,提供AggregateFunction来实现聚合。
FoldingState<T, ACC>
: 与AggregatingState类似,除了使用FoldFunction进行聚合。
MapState<UK, UV>
: 保持一组映射,可以将kv放进这个状态,使用put(UK, UV)
or putAll(Map<UK, UV>)添加,或者使用get(UK)获取。
所有类型的状态都有一个clear()方法,可以清除当前的状态。
注意:FoldingState已经不推荐使用,可以用AggregatingState来代替。
需要注意,如上的状态对象只用来和状态打交道,可能会被存储在磁盘或者其他地方。另外,你拿到的状态的值是与key相关的,所以在这个实例中拿到的值可能和别的实例中拿到的不一样。
要使用一个状态对象,需要先创建一个StateDescriptor,他包含了状态的名字,状态的值的类型,或许还有一个用户定义的函数,比如ReduceFunction。取决于你要使用的state,你可以创建ValueStateDescriptor
或者 ListStateDescriptor
或者 ReducingStateDescriptor
或者 FoldingStateDescriptor
或者 MapStateDescriptor。
状态只能通过RuntimeContext获取,所以只能是在rich functions里面。通过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)
5.使用托管Operator State
为了使用托管的Operator State,必须有一个有状态的函数,这个函数比较继承CheckpointedFunction或者ListCheckpointed<T extends Serializable>。
CheckpointedFunction
CheckpointedFunction有如下两个方法需要实现。
void snapshotState(FunctionSnapshotContext context) throws Exception; void initializeState(FunctionInitializationContext context) throws Exception;
当checkpoint执行的时候,snapshotState()就会被调用。initializeState()会在第一次运行的时候被调用,或者从更早的checkpoint恢复的时候被调用。
目前,支持List类型的托管状态。状态被期望是一个可序列话的对象的List,彼此独立,这样便于重分布。不同的状态获取方式,会导致不同的重分布策略:
Even-split redistribution:每个operator会返回一组状态,所有的状态就变成了统一的状态。在重分布或者恢复的时候,一组状态会被按照并行度分为子组,每个operator会得到一个子组。
Union redistribution: 每个operator会返回一组状态,所有的状态一起组成了统一的状态。在重分布或者恢复的时候,每个operator都会得到所有的状态。
如下示例是一个有状态的SinkFunction使用CheckpointedFunction来在发送到外部之前缓存数据,使用了Even-split策略。
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<String, Integer>>() {})); checkpointedState = context.getOperatorStateStore().getListState(descriptor); if (context.isRestored()) { for (Tuple2<String, Integer> element : checkpointedState.get()) { bufferedElements.add(element); } } } }
initializeState
有一个形参FunctionInitializationContext,用来初始化non-keyed状态容器。
注意上面代码中是如何初始化的,也是调用了StateDescriptor
来传递状态名字和状态的值的类型,如下:
ListStateDescriptor<Tuple2<String, Integer>> descriptor = new ListStateDescriptor<>( "buffered-elements", TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})); checkpointedState = context.getOperatorStateStore().getListState(descriptor);
状态存取方法的命名方式反映了重分布的方式,比如getUnionListState(descriptor),标志着使用list state并使用union 重分布。如果方法命名上没有反映重分布策略,比如getListState(descriptor),意味着最基础的even-split重分布策略会被使用。
初始化之后,使用isRestored()来判断是否是一个错误恢复。如果是,则需要执行错误分配的逻辑。
正如在上面的BufferingSink中所示,在状态初始化的时候恢复出来的ListState被保存在类变量中以便在snapshotState()中使用。然后ListState清空了上次checkpoint的对象,并填充了新的对象,以便做checkpoint。
再说一点,keyed state也可以在initializeState()中初始化,这个可以通过使用FunctionInitializationContext来实现。
ListCheckpointed
是一种受限的CheckpointedFunction,只支持List风格的状态和even-spit的重分布策略
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
与其他operator相比,有状态的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; } }
或许有些operator想知道什么时候checkpoint全部做完了,可以参考使用org.apache.flink.runtime.state.CheckpointListener。