1. Working with State
本部分将介绍Flink提供的写状态程序的API。
1.1 Keyed DataStream
如果想使用键控状态(keyed state),首先需要指定一个DataStream的key,以key将状态分区。通常使用keyBy(KeySelector/键选择器)指定DataStream的key,形成一个键控流(KeyedStream),允许在其上运用键控状态。
键选择器函数(key selector function)将处理的每条记录数据作为输入,返回一每条记录的key。key可以是任意的类型,但是必须从确定性的计算得出。
Flink的数据模型不是基于key-value对,因此不需要物理地将数据转换成k-v对,key是虚拟的。
下面是一个简单的例子展示键选择器函数返回对象的字段作为key:
// some ordinary POJO
public class WC {
public String word;
public int count;
public String getWord() { return word; }
}
DataStream<WC> words = // [...]
KeyedStream<WC> keyed = words
.keyBy(WC::getWord);
1.1.1 Tuple Keys and Expression Keys
有两种方法定义key,Tuple Keys 和 Expression Keys,可以分别用元组的索引或对象的字段指定key。使用 Expression Keys结合Java的lambda表达式非常简单且可以减少运行时开销。
2. Using Keyed State
有以下五种键空状态:
- ValueState<T>
对每个key保存一个值,可以更新和取回;设置或更新使用update(T)方法,取回用T value()方法;
- ListState<T>
对每个key保存其value成一个list,可以向其中添加元素和获取一个Tterable。添加元素用add(T)或addAll(List<T>),获取元素可以使用Iterable<T> get(),也可以用update(List<T>)替换原来的list。
- ReducingState<T>
保存被添加进来的值的聚合结果,和ListState相似,除了用add(T)添加进来的值被用一个ReduceFunction函数聚合。
- AggregatingState<IN, OUT>
和ReducingState很相似,相比于ReduceState,它的聚合类型可能和输入值的类型不同;和ListState在添加元素时也很相似,除了被添加的元素被指定的AggregateFunction函数聚合。
- MapState<UK, UV>
保存映射关系。可以存放key-value对和获取一个保存所有映射关系的Iterable。可以通过put(UK, UV)或者putAll(Map<UK, UV>)添加元素。可以通过get(UK)获取单个键所对应的值。获取所有的映射、所有的key、所有的value可以分别使用entries(), keys(), values()。也可以使用isEmpty()检查map是否有映射。
所有状态都有一个clear()方法用于清空当前key的状态。
创建状态,需要首先需要创建StateDescriptor。它保存状态的名称、状态值类型或者用户自定义的一个函数,如RecduceFunction。根据需要类型的状态,对应以下五种StateDescriptor:
ValueStateDescriptor
ListStateDescriptor
AggregatingStateDescriptor
ReducingStateDescriptor
MapStateDescriptor
使用状态需要运行时环境(RuntimeContext),只能来源于rich Functions。从RichFunction的运行是环境中可以通过以下方法获得所需要的状态:
ValueState getState(ValueStateDescriptor)
ReducingState getReducingState(ReducingStateDescriptor)
ListState getListState(ListStateDescriptor)
AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)
MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class ValueStateRFlatMap {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<Tuple2<Long, Long>> inputStream = env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 4L), Tuple2.of(1L, 2L));
// 此处用到Java 的lambda表达式和Scala推演符号不同
KeyedStream<Tuple2<Long, Long>, Long> tuple2LongKeyedStream = inputStream.keyBy(value -> value.f0);
SingleOutputStreamOperator<Tuple2<Long, Long>> tuple2SingleOutput = tuple2LongKeyedStream.flatMap(new CountWindowAverage());
// 打印结果
tuple2SingleOutput.print();
env.execute();
}
}
class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {
// 定义值类型的状态 是一个两个值的Tuple类型
private transient ValueState<Tuple2<Long, Long>> sum;
@Override
public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {
// 获取状态
Tuple2<Long, Long> currentValue = sum.value();
// 如果状态为空,则将值保存在状态中,否者输出(input.f0, (currentValue.f1 + input.f1) / 2))元组
if (currentValue == null) {
sum.update(input);
} else {
out.collect(Tuple2.of(input.f0, (currentValue.f1 + input.f1) / 2));
sum.clear();
}
}
@Override
public void open(Configuration parameters) throws Exception {
// sum = getRuntimeContext().getState(new ValueStateDescriptor<>("average"
// , TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
// })));
// 用于设置状态存活周期
StateTtlConfig stateTtlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
// 设置状态的更行类型1.OnCreateAndWrite(默认类型):创建和写入的时候更新状态;OnReadAndWrite:除创建和写入,读的时候也可以更新状态
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
// 设置过期的状态是否还能读取,1.NeverReturnExpired(默认设置):过期的状态不能再读取2.ReturnExpiredIfNotCleanedUp过期后,仍可以读取,在cleanup之前
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
"average",
TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {
}));
descriptor.enableTimeToLive(stateTtlConfig);
sum = getRuntimeContext().getState(descriptor);
}
}
2.1 State Time-To-Live (TTL)
可以对键空状态设置TTL(time-to-live),当状态生命周期结束会自动被清除。
所有集合类型状态都支持per-entry TTLs,意味着list和map类型的状态可以对单个元素或键值对设置TTL。
使用状态TTL必须首先构建StateTtlConfig配置对象,TTL功能可以通过状态描述符(state descriptor)传递。
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);
TTL配置有以下几个选择性考虑:
首先newBuilder方法的参数是必须的,它设置TTL的值。
The update type configures when the state TTL is refreshed (by default OnCreateAndWrite):
StateTtlConfig.UpdateType.OnCreateAndWrite - only on creation and write access
StateTtlConfig.UpdateType.OnReadAndWrite - also on read access
The state visibility configures whether the expired value is returned on read access if it is not cleaned up yet (by default NeverReturnExpired):
StateTtlConfig.StateVisibility.NeverReturnExpired - expired value is never returned
StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp - returned if still available
NeverReturnExpired意味着过期的状态不会再被返回,即使其还没被清除。
ReturnExpiredIfNotCleanedUp 意味着即使过期了只要状态没有被清除,都会被返回。
注意:
- 状态后端存储了最后一次修改的时间戳和用户值,意味着它增加了消耗状态的存储空间。堆状态后端存储一个Java对象以及其指向用户状态对象的索引和在内存中的原始长值。RocksDB状态后端每增加一个存储值、list 值、map键值对都会占用8字节的存储空间。
- Only TTLs in reference to processing time are currently supported.
- 当恢复状态,使用TTL设置窗台描述符,但没有体检配置TTL会褒奖用性错误和StateMigrationException异常。
- TTL配置不是checkpoing或savepoint的一部分,而是操作运行job的一种方式。
- map State目前支持null值但是该值的序列化必须能够处理null。如果序列化不支持null值,可能会用NullableSerializer包装,但是会消耗额外的内存。
6.State TTL在python语言中还不支持。
2.2 Cleanup of Expired State
默认过期的值在读取的时候会被清除。但是可以设置StateTtlConfig使cleanup机制失效。
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.disableCleanupInBackground()
.build();
Flink提供了更细粒度状态清理机制
2.2.1 Cleanup in full snapshot
为了缩减状态所占空间,可以在获取到状态的快照后激活清理状态机制。在目前的实现中,除了可以清除本地的过期状态,其他的本地状态不会被清除。可以在StateTtlConfig中设置:
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.time.Time;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupFullSnapshot()
.build();
这种快照方式不能在RocksDB状态后端的增量checkpointing中使用。
对于已经存在的job,清除状态的策略可以在StateTtlConfig中任何时候被激活或暂停,例如从savepoint中恢复。
2.2.2 Incremental cleanup
另一个清除状态的策略是增量的清除状态的元素。激活该种策略可以在每次读取或写入数据时执行一次该清除策略。系统的state backend会持有所有状态的⼀个全局迭代器。每⼀次当⽤⽤户访问状态,该迭代器就会增量迭代⼀个批次数据,检查是否存在过期的数据,如果存在就删除。
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupIncrementally(10, true)
.build();
该策略有两个参数,第一个是清除策略激活后每次检查几个条目,第二个参数定义是否每次处理记录时触发清除状态机制。默认每次检查5个条目不用每次处理记录时激活清除机制。
注意:
- 如果没有读取状态或没有处理记录,过期的状态会被持久化。
- 花费在增量清除状态上的时间将增加处理记录的延迟。
- 目前增量清除机制只对堆状态后端有效。对于RocksDB是无效的。
- 如果对状态后端和同步快照一起使用,当迭代的时候,全局迭代器会保存一个所有key的副本,因为其不支持同时修改。同时启动快照会增加内存消耗,如果时异步快照将不会有这种问题。
- 对于已经存在的job,清除状态的策略可以在StateTtlConfig中任何时候被激活或暂停,例如从savepoint中恢复。
2.2.3 Cleanup during RocksDB compaction
如果⽤户使⽤的是RocksDB作为状态后端实现,⽤户可以在RocksDB在做Compation的时候加⼊Filter,对过期的数据进⾏检查。删除过期数据。
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupInRocksdbCompactFilter(1000)
.build();
每次处理一定数量的状态后,RocksDB compaction filter会查询目前的时间戳检查其是否过期。可以传递一个值改变这个值。增加更新时间戳的频率可以改善清除过期状态的速率但是将减少compaction的性能,因为它使⽤本地代码中的JNI调⽤,因此会降低压缩性能。默认该值时1000。
You can activate debug logs from the native code of RocksDB filter by activating debug level for FlinkCompactionFilter:
log4j.logger.org.rocksdb.FlinkCompactionFilter=DEBUG
Notes:
- 调用该TTL filter时,会降低compaction的速度,因为TTL filter 需要解析上次访问状态的时间戳和检查将要被compaction每个key的状态。对于集合状态的状态,检查时也会调用每个被存储的元素。
- 如果这个特性和list state状态联合使用且其有不定长的元素,the native TTL filter has to call additionally a Flink java type serializer of the element over JNI per each state entry where at least the first element has expired to determine the offset of the next unexpired element。
- 对于已经存在的job,清除状态的策略可以在StateTtlConfig中任何时候被激活或暂停,例如从savepoint中恢复。
3. State in the Scala DataStream API
除了上述描述的接口,Scala API 还有具有状态的map()或flatMap()方法,其具有ValueState状态,可以应用在keyedStream中。用户函数获取当前在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) )
})
mapWithState和flatMapWithState的区别:
- map和flatMap的固有不同:前者是输入一个元素返回一个元素,后者则是输入一个元素可以返回0到多个元素
3. Operator State
算子状态(Operator State/ non-keyed state)是一种绑定在并行算子实例的状态。Kafka连接器是Flink算子状态的一个很好的应用示例。每一个并行的kafka消费者都会保存一个topic分区和offsets的映射作为算子状态。
算子状态支持分布式并行算子的并行度改变,有多种方案重新分配状态。
在Flink状态应用程序种一般不适用算子状态,它通常用在source/sink的应用场景种,因为没有key为状态进行分区。
4. Broadcast State
广播状态(Broadcast State)是一个特殊的算子状态。主要用来处理一条流被下游流的所有子任务所使用,它的状态可以被第二条流使用。有这样一个例子可以很好的解释广播流的使用场景:一个低吞吐量的流包含一系列评估来自另一个流的元素的规则。
广播流状态和算子状态区别在于算子流:
1.是一个map的格式
2.it is only available to specific operators that have as inputs a broadcasted stream and a non-broadcasted one, and
3.such an operator can have multiple broadcast states with different names.
5. Using Operator State
使用算子需要状态函数实现CheckpointFunction接口。
5.1 CheckpointedFunction
CheckpointedFunction 接口提供了多种分布式访问非键空状态方案,它需要实现以下两种方法:
void snapshotState(FunctionSnapshotContext context) throws Exception;
void initializeState(FunctionInitializationContext context) throws Exception;
当执行checkpoint就会调用snapshotState()方法。当状态函数第一次被初始化或状态函数从checkpoint恢复时相应的initializeState()方法。状态初始化或状态恢复都会调用initializeState()方法。
目前支持list类型的算子状态,算子状态应该是可以序列化对象的list,每个元素相互独立,因此可以重新分布于分布式计算中。基于获取状态的方法,有以下分布式方案可以选用:
1.Even-split redistribution(平均拆分):每个并行算子状态返回一个list类型状态,整个状态逻辑上由所有并行状态构成。在恢复或重新分配状态时,list被平均的分配到子任务中。每个算子维护一个sublist可以是空的list或者包含多个元素的list。例如,如果并行度是1,算子的检查点状态包含两个元素,当增加并行度为2时,这两个元素就会分别分配到子任务中。
2.Union redistribution:每个并行算子状态返回一个list类型状态,整个状态逻辑上由所有并行状态构成。在恢复或重新分配状态时,每个子任务获取一个完整的list,如果状态很大,最好不要这样使用。检查点元素据回为每个list entry保存一个offset,可能导致 RPC framesize or out-of-memory errors。
下面是一个 使用even-split redistribution 的示例,状态算子SinkFunction使用CheckpointedFunction先缓存一定的元素然后再将元素输出:
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import scala.Tuple2;
import java.util.ArrayList;
import java.util.List;
class BufferingSink implements SinkFunction<Tuple2<String, Integer>>, CheckpointedFunction {
// 定义缓存多少条数据后开始sink,示例中用final但没有初始化值
private int threshold;
// 定义checkpoint状态
private transient ListState<Tuple2<String, Integer>> checkpointState;
// 用于缓存数据
private List<Tuple2<String, Integer>> bufferedElements;
// 构造器
public BufferingSink(int threshold) {
this.threshold = threshold;
this.bufferedElements = new ArrayList<>();
}
@Override // sink操作
public void invoke(Tuple2<String, Integer> value, Context context) throws Exception {
bufferedElements.add(value);
if (bufferedElements.size() == threshold) {
for (Tuple2<String, Integer> element : bufferedElements) {
// send it to the sink
}
bufferedElements.clear();
}
}
@Override // checkpoint操作
public void snapshotState(FunctionSnapshotContext context) throws Exception {
checkpointState.clear();
for (Tuple2<String, Integer> element : bufferedElements) {
checkpointState.add(element);
}
}
@Override // 初始化状态或恢复状态
public void initializeState(FunctionInitializationContext context) throws Exception {
ListStateDescriptor<Tuple2<String, Integer>> descriptor =
new ListStateDescriptor<Tuple2<String, Integer>>(
"buffered-elements",
TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {
}));
checkpointState = context.getOperatorStateStore().getListState(descriptor);
if (context.isRestored()) {
for (Tuple2<String, Integer> element : checkpointState.get()) {
bufferedElements.add(element);
}
}
}
}
initializeState方法的FunctionInitializationContext参数是初始化非键空状态的一个容器,当设置检查点时会将非键空状态对象存放于ListState类型的状态中。
PS:键空状态对应 KeyedStateStore getKeyedStateStore()方法。
算子状态的初始化和键空状态的初始化很相似,使用StateDescriptor对象包含状态名称和状态值类型的信息:
ListStateDescriptor<Tuple2<String, Integer>> descriptor =
new ListStateDescriptor<>(
"buffered-elements",
TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {}));
checkpointedState = context.getOperatorStateStore().getListState(descriptor);
getListState(descriptor),getUnionListState(descriptor)分别对应上面个介绍的两种重分配分区策略。
初始化状态容器后使用isRestored()方法检查是否需要错误恢复。
6. Stateful Source Functions
状态源算子相对其他算子状态要复杂。为了保证对状态和输出集合的原子性,必须从源上下文环境获取锁。
public static class CounterSource
extends RichParallelSourceFunction<Long>
implements CheckpointedFunction {
/** current offset for exactly once semantics */
private Long offset = 0L;
/** flag for job cancellation */
private volatile boolean isRunning = true;
/** Our state object. */
private ListState<Long> state;
@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 void initializeState(FunctionInitializationContext context) throws Exception {
state = context.getOperatorStateStore().getListState(new ListStateDescriptor<>(
"state",
LongSerializer.INSTANCE));
// restore any state that we might already have to our fields, initialize state
// is also called in case of restore.
for (Long l : state.get()) {
offset = l;
}
}
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
state.clear();
state.add(offset);
}
}