基于时间的窗口分配器既可以处理数据的事件时间(EventTime)也可以处理数据的处理时间(ProcessTime)(Flink处理数据的那一个时间点)。
它通常由事件中的时间戳描述,例如采集的日志数据,每一条记录都会记录自己的生成时间,Flink通过时间戳分配器访问时间时间戳。
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
Tips:在Flink-1.12以后,系统默认的就是Event time方式,无需使用该API设置时间语义
//滚动事件时间窗口
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//滑动事件时间窗口
.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(2)))
//会话事件时间窗口
.window(EventTimeSessionWindows.withGap(Time.seconds(15)))
数据进入Flink的时间。
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
它是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是Processing Time。
缺陷:
优势:
在低版本的Flink中设置使用ProcessingTime的时间语义的方式如下
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
在高版本中上述方式被弃用,通过Window的生成器设定
如设定使用处理时间的滚动窗口
//处理时间滚动窗口
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
//处理时间滑动窗口
.window(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(2)))
//处理时间Session窗口
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(15)))
我们知道,流处理从事件产生,到流经 source,再到 operator,中间是有一个过 程和时间的,虽然大部分情况下,流到 operator 的数据都是按照事件产生的时间顺 序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就 是指 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。
对于理想的情况,数据如下图一样都是有序的,那么我们可以按照数据的Eventtime进行处理,如每5秒一个窗口进行数据计算,这样是没有任何问题的,也不会发生数据丢失状况,也就无需使用到watermark
实际情况可能因为网络的传输、分布式等等导致数据是乱序的,如下图所示,如果我们再按照数据的EventTime进行处理,我们会发现如5后面的数据2、4、3都被丢失了,从而导致计算结果发生错误。但是我们又不知道数据延迟多久,又不能无限等待下去,为了解决这一问题,Flink引入了WaterMark机制来保证对乱序数据的正确处理。
如下图所示,有一堆乱序数据流,我们假定数据的最大延迟是3,需要每5秒一个周期进行统计。
1、第一条数据是时间戳是1,最大延迟是3秒,那么watermark=1-3 为一个负数,但是时间是不可能小于0的,因此,我们设定当前的EventTime为1,也就是watermark是1,数据1放到[0~5)的窗口中。
2、第二条数据时间戳是5,watermark=当前最大时间戳5-最大延迟3,也就是2,2比之前的watermark 1要大,那么也就意味着时间戳为1以前数据全部到达,1秒的窗口可以关闭,设定当前watermark为2,数据5进入到[510)的窗口中,[05)的窗口依旧处于等待中。
3、第三条数据的时间戳为2,当前已经到达的最大时间戳是5,watermark=5-3,还是2,watermark不变,意味着当前的时间是2,没有窗口需要关闭,数据2进入到[0~5)的窗口中。
4、第四条数据的时间戳是4,当前已经到达的最大时间戳是5,watermark=5-3,还是2,watermark不变,意味着当前的时间是2,没有窗口需要关闭,数据4进入到[0~5)的窗口中。
5、第五条数据的时间戳是3,当前已经到达的最大时间戳是5,watermark=5-3,还是2,watermark不变,意味着当前的时间是2,没有窗口需要关闭,数据3进入到[0~5)的窗口中。
6、第6条数据的时间戳是6,当前已经到达的最大时间戳就变成了6,watermark = 6 - 3 = 3,watermark变成3,意味着当前的时间是3,当前没有窗口需要关闭,数据6进入到[5~10)的窗口中
7、第7条数据的时间戳是9,当前已经到达的最大时间戳就变成了9,watermark = 9 - 3 = 6,watermark变成6,意味着当前的时间变成了6,6以前的数据全部到达,数据9进入[510)的窗口,触发[05)的窗口关闭,进行下一步计算处理
8、第8条数据的时间戳是7,当前已经到达的最大时间戳仍是9,watermark = 9 - 3 = 6,watermark还是6,当前时间还是6,6以前的数据全部到达,数据7进入[5~10)的窗口,没有窗口需要关闭
9、第9条数据的时间戳是8,当前已经到达的最大时间戳仍是9,watermark = 9 - 3 = 6,watermark还是6,当前时间还是6,6以前的数据全部到达,数据8进入[5~10)的窗口,没有窗口需要关闭
10、第10条数据的时间戳是13,当前已经到达的最大时间戳变成13,watermark = 13 - 3 = 10,watermark变成10,当前时间变成10,意味着10以前的数据全部到达,数据13进入[1015)的窗口,触发[510)的窗口关闭、进行下一步计算处理
11、接下来的数据处理过程和上面一样,如下图所示,Flink将所有的乱序数据全部正确的处理到各自的窗口中。
如上图所示:当并行度是4时,Flink会为每一个并行任务建立一个分区,用于存放当前分区的watermark。
<dependencies>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-streaming-scala_2.12artifactId>
<version>1.14.4version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-scala_2.12artifactId>
<version>1.14.4version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-clients_2.12artifactId>
<version>1.14.4version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simpleartifactId>
<version>1.7.25version>
dependency>
package com.hjt.yxh.apitest
import org.apache.flink.streaming.api.scala._
import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, WatermarkStrategy}
import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala.OutputTag
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import java.time.Duration
object watermarkTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream = env.socketTextStream("192.168.0.52",7777)
val sensorStream = dataStream.filter(_.nonEmpty).map(data=>{
val array = data.split(",")
Sensor(array(0),array(1).toLong,array(2).toDouble)
})
.assignTimestampsAndWatermarks(
WatermarkStrategy
.forBoundedOutOfOrderness[Sensor](Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner[Sensor]{
override def extractTimestamp(element: Sensor, recordTimestamp: Long): Long = {
element.timestamp*1000L
}
})
)
val resultStream = sensorStream
.keyBy((value: Sensor) => {
value.Id
})
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2))
.sideOutputLateData(new OutputTag[Sensor]("late"))
.reduce(new ReduceFunction[Sensor] {
override def reduce(value1: Sensor, value2: Sensor): Sensor = {
Sensor(value1.Id,value1.timestamp.max(value2.timestamp),value1.temperator.min(value2.temperator))
}
})
resultStream.print("result")
val lateStream = resultStream.getSideOutput(new OutputTag[Sensor]("late"))
lateStream.print("late")
env.execute("watermark test job")
}
}
只需要给window函数传递SlidingWindow的生成器就行了,其他代码一样
val resultStream = sensorStream
.keyBy((value: Sensor) => {
value.Id
})
.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(2)))
.reduce(new ReduceFunction[Sensor] {
override def reduce(value1: Sensor, value2: Sensor): Sensor = {
Sensor(value1.Id,value1.timestamp.max(value2.timestamp),value1.temperator.min(value2.temperator))
}
})
val resultStream = sensorStream
.keyBy((value: Sensor) => {
value.Id
})
.window(EventTimeSessionWindows.withGap(Time.seconds(15)))
.reduce(new ReduceFunction[Sensor] {
override def reduce(value1: Sensor, value2: Sensor): Sensor = {
Sensor(value1.Id,value1.timestamp.max(value2.timestamp),value1.temperator.min(value2.temperator))
}
})
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.getConfig.setAutoWatermarkInterval(500L)
.assignTimestampsAndWatermarks(
WatermarkStrategy
.forBoundedOutOfOrderness[Sensor](Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner[Sensor]{
override def extractTimestamp(element: Sensor, recordTimestamp: Long): Long = {
element.timestamp*1000L
}
})
)
.assignTimestampsAndWatermarks(
WatermarkStrategy.forMonotonousTimestamps[Sensor]()
.withTimestampAssigner(new SerializableTimestampAssigner[Sensor] {
override def extractTimestamp(element: Sensor, recordTimestamp: Long): Long = {
element.timestamp*1000L
}
})
)
如watermark图解中最后一个图,加入后续一直没有数据输入,那么剩余的两个窗口永远都不会被触发关闭,那么此时应该怎么处理呢?可以设置处理空闲数据的策略来规避这一情况。设置一个时间,如果没有数据到来,就触发窗口关闭事件。
.assignTimestampsAndWatermarks(
WatermarkStrategy.forBoundedOutOfOrderness[Sensor](Duration.ofSeconds(3))
.withIdleness(Duration.ofMinutes(1))
.withTimestampAssigner(new SerializableTimestampAssigner[Sensor] {
override def extractTimestamp(element: Sensor, recordTimestamp: Long): Long = {
element.timestamp*1000L
}
})
)
由上面的图解过程,我们知道,watermark对于乱序数据的处理的准确性取决于我们设置的延迟时间,这个时间如果设置的越大,肯定处理的正确性越高,但是实际情况因为需要处理庞大的数据,所以我们不可能将这个延迟时间设置的太长,所以会做一个平衡,设置的延时时间能够覆盖绝大部分数据就行。对于一些超过我们设定的延迟时间的数据,Flink提供了两种方式:
val resultStream = sensorStream
.keyBy((value: Sensor) => {
value.Id
})
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2))
.reduce(new ReduceFunction[Sensor] {
override def reduce(value1: Sensor, value2: Sensor): Sensor = {
Sensor(value1.Id,value1.timestamp.max(value2.timestamp),value1.temperator.min(value2.temperator))
}
})
设置延迟时间后,如果窗口关闭后还有窗口时间范围内的数据到来,只要没有超过我们设定的延迟时间+watermark 设置的最大延迟时间,就会在该数据到来时触发一次运算,并输出结果。
val resultStream = sensorStream
.keyBy((value: Sensor) => {
value.Id
})
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2))
.sideOutputLateData(new OutputTag[Sensor]("late"))
.reduce(new ReduceFunction[Sensor] {
override def reduce(value1: Sensor, value2: Sensor): Sensor = {
Sensor(value1.Id,value1.timestamp.max(value2.timestamp),value1.temperator.min(value2.temperator))
}
})
resultStream.print("result")
val lateStream = resultStream.getSideOutput(new OutputTag[Sensor]("late"))
lateStream.print("late")