Flink Event-Time with WarterMark

Flink Event-Time with WarterMark

  • 前缀
  • 直接在数据流源中生成
  • 通过时间戳分配器/水印生成器
  • 1.8.2
  • 1.10.0
    • Periodic
    • Punctuated
    • 比较杂乱的一些结论
    • 整合代码与水印介绍(看这里)

前缀

  • 有两种分配时间戳和生成水印的方法:
    直接在数据流源中(这句话是官网翻译,说的是摄入时间模式下时间戳和水印是自动生成的,或者,例如Eventtime模式下,kafka为数据源时,在未指定时间戳的情况下,你可以看到那个数据是自带了时间戳的,已经验证过,就是数据进入kafka的时间)。
    通过时间戳分配器/水印生成器:在Flink中,时间戳分配器还定义要发送的水印。

  • 举一个小例子,比如用Flink做kafka的wordcount,使用的时间类型为Event-Time时间类型,那么我们如何指定数据流的时间进度是依赖于每条数据中的哪个部分的呢。
    目前我知道的有三种,会用的只有两种,分别来自于下面两个版本。1.12的版本中对于时间时间的指定貌似更为方便,使用的是DDL。
    本贴着重介绍1.10.0部分

直接在数据流源中生成

  • StreamSource可以直接将时间戳分配给它们生成的元素,还可以发出水印。完成此操作后,不需要时间戳分配程序。请注意,如果使用时间戳赋值器,则源提供的任何时间戳和水印都将被覆盖。

  • 要将时间戳直接分配给源中的元素,源必须在SourceContext上使用collectWithTimestamp(…)方法。要生成水印,源必须调用emitWatermark(Watermark)函数。

通过时间戳分配器/水印生成器

  • 时间戳分配程序获取一个流,并生成一个带有时间戳元素和水印的新流。如果原始流已经具有时间戳和/或水印,则时间戳分配者将覆盖它们。
  • 时间戳赋值器通常紧跟在数据源之后指定,但并不严格要求这样做。例如,一个常见的模式是在时间戳赋值器之前解析(MapFunction)和filter(FilterFunction)。在任何情况下,时间戳赋值器都需要在事件时间的第一个操作(例如第一个窗口操作)之前指定。作为一种特殊情况,当使用Kafka作为流作业的源时,Flink允许在源(或消费者)本身内部指定一个时间戳分配者/水印发射器。关于如何做到这一点的更多信息可以在Kafka连接器文档中找到。

https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/connectors/kafka.html

1.8.2

一种是像下面这一种1.8版本的,在创建kafkatable时使用,同时还可以指定最大延迟时间。
Flink Event-Time with WarterMark_第1张图片

1.10.0

水印(Watermarks)的生成方式有两种,分别是Periodic(周期性的)和Punctuated(可以在每条数据上都生成水印)。这两种方式还有不同的使用方法,下面分别展示和官网上类似的用法。

Periodic

  • AssignerWithPeriodicWatermarks 定期分配时间戳并生成水印(可能取决于流元素,或完全基于处理时间)。

  • 通过定义生成水印的间隔(n默认是200毫秒) ExecutionConfig.setAutoWatermarkInterval(n)。分配器的getCurrentWatermark()方法每次都会被调用,如果返回的水印非空且大于前一个水印,则将发出新的水印。

      new AssignerWithPeriodicWatermarks[Tuple2[String, Long]] {
        var currentMaxTimestamp = 0L // 用来记录目前接收到的最大的时间戳(来源于数据本身)
        val maxOutOfOrderness = 20000L // 最大延迟时间,单位是毫秒

        /**
          * 返回当前水印。系统定期调用该方法来检索当前水印(通过StreamExecutionEnvironment
          * .getConfig.setAutoWatermarkInterval()来设定周期间隔)。
          * 该方法可能会返回{@code null}来表示没有可用的新水印,不过貌似是是针对于Punctuated模式的。
          * @return
          */
        override def getCurrentWatermark: Watermark = {
          new Watermark(currentMaxTimestamp - maxOutOfOrderness)
        }

        // 
        /**
          * 为每条进入数据流的数据分配时间戳。
          * 例如kafka : 顺序反着看
          *   extractTimestamp
          *   ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPeriodicWatermarks#getTimestampForRecord
          *   ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher#emitRecordWithTimestampAndPeriodicWatermark
          * @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
          * @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
          * @return 当前element的时间戳
          */
        override def extractTimestamp(element: (String, Long), previousElementTimestamp: Long): Long = {
          val timestamp = element._2
          currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
          timestamp
        }
      }

