ProcessFunction
ProcessFunction是一个低级流处理操作,允许访问所有(非循环)流应用程序的基本构建块:
- events (stream elements)
- state (fault-tolerant, consistent, only on keyed stream)
- timers (event time and processing time, only on keyed stream)
ProcessFunction可以被认为是一个FlatMapFunction,可以访问键控状态和计时器。它通过调用输入流中接收到的每个事件来处理事件。
对于容错状态,ProcessFunction提供了对Flink的键控状态的访问,可以通过RuntimeContext访问,类似于其他有状态函数访问键控状态的方式。
计时器允许应用程序对处理时间和事件时间的变化作出反应。每次调用函数processElement(…)都会获得一个Context对象,该对象允许访问元素的事件时间戳和TimerService。TimerService可用于为未来的事件/处理时间瞬间注册回调。对于事件时间计时器,当当前水印提前到或超过计时器的时间戳时调用onTimer(…)方法,而对于处理时间计时器,当挂钟时间到达指定时间时调用onTimer(…)方法。。在调用期间,所有状态再次限定为创建计时器时使用的键,从而允许计时器操纵键控状态。
如果你想访问键控状态和计时器,你必须在一个键控流上应用ProcessFunction:
stream.keyBy(...).process(new MyProcessFunction());
Low-level Joins
要实现对两个输入的低级操作,应用程序可以使用CoProcessFunction或KeyedCoProcessFunction。这个函数被绑定到两个不同的输入,并从两个不同的输入获取对processElement1(…)和processElement2(…)的单独调用。
实现低级连接通常遵循以下模式:
- 为一个(或两个)输入创建一个状态对象
- 从输入接收元素时更新状态
- 在从其他输入接收元素时,探测状态并生成连接结果
例如,您可能要将客户数据与金融交易连接起来,同时为客户数据保留状态。如果您关心在无序事件发生时具有完整和确定性的连接,那么您可以使用计时器来评估和发出交易的连接,当客户数据流的水印已经超过该交易的时间时。
举例
在下面的例子中,KeyedProcessFunction维护每个键的计数,并在一分钟过去(在事件时间内)没有更新该键时发出一个键/计数对:
- 计数、键和最后修改时间戳存储在ValueState中,它隐式地由键确定作用域
- 对于每个记录,KeyedProcessFunction递增计数器并设置最后修改的时间戳
- 该函数还安排了一个一分钟后的回调(事件时间)
- 在每个回调中,它检查回调的事件时间戳与存储的计数的最后修改时间,并发出键/计数,如果它们匹配(即,在这一分钟内没有发生进一步的更新)
这个简单的示例可以用会话窗口实现。我们在这里使用KeyedProcessFunction来说明它提供的基本模式。
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction.Context;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction.OnTimerContext;
import org.apache.flink.util.Collector;
// the source data stream
DataStream<Tuple2<String, String>> stream = ...;
// apply the process function onto a keyed stream
DataStream<Tuple2<String, Long>> result = stream
.keyBy(value -> value.f0)
.process(new CountWithTimeoutFunction());
/**
* The data type stored in the state
*/
public class CountWithTimestamp {
public String key;
public long count;
public long lastModified;
}
/**
* The implementation of the ProcessFunction that maintains the count and timeouts
*/
public class CountWithTimeoutFunction
extends KeyedProcessFunction<Tuple, Tuple2<String, String>, Tuple2<String, Long>> {
/** The state that is maintained by this process function */
private ValueState<CountWithTimestamp> state;
@Override
public void open(Configuration parameters) throws Exception {
state = getRuntimeContext().getState(new ValueStateDescriptor<>("myState", CountWithTimestamp.class));
}
@Override
public void processElement(
Tuple2<String, String> value,
Context ctx,
Collector<Tuple2<String, Long>> out) throws Exception {
// retrieve the current count
CountWithTimestamp current = state.value();
if (current == null) {
current = new CountWithTimestamp();
current.key = value.f0;
}
// update the state's count
current.count++;
// set the state's timestamp to the record's assigned event time timestamp
current.lastModified = ctx.timestamp();
// write the state back
state.update(current);
// schedule the next timer 60 seconds from the current event time
ctx.timerService().registerEventTimeTimer(current.lastModified + 60000);
}
@Override
public void onTimer(
long timestamp,
OnTimerContext ctx,
Collector<Tuple2<String, Long>> out) throws Exception {
// get the state for the key that scheduled the timer
CountWithTimestamp result = state.value();
// check if this is an outdated timer or the latest timer
if (timestamp == result.lastModified + 60000) {
// emit the state on timeout
out.collect(new Tuple2<String, Long>(result.key, result.count));
}
}
}
在Flink 1.4.0之前,当从processing-time计时器调用ProcessFunction.onTimer()方法时,将当前处理时间设置为事件时间戳。这种行为非常微妙,可能不会被用户注意到。这是有害的,因为处理时间的时间戳是不确定的,并且与水印不对齐。此外,用户实现的逻辑依赖于这个错误的时间戳,很可能是无意的错误。所以我们决定修复它。升级到1.4.0后,使用不正确事件时间戳的Flink作业将会失败,用户应该使他们的作业适应正确的逻辑。
The KeyedProcessFunction
KeyedProcessFunction,作为ProcessFunction的扩展,在它的onTimer(…)方法中提供了对计时器键的访问。
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<OUT> out) throws Exception {
K key = ctx.getCurrentKey();
// ...
}
Timers
这两种类型的计时器(处理时间和事件时间)都由TimerService在内部维护,并排队等待执行。
TimerService会根据每个键和时间戳重复删除计时器,也就是说,每个键和时间戳最多只能有一个计时器。如果为同一个时间戳注册了多个计时器,onTimer()方法只会被调用一次。
Flink同步onTimer()和processElement()的调用。因此,用户不必担心并发修改状态。
Fault Tolerance
计时器与应用程序的状态一起具有容错性和检查点性。在故障恢复或从保存点启动应用程序时,将恢复计时器。
在恢复之前应该启动的检查点处理时间计时器将立即启动。当应用程序从故障中恢复或从保存点启动时,可能会发生这种情况。
计时器总是异步检查点,除了RocksDB后端/与增量快照/与基于堆的计时器的组合(将通过FLINK-10026解析)。注意,大量的计时器会增加检查点时间,因为计时器是检查点状态的一部分。有关如何减少计时器数量的建议,请参阅“计时器合并”部分。
计时器合并
由于Flink每个键和时间戳只维护一个计时器,您可以通过减少计时器分辨率来合并它们来减少计时器的数量。
对于1秒的计时器分辨率(事件或处理时间),可以将目标时间四舍五入到整秒。计时器将最多提前1秒启动,但不迟于请求的毫秒精度。因此,每个键和秒最多有一个计时器。
long coalescedTime = ((ctx.timestamp() + timeout) / 1000) * 1000;
ctx.timerService().registerProcessingTimeTimer(coalescedTime);
由于事件时间计时器只触发水印进来,你也可以通过使用当前的水印来调度和合并这些计时器与下一个水印:
long coalescedTime = ctx.timerService().currentWatermark() + 1;
ctx.timerService().registerEventTimeTimer(coalescedTime);
计时器也可以按以下方式停止和删除:
停止处理时间计时器:
long timestampOfTimerToStop = ...;
ctx.timerService().deleteProcessingTimeTimer(timestampOfTimerToStop);
停止事件时间计时器:
long timestampOfTimerToStop = ...;
ctx.timerService().deleteEventTimeTimer(timestampOfTimerToStop);
如果没有注册具有给定时间戳的计时器,则停止计时器没有效果。