Flink:水位和窗口关系

一、数据乱序的现象

实时计算中,对数据时间比较敏感,有 EventTime 和 ProcessTime 之分,一般来说 EventTime 是从原始消息中提取出来的,ProcessTime 是 Flink 自己提供的。

绝大部分的业务都会使用EventTime,一般只在EventTime无法使用时,才会被迫使用ProcessingTime或者IngestionTime。

在实际应用中,数据源往往很多个且时钟无法严格同步,数据汇集过程中传输的距离和速度也不尽相同,在上游多个节点处理过程的处理速度也有差异,这些因素使得 Event Time 的乱序基本是一个必然现象。

所以 Flink 提供了窗口和水位线的功能,使其在一定时间范围内可以正确处理数据乱序的现象。

数据正序 

在这里插入图片描述

 数据乱序

在这里插入图片描述

 二、Window 的概念

在 Flink 中,window 可以分为 基于时间(Time-based)的 window 以及基于数量(Count-based)的 window,另外还有基于 session 的 window,同时由于某些特殊需要,还可以自定义 window。

1、Tumbling window (翻滚窗口) 

  • 比如每多长时间统计一次(基于时间)
  • 比如每多少数量统计一次(基于数量)

在这里插入图片描述

object CountWindowsTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val wordDS = env.socketTextStream("master102",3456)
    wordDS
      .map((_,1))
      .keyBy(0)
      //累计单个Key中3条数据就进行处理
      .countWindow(3)
      .sum(1)
      .print("测试:")
    env.execute()
  }
}

object WindowTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val dataDS = env.socketTextStream("bigdata101", 3456)
    val tsDS = dataDS.map(str => {
      val strings = str.split(",")
      (strings(0), strings(1).toLong, 1)
    }).keyBy(0)
      //窗口大小为5s的滚动窗口
      //.timeWindow(Time.seconds(5))和下面的这种写法都是可以的
      .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
      .apply {
        (tuple: Tuple, window: TimeWindow, es: Iterable[(String, Long, Int)], out: Collector[String]) => {
          val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
          //out.collect(s"window:[${sdf.format(new Date(window.getStart))}-${sdf.format(new Date(window.getEnd))}]:{ ${es.mkString(",")} }")
          out.collect(s"window:[${window.getStart}-${window.getEnd}]:{ ${es.mkString(",")} }")
        }
      }.print("windows:>>>")
    env.execute()
  }
}

 (window 就简单的介绍一下概念,本次重点是讲 watermark 和 window 处理乱序数据)

Flink 的窗口划分:不是基于数据的时间来划分的,而是基于自然时间来划分的。比如我们设置窗口大小为3s,事件时间为 2019-11-12 15:00:05

那窗口的时间范围并不是想象中的:
[2019-11-12 15:00:05,2019-11-12 15:00:08]

而是 一个前闭后开的区间:
[2019-11-12 15:00:00 , 2019-11-12 15:00:03 )
[2019-11-12 15:00:03 , 2019-11-12 15:00:06 )
[2019-11-12 15:00:06 , 2019-11-12 15:00:09 ) 

 2、Sliding window (滑动窗口)

  • 比如每隔30秒统计过去1分钟的数据量(基于时间)
  • 比如每隔10个元素统计过去100个元素的数据量(基于数量)

在这里插入图片描述

//滚动5秒,滑动3秒
//.window(SlidingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(3)))和下面的这句话是一样的
 .timeWindow(Time.seconds(5),Time.seconds(3))

 非常关键的是:大家发现,flink默认的分配窗口是从每秒从0开始数的,举例:会把5秒的窗口分为:
[0-5),[5,10),[10-15),....
3秒的窗口为:
[0-3),[3,6),[6-9),....

3、 会话窗口

与滚动窗口和滑动窗口相比,会话窗口不重叠且没有固定的开始和结束时间。相反,会话窗口在一定时间段内未收到元素时(即,出现不活动间隙时)关闭。随后的元素将分配给新的会话窗口。

 .window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))

三、WaterMark 的概念

在Flink中,水位线是一种衡量Event Time进展的机制,用来处理实时数据中的乱序问题的,通常是水位线和窗口结合使用来实现。

