Flink| time| watermark| Windows窗口

Time与Window

在Flink的流式处理中,会涉及到时间的不同概念,如下图所示

                             

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

Ingestion Time:是数据进入Flink的时间。

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

例如,一条日志进入Flink的时间为2017-11-12 10:00:00.123,到达Window的系统时间为2017-11-12 10:00:01.234,日志的内容如下:

2017-11-02 18:37:15.624 INFO Fail over to rm2

对于业务来说,要统计1min内的故障日志个数,哪个时间是最有意义的?—— eventTime,因为我们要根据日志的生成时间进行统计。

 

 Watermark

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

扫描二维码关注公众号,回复: 8373049 查看本文章

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

  Watermark是一种衡量Event Time进展的机制,它是数据本身的一个隐藏属性,数据本身携带着对应的Watermark。

  Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现

数据流中的Watermark用于表示timestamp小于Watermark的数据,都已经到达了,因此,window的执行也是由Watermark触发的

Watermark可以理解成一个延迟触发机制,我们可以设置Watermark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime - t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行

                         

Window概述

一般真实的流都是无界的,怎么处理无界的数据?可以把无限的数据流进行切分,得到有限的数据集进行处理----也就是得到有界流。

streaming流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集;

而window是把无限数据流为有限流的一种方式,Window将一个无限的stream拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。

Window可以分成两类:

   时间窗口TimeWindow滚动时间窗口(Tumbling Window)、滑动时间窗口(Sliding Window)、会话时间窗口(Session Window)。

   计数窗口CountWindow:按照指定的数据条数生成一个Window,与时间无关。分为滚动计数窗口、滑动计数窗口。

TimeWindow

  • 滚动窗口(Tumbling Windows)

    将数据依据固定的窗口长度对数据进行切片

    特点时间对齐,窗口长度固定,没有重叠。  它是步长 = site的滑动窗口;  

      使用场景:商业BI分析统计(关注的商业指标往往是某个时间段的指标,如一天或一周的销售额,每个时间段的聚合操作);

    滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,窗口的创建如下图所示:

                    

import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, StreamExecutionEnvironment, WindowedStream}
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.api.scala._
object StreamEventTimeApp {
  def main(args: Array[String]): Unit = {
    //环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //声明使用eventTime;引入EventTime    从调用时刻开始给env创建的每一个stream追加时间特征
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    val dstream: DataStream[String] = env.socketTextStream("hadoop101", 7777)

    val textWithTsDStream: DataStream[(String, Long, Int)] = dstream.map { text =>
      val arr: Array[String] = text.split(" ")
      (arr(0), arr(1).toLong, 1)
    }
    // 1 告知 flink如何获取数据中的event时间戳  2 告知延迟的watermark为 3s
    val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks(
      new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(3000)) { //time别导错包了
        override def extractTimestamp(element: (String, Long, Int)): Long = {
          return element._2
        }
      }).setParallelism(1)
    //每5秒开一个窗口 统计key的个数  5秒是一个数据的时间戳为准
    val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDStream.keyBy(0)
    textKeyStream.print("textKey: ")
    //滚动窗口
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.milliseconds(5000)))

    windowStream.sum(2).print("windows: ").setParallelism(1)

    env.execute()
  }
[kris@hadoop101 gmall]$ nc -lk 7777
abc 1000
abc 3000
abc 4000
abc 5000
abc 6000
abc 7000
abc 7500
abc 8000
abc 9000
abc 12000
abc 12999
abc 14000
abc 15000
abc 17000
abc 18000

textKey: :8> (abc,1000,1)
textKey: :8> (abc,3000,1)
textKey: :8> (abc,4000,1)
textKey: :8> (abc,5000,1)
textKey: :8> (abc,6000,1)
textKey: :8> (abc,7000,1)
textKey: :8> (abc,7500,1)
textKey: :8> (abc,8000,1)
Window: > (abc,1000,3)
textKey: :8> (abc,9000,1)
textKey: :8> (abc,12000,1)
textKey: :8> (abc,12999,1)
Window: > (abc,5000,6)
textKey: :8> (abc,14000,1)
textKey: :8> (abc,15000,1)
textKey: :8> (abc,17000,1)
textKey: :8> (abc,18000,1)
Window: > (abc,12000,3)

  滚动窗口:
  X秒开一个窗口,上例中5s开一个窗;
  上例watermark 3s

  第n次发车时间:nX+3,车上携带的[X, nX)秒内的
    如第一次车上携带 [0, 5)以内的,在第 5 + 3 = 8s时间点发车
     第二次车上携带 [5, 10)以内的,在第10 + 3 = 13s时间点发车
     第三次车上携带 [10, 15)以内的,在第15 + 3 = 18s时间点发车;

  • 滑动窗口(Sliding Windows)

    滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成

    特点窗口长度固定,有重叠

     适用场景:对最近一个时间段内的统计(求某接口最近5min的失败率来决定是否要报警);

      灵活;连续的波浪;比如股票交易所它是最近24小时的涨跌幅度,随时往后算随时往后划;

 滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。

