Flink之时间语义与Wartermark实例详解

1:时间语义的引出

在说时间语义时我们引出一个小案例,在平时日常生活中,我们在上下班做地铁的途中为了消磨时间难免会玩会游戏,假设某个游戏设定在一分钟之内可以通过几关就奖励几关的积分,张三在游戏开始的前45秒已经通了3关,然而在45秒以后地铁进入了山里隧道中没有了信号,但是在隧道内的35秒内他连通了5关,在隧道35秒后地铁从隧道驶出网络恢复正常此时缓存中的数据被发往服务器,由于此时段山里隧道没有信号导致连通5关的信息没有及时的被服务器端所接受。所以在游戏方设定的1分钟内通n关就送n个积分的规则在张三这里就彰显出问题来了,按照游戏方的规则就是在隧道的时间内虽然你张三通关了但是我服务器没有你通关的记录呀,但是一方面对于张三来说虽然我剩下的15秒没有通5关,但是我最起码加上之前有信号的时间通了5关还是有的吧,所以游戏方必须制定一个非常完善的规则否则会极大的降低用户的产品体验度,也因此由以上的一个小案例引出了我们Flink中的时间语义。

在这里插入图片描述

1.1时间语义的定义

在流处理中,时间是一个非常核心的概念,如何规定将相应的数据按照时间合理的进入到不同的窗口是重中之重的问题,在Flink的流式处理中支持不同的时间概念,如下图所示:

在这里插入图片描述

Event Time:

是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。

Ingestion Time:

Ingestion Time是事件到达Flink Souce的时间。从Source到下游各个算子中间可能有很多计算环节,任何一个算子的处理速度快慢可能影响到下游算子的Processing Time。而Ingestion Time定义的是数据流最早进入Flink的时间,因此不会被算子处理速度影响。

Processing Time:

是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是Processing Time。

1.2三种时间语义的优缺点:

Event Time:

一个基于Event Time的Flink程序中必须定义Event Time,以及如何生成Watermark。我们可以使用元素中自带的时间,也可以在元素到达Flink后人为给Event Time赋值,使用Event Time的优势是结果的可预测性(可类比上述的玩游戏事件),缺点是缓存较大,增加了延迟,且调试和定位问题更复杂。

注: 在Flink的流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,才会被迫使用ProcessingTime或者IngestionTime。

Ingestion Time:

Ingestion Time通常是Event Time和Processing Time之间的一个折中方案。比起Event Time,Ingestion Time可以不需要设置复杂的Watermark,因此也不需要太多缓存,延迟较低。比起Processing Time,Ingestion Time的时间是Souce赋值的,一个事件在整个处理过程从头至尾都使用这个时间,而且后续算子不受前序算子处理速度的影响,计算结果相对准确一些,但计算成本稍高。

Processing Time:

Processing Time只依赖当前执行机器的系统时钟,不需要依赖Watermark,无需缓存。Processing Time是实现起来非常简单也是延迟最小的一种时间语义,但是我们一般都很少用到Processing Time此种时间语义。

1.3关于EventTime的引入:

EventTime的引入非常的简单,只需要创建环境后调用setStreamTimeCharacteristic方法即可.

val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env创建的每一个stream追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

2:Wartermark

2.1非理想情况下所引出的Wartermark:

我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。

在这里插入图片描述

那么此时出现一个问题,一旦出现乱序,如果只根据eventTime决定window的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark。

2.2Wartermark概述:

1.Watermark是一种衡量Event Time进展的机制。
2. Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现
3. 数据流中的Watermark用于表示timestamp小于Watermark的数据,都已经到达了,因此,window的执行也是由Watermark触发的。
4. Watermark可以理解成一个延迟触发机制,我们可以设置Watermark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime – t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行

2.3Wartermark的特点

在这里插入图片描述

1.watermark 是一条特殊的数据记录,它本质上是时间戳,与业务数据一样无差别地传递下去,目的是衡量事件时间的进度。

2.watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不
是在后退

3.watermark 与数据的时间戳相关

2.4图解Wartermark

在这里插入图片描述

注意:Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。
只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。

2.5Wartermark的引入

对于watermark的引入Flink底层已经帮我们封装好了很多的内容,我们只需要先设置一下时间语义(设置为EventTime), 调用assignTimestampsAndWatermarks方法然后在实现TimestampAssigner接口即可, (也可以调用assignAscendingTimestamps,升序数据直接提取时间戳,理想状态下的顺序数据使用此种方法,因为在实际的生活中存在网络延迟等问题,所以一般情况下此方法用的很少)

