【Flink】(五)時間語義和水位線 (Watermark)

寫在前面:我是「雲祁」,一枚熱愛技術、會寫詩的大數據開發猿。暱稱來源於王安石詩中一句 [ 雲之祁祁,或雨於淵 ] ,甚是喜歡。


寫博客一方面是對自己學習的一點點總結及記錄,另一方面則是希望能夠幫助更多對大數據感興趣的朋友。如果你也對 數據中臺、數據建模、數據分析以及Flink/Spark/Hadoop/數倉開發 感興趣,可以關注我的動態 https://blog.csdn.net/BeiisBei ,讓我們一起挖掘大數據的價值~


每天都要進步一點點,生命不是要超越別人,而是要超越自己! (ง •_•)ง

一、Flink 中的時間語義

在 Flink 的流式處理中,會涉及到時間的不同概念,如下圖所示:

在這裏插入圖片描述

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

哪種時間語義更重要?

在這裏插入圖片描述
例如,一條日誌進入 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

二、EventTime 的引入

在 Flink 的流式處理中,絕大部分的業務都會使用 eventTime,一般只在eventTime 無法使用時,纔會被迫使用 ProcessingTime 或者 IngestionTime。

如果要使用 EventTime,那麼需要引入 EventTime 的時間屬性,引入方式如下所示:

我們可以直接在代碼中,對執行環境調用 setStreamTimeCharacteristic 方法,設置流的時間特性。

具體的時間,還需要從數據中提取時間戳 (timestamp)

val env = StreamExecutionEnvironment.getExecutionEnvironment

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

三、 Watermark(水位線)

3.1 基本概念

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

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

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

在這裏插入圖片描述
當 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 的特點

在這裏插入圖片描述

  • watermark 是一條特殊的數據記錄
  • watermark 必須單調遞增,以確保任務的事件時間時鐘在向前推進,而不是在後退。
  • watermark 與數據的時間戳相關

3.3 watermark 的傳遞

在這裏插入圖片描述

3.4 Watermark 的引入

watermark 的引入很簡單,對於亂序數據,最常見的引用方式如下:

dataStream.assignTimestampsAndWatermarks( new 
BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.millisecond
s(1000)) {
 override def extractTimestamp(element: SensorReading): Long = {
 element.timestamp * 1000
 }
} )

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

我們看到上面的例子中創建了一個看起來有點複雜的類,這個類實現的其實就是分配時間戳的接口。Flink 暴露了 TimestampAssigner 接口供我們實現,使我們可以自定義如何從事件數據中抽取時間戳。

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

val readings: DataStream[SensorReading] = env
.addSource(new SensorSource) 
.assignTimestampsAndWatermarks(new MyAssigner())

MyAssigner 有兩種類型

  • AssignerWithPeriodicWatermarks
  • AssignerWithPunctuatedWatermarks

以上兩個接口都繼承自 TimestampAssigner。

Assigner with periodic watermarks

週期性的生成 watermark:系統會週期性的將 watermark 插入到流中(水位線也是一種特殊的事件!)。默認週期是 200 毫秒。可以使用
ExecutionConfig.setAutoWatermarkInterval()方法進行設置。

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

// 每隔 5 秒產生一個 watermark
env.getConfig.setAutoWatermarkInterval(5000)

產生 watermark 的邏輯:每隔 5 秒鐘,Flink 會調用
AssignerWithPeriodicWatermarks 的 getCurrentWatermark()方法。

如果方法返回一個時間戳大於之前水位的時間戳,新的 watermark 會被插入到流中。這個檢查保證了水位線是單調遞增的。如果方法返回的時間戳小於等於之前水位的時間戳,則不會產生新的 watermark。

例子,自定義一個週期性的時間戳抽取:、

	class PeriodicAssigner extends
AssignerWithPeriodicWatermarks[SensorReading] {
val bound: Long = 60 * 1000 // 延時爲 1 分鐘
var maxTs: Long = Long.MinValue // 觀察到的最大時間戳

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

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

種簡單的特殊情況是,如果我們事先得知數據流的時間戳是單調遞增的,也
就是說沒有亂序,那我們可以使用 assignAscendingTimestamps,這個方法會直接使用數據的時間戳生成 watermark。

val stream: DataStream[SensorReading] = ...
val withTimestampsAndWatermarks = stream
	.assignAscendingTimestamps(e => e.timestamp)

>> result: E(1), W(1), E(2), W(2), ...

而對於亂序數據流,如果我們能大致估算出數據流中的事件的最大延遲時間,就可以使用如下代碼:

val stream: DataStream[SensorReading] = ...
val withTimestampsAndWatermarks = stream.assignTimestampsAndWatermarks(
	new SensorTimeAssigner
)
	class SensorTimeAssigner extends
BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
// 抽取時間戳
override def extractTimestamp(r: SensorReading): Long = r.timestamp
}

>> relust: E(10), W(0), E(8), E(7), E(11), W(1), ...

Assigner with punctuated watermarks

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

	class PunctuatedAssigner extends
