Flink-时间语义与Watermark
1.时间语义

Event Time:是事件创建的时间。它通常由事件中的时间戳述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。
Ingestion Time:是数据进入 Flink 的时间。
Processing Time:是每一个执行基于时间操作的算子的本地统时间,与机器相关,默认的时间属性就是 Processing Time。
2.EventTime的引入
在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。
如果要使用 EventTime,那么需要引入 EventTime 的时间属性,引入方式如下所示:
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
3.Watermark
3.1 基本概念
我们知道,流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的,虽然大部分情况下,流到 operator 的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。

那么此时出现一个问题,一旦出现乱序,如果只根据 eventTime 决定 window 的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发 window 去进行计算了,这个特别的机制,就是 Watermark。
Watermark 是一种衡量 Event Time 进展的机制。
Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark 机制结合 window 来实现。
数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据都已经到达了,因此,window 的执行也是由 Watermark 触发的。
Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。
有序流的 Watermarker 如下图所示:(Watermark 设置为 0)

乱序流的 Watermarker 如下图所示:(Watermark 设置为 2)

当 Flink 接收到数据时,会按照一定的规则去生成 Watermark,这条 Watermark就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是基于数据携带的时间戳生成的,一旦 Watermark 比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行。由于 event time 是由数据携带的,因此,如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发。
上图中,我们设置的允许最大延迟到达时间为 2s,所以时间戳为 7s 的事件对应的 Watermark 是 5s,时间戳为 12s 的事件的 Watermark 是 10s,如果我们的窗口 1是 1s~5s,窗口 2 是 6s~10s,那么时间戳为 7s 的事件到达时的 Watermarker 恰好触发窗口 1,时间戳为 12s 的事件到达时的 Watermark 恰好触发窗口 2。
Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。
只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。

watermark是一条特殊的数据记录
watermark必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退
watermark与数据的时间戳相关
3.2Watermark在并行任务处理时的传递

