Flink基於EventTime和WaterMark處理亂序事件和晚到的數據

在實際的業務中,我們經常會遇到數據遲到的情況,這個時候基於窗口進行計算的結果就不對了,Flink中watermark就是爲了解決這個問題的,理解watermark之前,先來說一下flink中的三個與流數據相關的概念,ProcessTime、EventTime、IngestionTime,不然很難理解watermark是怎麼回事.

我們先來看一下官網給出的一張圖,非常形象地展示了Process Time、Event Time、Ingestion Time這三個時間分別所處的位置,如下圖所示:
在這裏插入圖片描述
我們按照上圖從左到右的順序介紹這幾個概念,依次是event time,ingestion time,processing time.

Event Time

事件時間:事件時間是每條事件在它產生的時候記錄的時間,該時間記錄在事件中,在處理的時候可以被提取出來。小時的時間窗處理將會包含事件時間在該小時內的所有事件,而忽略事件到達的時間和到達的順序事件時間對於亂序、延時、或者數據重放等情況,都能給出正確的結果。事件時間依賴於事件本身,而跟物理時鐘沒有關係。利用事件時間編程必須指定如何生成事件時間的watermark,這是使用事件時間處理事件的機制。機制是這樣描述的:事件時間處理通常存在一定的延時,因此自然的需要爲延時和無序的事件等待一段時間。因此,使用事件時間編程通常需要與處理時間相結合。

Ingestion Time

攝入時間:攝入時間是事件進入flink的時間,在source operator中,每個事件拿到當前時間作爲時間戳,後續的時間窗口基於該時間。攝入時間在概念上處於事件時間和處理時間之間,與處理時間相比稍微昂貴一點,但是能過夠給出更多可預測的結果。因爲攝入時間使用的是source operator產生的不變的時間,後續不同的operator都將基於這個不變的時間進行處理,但是處理時間使用的是處理消息當時的機器系統時鐘的時間。與事件時間相比,攝入時間無法處理延時和無序的情況,但是不需要明確執行如何生成watermark。在系統內部,攝入時間採用更類似於事件時間的處理方式進行處理,但是有自動生成的時間戳和自動的watermark。

Process Time

處理時間:當前機器處理該條事件的時間流處理程序使用該時間進行處理的時候,所有的操作(類似於時間窗口)都會使用當前機器的時間,例如按照小時時間窗進行處理,程序將處理該機器一個小時內接收到的數據。處理時間是最簡單的概念,不需要協調機器時間和流中事件相關的時間。他提供了最小的延時和最佳的性能。但是在分佈式和異步環境中,處理時間不能提供確定性,因爲它對事件到達系統的速度和數據流在系統的各個operator之間處理的速度很敏感

基於處理時間的系統

對於這個例子,我們期望消息具有格式值,timestamp,其中value是消息,timestamp是在源生成此消息的時間。由於我們正在構建基於處理時間的系統,因此以下代碼忽略了時間戳部分。

val text = senv.socketTextStream("localhost", 9999)
val counts = text.map {(m: String) => (m.split(",")(0), 1) }
    .keyBy(0)
    .timeWindow(Time.seconds(10), Time.seconds(5))
    .sum(1)
counts.print
senv.execute("ProcessingTime processing example")

情況1:消息到達不間斷

假設源分別在時間13秒,第13秒和第16秒產生類型a的三個消息。(小時和分鐘不重要,因爲窗口大小隻有10秒)。

在這裏插入圖片描述

這些消息將落入Windows中,如下所示。在第13秒產生的前兩個消息將落入窗口1 [5s-15s]和window2 [10s-20s],第16個時間生成的第三個消息將落入window2 [ 10s-20s]和window3 [15s-25s] ]。每個窗口發出的最終計數分別爲(a,2),(a,3)和(a,1)。

該輸出可以被認爲是預期的行爲。現在我們將看看當一個消息到達系統的時候會發生什麼。

情況2:消息到達延遲

