5分钟Flink - 时间语义和Watermark

2020-09-08 15:28:09 浏览数 (1)

摘要

  • 一、Flink 中的时间语义
  • 二、EventTime 的引入
  • 三、Event Time 和 Watermark
  • 四、EventTime在window中的使用

一、Flink 中的时间语义

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

  • Event Time:事件实际发生而产生的时间
  • Ingestion time:数据进入 Flink 处理框架的时间
  • Processing Time:事件被处理时当前系统的时间,是基于机器的时间属性

由于平常最常用的是EventTime,所以这里只说EventTime

二、EventTime 的引入

在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在 eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。

如果要使用 EventTime,那么需要引入 EventTime 的时间属性,引入方式如下所示

代码语言:javascript复制
val env = StreamExecutionEnvironment.getExecutionEnvironment

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

// 其他选择项:
// env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
// env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)

val stream: DataStream[MyEvent] = env.addSource(new FlinkKafkaConsumer09[MyEvent](topic, schema, props))

stream
    .keyBy( _.getUser )
    .timeWindow(Time.hours(1))
    .reduce((a, b) => a.add(b))
    .addSink(...)

三、Event Time 和 Watermark

用于处理乱序事件

3.1 概述

我们知道,流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的,虽然大部分情况下,流到 operator 的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。特别是使用kafka的话,多个分区的数据无法保证有序。所以在进行window计算的时候,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark,watermark是用于处理乱序事件的。

  • Watermark 是一种衡量 Event Time 进展的机制。
  • Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用 Watermark 机制结合 window 来实现。
  • 数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了。因此,window 的执行也是由 Watermark 触发的。
  • Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime 小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于 maxEventTime – t,那么这个窗口被触发执行。 有序的流的watermarks

无序的流的watermarks

当 Flink 接收到数据时,会按照一定的规则去生成 Watermark,这条 Watermark 就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是由数据携带的一旦数据携带的 Watermark 比当前未触发的窗口的停止时间要晚, 那么就会触发相应窗口的执行由于 Watermark 是由数据携带的,因此,如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发

上图中,我们设置的允许最大延迟到达时间为 2s,所以时间戳为 7s 的事件对应的 Watermark 是 5s,时间戳为 12s 的事件的 Watermark 是 10s,如果我们的窗口 1 是 1s~5s,窗口 2 是 6s~10s,那么时间戳为 7s 的事件到达时的 Watermarker 恰好触发窗口 1,时间戳为 12s 的事件到达时的 Watermark 恰好触发窗口 2。

Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。

只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。

3.2 Watermark 的引入

先看下我的数据格式

代码语言:javascript复制
SendMessageToKafka: {"id":1,"name":"Johngo1","age":11,"sex":1,"email":"Johngo1@flink.com","time":1592471546035}

Json 格式,其中有关于 time 的字段

然后,watermark 的引入很简单,对于乱序数据,最常见的引用方式如下:

代码语言:javascript复制
stream.assignTimestampsAndWatermarks(
  new BoundedOutOfOrdernessTimestampExtractor[String](Time.milliseconds(1000)) {
    override def extractTimestamp(element: String): Long = {
      val ts = JSON.parseObject(element).get("time").toString
      ts.toLong
    }
  })

Event Time 的使用一定要指定数据源中的时间戳。否则程序无法知道事件的事件时间是什么(数据源里的数据没有时间戳的话,就只能使用 Processing Time 了)。

四、EventTime在window中的使用

以滚动窗口为例:

代码语言:javascript复制
package com.tech.timeandwatermark


import java.text.SimpleDateFormat
import org.apache.flink.api.common.functions.{AggregateFunction, MapFunction}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.util.Collector
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow


class MyAggregateFunction extends AggregateFunction[(String, Long, Int), Int, Int] {
  override def createAccumulator(): Int = {
    /*访问量初始化为0*/
    0
  }

  override def add(in: (String, Long, Int), acc: Int): Int = {
    /*访问量直接  1 即可*/
    acc   in._3
  }

  override def getResult(acc: Int): Int = {
    acc
  }

  override def merge(acc: Int, acc1: Int): Int = {
    acc   acc1
  }
}


class MyWindowFunction extends WindowFunction[Int, String, String, TimeWindow] {
  override def apply(key: String,
                     window: TimeWindow,
                     input: Iterable[Int],
                     out: Collector[String]): Unit = {
    out.collect("key:"   key   "count"   input.iterator.next() )
  }
}

object TumblingEventTimeWindowsDome {
  def main(args: Array[String]): Unit = { // 环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 时间属性 EventTime
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)

    val formatter: SimpleDateFormat  = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")


    val dstream: DataStream[String] = env
        .socketTextStream("127.0.0.1", 9000)
        .assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[String] {
          var currentTimeStamp = 0L
          //设置允许乱序时间
          var maxOutOfOrderness = 10000L

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

          override def extractTimestamp(element: String, previousElementTimestamp: Long): Long = {
            val arr: Array[String] = element.split(",")
            val timeStamp: Long = arr(1).toLong
            currentTimeStamp = Math.max(timeStamp, currentTimeStamp)
            println(element  
              ",EventTime:"   formatter.format(timeStamp)  
              ",MaxTime:"   formatter.format(currentTimeStamp)  
              ",watermark:"   formatter.format(currentTimeStamp - maxOutOfOrderness))
            timeStamp
          }
        })


    val groupDStream = dstream.map(new MapFunction[String, Tuple3[String, Long, Int]] {
      override def map(s: String): (String, Long, Int) = {
        val arr: Array[String] = s.split(",")
        new Tuple3[String, Long, Int](arr(0), arr(1).toLong, 1)
      }
    }).keyBy(0)
      .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
      .aggregate(new MyAggregateFunction())   // , new MyWindowFunction()
      .print()


    env.execute()
  }
}

作者:Johngo

配图:Pexels

0 人点赞