在本节中,您将了解 Flink 提供的用于编写有状态程序的 API。请查看 Stateful Stream Processing以了解有状态流处理背后的概念。
Keyed
DataStream如果要使用Keyed state
,首先需要在 DataStream
上指定一个key
,该key
用于对状态进行分区。你可以在DataStream
上使用Java/Scala API
中的keyBy(KeySelector)
或Python API
中的key_by(KeySelector)
指定一个key
。这将产生一个KeyedStream
,然后允许使用keyed
的操作。
key
选择器函数将单个记录作为输入并返回该记录的key
。key
可以是任何类型,并且必须是从确定性计算中派生出来的。
Flink 的数据模型不是基于键值对的。因此不需要将数据集包装成键和值。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);
keyed state
keyed state
接口提供了对不同类型状态的访问,这些状态的范围都限定在当前输入元素的键内。这意味着这种类型的状态只能在 KeyedStream
上使用,它可以通过 Java/Scala API
中的 stream.keyBy(…)
或 Python API
中的 stream.key_by(…)
创建.
现在,我们将首先查看不同类型的可用状态,然后我们将看到如何在程序中使用它们。以下是不同类型的可用状态:
ValueState
:该状态只能保存一个可以更新和检索的值(如上所述,范围限定为输入元素的键内,所以此操作在每个键内可能都有一个值)。
ListState
:改状态是一个列表,用于保存多个元素。您可以在向改状态中追加元素,并且可以获取所有元素的Iterable
。使用 add(T)
或 addAll(List
添加元素,可以使用 Iterable
获取 Iterable
。您还可以使用 update(List
覆盖现有列表。
ReducingState
:改状态仅保存一个值,该值等于向该状态中添加的所有元素的聚合值。该接口类似于ListState
,但是使用add(T)
增加的元素会通过使用指定的ReduceFunction
进行聚合。该状态保存的就是聚合后的值。
AggregatingState
:改状态仅保存一个值,该值表示添加到该状态中的所有值的聚合结果。与 ReducingState 相反,聚合类型可能与添加到状态中的元素类型不同。该接口与 ListState
相同,但使用 add(IN)
添加到该状态中的元素将使用指定的 AggregateFunction
进行聚合。
MapState
:用于保存一个映射列表。您可以将键值对放入该状态并检索当前存储的所有映射的 Iterable
。使用 put(UK, UV)
或 putAll(Map
添加映射。可以使用 get(UK
) 检索值。可以分别使用 entry()、keys()
和 values()
检索映射、键和值。您还可以使用 isEmpty()
来检查此映射是否为空。
所有类型的状态也有一个 clear()
方法,用于清除当前key
的状态。
重要的是要记住,这些状态对象仅用于与状态相联系。状态不一定存储在内部,可能存储在磁盘或其他地方。要记住的第二件事是,您从状态中获得的值取决于输入元素的键。因此,如果涉及的键不同,返回的值有可能不同。
要获得状态句柄,您必须创建一个StateDescriptor
。StateDescriptor
包含状态的名称(正如我们稍后将看到的,您可以创建多个状态,并且它们必须具有唯一的名称以便您可以引用它们)、状态所保存值的类型,以及用户可能指定的函数,例如 ReduceFunction
。根据您要检索的状态类型,您可以创建 ValueStateDescriptor、ListStateDescriptor、AggregatingStateDescriptor、ReducingStateDescriptor
或 MapStateDescriptor
。
状态是使用 RuntimeContext
访问的。 RuntimeContext
具有以下访问状态的方法:
ValueState getState(ValueStateDescriptor)
ReducingState getReducingState(ReducingStateDescriptor)
ListState getListState(ListStateDescriptor)
AggregatingState getAggregatingState(AggregatingStateDescriptor)
MapState getMapState(MapStateDescriptor)
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(value -> value.f0)
.flatMap(new CountWindowAverage())
.print();
// the printed output will be (1,4) and (1,5)
可以将生存时间(TTL
)分配给任何类型的Keyed state
。如果配置了一个TTL
,并且一个状态值已经过期,则存储的值将会尽可能地清除,下面将详细讨论这个问题。
所有集合类型的状态都支持对每个条目进行ttl
设置。这意味着列表元素和映射项独立过期。
为了使用状态 TTL
,必须首先构建一个 StateTtlConfig
配置对象。通过配置,TTL
功能可以在任何状态描述符中启用:
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);
该配置有几个选项需要注意:
newBuilder
方法的第一个参数是必需的,它是生存时间值。
状态TTL的刷新通过更新的类型来决定(默认情况下为OnCreateAndWrite):
StateTtlConfig.UpdateType.OnCreateAndWrite
: 仅在创建和写入访问的时候刷新TTLStateTtlConfig.UpdateType.OnReadAndWrite
-:仅在读和写入的时候刷新TTL。状态可见性配置决定是否在读取时返回过期值(如果尚未清除)(默认为 NeverReturnExpired
):
StateTtlConfig.StateVisibility.NeverReturnExpired
:过期值从不会被返回StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp
:如果仍然可用则返回ReturnExpiredIfNotCleanedUp
允许在清除之前返回过期状态。
默认情况下,过期值会在读取时显式删除,例如 ValueState#value
,如果配置的状态后端支持,则在后台定期垃圾清理。可以在 StateTtlConfig
中禁用后台清理:
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.disableCleanupInBackground()
.build();
要对后台的某些特殊清理进行更细粒度的控制,您可以按如下所述单独配置它。目前,堆状态后端依赖于增量清理,RocksDB后端使用压缩过滤器进行后台清理。
此外,您可以在获取完整状态快照时激活清理,这将减少快照的大小。在当前实现下,本地状态不会被清除,但在从上一个快照恢复的情况下,它将不包括已删除的过期状态。可以在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 状态后端的增量检查点。
另一个选项是以增量方式触发某些状态项的清理。触发器可以是来自每个状态访或者处理每个记录的回调,如果此清理策略在特定状态下处于活动状态,则存储后端会在其所有条目上为此状态保留一个惰性全局迭代器,每次触发增量清理时,遍历的状态里的条目将被检查,过期的条目将被清除。
这个特性可以在 StateTtlConfig
中配置:
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupIncrementally(10, true)
.build();
这个策略有两个参数。第一个是每次清除触发时检查的状态条目的数量。每访问状态时都会触发。第二个参数定义是否在每个记录处理时额外触发清理。
提示:
StateTtlConfig
中激活或停用此清理策略,例如从保存点重新启动后。如果使用了RocksDB状态后端,则会调用一个Flink特定的压缩过滤器来进行后台清理。RocksDB定期运行异步压缩来合并状态从而更新状态并减少存储。Flink压缩过滤器检查带有TTL的状态项的过期时间戳,并清理过期值。
这个特性可以在 StateTtlConfig
中配置:
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupInRocksdbCompactFilter(1000)
.build();
RocksDB
压缩过滤器会在每次处理一定数量的状态条目后从 Flink 查询当前时间戳,用于检查过期时间。您可以更改它并将自定义值传递给 StateTtlConfig.newBuilder(...).cleanupInRocksdbCompactFilter(long queryTimeAfterNumEntries)
方法。更频繁地更新时间戳可以提高清理速度,但会降低压缩性能,因为它使用来自本机代码的 JNI
调用。 RocksDB
后端的默认后台清理每处理 1000
个条目就查询当前时间戳。
通过激活FlinkCompactionFilter
的调试级别,您可以从RocksDB
过滤器的本地代码中激活调试日志:
log4j.logger.org.rocksdb.FlinkCompactionFilter=DEBUG
提示:
StateTtlConfig
中激活或停用此清理策略,例如从保存点重新启动后。算子状态(或非键态)是绑定到一个并行算子实例的状态。Kafka连接器是在Flink中使用算子 State
的一个很好的例子。Kafka消费者的每个并行实例都维护一个主题分区和偏移量的映射,作为它的算子状态。
算子状态接口支持在并行度改变时在并行算子实例之间重新分配状态。这种再分配有不同的方案。
在典型的有状态 Flink 应用程序中,您不需要算子状态。它主要是一种特殊类型的状态。
注意: Python DataStream API
仍然不支持 算子 状态。
广播状态是一种特殊的算子状态。引入它是为了支持需要将一个流的记录广播到所有下游任务中,这些记录用于在所有子任务之间维护相同的状态。然后可以在处理第二个流的记录时访问此状态。举个例子,我们可以想象一个低吞吐量的流包含一组规则,我们想要利用这组规则对来自另一个流的所有元素进行评估。考虑到上述类型的用例,广播状态与其他算子状态的不同之处在于:
注意:Python DataStream API 仍然不支持广播状态。
要使用算子状态,有状态函数可以实现CheckpointedFunction
接口。
CheckpointedFunction
接口通过不同的重新分配方案提供对非键状态的访问。它需要实施两种方法:
void snapshotState(FunctionSnapshotContext context) throws Exception;
void initializeState(FunctionInitializationContext context) throws Exception;
每当需要执行检查点时,就会调用snapshotState()
。对应的initializeState()
在每次初始化用户定义函数时调用,无论是在函数首次初始化时,还是在函数实际上从较早的检查点恢复时。因此,initializeState()
不仅是初始化不同类型状态的地方,而且也是包含状态恢复逻辑的地方。
当前只支持列表类型的算子状态。状态应该是可序列化对象的列表,彼此独立。换句话说,这些对象是可以重新分配非键控状态的最佳粒度。根据状态访问方法,定义了以下重新分配方案:
下面是一个有状态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, Context contex) 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.get算子StateStore().getListState(descriptor);
if (context.isRestored()) {
for (Tuple2<String, Integer> element : checkpointedState.get()) {
bufferedElements.add(element);
}
}
}
}
initializeState
方法接受FunctionInitializationContext
作为参数。他是初始化非监控状态的容器。这些是ListState
的容器。其中非键控状态对象将在checkpoint
的时后被存储。
请注意状态是如何初始化的,类似于键控状态,其 StateDescriptor
包含状态名称和有关状态的其他信息。
ListStateDescriptor<Tuple2<String, Integer>> descriptor =
new ListStateDescriptor<>(
"buffered-elements",
TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {}));
checkpointedState = context.getOperatorStateStore().getListState(descriptor);
状态访问方法的命名约定包含了它的再分配模式和状态结构。例如,要在恢复时使用联合重新分配方案的列表状态,可以使用getUnionListState(descriptor)
访问状态。如果方法名不包含重新分配模式,例如getListState(descriptor)
,它仅仅意味着将使用基本的均匀分割重新分配模式。
初始化容器后,我们使用上下文的 isRestored()
方法来检查我们是否在失败后正在恢复。如果这是真的,即我们正在恢复,则应用恢复逻辑。
如修改后的BufferingSink
的代码所示,在状态初始化期间恢复的这个ListState
保存在一个类变量中,以便将来在snapshotState()
中使用。在那里,ListState
将清除前一个检查点包含的所有对象,然后填充我们想要checkpoint
的新对象。
顺便说明一下,监控状态也可以在initializeState()
方法中初始化。这可以使用提供的FunctionInitializationContext
来完成。
与其他操作符相比,有状态源需要更多的关注。为了使状态和输出集合的更新是原子的(对于故障/恢复时只需要一次语义),用户需要从源的上下文中获得一个锁。
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);
}
}
当Flink完全确认某个检查点时,某些算子可能需要该信息,以便与外部世界进行通信。在本例中,请参见org.apache.flink.api.common.state.CheckpointListener
接口。