val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)//时间语义设置为EnevtTime,默认为ProcessingTime

dataStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[thermometer](Time.milliseconds(50)) {
    
    
  override def extractTimestamp(element: thermometer): Long = {
    
    
    element.timestamp * 1000L  //
  }
} )

其中Time.milliseconds(50)为上述WaterMark中的延时时间 T,这里放上TimestampAssigner接口的继承图

在这里插入图片描述

2.6基于时间语义下的窗口测试

import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.{
    
    EventTimeSessionWindows, SlidingEventTimeWindows, TumblingEventTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time


case class thermometer(id : String ,time : Long,Temp : Double)
//温度计样例类

object Time_window {
    
    
  def main(args: Array[String]): Unit = {
    
    

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)//时间语义设置为EnevtTime,默认为ProcessingTime
    env.getConfig.setAutoWatermarkInterval(50)

    //从socket文本流中读取数据
    val inputStream = env.socketTextStream("hadoop102",7777)

    // 先转换成样例类类型
    val dataStream = inputStream
      .map( data => {
    
    
        val arr = data.split(",")
        thermometer(arr(0), arr(1).toLong, arr(2).toDouble)
      } )
     // .assignAscendingTimestamps(_.time * 1000L)    // 升序数据提取时间戳,理想状态下的顺序数据使用此种方法,此方法不需要定义
      //WaterMark,因为WaterMark直接采用的是数据进来时的时间戳

      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[thermometer](Time.seconds(3)) {
    
    
        override def extractTimestamp(element: thermometer): Long = element.time.toLong * 1000L
      })

    val latetag = new OutputTag[(String, Double, Long)]("late")

    val res = dataStream
      .map(data => (data.id,data.Temp,data.time))
      .keyBy(_._1)   //按照id进行分组
//      .window(TumblingEventTimeWindows.of(Time.seconds(15)))  底层滚动窗口的实现
//      .window(SlidingEventTimeWindows.of(Time.seconds(15),Time.milliseconds(3))) //底层滑动窗口的实现
//      .window(EventTimeSessionWindows.withGap(Time.seconds(15)))  会话窗口
//      .countWindow(10)  滚动计数窗口
//      .countWindow(10,2) 滑动计数窗口
      .timeWindow(Time.seconds(10))  //使用Flink为我们封装好的滑动或者滚动窗口的实现方法
      .allowedLateness(Time.minutes(1)) //允许迟到1minute的数据
      .sideOutputLateData(latetag)

      .reduce((currdata,newdata)=>(currdata._1,currdata._2.min(newdata._2),newdata._3))  //每10s求出当前时间下各个温度计的最小值

    res.getSideOutput(latetag).print("late")
    res.print("result")


    env.execute("EventTime_Tumblingwindow test")

  }
}

测试所用温度计数据

1,1609745003,10.6
1,1609745004,8.2
1,1609745010,25.6
4,1609745003,24.7
5,1609745005,18.4
1,1609745013,7.2
1,1609745006,1.2
1,1609745014,35.4
1,1609745015,17.4
1,1609745023,22.9
1,1609745007,1.6
1,1609745100,7.0

在这里插入图片描述

从当前输入数据可以看出窗口的范围为【0,10),时间戳为013时对应的WaterMark刚好为10,此时触发0 ~10桶内数据进行处理。

在这里插入图片描述

依据上面的窗口,当前窗口范围为【10,20),当时间戳为023时触发桶内数据10 ~20的运算,对于窗口10 ~20来说,最低温度为时间戳013对应的7.2,对于0 ~10窗口来说,此时在之前聚合运算结果已经出来的情况下又来了006这条时间戳的数据,由于我们在代码中设置了allowedLateness(Time.minutes(1)) //允许迟到1minute的数据 ,故0 ~10窗口当前的最小温度变为1.2,时间戳也相应的根据代码中改为最新的时间戳.

在这里插入图片描述

对于0 ~10窗口来讲,虽然在此时又插入了之前窗口内的时间戳,因为1.6的温度没有之前最低的1.2低,故此时最低温度还是1.2,但当前的时间戳已经变化为最新时间戳007.

猜你喜欢

转载自blog.csdn.net/weixin_44080445/article/details/112131990