Flink流计算编程--Flink中allowedLateness详细介绍及思考

1、简介

Flink中借助watermark以及window和trigger来处理基于event time的乱序问题,那么如何处理“late element”呢?

也许有人会问,out-of-order element与late element有什么区别?不都是一回事么?

答案是一回事,都是为了处理乱序问题而产生的概念。要说区别,可以总结如下:

1、通过watermark机制来处理out-of-order的问题,属于第一层防护,属于全局性的防护,通常说的乱序问题的解决办法,就是指这类;
2、通过窗口上的allowedLateness机制来处理out-of-order的问题,属于第二层防护,属于特定window operator的防护,late element的问题就是指这类。

下面我们重点介绍allowedLateness。

2、allowedLateness介绍

默认情况下,当watermark通过end-of-window之后,再有之前的数据到达时,这些数据会被删除。

为了避免有些迟到的数据被删除,因此产生了allowedLateness的概念。

简单来讲,allowedLateness就是针对event time而言,对于watermark超过end-of-window之后,还允许有一段时间(也是以event time来衡量)来等待之前的数据到达,以便再次处理这些数据。

默认情况下,如果不指定allowedLateness,其值是0,即对于watermark超过end-of-window之后,还有此window的数据到达时,这些数据被删除掉了。

注意:对于trigger是默认的EventTimeTrigger的情况下,allowedLateness会再次触发窗口的计算,而之前触发的数据,会buffer起来,直到watermark超过end-of-window + allowedLateness()的时间,窗口的数据及元数据信息才会被删除。再次计算就是DataFlow模型中的Accumulating的情况。

同时,对于sessionWindow的情况,当late element在allowedLateness范围之内到达时,可能会引起窗口的merge,这样,之前窗口的数据会在新窗口中累加计算,这就是DataFlow模型中的AccumulatingAndRetracting的情况。

3、allowedLateness的例子

3.1、TumblingEventTime窗口

这里watermark允许3秒的乱序,allowedLateness允许数据迟到5秒。

import java.text.SimpleDateFormat

import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.{CheckpointingMode, TimeCharacteristic}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time

/**
  * windowedStream.allowedLateness() test,this is also called window accumulating.
  * allowedLateness will trigger window again when 'late element' arrived to the window 
  */
object TumblingWindowAccumulatingTest {

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

    if (args.length != 2) {
      System.err.println("USAGE:\nSocketTextStreamWordCount <hostname> <port>")
      return
    }

    val hostName = args(0)
    val port = args(1).toInt


    val env = StreamExecutionEnvironment.getExecutionEnvironment //获取流处理执行环境
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //设置Event Time作为时间属性
    //env.setBufferTimeout(10)
    //env.enableCheckpointing(5000,CheckpointingMode.EXACTLY_ONCE)

    val input = env.socketTextStream(hostName,port) //socket接收数据

    val inputMap = input.map(f=> {
      val arr = f.split("\\W+")
      val code = arr(0)
      val time = arr(1).toLong
      (code,time)
    })

    /**
      * 允许3秒的乱序
      */
    val watermarkDS = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[(String,Long)] {

      var currentMaxTimestamp = 0L
      val maxOutOfOrderness = 3000L

      val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

      override def getCurrentWatermark: Watermark = {
        new Watermark(currentMaxTimestamp - maxOutOfOrderness)
      }

      override def extractTimestamp(t: (String,Long), l: Long): Long = {
        val timestamp = t._2
        currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
        timestamp
      }
    })

    /**
      * 对于此窗口而言,允许5秒的迟到数据,即第一次触发是在watermark > end-of-window时
      * 第二次(或多次)触发的条件是watermark < end-of-window + allowedLateness时间内,这个窗口有late数据到达
      */
    val accumulatorWindow = watermarkDS
      .keyBy(_._1)
      .window(TumblingEventTimeWindows.of(Time.seconds(10)))
      .allowedLateness(Time.seconds(5))
      .apply(new AccumulatingWindowFunction)
      .name("window accumulate test")
      .setParallelism(2)

    accumulatorWindow.print()


    env.execute()

  }
}

AccumulatingWindowFunction:

主要是计算窗口内的元素个数以及累计个数

import java.text.SimpleDateFormat

import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala.function.RichWindowFunction
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector


