概述

本文介绍flink的一个重要特性:水印(Watermarks)的原理,并通过实际的例子说明如何使用该特性。
环境:flink-1.7.1

水印(Watermarks)简介

我们看到对事件时间(Event Time)的支持是流体系结构的核心。当计算基于事件时间时,我们如何知道所有事件是否已经全部到达,我们是否可以计算并输出窗口的结果?换句话说,我们如何跟踪事件时间并知道输入流中已达到某个事件时间?为了跟踪事件时间,我们需要某种由数据驱动的时钟,而不是由系统时间驱动的时钟。

当时间是数据本身的一部分时,我们是否可以根据数据本身的时间来设定时间窗口,我们该如何设定呢?

Flink通过水印实现了这一目标,这是一种推进事件时间的机制。水印是嵌入在流中的常规记录,基于事件时间,通知计算已达到特定时间。当上述窗口接收到时间标记大于10:01:00的水印时(例如,带有时间标记10:01:00的水印和带有时间标记10:03:43的水印都将起作用),它知道没有其他记录的时间戳大于标记;所有时间小于或等于时间戳的事件都已发生。然后它可以安全地计算并发出窗口的结果(总和)。使用水印,事件时间完全独立于处理时间。例如,如果水印迟到(在处理时间内测量“迟到”),这不会影响结果的正确性,只会影响我们得到结果的速度。

如何生成水印

在Flink中,应用程序开发人员生成水印,因为它需要一些领域知识。完美的水印是一个永远不会错的水印;也就是说,在具有水印之前的事件时间的水印之后,将不会到达任何事件。在特殊情况下,最新事件的时间戳可能是一个完美的水印。例如,如果我们的输入数据完全有序,就会发生这种情况。相反,启发式水印只是对时间进度的估计,但有时可能是错误的,这意味着一些迟到的事件可能发生在承诺它们不会来的水印之后。当水印具有启发性时,Flink提供了处理后期元素的机制。

领域知识通常用于指定水印。例如,我们可能知道我们的事件可能会延迟,但迟到的时间可能不会超过五秒,这意味着我们可以发出最大时间戳的水印,减去五秒钟。或者,不同的Flink作业可以监视流并构建用于生成水印的模型,从事件到达时的迟到中学习。

水印提供了一种(可能是启发式的)机制来指定事件时间内输入的完整性。

如果水印太慢,我们可能会看到输出速度的减慢,但我们可以通过在水印之前输出近似结果来解决这个问题(Flink提供了这样做的机制)。如果水印太快,我们可能得到一个我们认为正确但不是的结果,我们可以通过使用Flink的后期数据机制来解决这个问题。如果所有这些看起来都很复杂,请记住,现实世界中的大多数事件流都是乱序的,并且没有(通常)这样的事情(通常)是关于它们如何失序的完美知识。 (理论上,我们必须考虑未来。)水印是唯一需要我们处理无序数据并限制结果正确性的机制;另一种选择是忽视现实并假装我们的结果是正确的,如果不是,那么它们的正确性没有任何限制。

水印的使用

以下的例子,用来对单词进行计数。

  • 开启输入终端
    开启一个终端,在终端输入以下命令,并输入以下数据:
$ nc -lk 9999
a,1550441750001
a,1550442057548
b,1550442057548
c,1550442057548
d,1550442124556
e,1550442124556
f,1550442124556

注意:我们的水印是从数据的第2个字段获取时间戳。

  • 完整代码例子
    完整的例子如下:
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.watermark.Watermark


object wartermarkTest {
  def main(args: Array[String]): Unit =  {
    println("start watermarkTest")
    val wsenv = StreamExecutionEnvironment.getExecutionEnvironment

    wsenv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    val text = wsenv.socketTextStream("localhost", 9999).
                  assignTimestampsAndWatermarks(new TimestampExtractor)  //设置基于时间戳的水印

    val counts = text.map {(m: String) => {
      m.trim
      (m.split(",")(0), 1)
    } }
      .keyBy(0)
      .timeWindow(Time.seconds(10), Time.seconds(5)) //开启滑动时间窗口,窗口大小10秒,每5秒滑动一次
      .sum(1)

    print("start print result")
    counts.print()
    wsenv.execute("EventTime processing example")
  }
}


class TimestampExtractor extends AssignerWithPeriodicWatermarks[String] with Serializable {
  override def extractTimestamp(e: String, prevElementTimestamp: Long) = { 
    e.split(",")(1).toLong
  }
  override def getCurrentWatermark(): Watermark = { //假设事件时间比当前的系统时间少5秒
    new Watermark(System.currentTimeMillis - 5000)
  }
}

总结

本文介绍了flink水印的基本概念和原理,并通过一个实际的例子来说明如何使用水印。

参考资料