例如,你有10分钟的窗口和5分钟的滑动,那么每个窗口中5分钟的窗口里包含着上个10分钟产生的数据,如下图所示:

                 

 

object StreamEventTimeApp {
  def main(args: Array[String]): Unit = {
    //环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //声明使用eventTime
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    val dstream: DataStream[String] = env.socketTextStream("hadoop101", 7777)

    val textWithTsDStream: DataStream[(String, Long, Int)] = dstream.map { text =>
      val arr: Array[String] = text.split(" ")
      (arr(0), arr(1).toLong, 1)
    }
    // 1 告知 flink如何获取数据中的event时间戳  2 告知延迟的watermark
    val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks(
      new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(3000)) { //time别导错包了
        override def extractTimestamp(element: (String, Long, Int)): Long = {
          return element._2
        }
      }).setParallelism(1)
    //每5秒开一个窗口 统计key的个数  5秒是一个数据的时间戳为准
    val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDStream.keyBy(0)
    textKeyStream.print("textKey: ")
    //滚动窗口
    //val windowDStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.milliseconds(5000)))

    //滑动窗口
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.milliseconds(5000L), Time.milliseconds(2000L)))
    windowStream.sum(2).print("windows: ").setParallelism(1)

    env.execute()
  }
}
[kris@hadoop101 gmall]$ nc -lk 7777
aaa 100
aaa 500
aaa 1000
aaa 3000
aaa 3999
abc 100
abc 1000
abc 3998
abc 3999
abc 5000
abc 8000
abc 10000

textKey: :8> (abc,100,1)
textKey: :8> (abc,1000,1)
textKey: :8> (abc,3998,1)
textKey: :8> (abc,3999,1) //窗口大小0-4999;前面这些都是在4999窗口以下的范围内,但是开车的时机是在步长+watermark=4000,但开车的时候只有100这一个在里边;步长为1
windows: > (abc,100,1)
textKey: :8> (abc,5000,1) //开车取决于时间间隔步长1s, 每隔1s发一次;第二次发车是在2s的时候,延迟3s,即5s的时候发车,但这个时候车里就只有100和1000两个;
windows: > (abc,100,2)
textKey: :8> (abc,8000,1) //一车接5s的人;8000--5000--4000--3000--(这个时候它俩已经开车走了,不要了)-2000-1000
windows: > (abc,100,2) //3000那辆车
windows: > (abc,100,4) //走的是4000那辆车--100、1000、3998、3999
windows: > (abc,100,4)//5000,走的还是100、1000、3998、3999这四个,5000应该是在下一个窗口大小的范围;
textKey: :8> (abc,10000,1) //10000-3000 ==> 7000s,到5000是走完第一个窗口大小, 6000走一辆(5999--1000);7000(发车的6999-2000)
windows: > (abc,1000,4) //6000: 1000/3998/3999/5000/
windows: > (abc,3998,3) //7000: 3998/3999/5000
窗口大小设置为5s,步长为2s,watermark为3s;
开车时间:每隔2s,例如下例4s、6s、8s、10s...
第一次开车4s, 携带[0,1);
 第二次开车6s, 携带[0,2];
第三次开车8s,携带[0, 4];
第四次开车10s,携带[2, 6]
第五次开车12s,携带[4, 8]
第六次开车14s,携带[6, 10]
[kris@hadoop101 ~]$ nc -lk 7777 abc 399 abc 800 abc 1000 abc 1800 abc 2000 abc 3000 abc 4000 abc 5000 abc 5900 abc 6000 abc 7000 abc 8000 abc 9000 abc 10000 abc 11000 abc 11800 abc 12000 abc 13000 abc 14000 ============>>> textKey: :8> (abc,399,1) textKey: :8> (abc,800,1) textKey: :8> (abc,1000,1) textKey: :8> (abc,1800,1) textKey: :8> (abc,2000,1) textKey: :8> (abc,3000,1) textKey: :8> (abc,4000,1) windows: > (abc,399,2) textKey: :8> (abc,5000,1) textKey: :8> (abc,5900,1) textKey: :8> (abc,6000,1) windows: > (abc,399,5) textKey: :8> (abc,7000,1) textKey: :8> (abc,8000,1) windows: > (abc,399,7) textKey: :8> (abc,9000,1) textKey: :8> (abc,10000,1) windows: > (abc,2000,6) textKey: :8> (abc,11000,1) textKey: :8> (abc,11800,1) textKey: :8> (abc,12000,1) windows: > (abc,4000,6) textKey: :8> (abc,13000,1) textKey: :8> (abc,14000,1) windows: > (abc,6000,5)