Punctuated

  • 若要在特定事件下才可能生成新水印时,可以使用带标点水印。Flink将首先调用extractTimestamp(…)方法来为元素分配一个时间戳,然后立即对该元素调用checkAndGetNextWatermark(…)方法。

  • checkAndGetNextWatermark(…)方法将传递extractTimestamp(…)方法中返回的时间戳,并可以决定是否要生成水印。每当checkAndGetNextWatermark(…)方法的返回非空且大于上一个水印时,则将发出该新水印。

new AssignerWithPunctuatedWatermarks[Tuple2[String, Long]]{

        /**
          * 为每条进入数据流的数据分配时间戳。
          * 例如kafka : 顺序反着看
          *   extractTimestamp
          *   ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getTimestampForRecord
          *   ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
          *   下面是去获取所有partition中最小的水印并发射的,在这之前还会调用一次 checkAndGetNextWatermark 方法
          *   ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark
          *   ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getCurrentPartitionWatermark
          * @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
          * @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
          * @return 当前element的时间戳
          */
        override def extractTimestamp(element: (String, Long), previousElementTimestamp: Long): Long = {
          element._2
        }

        /**
          * 询问此实现是否要发出水印。这个方法在{@link #extractTimestamp(Object, long)}方法之后被调用。
          * 这一层的返回会在调用它的方法中再次判断,判断你返回的时候为 null 或者小于之前的水印,
          * 这两种情况皆沿用之前在 checkAndGetNewWatermark 方法中成功赋值的水印。
          * 虽然保留了原来的水印,但并不代表发射的一定就是当前分区的水印,因为每次都是对比后才去发射的watermark。
          *    例如kafka:书序倒着看
          *    checkAndGetNextWatermark
          *    ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# checkAndGetNewWatermark
          *    ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
          *    ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark (这里发射用的水印是在上面那个方法里赋的值)
          *        在updateMinPunctuatedWatermark方法里轮询找出所有partion中最小的那个水印并发射
          * @param lastElement  刚刚进入过extractTimestamp方法的数据
          * @param extractedTimestamp {@link #extractTimestamp(Object, long)}方法提取的时间戳
          * @return
          */
        override def checkAndGetNextWatermark(lastElement: (String, Long), extractedTimestamp: Long): Watermark = {
          // 简单判断一下再返回水印
          if (lastElement._2 != 0) new Watermark(extractedTimestamp) else null
        }
        
      }

比较杂乱的一些结论

1.10.0
flink-planner

周期性水印生成的最简单的特例是给定源任务看到的时间戳按升序出现的情况。在这种情况下,当前时间戳始终可以充当水印,因为不会有更早的时间戳到达。
注意,对于每个并行数据源任务,时间戳只需要升序。例如,在特定的设置中,如果一个Kafka分区被一个并行数据源实例读取,那么只需要在每个Kafka分区内时间戳是递增的。Flink的水印合并机制将在并行流被洗牌、联合、连接或合并时生成正确的水印。
就是不用担心单个多并行度下kafka每个分区的时间戳问题。

org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#cleanupTime
cleanupTime(清除时间)=当前窗口的结束时间+allowedLateness
如果不是EventTime模式,则没有allowedLateness就算设置了也不起作用。


窗口(例如长度2s)的结束开始结束界限只精确到秒(1596695796199是窗口的第一条数据,那开始时间就是1596695796000,结束时间就是1596695798000,能够被算进这个窗口的最大时间戳是结束时间就是1596695797999)

窗口:start_time >= 数据时间 < end_time 
如果数据在窗口触发之前到了,就算数据的时间戳小于当前水位线,也依然会被计算在窗口内。
a,0,1596695796199,EventTime:2020-08-06 14:36:36.199,watermark:2020-08-06 14:36:35.199
a,1,1596695797213,EventTime:2020-08-06 14:36:37.213,watermark:2020-08-06 14:36:36.213
a,2,1596695798999,EventTime:2020-08-06 14:36:38.999,watermark:2020-08-06 14:36:37.999
a,3,1596695796000,EventTime:2020-08-06 14:36:36.000,watermark:2020-08-06 14:36:37.999
a,4,1596695899215,EventTime:2020-08-06 14:38:19.215,watermark:2020-08-06 14:38:18.215
(a,3,1596695796000)

