Watermark
Watermark 是一种衡量 Event Time 进展的机制。
Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark 机制结合 window 来实现。
数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了,因此,window 的执行也是由 Watermark 触发的。
Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于 maxEventTime - t 的所有数据都已经到达,如果有窗口停止时间等于maxEventTime – t,那么这个窗口被触发执行
Watermark数值的产生规则
Wartermark = max(event_time) – delay
Watermark 是基于数据携带的时间戳生成的,一旦 Watermark 比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行。由于 event time 是由数据携带的,因此,如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发,只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗
Watermark在代码中的引入
①特殊情况事先得知数据流的时间戳是单调递增的
也就是说没有乱序,那我们可以使用 assignAscendingTimestamps,这个方法会直接使用数据的时间戳生成 watermark
val dataStreaam = env.socketTextStream("192.168.31.184",7777)
.map(data => {
val arr = data.split(",")
SensorReading(arr(0),arr(1).toLong,arr(2).toDouble)
})
.assignAscendingTimestamps(_.timestamp*1000L)
②周期性生成watermark
假设数据为乱序情况下,有两种指定watermark生成的方法
AssignerWithPeriodicWatermarks //周期性生成watermark
AssignerWithPunctuatedWatermarks //每个事件生成watermark
在代码中指定使用时间时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.getConfig.setAutoWatermarkInterval(500)
在代码中提取事件时间、水印生成机制、指定时间最大延迟时间Watermark默认为毫秒
val dataStreaam = env.socketTextStream("192.168.31.184",7777)
.map(data => {
val arr = data.split(",")
SensorReading(arr(0),arr(1).toLong,arr(2).toDouble)
})
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(3)) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp *1000L
})
产生 watermark的逻辑:每隔500毫秒,Flink 会调用AssignerWithPeriodicWatermarks 的getCurrentWatermark()方法。如果方法返回一个时间戳大于之前水位的时间戳,新的 watermark 会被插入到流中。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的 watermark
③每个事件生成watermark
这种水位线的生成方式 Flink 没有提供内置实现,需要实现
AssignerWithPunctuatedWatermarks接口
val dataStreaam = env.socketTextStream("192.168.31.184",7777)
.map(data => {
val arr = data.split(",")
SensorReading(arr(0),arr(1).toLong,arr(2).toDouble)
})
.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks[SensorReading] {
val delay = 3000L
val initEventTime = Int.MinValue
override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
new Watermark(extractedTimestamp - delay)
}
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
val maxEventTime = Math.max(Math.max(element.timestamp,initEventTime.toLong),previousElementTimestamp)
maxEventTime
}
})
事件上生成水印,由于每个水印都会在下游引起一些计算,因此过多的水印会降低性能
ps.在1.11版本以后AssignerWithPeriodicWatermarks
AssignerWithPunctuatedWatermarks都已经不在建议使用,为了降低开发复杂度,推荐使用默认WatermarkStrategy 静态方法
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20))
.withTimestampAssigner(new SerializableTimestampAssigner[(Long, String)] {
override def extractTimestamp(element: (Long, String), recordTimestamp: Long): Long = element._1
})
Watermark的传递
多并发的场景下,Watermark 是 source task 产生,Watermark单调递增,经过 redistributing的keyBy分组后触发窗口计算
①对于上游中的每个并行子任务,每个并行度中的watermark都会以广播的形式发送给下游的所有并行度。注意watermark作为一种特殊的记录,是没有key的,只能广播给下游所有子任务。
②下游接收到上游所有子任务发送过来的watermark后,选择最小的作为本并行任务的watermark,并将其广播给下游。
Flink window窗口起始时间的确定
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
return timestamp - (timestamp - offset + windowSize) % windowSize;
}
timestamp为最早时间的记录时间戳
offset 为参数偏移时间
windowSize为窗口大小
以上计算单位全部统一到毫秒再进行计算
timestamp = 1629722783
offset = 0
windowSize = 25000
windowStartTime = 1629722783000– (1629722783000– 0 + 25000)% 25000
= 1629722775000
各个窗口长度为(秒)
[1629722775,1629722800) [1629722800,1629722825) [1629722825,1629722850) [1629722850,1629722915) …
Flink滑动窗口与事件时间及watermark测试
代码
class WindowAndWaterMark {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.getConfig.setAutoWatermarkInterval(500)
val dataStreaam = env.socketTextStream("192.168.31.184",7777)
.map(data => {
val arr = data.split(",")
SensorReading(arr(0),arr(1).toLong,arr(2).toDouble)
})
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(3)) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp *1000L
})
val lateTag = new OutputTag[SensorReading]("late")
val resultStream = dataStreaam
.keyBy(_.id)
.timeWindow(Time.seconds(25))
.allowedLateness(Time.minutes(1))
.sideOutputLateData(lateTag)
.reduce((curData,newData) => {
SensorReading(curData.id,newData.timestamp,curData.temperature.min(newData.temperature))
})
resultStream.print("result")
resultStream.getSideOutput(lateTag).print("late")
env.execute()
}
}
测试数据
sensor_1,1629722783,35.1
sensor_1,1629722789,30.8
sensor_1,1629722805,25.3
sensor_1,1629722783,12.5
sensor_1,1629722835,15.1
sensor_1,1629722838,11.8
sensor_1,1629722863,35.1
sensor_1,1629722783,35.1
生成窗口
[1629722775,1629722800)
[1629722800,1629722825)
[1629722825,1629722850)
[1629722850,1629722915)
…
输出结果
sensor_1,1629722783,35.1
–watermark 1629722780
–窗口[1629722775,1629722800)
sensor_1,1629722789,30.8
–watermark 1629722786
–窗口[1629722775,1629722800)
sensor_1,1629722805,25.3
–watermark 1629722802
–窗口[1629722800,1629722825)
–输出result>SensorReading(sensor_1,1629722789,30.8)
–第一个窗口末尾,触发计算
sensor_1,1629722783,12.5
–watermark 1629722805
–窗口[1629722775,1629722800)
–输出result>SensorReading(sensor_1,1629722783,12.5)
sensor_1,1629722835,15.1
–watermark 1629722832
–窗口[1629722825,1629722850)
sensor_1,1629722838,11.8
–watermark 1629722835
–窗口[1629722825,1629722850)
sensor_1,1629722863,35.1
–watermark 1629722860
–窗口[1629722850,1629722915)
–输出result>SensorReading(sensor_1,1629722838,11.8)
–watermark到达第一个窗口末尾1min后
sensor_1,1629722783,35.1
–watermark 1629722860
–迟到大于1min侧输出流
–输出late> SensorReading(sensor_1,1629722783,35.1)