从设备生成实时流事件,到Flink的source,再到多个oparator处理数据,过程中会受到网络延迟、背压等多种因素影响造成数据乱序。在进行窗口处理时,不可能无限期的等待延迟数据到达,当到达特定watermark时,认为在watermark之前的数据已经全部达到(即使后面还有延迟的数据), 可以触发窗口计算,这个机制就是 Watermark(水位线),具体如下图所示。

水位线生成

3.1 生成的时机

        水位线生产的最佳位置是在尽可能靠近数据源的地方,因为水位线生成时会做出一些有关元素顺序相对时间戳的假设。由于数据源读取过程是并行的,一切引起Flink跨行数据流分区进行重新分发的操作(比如:改变并行度,keyby等)都会导致元素时间戳乱序。但是如果是某些初始化的filter、map等不会引起元素重新分发的操作,可以考虑在生成水位线之前使用。

周期性分配水位线比较常用,是我们会指示系统以固定的时间间隔发出的水位线。在设置时间为事件时间时,会默认设置这个时间间隔为200ms, 如果需要调整可以自行设置。比如下面的例子是手动设置每隔1s发出水位线。 

周期水位线需要实现接口:AssignerWithPeriodicWatermarks,下面是示例:

生成水位的代码

public class TestPeriodWatermark implements AssignerWithPeriodicWatermarks<Tuple2<String, Long>> {
    Long currentMaxTimestamp = 0L;
    final Long maxOutOfOrderness = 10000L;// 最大允许的乱序时间是10s
     val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
    // 水印 是 当前时间减去10s
    @Nullable
    @Override
    public Watermark getCurrentWatermark() {
        return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
    }
    @Override
    public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
        long timestamp = element.f1;
        currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
        return timestamp;
    }
}

 设置了一个 3s 的窗口,并生成水印,并处理每一个窗口的数据,并打印出来

// 输入数据的格式:0001,2019-11-12 11:25:00
env.addSource(consumer)
    // 过滤空数据
    .filter(!_.isEmpty)  
    .map(f => {
      val arr = f.split(",")
      val code = arr(0)
      val time = parseDateNewFormat(arr(1)).getTime
      //        val time = arr(1).toLong
      (code, time)
    })
    // 指定水印生成的逻辑
    .assignTimestampsAndWatermarks(new TestPeriodWatermark )
    // 按照 code 来逻辑划分窗口,并行计算
    .keyBy(_._1)
    // 指定 翻滚窗口,3s生成一个窗口
    .window(TumblingEventTimeWindows.of(Time.seconds(3)))
    // 允许延迟5s之后才销毁计算过的窗口
    //.allowedLateness(Time.seconds(5))
    // 处理窗口数据
    .process(new MyProcessWindowFunction)
    // 打印处理完的数据
    .print() 



// assignTimestampsAndWatermarks(   new AssignerWithPeriodicWatermarks 直接new 新建


    val tsDS = dataDS.map(str => {
      val strings = str.split(",")
      (strings(0), strings(1).toLong, 1)
    }).assignTimestampsAndWatermarks( 
      new AssignerWithPeriodicWatermarks[(String,Long,Int)]{
        var maxTs :Long= 0
//得到水位线,周期性调用这个方法,得到水位线,我这里设置的也就是延迟5秒
        override def getCurrentWatermark: Watermark = new Watermark(maxTs - 5000)
//负责抽取事件事件
        override def extractTimestamp(element: (String, Long, Int), previousElementTimestamp: Long): Long = {
          maxTs = maxTs.max(element._2 * 1000L)
          element._2 * 1000L
        }
      }
 )

3、处理窗口数据的逻辑 

class MyProcessWindowFunction extends ProcessWindowFunction[(String, Long), String, String, TimeWindow] {
  override def process(key: String, context: Context, elements: Iterable[(String, Long)], out: Collector[String]): Unit = {

    val arr = ArrayBuffer[(String, Long)]()
    val iterator = elements.iterator
    while (iterator.hasNext) {
      val value = iterator.next()
      println(value._1, DateUtils.getNewFormatDateString(new Date(value._2)))
      arr += value
    }
    println(arr)
    val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
    val timeWindow = context.window
    out.collect(key + "," + arr.size + "," + format.format(arr.head._2) + "," + format.format(arr.last._2) + "," + format.format(timeWindow.getStart) + "," + format.format(timeWindow.getEnd))
  } 
} 

Guess you like

Origin blog.csdn.net/qq_22473611/article/details/119420980