并不会去拿到来的数据的时间戳和水印对比此条数据是否应该抛弃,水位线和窗口配合是为了让这个窗口晚些被触发,尽量多等一些属于这个窗口的数据的到来(防止有延迟数据)。


org.apache.flink.streaming.api.operators.InternalTimerServiceImpl#advanceWatermark
在这里去判断的是否触发窗口(在这之前数据应该已经计算好了)
还会去判断 timer 是否小于水印,这个方法就是判断水印是否>= timer ,是否触发窗口。
那么 timer 是怎么得来的呢?
timer就是用这个这个窗口中能出现的最大的一个时间戳注册的。

org.apache.flink.streaming.runtime.io.StreamTaskNetworkInput#processElement
在这里会去判断过来的是水印、latencymark、record、……中的哪一种。
每过来一个水印或者record就会去调用一次,分别去判断是否触发窗口(根据窗口的 timer(窗口的endTime-1+allowedLateness)去判断是否触发)和更新窗口状态。
我得看它是不是触发和平常的收集单条记录走的是不同的路径,是不是在触发之前就已经计算了,在哪里计算的。
是的,在触发之前窗口中的数据就已经计算好了。

这是以socket->flatMap->keyBy->window->maxBy-print为例:
数据:
org.apache.flink.streaming.runtime.tasks.OneInputStreamTask.StreamTaskNetworkOutput#emitRecord
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#processElement
	windowAssigner.assignWindows()为当前数据指定窗口。
	如果有迟到数据,在if (isWindowLate(window))这里会根据设置的allowedLateness判断是否丢弃。
	triggerContext.onElement(element);为当前窗口设置 timer。
	emitWindowContents(window, contents);这里也能触发窗口,但默认是不触发的(交给水印那个路径(inputWatermark)去触发),执行这个方法前会有判断,如果有迟到数据未被丢弃,就会执行这个方法。
	org.apache.flink.runtime.state.heap.HeapReducingState# add
		窗口聚合操作在被触发之前就会根据新来的数据去计算并更新结果(状态),在timer与水印对比触发窗口之前,数据就已经计算好了。
		org.apache.flink.runtime.state.heap.HeapReducingState.ReduceTransformorg.apache.flink.runtime.state.heap.StateTable# transform
		org.apache.flink.runtime.state.heap.CopyOnWriteStateMap# transform
		ation# apply
	registerCleanupTimer(window);在这里设置清理窗口的 timer(windoMaxTimestamp + allowedLateness(windoMaxTimestamp = endtime - 1)。
org.apache.flink.runtime.state.heap.HeapReducingState#add  在这个方法后面去计算并更新状态
org.apache.flink.runtime.state.heap.StateTable#transform
org.apache.flink.runtime.state.heap.CopyOnWriteStateMap#transform
	将apply计算后的数据放入对应的key+namespace(starttime-endtime)的state中
org.apache.flink.runtime.state.heap.HeapReducingState.ReduceTransformation#apply
	会先去判断这个窗口内是否已经有计算过的数据存在,如果没有的话直接把当前这条数据存入state
org.apache.flink.streaming.api.functions.aggregation.ComparableAggregator#reduce
	计算state中的这个窗口内上一条计算结果与新来的数据。

水印:
org.apache.flink.streaming.runtime.streamstatus.StatusWatermarkValve#inputWatermark
org.apache.flink.streaming.runtime.streamstatus.StatusWatermarkValve#findAndOutputNewMinWatermarkAcrossAlignedChannels
org.apache.flink.streaming.runtime.tasks.OneInputStreamTask.StreamTaskNetworkOutput#emitWatermark
org.apache.flink.streaming.api.operators.AbstractStreamOperator#processWatermark
org.apache.flink.streaming.api.operators.InternalTimeServiceManager#advanceWatermark
org.apache.flink.streaming.api.operators.InternalTimerServiceImpl#advanceWatermark
	这个方法里的while循环就对应着那句话,触发窗口的条件就是:窗口中有数据,并且水印大于等于窗口的结束时间。
	eventTimeTimersQueue.poll();把eventTimeTimersQueue中 <= wartermark的 timer 都poll出来。
	就是在这里去触发那些被水印>=的 timer
	eventTimeTimersQueue.peek()把eventTimeTimersQueue里面的 timer 都拿出来,然后再看这个 timer 是不是 <= 水印,是的话才会接着向下走去触发窗口。
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#onEventTime
	这里调用的是WindowOperator的方法。
	triggerContext.onEventTime(timer.getTimestamp());用窗口的 timer 去判断是否=window的maxtimestamp(就是endtime-1true的话接下来才会调用windowState.get()返回状态中保存的此窗口的计算结果,只要返回的contents不为null接着往下走。
		org.apache.flink.runtime.state.heap.AbstractHeapAppendingState#getInternal -> return stateTable.get(currentNamespace);
org.apache.flink.streaming.runtime.operators.windowing.WindowOperator#emitWindowContents
org.apache.flink.streaming.runtime.operators.windowing.functions.InternalSingleValueWindowFunction#process
org.apache.flink.streaming.api.functions.windowing.PassThroughWindowFunction#apply
org.apache.flink.streaming.api.operators.TimestampedCollector#collect(T)
org.apache.flink.streaming.api.operators.AbstractStreamOperator.CountingOutput#collect(org.apache.flink.streaming.runtime.streamrecord.StreamRecord<OUT>)
org.apache.flink.streaming.runtime.tasks.OperatorChain.CopyingChainingOutput#collect(org.apache.flink.streaming.runtime.streamrecord.StreamRecord<T>)
org.apache.flink.streaming.runtime.tasks.OperatorChain.CopyingChainingOutput#pushToOperator
org.apache.flink.streaming.api.operators.StreamSink#processElement
org.apache.flink.streaming.api.functions.sink.SinkFunction#invoke(IN, org.apache.flink.streaming.api.functions.sink.SinkFunction.Context)
org.apache.flink.streaming.api.functions.sink.PrintSinkFunction#invoke
org.apache.flink.api.common.functions.util.PrintSinkOutputWriter#write 到这一步结果就打印出来了

下面是附加的部分代码运行顺序:
StreamSource run()
FlinkKafkaConsumer run()
----------在这里会去调用FlinkKafkaConsumer010的createFetcher()->Kafka010Fetcher()->Kafka09Fetcher()->AbstractFetcher()
----------AbstractFetcher里会去判断使用哪种水印策略
----------在这里会去判断是否使用轮循发现分区后,往下走
Kafka09Fetcher runfetchLoop()
Kafka010Fetcher emitRecord() 在这里consumerRecord携带的时间戳,应该就是数据进入kafka的时间(有待验证,因为那个时间戳既不是当前系统时间,也不是数据本身携带的时间,而是一个小时左右前的,我好像就是在哪个时候塞数据进入kafka的)
AbstractFetcher emitRecordWithTimestamp()
----------如果是NO_TIMESTAMPS_WATERMARKS模式,直接调用 collectWithTimestamp()发出数据。
----------否则就会去判断使用 emitRecoudWithTimestampAndPariodicWatermark()或者 emitRecoudWithTimestampAndPunctuatedWatermark()
----------调用collectWithTimestamp:
--------------------StreamSourceContext collectWithTimestamp()
--------------------AbstractStreamOperator collect()
--------------------OperatorChain collect() -> pushToOperator()
--------------------StreamSink processElement() 因为我是Source直接就到sink,所以才是这个流程(并且使用的是NO_TIMESTAMPS_WATERMARKS)。
----------调用emitRecoudWithTimestampAndPariodicWatermark():
--------------------AbstractFetcher emitRecoudWithTimestampAndPariodicWatermark()
------------------------------KafkaTopicPartitionStateWithPeriodicWatermarks getTimestampForRecord()
----------------------------------------AccendingTimestampExtractor extractTimestamp() 这里会去调用我重写的extractAscendingTimestamp()(获取每条数据本身携带的时间戳,紧跟着FlinkKafkaConsumer写的)

整合代码与水印介绍(看这里)

  • 测试数据
a,0,1596695796199
a,1,1596695797999
a,2,1596695798000  # 这个不被计算在第一个窗口里(窗口:start_time >= 数据时间 < end_time )
a,3,1596695798999  # 这里应该触发了,水印 >= endtime-1
a,4,1596695796000  # allowedLateness = 2000 所以它和下面这两条还能计入第一个窗口,并再次输出第一个窗口重计算后的结果。
a,5,1596695796234
a,6,1596695800999  
# 一旦水印超过或赶上(> or =)一个窗口的 windoMaxTimestamp + allowedLateness(windoMaxTimestamp = endtime - 1),那么属于这个窗口的数据再到来时将会被抛弃。
# 第一个窗口 796000 <-> 798000,cleanupTime = 797999 + 2000 = 799999。这条数据的wartermark = 800999 - 1000 = 799999。
# 所以在这条数据发射水印之后,属于第一个窗口的数据在这之后到来将会被丢弃
# 这时水印正好等于第二个窗口的 windoMaxTimestamp,触发了第二个窗口。
a,7,1596695796567 
  • 下面的代码是可以正常运行的
    入口在WarterMarkTest、水印的申城策略可以用不用的代码实现,但归根结底就两种类型,周期和标点(官网的翻译过来叫这个),周期策略实现是MyPeriodicWatermarks、标点策略实现是MyPunctuatedWatermarks。
import java.text.SimpleDateFormat
import java.util.Date

import org.apache.flink.api.common.functions.{FlatMapFunction, ReduceFunction}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks, TimestampAssigner}
import org.apache.flink.streaming.api.scala.{DataStream, 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
import org.apache.flink.util.Collector
import org.apache.flink.api.scala._

object WarterMarkTest{
  val env = StreamExecutionEnvironment.getExecutionEnvironment
  env.setParallelism(1)
  env.enableCheckpointing(60000)
  env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
  env.getConfig.setAutoWatermarkInterval(200) // 默认就是200ms
  val ds = env.socketTextStream("127.0.0.1", 5688, '\n')

  def main(args: Array[String]): Unit = {
    // 周期
    val dss = ds.assignTimestampsAndWatermarks(new MyPeriodicWatermarks(1000))
    // 标点
//    val dss = ds.assignTimestampsAndWatermarks(new MyPunctuatedWatermarks(1000))

    publicFunction(dss,10)
    env.execute("test")
  }

  def publicFunction(ds:DataStream[String],tetw:Int): Unit = {
    ds.flatMap(new FlatMapFunction[String, Tuple3[String, Long, String]] {
      override def flatMap(value: String, out: Collector[(String, Long, String)]): Unit = {
        val sp = value.split(",")
        out.collect((sp(0), sp(1).toLong, sp(2)))
      }
    })
      .keyBy(0)
      .window(TumblingEventTimeWindows.of(Time.seconds(tetw)))
      .allowedLateness(Time.milliseconds(2000))// 仅对EventTime窗口有效
      .maxBy(1)
//            .reduce(new ReduceFunction[(String, Long, String)] {
//            override def reduce(value1: (String, Long, String), value2: (String, Long, String)): (String, Long, String) = {
//              (value1._1 + "," + value2._1, value1._2 + value2._2, value1._3 + "," + value2._3)
//            }
//          })
      .print()
  }
}

/**
  * AssignerWithPeriodicWatermarks 定期分配时间戳并生成水印(可能取决于流元素,或完全基于处理时间(这句话是官网翻译,不用在意))。
  */
class MyPeriodicWatermarks(outTime:Long) extends AssignerWithPeriodicWatermarks[String] {

  val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
  var currentMaxTimestamp = 0L // 用来记录目前接收到的最大的时间戳(来源于数据本身)
  val maxOutOfOrderness = outTime // 最大延迟时间,单位是毫秒

  /**
    * 返回当前水印。系统定期调用该方法来检索当前水印(通过StreamExecutionEnvironment
    * .getConfig.setAutoWatermarkInterval()来设定周期间隔,默认是200毫秒)。
    * 该方法可能会返回{@code null}来表示没有可用的新水印,不过貌似是是针对于Punctuated模式的。
    *
    * @return
    */
  override def getCurrentWatermark: Watermark = {
//    println("Send WarterMark:" + sdf.format(new Date((currentMaxTimestamp - maxOutOfOrderness))) + "    Current Date:" + sdf.format(new Date()))
    new Watermark(currentMaxTimestamp - maxOutOfOrderness)
  }

  /**
    * 为每条进入数据流的数据分配时间戳。
    * 例如kafka : 调用顺序反着看
    *   extractTimestamp
    *   ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPeriodicWatermarks#getTimestampForRecord
    *   ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher#emitRecordWithTimestampAndPeriodicWatermark
    * @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
    * @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
    * @return 当前element的时间戳
    */
  override def extractTimestamp(element: String, previousElementTimestamp: Long): Long = {
    val timestamp = element.split(",")(2).toLong
    currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp)
    println(element + ",EventTime:" + sdf.format(new Date(timestamp)) + ",watermark:" + sdf.format(new Date((currentMaxTimestamp - maxOutOfOrderness))))
    timestamp
  }

}

/**
  * 若要在特定事件下才可能生成新水印时,可以使用带PunctuatedWatermarks。
  * Flink将首先调用extractTimestamp(…)方法来为元素分配一个时间戳,
  * 然后立即对该元素调用checkAndGetNextWatermark(…)方法。
  *
  * checkAndGetNextWatermark(…)方法将传递extractTimestamp(…)方法中返回的时间戳,并可以决定是否要生成水印。
  * 每当checkAndGetNextWatermark(…)方法的返回非空且大于上一个水印时,则将发出该新水印。
  */
class MyPunctuatedWatermarks(outTime:Long) extends AssignerWithPunctuatedWatermarks[String] {

  val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
  var currentMaxTimestamp = 0L // 用来记录目前接收到的最大的时间戳(来源于数据本身)
  val maxOutOfOrderness = outTime // 允许的延迟时间,单位是毫秒

  /**
    * 询问此实现是否要发出水印。这个方法在{@link #extractTimestamp(Object, long)}方法之后被调用。
    * 这一层的返回会在调用它的方法中再次判断,判断你返回的时候为 null 或者小于之前的水印,
    * 这两种情况皆沿用之前在 checkAndGetNewWatermark 方法中成功赋值的水印。
    * 虽然保留了原来的水印,但并不代表发射的一定就是当前分区的水印,因为每次都是对比后才去发射的watermark。
    *    例如kafka:书序倒着看
    *    checkAndGetNextWatermark
    *    ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# checkAndGetNewWatermark
    *    ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
    *    ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark (这里发射用的水印是在上面那个方法里赋的值)
    *        在updateMinPunctuatedWatermark方法里轮询找出所有partion中最小的那个水印并发射
    * @param lastElement  刚刚进入过extractTimestamp方法的数据
    * @param extractedTimestamp {@link #extractTimestamp(Object, long)}方法提取的时间戳
    * @return
    */
  override def checkAndGetNextWatermark(lastElement: String, extractedTimestamp: Long): Watermark = {
    // 该样去返回水印,可以在这个方法里定义自己的逻辑
    val timestamp = lastElement.split(",")(2).toLong
    new Watermark(timestamp - maxOutOfOrderness)
  }

  /**
    * 为每条进入数据流的数据分配新的时间戳(会覆盖掉原来的时间戳,比如从kafka过来的数据会带有它进入kafka时的时间)。
    * 例如kafka : 调用顺序反着看
    *   extractTimestamp
    *   ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getTimestampForRecord
    *   ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# emitRecordWithTimestampAndPunctuatedWatermark
    *   下面是去获取所有partition中最小的水印并发射的,在这之前还会调用一次 checkAndGetNextWatermark 方法
    *   ->  org.apache.flink.streaming.connectors.kafka.internals.AbstractFetcher# updateMinPunctuatedWatermark
    *   ->  org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartitionStateWithPunctuatedWatermarks# getCurrentPartitionWatermark
    * @param element 将为其分配时间戳的数据(element type取决于上游输入的类型)。
    * @param previousElementTimestamp 数据以前的内部时间戳,如果还没有分配时间戳,则为负值。
    * @return 当前element的时间戳
    */
  override def extractTimestamp(element: String, previousElementTimestamp: Long): Long = {
    val timestamp = element.split(",")(2).toLong
    println(element + ",EventTime:" + sdf.format(new Date(timestamp)) + ",watermark:" + sdf.format(new Date((currentMaxTimestamp - maxOutOfOrderness))))
    timestamp
  }
}
  • 这是我理解的的容易理解有简短的介绍了,这是在源码中得出的结论。
    水印就是用来配合窗口使用的,水印的发射策略有两种,一种是周期型,一种是标点型。水印的存在是为了防止存在数据延迟到达,就是以事件时间为基础生成延后固定时间的水印,然后以水印作为流的整体时间进度,只有当水印大于等于窗口的最大时间戳,并且窗口中存在数据时,那个窗口才会被触发。而在触发之后如果还有属于那个窗口的数据到来,将会被丢弃。
    当然这是在没有设置窗口的 allowedLateness。如果设置了,即便属于某条数据的窗口已经被触发,它也可以根据那个窗口保存的状态重新计算并向下游输出。要做到这一步还要一个前提,就是那个窗口的cleanupTime(窗口最大时间戳+allowedLateness)并没有被水印赶上,即wartermark < cleanupTime。否则此条数据将会被丢弃。

你可能感兴趣的:(flink)