上游向下游是以广播的形式做watermark传递,下游会以分区的形式存watermark,以最小的分区watermark作为自己的watermark
3.3 Watermark的引入及自定义
Assigner 有两种类型
AssignerWithPeriodicWatermarks:周期性生成watermark,数据密集时可用此assigner,默认周期为200ms,可自行配置(env.getConfig.setAutoWatermarkInterval(500))
产生 watermark 的逻辑:每隔 5 秒钟,Flink 会调用AssignerWithPeriodicWatermarks 的 getCurrentWatermark()方法。如果方法返回一个时间戳大于之前水位的时间戳,新的 watermark 会被插入到流中。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的 watermark。
AssignerWithPunctuatedWatermarks:间歇性生成watermark,数据稀疏时可用此assigner
间断式地生成 watermark。和周期性生成的方式不同,这种方式不是固定时间的,而是可以根据需要对每条数据进行筛选和处理
以上两个接口都继承自 TimestampAssigner。
package com.zhen.flink.api import java.time.Duration import java.util.concurrent.TimeUnit import com.zhen.flink.api.WindowTest.SerializableTimestampAssignerTest import org.apache.flink.api.common.eventtime.{BoundedOutOfOrdernessWatermarks, SerializableTimestampAssigner, WatermarkStrategy} import org.apache.flink.api.common.functions.ReduceFunction import org.apache.flink.streaming.api.{TimeCharacteristic, watermark} import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks, timestamps} import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment} import org.apache.flink.streaming.api.scala._ import org.apache.flink.streaming.api.watermark.Watermark import org.apache.flink.streaming.api.windowing.assigners.{EventTimeSessionWindows, SlidingEventTimeWindows, TumblingEventTimeWindows, TumblingProcessingTimeWindows} import org.apache.flink.streaming.api.windowing.time.Time /** * @Author FengZhen * @Date 7/12/22 3:43 PM * @Description TODO */ object WindowTest { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment /** * 需要设置时间语义 * 如果设置 env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime): * * 则后面 不能使用 .window(TumblingEventTimeWindows.of等 EventTime的窗口,不然会报这个错!注意:ProcessingTime本身就是单调递增的,不必设置水位线! */ import org.apache.flink.streaming.api.TimeCharacteristic env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) // 周期性生成水位线,默认200ms,可自行设置 env.getConfig.setAutoWatermarkInterval(500) env.setParallelism(1) // 0.读取数据 // nc -lk 7777 val inputStream = env.socketTextStream("localhost", 7777) // 1.先转换成样例数据 val dataStream: DataStream[SensorReading] = inputStream .map( data => { val arr = data.split(",") SensorReading(arr(0), arr(1).toLong, arr(2).toDouble) } ) // .assignAscendingTimestamps(_.timestamp * 1000L) //表示目前数据已经是根据时间戳升序排列了,不需要watermark // .assignTimestampsAndWatermarks(new MyPeriodicAssigner) // .assignTimestampsAndWatermarks(new MyPunctuatedAssigner) .assignTimestampsAndWatermarks( /** * AssignerWithPeriodicWatermarks : 周期性的生成watermark,默认周期是200ms * 目前已不推荐使用,直接使用下边的WatermarkStrategy * * BoundedOutOfOrdernessTimestampExtractor 中的生成watermark方法 * currentMaxTimestamp:当前最大时间戳 * maxOutOfOrderness:最大乱序程度,new提取器时指定的Time.seconds(3) * @Override * public final Watermark getCurrentWatermark() { * // this guarantees that the watermark never goes backwards. * long potentialWM = currentMaxTimestamp - maxOutOfOrderness; * if (potentialWM >= lastEmittedWatermark) { * lastEmittedWatermark = potentialWM; * } * return new Watermark(lastEmittedWatermark); * } */ new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(3)){ override def extractTimestamp(element: SensorReading): Long = { element.timestamp * 1000L } } ) // .assignTimestampsAndWatermarks( // WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3)) // .withTimestampAssigner(new SerializableTimestampAssignerTest) // ) // 每15s统计一次窗口内各传感器所有温度的最小值, 以及最新的时间戳 val resultStream = dataStream .map( data => (data.id, data.temperature, data.timestamp)) .keyBy(_._1) // 按照二元组的第一个元素(id)进行分组 // .window(TumblingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(3))) .window( TumblingEventTimeWindows.of(Time.seconds(5), Time.seconds(3)) ) // 滚动时间窗口,第二个参数为偏移量,可解决时区问题,或者统计时具体到某分钟 // .window( SlidingEventTimeWindows.of(Time.seconds(15), Time.seconds(5), Time.seconds(3))) // 滑动时间窗口 // .window( EventTimeSessionWindows.withGap(Time.seconds(15))) // 会话窗口 // .countWindow(10) // 滚动窗口 // .countWindow(10, 2) //滑动窗口 // .minBy(1) .reduce((curRes, newData) => (curRes._1, curRes._2.min(newData._2), curRes._3)) //另外一种方式,自定义reducer方法 // val resultStream = dataStream // .keyBy(_.id) // .window( TumblingEventTimeWindows.of(Time.seconds(15), Time.seconds(3)) ) // 滚动时间窗口,第二个参数为偏移量,可解决时区问题,或者统计时具体到某分钟 // .reduce(new MyReducer) resultStream.print() env.execute("window test") } class MyReducer extends ReduceFunction[SensorReading]{ override def reduce(value1: SensorReading, value2: SensorReading): SensorReading = { SensorReading(value1.id, value2.timestamp, value1.temperature.min(value2.temperature)) } } class SerializableTimestampAssignerTest extends SerializableTimestampAssigner[SensorReading] { override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = { val eventTime = element.timestamp //recordTimestamp即element事件的时间 eventTime } } /** * 自定义周期性watermark生成器 */ class MyPeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorReading]{ val bound: Long = 6* 1000 //延时为6秒 var maxTs: Long = Long.MinValue //观察到的最大时间戳 // 根据指定周期时间调用生成 override def getCurrentWatermark: Watermark = { new Watermark(maxTs - bound) } //每条数据都调用 override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = { val eventTime = element.timestamp * 1000L maxTs = maxTs.max(eventTime) //recordTimestamp即element事件的时间 eventTime } } /** * 自定义间歇性watermark生成器 * 只给sensor_1的数据流插入watermark */ class MyPunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorReading]{ val bound: Long = 6* 1000 //延时为6秒 //每条数据都调用 override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = { if (lastElement.id == "sensor_1"){ new watermark.Watermark(extractedTimestamp - bound) }else{ null } } override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = { element.timestamp } } }
3.4 Watermark的设定
在Flink中,watermark由应用程序开发人员生成,这通常需要对响应的领域有一定的了解
如果watermark设置的延迟太久,收到的结果的速度可能就会很慢,解决办法是在水位线到达之前输出一个近似结果
如果watermark到达的太早,则可能收到错误结果,不过Flink处理迟到数据的机制可以解决这个问题
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示