class AccumulatingWindowFunction extends RichWindowFunction[(String, Long),(String,String,String,Int, String, Int),String,TimeWindow]{

  val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

  var state: ValueState[Int] = _

  var count = 0

  override def open(config: Configuration): Unit = {
    state = getRuntimeContext.getState(new ValueStateDescriptor[Int]("AccumulatingWindow Test", classOf[Int], 0))
  }

  override def apply(key: String, window: TimeWindow, input: Iterable[(String, Long)], out: Collector[(String, String, String, Int, String, Int)]): Unit = {
    count = state.value() + input.size

    state.update(count)

    // key,window start time, window end time, window size, system time, total size
    out.collect(key, format.format(window.getStart),format.format(window.getEnd),input.size, format.format(System.currentTimeMillis()),count)
  }
}

测试:

输出结果:

解释:

对于key1而言,1487225040000代表2017-02-16 14:04:00.000,10秒钟的窗口范围是[2017-02-16 14:04:00.000,2017-02-16 14:04:10.000),因此当watermark超过2017-02-16 14:04:10.000时,窗口会被触发,由于watermark是允许3秒乱序,因此当标签时间是1487225053000的数据到来时,窗口被触发了。我们看到结果中的第一行就是第一次触发的窗口,窗口内的元素个数是2,累加个数也是2。

由于此窗口还允许5秒的late,因此在watermark < end-of-window + allowedLateness(2017-02-16 14:04:15.000)之内到达的数据,都会被再次触发窗口的计算。我们看标签是key1,1487225045000的数据,此时的watermark是2017-02-16 14:04:12.000(1487225055000 - 3000得来),因此符合触发条件,立刻触发窗口的计算。也就是我们看到的第二条数据的结果。此时窗口内的元素个数是3,累加个数却是5!!!

同理,key1,1487225048000这条数据第三次触发了窗口的计算。窗口内的个数是4,累加个数是9!!!

当key1,1487225058000的数据到来时,此时的watermark是2017-02-16 14:04:15.000,已经超过了这个窗口的触发范围(窗口都是前闭后开的区间),因此,这个窗口再有迟到的数据,将直接被删除。

我们看到key1,1487225049000这条数据到达时,系统没有任何的反应,即此数据太晚了,被删除了。

最后的一条数据是key1,1487225063000,其watermark是2017-02-16 14:04:20.000,已经超过了下一个窗口的触发时间,因此会触发下一个窗口的计算,结果就是我们看到的最后一条输出的数据,窗口内的元素个数是4(分别是1487225050000,1487225053000,1487225055000和1487225058000),但是累加个数是13!!!

思考:

对于TumblingEventTime窗口的累加处理,很好的一点是及时更新了最后的结果,但是也有一个令人无法忽视的问题,即再次触发的窗口,窗口内的UDF状态错了!这就是我们要关注的问题,我们要做的是去重操作。

至于这个去重怎么做,这个就要看UDF中的状态具体要算什么内容了,例如本例中的state只是简单的sum累计值,此时可以在UDF中添加一个hashMap,map中的key就设计为窗口的起始与结束时间,如果map中已经存在这个key,则state.update时,就不要再加上window.size,而是直接加1即可。这里不再做具体的演示。

附上一张这个测试程序的DAG图:

可以看到,window操作接到10条数据(输入),触发了4次window计算(输出)。而且Flink1.2中对与DAG也增加了一些reason元数据信息,例如每个边的类型(Hash或Rebalance)。

5、总结

Flink中处理乱序依赖watermark+window+trigger,属于全局性的处理;

同时,对于window而言,还提供了allowedLateness方法,使得更大限度的允许乱序,属于局部性的处理;

其中,allowedLateness只针对Event Time有效;

allowedLateness可用于TumblingEventTimeWindow、SlidingEventTimeWindow以及EventTimeSessionWindows,要注意这可能使得窗口再次被触发,相当于对前一次窗口的窗口的修正(累加计算或者累加撤回计算);

要注意再次触发窗口时,UDF中的状态值的处理,要考虑state在计算时的去重问题。

最后要注意的问题,就是sink的问题,由于同一个key的同一个window可能被sink多次,因此sink的数据库要能够接收此类数据。

 

转载于:https://my.oschina.net/u/2000675/blog/1544459

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值