Flink-時間語義與Wartmark及EventTime在Window中的使用

代碼GitHub:https://github.com/SmallScorpion/flink-tutorial.git

Flink中的時間語義

Event Time:是事件創建的時間。它通常由事件中的時間戳描述,例如採集的日誌數據中,每一條日誌都會記錄自己的生成時間,Flink通過時間戳分配器訪問事件時間戳。
Ingestion Time:是數據進入Flink的時間。
Processing Time:是每一個執行基於時間操作的算子的本地系統時間,與機器相關,默認的時間屬性就是Processing Time。

在這裏插入圖片描述

例子

某些應用場合,不應該使用 Processing Time,Event Time 可以從日誌數據的時間戳(timestamp)中提取
在這裏插入圖片描述
例如,一條日誌進入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,因爲我們要根據日誌的生成時間進行統計。
在這裏插入圖片描述

EventTime的引入

在Flink的流式處理中,絕大部分的業務都會使用eventTime,一般只在eventTime無法使用時,纔會被迫使用ProcessingTime或者IngestionTime。如果要使用EventTime,那麼需要引入EventTime的時間屬性,引入方式如下所示:

val env = StreamExecutionEnvironment.getExecutionEnvironment
// 從調用時刻開始給env創建的每一個stream追加時間特徵
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

在這裏插入圖片描述

Watermark的產生背景

我們知道,流處理從事件產生,到流經source,再到operator,中間是有一個過程和時間的,雖然大部分情況下,流到operator的數據都是按照事件產生的時間順序來的,但是也不排除由於網絡、分佈式等原因,導致亂序的產生,所謂亂序,就是指Flink接收到的事件的先後順序不是嚴格按照事件的Event Time順序排列的。

在這裏插入圖片描述

那麼此時出現一個問題,一旦出現亂序,如果只根據eventTime決定window的運行,我們不能明確數據是否全部到位,但又不能無限期的等下去,此時必須要有個機制來保證一個特定的時間後,必須觸發window去進行計算了,這個特別的機制,就是Watermark。

在這裏插入圖片描述

水位線(Watermark)

  1. Watermark是一種衡量Event Time進展的機制。
  2. Watermark是用於處理亂序事件的,而正確的處理亂序事件,通常用Watermark機制結合window來實現。
  3. 數據流中的Watermark用於表示timestamp小於Watermark的數據,都已經到達了,因此,window的執行也是由Watermark觸發的。
  4. Watermark可以理解成一個延遲觸發機制,我們可以設置Watermark的延時時長t,每次系統會校驗已經到達的數據中最大的maxEventTime,然後認定eventTime小於maxEventTime - t的所有數據都已經到達,如果有窗口的停止時間等於maxEventTime – t,那麼這個窗口被觸發執行。
  5. watermark 用來讓程序自己平衡延遲和結果正確性

有序流的Watermarker如下圖所示:(Watermark設置爲0)

在這裏插入圖片描述

亂序流的Watermarker如下圖所示:(Watermark設置爲2)

在這裏插入圖片描述

小結:
在這裏插入圖片描述

WaterMark的特點

  1. watermark 是一條特殊的數據記錄
  2. watermark 必須單調遞增,以確保任務的事件時間時鐘在向前推進,而不是在後退
  3. watermark 與數據的時間戳相關
    在這裏插入圖片描述

WaterMark的傳遞

在這裏插入圖片描述

WaterMark引入

Event Time的使用一定要指定數據源中的時間戳。否則程序無法知道事件的事件時間是什麼(數據源裏的數據沒有時間戳的話,就只能使用Processing Time了)
在這裏插入圖片描述

TimestampAssigner

AssignerWithPeriodicWatermarks:

  1. 週期性的生成 watermark:系統會週期性的將 watermark 插入到流中
  2. 默認週期是200毫秒,可以使用 ExecutionConfig.setAutoWatermarkInterval() 方法進行設置
  3. 升序和前面亂序的處理 BoundedOutOfOrderness ,都是基於週期性 watermark 的。

AssignerWithPunctuatedWatermarks :

  1. 沒有時間週期規律,可打斷的生成 watermark

assignTimestampsAndWatermarks底層調用:
在這裏插入圖片描述
BoundedOutOfOrdernessTimestampExtractor底層生成WaterMark爲週期性
在這裏插入圖片描述

WaterMark的設定

在這裏插入圖片描述

自定義一個週期性的時間戳抽取

產生watermark的邏輯:每隔5秒鐘,Flink會調用AssignerWithPeriodicWatermarks的getCurrentWatermark()方法。如果方法返回一個時間戳大於之前水位的時間戳,新的watermark會被插入到流中。這個檢查保證了水位線是單調遞增的。如果方法返回的時間戳小於等於之前水位的時間戳,則不會產生新的watermark。

