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 的 keyed state,可通过 RuntimeContext
访问容错状态,类似于其他有状态函数访问 keyed state 的方式。
计时器允许应用程序对处理时间和事件时间的变化做出反应。每次调用函数 processElement(...)
都会获得一个 Context
对象,该对象可以访问元素的事件时间戳和 TimerService。TimerService
可用于为将来的事件/处理时间 Moment 注册回调。对于事件时间计时器,当前水印前进到或超过计时器的时间戳时,将调用 onTimer(...)
方法,而对于处理时间计时器,当挂钟时间达到指定时间时,将调用 onTimer(...)。
在该调用期间,所有状态的作用域再次限定为创建计时器时使用的键,从而允许计时器操作键控状态。
如果要访问键控状态和计时器,则必须在键控流上应用 ProcessFunction
:
stream.keyBy(...).process(new MyProcessFunction());
Low-level Joins(低级联接)
若要对两个 输入流 实现低级联接操作,应用程序可以使用 CoProcessFunction
或 KeyedCoProcessFunction
。此函数绑定到两个不同的输入,并获取对来自两个不同输入的记录的 processElement1(...)
和 processElement2(...)
的单独调用。
实现低级别联接通常遵循以下模式:
- Create a state object for one input (or both)
为一个 Input(或两个)创建一个 state 对象 - Update the state upon receiving elements from its input
从输入接收元素时更新状态 - Upon receiving elements from the other input, probe the state and produce the joined result
从另一个 input 接收到元素后,探测 state 并生成 join 结果
例如,您可能将客户数据联接到金融交易中,同时保留客户数据的状态。如果您关心在面对无序事件时具有完整和确定性的联接,则可以使用计时器来评估并在客户数据流的水印超过该交易的时间时发出该交易的联接。
Example # 示例
在以下示例中,KeyedProcessFunction
维护每个键的计数,并在一分钟过去时(在事件时间内)发出一个键/计数对,而该键没有更新:
1. count、key 和 last-modification-timestamp 存储在 ValueState 中,该 ValueState
的作用域由 key 隐式限定。
2. 对于每条记录,KeyedProcessFunction
递增计数器并设置上次修改时间戳
3.该函数还会在未来一分钟(以事件时间为单位)安排回调
4.在每次回调时,它会根据存储计数的最后修改时间检查回调的事件时间戳,如果它们匹配,则发出 key/count(即,在该分钟内没有发生进一步的更新)
这个简单的示例可以通过会话窗口实现。我们在这里使用 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(OpenContext openContext) 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,安排下一个计时器从当前事件时间开始60秒
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));
}
}
}
The KeyedProcessFunction
KeyedProcessFunction
作为 ProcessFunction
的扩展,在其 onTimer(...)
方法中提供对计时器键的访问;
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<OUT> out) throws Exception {
K key = ctx.getCurrentKey();
// ...
}
Timers # 计时器
两种类型的计时器(processing-time 和 event-time)都由 TimerService
内部维护,并排队等待执行。
TimerService
按键和时间戳删除重复的计时器,即每个键和时间戳最多有一个计时器。如果为同一时间戳注册了多个计时器,则 onTimer()
方法将只调用一次。
Flink 同步 onTimer()
和 processElement()
的调用。因此,用户不必担心 state 的并发修改。
Fault Tolerance (容错)
计时器具有容错能力,并与应用程序的状态一起设置检查点。如果发生故障恢复或从 Savepoint 启动应用程序,计时器将被恢复。
本应在恢复之前触发的检查点处理时间计时器将立即触发。当应用程序从故障中恢复或从 Savepoint 启动时,可能会发生这种情况。
计时器总是异步检查点的,除了 RocksDB 后端 / 与增量快照 / 基于堆的计时器的组合(将通过 FLINK-10026
解决)。请注意,大量计时器可能会增加检查点时间,因为计时器是检查点状态的一部分。有关如何减少计时器数量的建议,请参阅 “Timer Coalescing” 部分。
Timer Coalescing(计时器合并)
由于 Flink 为每个键和时间戳只维护一个计时器,因此您可以通过降低计时器分辨率来合并它们来减少计时器的数量。
对于 1 秒(事件或处理时间)的计时器分辨率,您可以将目标时间四舍五入到整秒。计时器将以毫秒级精度触发,但不会晚于请求的 1 秒。因此,每个键和秒最多有一个计时器。
long coalescedTime = ((ctx.timestamp() + timeout) / 1000) * 1000;
ctx.timerService().registerProcessingTimeTimer(coalescedTime);
由于事件时间计时器仅在 watermark 传入时触发,因此您还可以使用当前 watermark 来安排这些计时器并将其与下一个 watermark 合并:
long coalescedTime = ctx.timerService().currentWatermark() + 1;
ctx.timerService().registerEventTimeTimer(coalescedTime);
还可以按如下方式停止和删除计时器:
停止处理时间计时器:
long timestampOfTimerToStop = ...;
ctx.timerService().deleteProcessingTimeTimer(timestampOfTimerToStop);
停止事件时间计时器:
long timestampOfTimerToStop = ...;
ctx.timerService().deleteEventTimeTimer(timestampOfTimerToStop);
如果未注册具有给定时间戳的此类计时器,则停止计时器不起作用。