FLink学习笔记:07-Flink 的时间语义和Watermark

时间语义

基于时间的窗口分配器既可以处理数据的事件时间(EventTime)也可以处理数据的处理时间(ProcessTime)(Flink处理数据的那一个时间点)。

在这里插入图片描述

EventTime 事件时间

它通常由事件中的时间戳描述,例如采集的日志数据,每一条记录都会记录自己的生成时间,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)))

IngestionTime:

数据进入Flink的时间。

env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)

ProcessingTime:处理时间

它是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是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)))

WaterMark

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

有序数据

对于理想的情况,数据如下图一样都是有序的,那么我们可以按照数据的Eventtime进行处理,如每5秒一个窗口进行数据计算,这样是没有任何问题的,也不会发生数据丢失状况,也就无需使用到watermark
在这里插入图片描述

乱序数据

实际情况可能因为网络的传输、分布式等等导致数据是乱序的,如下图所示,如果我们再按照数据的EventTime进行处理,我们会发现如5后面的数据2、4、3都被丢失了,从而导致计算结果发生错误。但是我们又不知道数据延迟多久,又不能无限等待下去,为了解决这一问题,Flink引入了WaterMark机制来保证对乱序数据的正确处理。
在这里插入图片描述

WaterMark的作用

  • Watermark是一种衡量EventTime进展的机制
  • Watermark是用于处理乱事件的,而为了正确的处理乱序数据,通常采用watermark机制结合window来实现
  • 数据流中Watermark用于表示timestamp小于watermark的数据已经全部到达,因此,Window的执行是由Watermark进行触发的。
  • Watermark是一种延迟触发机制,我们可以设置Watermark的延时时长为t,每次系统校验达到的最大的EventTime,然后设定EventTime小于当前到达的maxEventTime-t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime - t,那么这个窗口就会被触发。

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)的窗口依旧处于等待中。
    数据5的图片

  • 3、第三条数据的时间戳为2,当前已经到达的最大时间戳是5,watermark=5-3,还是2,watermark不变,意味着当前的时间是2,没有窗口需要关闭,数据2进入到[0~5)的窗口中。
    时间戳为2的数据

  • 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将所有的乱序数据全部正确的处理到各自的窗口中。

最终情况

Watermark在并行任务中的传递

watermark并行任务传递
如上图所示:当并行度是4时,Flink会为每一个并行任务建立一个分区,用于存放当前分区的watermark。

  • 1、(图1)中,当收到第一个任务的的watermark 为4,watermark发生了变化,将第一个分区的watermark更新为4,如图(2)所示,此时所有分区中最小的watermark为3,说明不会再有比3小的时间戳的数据到来,当前数据处理完毕,将3这个watermark广播到下游。
  • 2、当收到分区2的watermark时,将分区2的watermark更新为7,此时所有分区的最小watermark仍旧是3,没有发生改变,无需广播到下游,如图(3)所示。
  • 3、当收到分区3的watermark6时,将分区3的watermark更新为6,此时所有分区的最小watermark是4,说明所有时间戳小于4的数据处理完毕,将watermark 4广播到下游。

Watermark结合Window的实例

watermark 结合TumblingWindow

pom.xml文件依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-scala -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-streaming-scala_2.12</artifactId>
    <version>1.14.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-scala -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-scala_2.12</artifactId>
    <version>1.14.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-clients_2.12</artifactId>
    <version>1.14.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.25</version>
</dependency>

scala代码
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")
  }
}

watermark 结合SlidingEventTimeWindows

只需要给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))
    }
  })

watermark 结合EventTimeSessionWindows

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))
    }
  })

watermark的生成方式

  • 周期性生成,通过下列方式可以设置watermark的生成周期(毫秒),Flink默认采用的是周期性生成,默认周期是200毫秒
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.getConfig.setAutoWatermarkInterval(500L)
  • 间断生成,也就是每隔多长数据间隔生成一个watermark

watermark的生成策略分类

  • 处理乱序数据的watermark策略WatermarkStrategy.forBoundedOutOfOrderness
.assignTimestampsAndWatermarks(
    WatermarkStrategy
      .forBoundedOutOfOrderness[Sensor](Duration.ofSeconds(3))
      .withTimestampAssigner(new SerializableTimestampAssigner[Sensor]{
        override def extractTimestamp(element: Sensor, recordTimestamp: Long): Long = {
          element.timestamp*1000L
        }
      })
  )

  • 处理顺序数据的watermark策略WatermarkStrategy.forMonotonousTimestamps
.assignTimestampsAndWatermarks(
WatermarkStrategy.forMonotonousTimestamps[Sensor]()
  .withTimestampAssigner(new SerializableTimestampAssigner[Sensor] {
    override def extractTimestamp(element: Sensor, recordTimestamp: Long): Long = {
      element.timestamp*1000L
    }
  })
)

watermark处理空闲数据流

如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
    }
  })
)

Flink对于延迟数据的补充处理方式

由上面的图解过程,我们知道,watermark对于乱序数据的处理的准确性取决于我们设置的延迟时间,这个时间如果设置的越大,肯定处理的正确性越高,但是实际情况因为需要处理庞大的数据,所以我们不可能将这个延迟时间设置的太长,所以会做一个平衡,设置的延时时间能够覆盖绝大部分数据就行。对于一些超过我们设定的延迟时间的数据,Flink提供了两种方式:

方式一、设置允许的延迟时间allowedLateness

    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 设置的最大延迟时间,就会在该数据到来时触发一次运算,并输出结果。

方式二、侧输出流sideOutputLateData

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")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值