AssignerWithPunctuatedWatermarks[SensorReading] {
	val bound: Long = 60 * 1000
	override def checkAndGetNextWatermark(r: SensorReading, extractedTS:
Long): Watermark = {
		if (r.id == "sensor_1") {
			new Watermark(extractedTS - bound) 
		} else {
		null
	} 
}
override def extractTimestamp(r: SensorReading, previousTS: Long): Long
= { 
		r.timestamp
	} 
}

3.5 Watermark 的設定

  • 在 Flink 中,watermark 由應用程序開發人員生成,這通常需要對相應的領域有一定的瞭解
  • 如果 watermark 設置的延遲太久,收到結果的速度可能就會很慢,解決辦法是在水位線到達之前輸出一個近似結果
  • 而如果 watermark 到達得太早,則可能收到錯誤結果,不過 Flink 處理遲到數據的機制可以解決這個問題

四、EvnetTime 在 window 中的使用

4.1 滾動窗口(TumblingEventTimeWindows)

def main(args: Array[String]): Unit = {
 // 環境
 val env: StreamExecutionEnvironment = 
StreamExecutionEnvironment.getExecutionEnvironment

 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
 env.setParallelism(1)
 
 val dstream: DataStream[String] = env.socketTextStream("localhost",7777)
 
 val textWithTsDstream: DataStream[(String, Long, Int)] = dstream.map

{ text =>
 val arr: Array[String] = text.split(" ")
 (arr(0), arr(1).toLong, 1)
 }
 val textWithEventTimeDstream: DataStream[(String, Long, Int)] = 
textWithTsDstream.assignTimestampsAndWatermarks(new 
BoundedOutOfOrdernessTimestampExtractor[(String, Long,
Int)](Time.milliseconds(1000)) {
 override def extractTimestamp(element: (String, Long, Int)): Long = {
 return element._2
 }
 })
 
 val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = 
textWithEventTimeDstream.keyBy(0)
 textKeyStream.print("textkey:")
 
 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] 
= textKeyStream.window(TumblingEventTimeWindows.of(Time.seconds(2)))
 
 val groupDstream: DataStream[mutable.HashSet[Long]] = 
windowStream.fold(new mutable.HashSet[Long]()) { case (set, (key, ts, count)) 
=>
 set += ts
 }
 
 groupDstream.print("window::::").setParallelism(1)
  env.execute()
	 } 
 }

結果是按照 Event Time 的時間窗口計算得出的,而無關係統的時間(包括輸入的快慢)。

4.2 滑動窗口(SlidingEventTimeWindows)

def main(args: Array[String]): Unit = {
 // 環境
 val env: StreamExecutionEnvironment = 
StreamExecutionEnvironment.getExecutionEnvironment
 
 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
 env.setParallelism(1)

 val dstream: DataStream[String] = env.socketTextStream("localhost",7777)

 val textWithTsDstream: DataStream[(String, Long, Int)] = dstream.map { text 
=>
 val arr: Array[String] = text.split(" ")
 (arr(0), arr(1).toLong, 1)
 }
 val textWithEventTimeDstream: DataStream[(String, Long, Int)] = 
textWithTsDstream.assignTimestampsAndWatermarks(new 
BoundedOutOfOrdernessTimestampExtractor[(String, Long, 
Int)](Time.milliseconds(1000)) {
 override def extractTimestamp(element: (String, Long, Int)): Long = {
 	return element._2
 }
 })
 
 val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = 
textWithEventTimeDstream.keyBy(0)
 textKeyStream.print("textkey:")

 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = 
textKeyStream.window(SlidingEventTimeWindows.of(Time.seconds(2),Time.millis
econds(500)))

 val groupDstream: DataStream[mutable.HashSet[Long]] = windowStream.fold(new 
mutable.HashSet[Long]()) { case (set, (key, ts, count)) =>
 set += ts
 }

 groupDstream.print("window::::").setParallelism(1)

 env.execute()
}

4.3 會話窗口(EventTimeSessionWindows)

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

def main(args: Array[String]): Unit = {
 // 環境
 val env: StreamExecutionEnvironment = 
StreamExecutionEnvironment.getExecutionEnvironment

 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
 env.setParallelism(1)

 val dstream: DataStream[String] = env.socketTextStream("localhost",7777)
 
 val textWithTsDstream: DataStream[(String, Long, Int)] = dstream.map { text 
=>
 val arr: Array[String] = text.split(" ")
 (arr(0), arr(1).toLong, 1)
 }
 val textWithEventTimeDstream: DataStream[(String, Long, Int)] = 
textWithTsDstream.assignTimestampsAndWatermarks(new 
BoundedOutOfOrdernessTimestampExtractor[(String, Long, 
Int)](Time.milliseconds(1000)) {
 override def extractTimestamp(element: (String, Long, Int)): Long = {

 return element._2
 }
 })

 val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = 
textWithEventTimeDstream.keyBy(0)
 textKeyStream.print("textkey:")

 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] 
=
textKeyStream.window(EventTimeSessionWindows.withGap(Time.milliseconds(500)
) )

 windowStream.reduce((text1,text2)=>
 ( text1._1,0L,text1._3+text2._3)
 ) .map(_._3).print("windows:::").setParallelism(1)

	 env.execute()

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