import com.atguigu.bean.SensorReading
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark

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

    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    // 從調用時刻開始給env創建的每一個stream追加時間特徵
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 設置watermark的默認生成周期 -> 100毫秒生成一個WaterMark
    env.getConfig.setAutoWatermarkInterval(100L)

    val inputDStream: DataStream[String] = env.readTextFile("D:\\MyWork\\WorkSpaceIDEA\\flink-tutorial\\src\\main\\resources\\SensorReading.txt")

    val dataDstream: DataStream[SensorReading] = inputDStream
      .map( data => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })
      // 自定義一個週期性
      .assignTimestampsAndWatermarks( MyPeriodicAssigner() )

    dataDstream.print("WaterMark")

    env.execute("eventTime test job")

  }
}

case class MyPeriodicAssigner() extends AssignerWithPeriodicWatermarks[SensorReading]{

  // 延時爲1分鐘
  val bound: Long = 60 * 1000

  // 觀察到的最大時間戳
  var maxTs: Long = Long.MinValue

  override def getCurrentWatermark: Watermark = new Watermark(maxTs - bound)

  override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
    maxTs = maxTs.max(element.timestamp)
    element.timestamp
  }
}

自定義一個間斷性生成的WaterMark

間斷式地生成watermark。和週期性生成的方式不同,這種方式不是固定時間的,而是可以根據需要對每條數據進行篩選和處理。直接上代碼來舉個例子,我們只給sensor_1的傳感器的數據流插入watermark:

import com.atguigu.bean.SensorReading
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPunctuatedWatermarks
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark

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


      val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

      // 從調用時刻開始給env創建的每一個stream追加時間特徵
      env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
      // 設置watermark的默認生成周期 -> 100毫秒生成一個WaterMark
      env.getConfig.setAutoWatermarkInterval(100L)

      val inputDStream: DataStream[String] = env.readTextFile("D:\\MyWork\\WorkSpaceIDEA\\flink-tutorial\\src\\main\\resources\\SensorReading.txt")

      val dataDstream: DataStream[SensorReading] = inputDStream
        .map( data => {
          val dataArray: Array[String] = data.split(",")
          SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
        })
        // 自定義一個週期性
        .assignTimestampsAndWatermarks( MyPunctuatedAssigner() )

      dataDstream.print("WaterMark")

      env.execute("eventTime test job")

    }
}

case class MyPunctuatedAssigner() extends AssignerWithPunctuatedWatermarks[SensorReading]{

  val bound: Long = 60 * 1000

  override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
    if(lastElement.id == "sensor_1") {
      new Watermark(extractedTimestamp - bound)
    } else {
      null
      }
  }

  override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = element.timestamp
}

在這裏插入圖片描述

滾動窗口和EventTime

import com.atguigu.bean.SensorReading
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time

/**
 * 滾動窗口 + EventTime
 */
object TumblingEventTimeWindowsTest {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    // 從調用時刻開始給env創建的每一個stream追加時間特徵
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 設置watermark的默認生成周期 -> 100毫秒生成一個WaterMark
    env.getConfig.setAutoWatermarkInterval(100L)

    val inputDStream: DataStream[String] = env.readTextFile("D:\\MyWork\\WorkSpaceIDEA\\flink-tutorial\\src\\main\\resources\\SensorReading.txt")

    val dataDstream: DataStream[SensorReading] = inputDStream
      .map( data => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })
      // 自定義一個週期性
      .assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading]
      ( Time.seconds(1000)) {
        override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
      } )

    val eventTimeDStream: DataStream[SensorReading] = dataDstream
      .keyBy(0)
      .timeWindow( Time.seconds(15))
      .reduce( (r1, r2) =>
        SensorReading(r1.id, (r2.temperature-r1.temperature).toLong, r1.timestamp.min(r2.timestamp).toDouble) )

    dataDstream.print("WaterMark")
    
    eventTimeDStream.print("EventTime")

    env.execute("eventTime test job")
  }
}

在這裏插入圖片描述

滑動窗口

 val eventTimeDStream: DataStream[SensorReading] = dataDstream
      .keyBy(0)
      .window(SlidingEventTimeWindows.of(Time.seconds(2),Time.milliseconds(500)))
      // .timeWindow( Time.seconds(2), Time.milliseconds(500))
      .reduce( (r1, r2) =>
        SensorReading(r1.id, (r2.temperature-r1.temperature).toLong, r1.timestamp.min(r2.timestamp).toDouble) )

在這裏插入圖片描述

會話窗口

相鄰兩次數據的EventTime的時間差超過指定的時間間隔就會觸發執行。如果加入Watermark, 會在符合窗口觸發的情況下進行延遲。到達延遲水位再進行窗口觸發。

 val eventTimeDStream: DataStream[SensorReading] = dataDstream
      .keyBy(0)
      .window(EventTimeSessionWindows.withGap(Time.milliseconds(500)) )
      .reduce( (r1, r2) =>
        SensorReading(r1.id, (r2.temperature-r1.temperature).toLong, r1.timestamp.min(r2.timestamp).toDouble) )

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章