时间
Flink 在流处理API中支持不同的时间概念:
- 事件时间(Event Time)每个事件在其生产设备上产生时的时间。通常由数据源产生,在进入Flink之前嵌入记录中。
- 进入时间(Ingestion Time)事件进入Flink的时间。
- 窗口处理时间(Window Processing Time)取自执行Operator的机器的系统时间。
理想情况,产生的数据被立即处理,那么事件时间等于处理时间。现实情况,因底层输入数据源、执行引擎和硬件的差异,事件时间和处理时间会有非常大的不同。
X 轴表示事件时间,Y 轴表示处理时间,虚线表示理想情况,红色实线表示现实情况。通过观察可以发现处理时间滞后(Processing Time Lag)和事件时间倾斜(Event Time Skew)的情况
设定时间特征
Flink DataStream程序的第一部分通常设置基本时间特性。该设置定义了数据流源的行为方式(例如,它们是否将分配时间戳),以及窗口操作应该使用的时间概念KeyedStream.timeWindow(Time.seconds(30))。设置方式如下:
// 获取运行时环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 三个设置三选一,不可同时设置
// 设置事件时间特征为EventTime,即事件时间。
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// 设置事件时间特征为IngestionTime,即进入时间。
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
// 设置事件时间特征为ProcessingTime,即窗口处理时间。
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
EventTime And Watermark(水位线)
watermark就是时间戳,由数据源嵌入或由Flink应用程序产生。用于处理使用eventTime时数据传入乱序(网络波动等)问题。
我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因,导致乱序的产生,特别是使用kafka的话,多个分区的数据无法保证有序。所以在进行window计算的时候,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark。
- 在事件顺序时,watermark表明小于其时间戳的事件均已送达且大于其时间戳的事件还没有被观察到。
下图中的数字代表事件时间戳,W(20)代表时间为20的时间戳。在水印到达之前,没有大于水印时间戳的事件到达。
- 在事件乱序时,大于watermark时间戳的事件可能会早于水印到达,例如下图中事件15在水印11到达前到达。
- 在分布式执行时,每个实例会将其收的的水印广播到与之相连的下一级节点。
下图标记的右侧为事件时间,事件时间的推进情况会被记录在每一个实例的缓存中,如map(2)的当前事件时间戳为17,这是因为此实例收到了水印W(17),并在收到此水印后将其广播到与window(1)和window(2)相连的传输通道中。
watermark的生成
通常,在接收到source的数据后,应该立刻生成watermark;但是,也可以在source后,应用简单的map或者filter操作后,再生成watermark。
注意:如果指定多次watermark,后面指定的会覆盖前面的值
自定义watermark生成器和内置watermark生成器
如果数据源没有嵌入水印,则应用程序必须自己生成水印以确保基于事件时间的窗口能够正常工作。为此,DataStream API提供了自定义水印生成器和内置水印生成器。
watermarks 的生成方式有两种
- With Periodic Watermarks
- 周期性的触发watermark的生成和发送,默认是100ms
- 每隔N秒自动向流里注入一个WATERMARK,时间间隔由ExecutionConfig.setAutoWatermarkInterval 决定,每次调用getCurrentWatermark 方法, 如果得到的WATERMARK 不为空且比之前得到的大就注入流中。
- 可以定义一个最大允许乱序的时间,这种比较常用
- 使用方法:实现AssignerWithPeriodicWatermarks接口
- With Punctuated Watermarks
- 基于某些事件触发watermark 的生成和发送
- 基于事件向流里注入一个WATERMARK,每一个元素都有机会判断是否生成一个WATERMARK,如果得到的WATERMARK 不为空并且比之前的大就注入流中
- 使用方法:实现AssignerWithPunctuatedWatermarks接口
周期性watermark生成器(Periodic Watermark)
周期性watermark生成器根据事件事件或处理事件周期性地触发watermark的生成和发送,两个水印时间戳之间并不一定具有固定时间间隔,方法原型如下:
/**
* This generator generates watermarks assuming that elements arrive out of order,
* but only to a certain degree. The latest elements for a certain timestamp t will arrive
* at most n milliseconds after the earliest elements for timestamp t.
*/
public class BoundedOutOfOrdernessGenerator implements AssignerWithPeriodicWatermarks<MyEvent> {
private final long maxOutOfOrderness = 3500; // 3.5 seconds
private long currentMaxTimestamp;
// 从数据本事中提取EventTime
@Override
public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
long timestamp = element.getCreationTime();
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
return timestamp;
}
// 获取当前Watermark,利用currentMaxTimestamp - maxOutOfOrderness(允许数据的最大时间)
@Override
public Watermark getCurrentWatermark() {
// return the watermark as current highest timestamp minus the out-of-orderness bound
return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
}
间歇性watermark生成器
间歇性watermark生成器在观察到某个时间后,会计算某个条件决定是否生成水印。方法原型如下:
public class PunctuatedAssigner implements AssignerWithPunctuatedWatermarks<MyEvent> {
@Override
public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
return element.getCreationTime();
}
@Override
public Watermark checkAndGetNextWatermark(MyEvent lastElement, long extractedTimestamp) {
return lastElement.hasWatermarkMarker() ? new Watermark(extractedTimestamp) : null;
}
}
递增式watermark生成器
递增式watermark生成器用于生成完美水印,用于顺序的无界数据集,这种watermark的含义是:小于其时间戳的元素均以到达并且大于其时间戳的元素还尚未被观察到。
Timestamps per Kafka Partition
使用Kafka作为数据源时,每个分区的消息时间通常时递增的,但Source节点从多个消息分区并行拉取数据时这种时间特征会被破坏,这时可以在连接器端创建Kafka分区水印,以确保分区消息的升序排列,代码如下:
FlinkKafkaConsumer09<MyType> kafkaSource = new FlinkKafkaConsumer09<>("myTopic", schema, props);
kafkaSource.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<MyType>() {
@Override
public long extractAscendingTimestamp(MyType element) {
return element.eventTimestamp();
}
});
DataStream<MyType> stream = env.addSource(kafkaSource);