現在假設其中一條消息(在第13秒生成)到達延遲6秒(第19秒),可能是由於某些網絡擁塞。你能猜測這個消息會落入哪個窗口?

在這裏插入圖片描述

延遲的消息落入窗口2和3,因爲19在10-20和15-25之間。在window2中計算沒有任何問題(因爲消息應該落入該窗口),但是它影響了window1和window3的結果。那怎麼辦呢?我們現在將嘗試使用EventTime處理來解決這個問題。

基於EventTime的系統

我們現在需要設置這個時間戳提取器,並將TimeCharactersistic設置爲EventTime。其餘的代碼與ProcessingTime的情況保持一致。

senv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val text = senv.socketTextStream("localhost", 9999)
                .assignTimestampsAndWatermarks(new TimestampExtractor) 
val counts = text.map {(m: String) => (m.split(",")(0), 1) }
      .keyBy(0)
      .timeWindow(Time.seconds(10), Time.seconds(5))
      .sum(1)
counts.print
senv.execute("EventTime processing example")

要啓用EventTime處理,我們需要一個時間戳提取器,從消息中提取事件時間信息。請記住,消息是格式值,時間戳。該extractTimestamp方法獲取時間戳部分並將其作爲一個長期。現在忽略getCurrentWatermark方法,我們稍後再回來。

class TimestampExtractor extends AssignerWithPeriodicWatermarks[String] with Serializable {
  override def extractTimestamp(e: String, prevElementTimestamp: Long) = {
    e.split(",")(1).toLong 
  }
  override def getCurrentWatermark(): Watermark = { 
      new Watermark(System.currentTimeMillis)
  }
}

運行上述代碼的結果如下圖所示

在這裏插入圖片描述

結果看起來更好,窗口2和3現在發出正確的結果,但是window1仍然是錯誤的。Flink沒有將延遲的消息分配給窗口3,因爲它現在檢查了消息的事件時間,並且理解它不在該窗口中。但是爲什麼沒有將消息分配給窗口1?原因是在延遲的信息到達系統時(第19秒),窗口1的評估已經完成了(第15秒)。現在讓我們嘗試通過使用水印來解決這個問題。請注意,在窗口2中,延遲的消息仍然位於第19秒,而不是第13秒(事件時間)。該圖中的描述是故意表示窗口中的消息不會根據事件時間進行排序。

Watermark

watermark是用於處理亂序事件的,而正確的處理亂序事件,通常用watermark機制結合window來實現。

我們知道,流處理從事件產生,到流經source,再到operator,中間是有一個過程和時間的。雖然大部分情況下,流到operator的數據都是按照事件產生的時間順序來的,但是也不排除由於網絡、背壓等原因,導致亂序的產生(out-of-order或者說late element)。

但是對於late element,我們又不能無限期的等下去,必須要有個機制來保證一個特定的時間後,必須觸發window去進行計算了。這個特別的機制,就是watermark。

水印本質上是一個時間戳。當Flink中的運算符接收到水印時,它明白(假設)它不會看到比該時間戳更早的消息。因此,在“EventTime”中,水印也可以被認爲是一種告訴Flink它有多遠的一種方式。

爲了這個例子的目的,把它看作是一種告訴Flink一個消息延遲多少的方式。在最後一次嘗試中,我們將水印設置爲當前系統時間。因此,不要指望任何延遲的消息。我們現在將水印設置爲當前時間-5秒,這告訴Flink希望消息最多有5s的延遲,這是因爲每個窗口僅在水印通過時被評估。由於我們的水印是當前時間-5秒,所以第一個窗口[5s-15s]將僅在第20秒被評估。類似地,窗口[10s-20s]將在第25秒進行評估,依此類推。

override def getCurrentWatermark(): Watermark = { 
      new Watermark(System.currentTimeMillis - 5000)
  }

通常最好保持接收到的最大時間戳,並創建具有最大預期延遲的水印,而不是從當前系統時間減去。
進行上述更改後運行代碼的結果是:

