Working with State
本文翻译自Streaming Guide/ Fault Tolerance / Working with State
----------------------------------------------------------------------------------------
Flink中所有transformation可能都看上去像是方法(在functional processing术语中),但事实上它们都是有状态的Operator。你可以通过使用Flink的状态接口或是将你的方法的实例域计入检查点来让所有transformatiuon(如map, filter等)都变成有状态的(stateful)。你可以通过实现一个接口来将任何实例的域注册作为managed状态。在该情况下以及使用FLink的原生状态接口的情况下,Flink将会自动地对你的状态进行周期性一致性快照,并且将它的值存储起来以防失败的发生。
最终的效果便是将任何形式的状态都更新到无失败执行与失败执行都一样的地步。
首先,我们介绍如何在失败的情况下使得实例的域仍是一致的,然后我们介绍Flink的状态接口。
默认地,状态检查点会存储在JobManager的内存中。为了长时间保留较大的状态,Flink支持将检查点存储在文件系统中(HDFS,S3或挂载后的POSIX文件系统),改设置可以通过配置文件flink-conf.yaml配置或是通过StreamExecutionEnvironment.setStateBackend(…)方法设置。有关如何配置可用的状态后端及其相关信息,请见文档state backend。
一、使用Key/Value状态接口
Key/Value状态接口提供了对许多不同类型的状态的访问入口,而这些状态都局限于当前输入element的关键字,这意味着这种类型的状态只可以用于KeyedStream,即通过stream.KeyBy(…)创建的流。
下面我们介绍三种可用的不同类型的状态,以及如何将它们用于程序中去。可用的原始状态如下:
· ValueState<T>:该状态维护一个可以更新和获取的值(如上所述的,它局限于输入element的关键字,所以对于一个Operation,它对于每个关键字可能都有一个值)。该值可以使用update(T)设置,可以使用T value()来获取。
· ListState<T>:该状态维护一个element列表。你可以追加element,也可以获取一个当前存储的element的Iterable。使用add(T)添加element,通过Iterable<T> get()获取Iterable。
· ReducingState<T>:改状态维护所有添加到这个状态的值的一个聚合的结果值。此接口与ListState相同,通过add(T)添加element,它需要指定一个ReduceFunction来对值进行聚合操作。
所有类型的状态都有一个clear()方法,它将清除当前活动的关键字(即当前输入lement的关键字)的状态。
开发者需要记住,这些状态对象只是用来与状态衔接的,而状态不一定要存储在这些对象里,它们还可以存储于硬盘或是其他地方。其次需要记住从状态中获取的值是取决于输入element的关键字的,所以如果element的关键字是不一样的,那么在你的用户方法中一次调用得到的值和另一次调用得到的值可能是不一样的。
为了获取状态句柄(handle),你必须创建一个StateDescriptor来维护状态的名字(在下面将会看到我们可以创建多个状态,所以它们需要一个唯一的名字来让我们引用)、状态中的值的类型以及可能的用户指定的方法(如一个ReduceFunction)。根据你想获取的状态类型,你可以创建ValueStateDescriptor、 ListStateDescriptor、 ReducingStateDescriptor之一。
状态可以通过RuntimeContext获取,所以它只在rich function中可用。详情请见链接,本文接下来会举一个简短的例子。RichFunction中可用的RuntimeContext拥有以下方法来访问状态:
· ValueState<T> getState(ValueStateDescriptor<T>)
· ReducingState<T> getReducingState(ReducingStateDescritpor<T>)
· ListState<T> getListState(ListStateDescriptor<T>)
这是一个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)
该例实现了一个计数窗口,我们以第一个域作为关键字(在该例中拥有一样的关键字"1")。该方法存储计数并在ValueState中运行加和,一旦计数到达2,它将发送平均值并且清除状态以从0重新开始。注意,如果输入tuple的第一个域的值不一样,则该方法将为每个输入关键字维护不同的状态。
1.1 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)
)
})
二、将实例域计入检查点
实例的域可以使用Checkpointed接口来计入检查点。
当我们使用用户定义方法来实现Checkpointed接口是,snapshotState(…)方法和restoreState(…)方法将会执行来快照并存储方法的状态。
此外,用户定义方法还可以实现CheckpointNotifier接口来通过notifyCheckpointComplete(long checkpointId)方法来接收检查点完成的通知。注意,若是在检查点完成和通知之间发生失效问题,Flink不保证用户方法可以收到通知。所以该通知应当设计为后来的检查点通知可以包括丢失的通知的形式。
上面ValueState的例子可以如下使用实例域:
public
class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long,
Long>, Tuple2<Long, Long>>
implements Checkpointed<Tuple2<Long,
Long>> {
private Tuple2<Long, Long> sum = null;
@Override
public void flatMap(Tuple2<Long,
Long> input, Collector<Tuple2<Long,
Long>> out) throws Exception {
//
update the count
sum.f0 += 1;
//
add the second field of the input value
sum.f1 +=
input.f1;
//
if the count reaches 2, emit the average and clear the state
if (sum.f0 >= 2)
{
out.collect(new
Tuple2<>(input.f0,
sum.f1 / sum.f0));
sum = Tuple2.of(0L,
0L);
}
}
@Override
public void open(Configuration
config) {
if (sum == null) {
// only recreate if null
// restoreState will be called before
open()
// so this will already set the sum to the
restored value
sum = Tuple2.of(0L,
0L);
}
}
//
regularly persists state during normal operation
@Override
public Serializable snapshotState(long
checkpointId, long
checkpointTimestamp) {
return sum;
}
//
restores state on recovery from failure
@Override
public void restoreState(Tuple2<Long,
Long> state) {
sum = state;
}
}
三、有状态数据源方法
有状态数据源方法与其他Operator相比,需要额外的一些处理。为了使状态的更新以及输出的收集是原子的(在失效恢复时的恰好执行一次语义时需要是原子的),用户需要从SourceContext获取一个lock
public static class CounterSource extends RichParallelSourceFunction<Long> implements Checkpointed<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 Long snapshotState(long checkpointId, long checkpointTimestamp) {
return offset;
}
@Override
public void restoreState(Long state) {
offset = state;
}
}
一些Operator在检查点被Flink完全接收时可能需要信息来与外界交流,该情况请见接口flink.streaming.api.checkpoint.CheckpointNotifier。
四、迭代Job中的状态检查点
Flink在当前版本中只对没有迭代的job提供保证,在迭代job上开启检查点机制将会导致一个异常。若要强制在迭代程序上使用检查点机制,用户需要在启用检查点时设置一个特殊标志:env.enableCheckpointing(interval, force = true)。
请注意,in flight in the loop edge的数据(以及由于它们而改变的状态)将会在失效中丢失。