如果watermark = 0,窗口大小为5,步长为2s的滑动窗口:

    val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks(
      new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(0)) { //time别导错包了
        override def extractTimestamp(element: (String, Long, Int)): Long = {
          return element._2
        }
      }).setParallelism(1)
    //滑动窗口
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.milliseconds(5000L), Time.milliseconds(2000L)))
    windowStream.sum(2).print("windows: ").setParallelism(1)

[kris@hadoop101 ~]$ nc -lk 7777
abc 299
abc 500
abc 1000
abc 2000
abc 2800
abc 3000
abc 3800
abc 4800
abc 5000
abc 6000
abc 6800
abc 7000
abc 8000
abc 8600
abc 9000
abc 9500
abc 10000

textKey: :8> (abc,299,1)
textKey: :8> (abc,500,1)
textKey: :8> (abc,1000,1) 为什么以1s为基点呢?
windows: > (abc,299,2)
textKey: :8> (abc,2000,1)
textKey: :8> (abc,2800,1)
textKey: :8> (abc,3000,1)
windows: > (abc,299,5)
textKey: :8> (abc,3800,1)
textKey: :8> (abc,4800,1)
textKey: :8> (abc,5000,1)
windows: > (abc,299,8)
textKey: :8> (abc,6000,1)
textKey: :8> (abc,6800,1)
textKey: :8> (abc,7000,1)
windows: > (abc,2000,8)
textKey: :8> (abc,8000,1)
textKey: :8> (abc,8600,1)
textKey: :8> (abc,9000,1)
windows: > (abc,4800,7)
textKey: :8> (abc,9500,1)
textKey: :8> (abc,10000,1)
  • 会话窗口(Session Windows)

    由一系列事件组合一个指定时间长度的timeout间隙组成,类似于web应用的session,也就是一段时间没有接收到新数据就会生成新的窗口

    特点时间无对齐

    session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。

            

object StreamEventTimeApp {
  def main(args: Array[String]): Unit = {
    //环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //声明使用eventTime
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    val dstream: DataStream[String] = env.socketTextStream("hadoop101", 7777)

    val textWithTsDStream: DataStream[(String, Long, Int)] = dstream.map { text =>
      val arr: Array[String] = text.split(" ")
      (arr(0), arr(1).toLong, 1)
    }
    // 1 告知 flink如何获取数据中的event时间戳  2 告知延迟的watermark
    val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks(
      new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(3000)) { //time别导错包了
        override def extractTimestamp(element: (String, Long, Int)): Long = {
          return element._2
        }
      }).setParallelism(1)
    //每5秒开一个窗口 统计key的个数  5秒是一个数据的时间戳为准
    val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDStream.keyBy(0)
    textKeyStream.print("textKey: ")
    //滚动窗口
    //val windowDStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.milliseconds(5000)))
    //滑动窗口
    //val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.milliseconds(5000L), Time.milliseconds(1000L)))

   //会话窗口
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(EventTimeSessionWindows.withGap(Time.milliseconds(5000L)))
    windowStream.sum(2).print("windows: ").setParallelism(1)
    env.execute()
  }
}

只能两次时间的间隔是否满足条件
在触发水位5s的基础上再加延迟3s,
[kris@hadoop101 gmall]$ nc -lk 7777
abc 1000
abc 7000
abc 10000
=======>>>
textKey: :8> (abc,1000,1)
textKey: :8> (abc,7000,1)
textKey: :8> (abc,10000,1) //在上一个基础上+延迟时间3s才会开车
windows: > (abc,1000,1)


[kris@hadoop101 gmall]$ nc -lk 7777
aaa 1000
aaa 2000
aaa 7001
aaa 9000
aaa 10000
=====>>
textKey: :5> (aaa,1000,1)
textKey: :5> (aaa,2000,1)
textKey: :5> (aaa,7001,1) //两个时间点之间相差达到鸿沟5s了,在这个基础之上再加3s才能开车;
textKey: :5> (aaa,9000,1)
textKey: :5> (aaa,10000,1)
windows: > (aaa,1000,2)

CountWindow

CountWindow根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果

注意:CountWindow的window_size指的是相同Key的元素的个数,不是输入的所有元素的总数

  滚动窗口

默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。

   滑动窗口

滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。

下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围是5个元素。

 

 

WindowAPI

 Windowall是所有数据都在一个分区上;keyBy之后是分到各个分区再window去处理

猜你喜欢

转载自www.cnblogs.com/shengyang17/p/11798549.html