最後我們得到了正確的結果,所有這三個窗口現在都按照預期的方式發射計數,這是(a,2),(a,3)和(a,1)。

allowedLateness

allowedLateness也是Flink處理亂序事件的一個特別重要的特性,默認情況下,當wartermark通過window後,再進來的數據,也就是遲到或者晚到的數據就會別丟棄掉了,但是有的時候我們希望在一個可以接受的範圍內,遲到的數據,也可以被處理或者計算,這就是allowedLateness產生的原因了,簡而言之呢,allowedLateness就是對於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的情況。

下面看一下完整的代碼:

package flink
 
import java.text.SimpleDateFormat
import java.util.Properties
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.util.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.triggers.CountTrigger
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer010
import org.apache.flink.streaming.connectors.redis.RedisSink
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, RedisCommandDescription, RedisMapper}
import org.slf4j.LoggerFactory
 
/**
  * flinkstreaming消費kafka的數據實現exactly-once的語義;
  */
object flinkStreamingJason {
  private val zk = "192.168.17.142:2181,192.168.17.145:2181,192.168.17.147:2181"
  private val broker = "192.168.17.142:9092,192.168.17.145:9092,192.168.17.147:9092"
  private val group_id = "jason_"
  private val topic = "jason_1027"
  def main(args: Array[String]): Unit = {
    lazy val logger  = LoggerFactory.getLogger(classOf[Nothing])
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    //設置時間Time Notion;默認使用的是ProcessTime;
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.getConfig.enableObjectReuse()
    env.enableCheckpointing(5000)
    env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
    val properties = new Properties()
    properties.setProperty("zookeeper.connect", zk)
    properties.setProperty("bootstrap.servers", broker)
    properties.setProperty("group.id", group_id)
    val consumer = new FlinkKafkaConsumer010[String](topic,new SimpleStringSchema, properties)
    val stream = env.addSource(consumer)
    val wordcount = stream
      .assignTimestampsAndWatermarks(new TimestampExtractor)
      .filter(_.nonEmpty)
      .filter(_.contains(","))
      .map(x=>{
        val arr = x.split(",")
        val code = arr(0)
        val time = arr(1).toLong
        (code,time)
      }) // 指派時間戳,並生成WaterMark
      .keyBy(0)
      .timeWindow(Time.seconds(10),Time.seconds(5))
      .trigger(CountTrigger.of(2)) //決定了一個窗口何時能被窗口函數計算或者清除,每個窗口都有自己的trigger;
      .sum(1)
      .map(x=> (x._1,x._2.toString))
    val conf = new FlinkJedisPoolConfig.Builder().setHost("192.168.17.142").setPort(6379).build()
    val sink = new RedisSink[(String,String)](conf, new RedisExampleMapperJason)
    wordcount.addSink(sink)
    env.execute("flink streaming Event Time And WaterMark Jason")
  }
}
 
class RedisExampleMapperJason extends RedisMapper[(String, String)] {
  override def getCommandDescription: RedisCommandDescription = {
    new RedisCommandDescription(RedisCommand.SET, null)
  }
  override def getKeyFromData(data: (String, String)): String = data._1
  override def getValueFromData(data: (String, String)): String = data._2
}
 
/**
  * 時間戳提取器需要實現AssignerWithPeriodicWatermarks;
  *
  */
class TimestampExtractor extends AssignerWithPeriodicWatermarks[String] with Serializable {
  private val maxOutOfOrderness = 5000L //允許數據晚到的最大時間5s;
  private var wm: Watermark = null
 
  override def getCurrentWatermark: Watermark = {
    wm = new Watermark(System.currentTimeMillis() - maxOutOfOrderness)
    wm
  }
  override def extractTimestamp(t: String, l: Long): Long = {
    l
  }
}

提交後到web,ui上面可以看到watermark的如下圖所示:
在這裏插入圖片描述

轉載自: https://blog.csdn.net/xianpanjia4616/article/details/84971274

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