Flink05——一文了解Flink的Window和Time

前言

窗口計算是流式計算中非常常用的數據計算方式之一,通過按照固定時間或長度將 數據流切分成不同的窗口,然後對數據進行相應的聚合運算,從而得到一定時間範圍內的統計結果。窗口(Windows)在SparkStreaming Flink中都是非常重要的概念。
例如統計最近5分鐘內某基站的呼叫數,此時基站的數據在不斷地產生,但是通過5分鐘的窗口將數據限定在固定時間範圍內,就可以對該範圍內的有界數據執行聚合處理,得出最近5分鐘的基站的呼叫數量。

window分類

1.Global Window 和 Keyed Window

在運用窗口計算時,Flink根據上游數據集是否爲KeyedStream類型,對應的Windows 也會有所不同

  • Keyed Window:上游數據集如果是 KeyedStream 類型,則調用 DataStream API 的 window() 方法,數據會根據 Key 在不同的Task 實例中並行分別計算,最後得出針對每個 Key 統 計的結果。
  • Global Window:如果是 Non-Keyed 類型,則調用 WindowsAll()方法,所有的數據都會在窗口算子中由到一個Task中計算,並得到全局統計結果。

舉個例子:

//讀取文件數據 
val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)   	
  .map(line=>{ 
  	var arr =line.split(",") 
  	new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long) 
  	}) 
  	//Global Window 
  	data.windowAll (自定義的WindowAssigner) 
  	//Keyed Window 
  	data.keyBy(_.sid) .window(自定義的WindowAssigner)

2.Time Window和Count Window

基於業務數據的方面考慮,Flink 又支持兩種類型的窗口,一種是基於時間的窗口叫Time Window。還有一種基於輸入數據數量的窗口叫Count Window。

根據不同的業務場景,Time Window 也可以分爲三種類型,分別是滾動窗口(Tumbling Window)、滑動窗口(Sliding Window)和會話窗口(Session Window)

1.滾動窗口(Tumbling Window)
滾動窗口是根據固定時間進行切分,且窗口和窗口之間的元素互不重疊。這種類型的窗 口的最大特點是比較簡單。只需要指定一個窗口長度(window size)。
在這裏插入圖片描述

//每隔5秒統計每個基站的日誌數量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) .timeWindow(Time.seconds(5)) 	
	//.window(TumblingEventTimeWindows.of(Time.seconds(5))) 
	.sum(1) //聚合

2. 滑動窗口(Sliding Window)
滑動窗口也是一種比較常見的窗口類型,其特點是在滾動窗口基礎之上增加了窗口滑動時間(Slide Time),且允許窗口數據發生重疊。當Windows size 固定之後,窗口並不像滾動窗口按照 Windows Size向前移動,而是根據設定的 Slide Time向前滑動。窗口之間的數據重疊大小根據 Windows size 和 Slide time決定,當Slide time小於Windows size便會發生窗口重疊,Slide size 大於 Windows size 就會出現窗口不連續,數據可能不能在 任何一個窗口內計算,Slide size 和 Windows size相等時,Sliding Windows其實就是 Tumbling Windows。
在這裏插入圖片描述

//每隔3秒計算最近5秒內,每個基站的日誌數量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1)  
	.timeWindow(Time.seconds(5),Time.seconds(3)) 
	//.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(3))) 
	.sum(1)

3. 會話窗口(Session Window)
會話窗口(Session Windows)主要是將某段時間內活躍度較高的數據聚合成一個窗口進行計算,窗口的觸發的條件是 Session Gap,是指在規定的時間內如果沒有數據活躍接入,則認爲窗口結束,然後觸發窗口計算結果。需要注意的是如果數據一直不間斷地進入窗口,也會導致窗口始終不觸發的情況。與滑動窗口、滾動窗口不同的是,Session Windows 不需要有固定 windows size 和 slide time,只需要定義 session gap,來規定不活躍數據的時間上限即可。
在這裏插入圖片描述

//3秒內如果沒有數據進入,則計算每個基站的日誌數量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.window(EventTimeSessionWindows.withGap(Time.seconds(3))) 
	.sum(1)

4) Count Window(數量窗口)
Count Window 也有滾動窗口、滑動窗口等。在實際應用中比較少。

window API

在以後的實際案例中 Keyed Window 使用最多,所以我們需要掌握 Keyed Window 的算子, 在每個窗口算子中包含了 Windows Assigner(窗口指定器)、Windows Trigger(窗口觸發器)、Evictor(數據剔除器)、Lateness(時延設定)、Output Tag(輸出標籤)以及Windows Funciton 等組成部分,其中 Windows Assigner 和 Windows Funciton是所有窗口算子必須指定的屬性,其餘的屬性都是根據實際情況選擇指定。

stream.keyBy(...) // 是Keyed類型數據集 
.window(...) //指定窗口分配器類型 
[.trigger(...)] //指定觸發器類型(可選) 
[.evictor(...)] //指定evictor或者不指定(可選) 
[.allowedLateness(...)] //指定是否延遲處理數據(可選) 
[.sideOutputLateData(...)] //指定Output Lag(可選) 
.reduce/aggregate/fold/apply() //指定窗口計算函數
[.getSideOutput(...)] //根據Tag輸出數據(可選)

Windows Assigner:指定窗口的類型,定義如何將數據流分配到一個或多個窗口;
Windows Trigger:指定窗口觸發的時機,定義窗口滿足什麼樣的條件觸發計算;
Evictor:用於數據剔除;
allowedLateness:標記是否處理遲到數據,當遲到數據到達窗口中是否觸發計算;
Output Tag:標記輸出標籤,然後在通過 getSideOutput 將窗口中的數據根據標籤輸出;
Windows Funciton:定義窗口上數據處理的邏輯,例如對數據進行 sum 操作。

window聚合函數

如果定義了 Window Assigner 之後,下一步就可以定義窗口內數據的計算邏輯,這也就 是 Window Function 的定義。Flink 中提供了四種類型的 Window Function,分別爲 ReduceFunction、AggregateFunction 以及 ProcessWindowFunction,(sum 和 max)等。

前三種類型的 Window Fucntion 按照計算原理的不同可以分爲兩大類:

  • 一類是增量聚合函數:對應有 ReduceFunction、AggregateFunction
  • 另一類是全量窗口函數,對應有 ProcessWindowFunction(還有 WindowFunction)

增量聚合函數計算性能較高,佔用存儲空間少,主要因爲基於中間狀態的計算結果,窗口中只維護中間結果狀態值,不需要緩存原始數據。而全量窗口函數使用的代價相對較高, 性能比較弱,主要因爲此時算子需要對所有屬於該窗口的接入數據進行緩存,然後等到窗口 觸發的時候,對所有的原始數據進行彙總計算。

1) ReduceFunction
ReduceFunction定義了對輸入的兩個相同類型的數據元素按照指定的計算方法進行聚合的邏輯,然後輸出類型相同的一個結果元素。

//每隔5秒統計每個基站的日誌數量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.window(TumblingEventTimeWindows.of(Time.seconds(5))) 
	.reduce((v1,v2)=>(v1._1,v1._2+v2._2))

2) AggregateFunction
和 ReduceFunction 相似,AggregateFunction也是基於中間狀態計算結果的增量計算函數,但AggregateFunction在窗口計算上更加通用。AggregateFunction接口相對ReduceFunction更加靈活,實現複雜度也相對較高。AggregateFunction接口中定義了三個需要複寫的方法,其中add()定義數據的添加邏輯,getResult 定義了根據 accumulator計算結果的邏輯,merge方法定義合併accumulator的邏輯

//每隔3秒計算最近5秒內,每個基站的日誌數量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.timeWindow(Time.seconds(5),Time.seconds(3)) 
	.aggregate(new AggregateFunction[(String,Int),(String,Long),(String,Long)] {
		override def createAccumulator() = ("",0) 
		override def add(in: (String, Int), acc: (String, Long)) = { 
			(in._1,acc._2+in._2) 
			}
		override def getResult(acc: (String, Long)) = acc 
		override def merge(acc: (String, Long), acc1: (String, Long)) = {
			 (acc._1,acc1._2+acc._2) 
			 } 
		})

3) ProcessWindowFunction
前面提到的ReduceFunction 和 AggregateFunction 都是基於中間狀態實現增量計算的窗口函數,雖然已經滿足絕大多數場景,但在某些情況下,統計更復雜的指標可能需要依賴於窗口中所有的數據元素,或需要操作窗口中的狀態數據和窗口元數據,這時就需要使用到 ProcessWindowsFunction,ProcessWindowsFunction能夠更加靈活地支持基於窗口全部數據元素的結果計算,例如對整個窗口數據排序取TopN,這樣的需要就必須使用 ProcessWindowFunction。

//每隔5秒統計每個基站的日誌數量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.timeWindow(Time.seconds(5)) 
	.process(new ProcessWindowFunction[(String,Int),(String,Int),String,TimeWindow] {
		 override def process(key: String, context: Context, elements: Iterable[(String, Int)], out: Collector[(String, Int)]): Unit = { 
		 	println("-------") out.collect((key,elements.size)) 
		 	} 
		})
		.print()

Time和watermark

Time

Flink根據時間產生的位置不同,將時間區分爲三種時間語義,分別爲事件生成時間(Event Time)、事件接入時 間(Ingestion Time)和事件處理時間(Processing Time)。

  • Event Time:事件產生的時間,它通常由事件中的時間戳描述。
  • Ingestion Time:事件進入 Flink 的時間。
  • Processing Time:事件被處理時當前系統的時間。

關於三者之間的區別,我們來看下面這張圖:
在這裏插入圖片描述
在 Flink 中默認情況下使用是 Process Time 時間語義,如果用戶選擇使用 Event Time 或 者 Ingestion Time 語 義 , 則 需 要 在 創 建 的 StreamExecutionEnvironment 中 調 用 setStreamTimeCharacteristic() 方 法 設 定 系 統 的 時 間 概 念 , 如 下 代 碼 使 用 TimeCharacteristic.EventTime 作爲系統的時間語義:

 //設置使用EventTime 
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 
 //設置使用IngestionTime 
 streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime) 

在日常應用中,Event Time是應用的最多的。
注意:但是上面的代碼還沒有指定具體的時間到底是什麼值,所以後面還有代碼需要設置!

(2020.6.17更新)

Processing Time和Event Time的區別

在這裏插入圖片描述

WaterMark

在使用 EventTime 處理 Stream 數據的時候會遇到數據亂序的問題,流處理從 Event(事件)產生,流經 Source,再到 Operator,這中間需要一定的時間。雖然大部分情況下,傳 輸到 Operator 的數據都是按照事件產生的時間順序來的,但是也不排除由於網絡延遲等原因而導致亂序的產生,特別是使用 Kafka 的時候,多個分區之間的數據無法保證有序。因此, 在進行 Window 計算的時候,不能無限期地等下去,必須要有個機制來保證在特定的時間後, 必須觸發Window 進行計算,這個特別的機制就是 Watermark(水位線)。Watermark 是用於 處理亂序事件的。

一句話概括:WaterMark就是一種延遲觸發窗口機制
Watermark 的使用存在三種情況:

  1. 本來有序的 Stream 中的 Watermark 如果數據元素的事件時間是有序的,Watermark 時間戳會隨着數據元素的事件時間按順 序生成,此時水位線的變化和事件時間保持一直(因爲既然是有序的時間,就不需要設置延遲時間,watermark=window_end_time),也就是理想狀態下的水位線。當 Watermark 時間大於 Windows 結束時間就會觸發對 Windows 的數據計算,以此類推, 下一個 Window 也是一樣。
    在這裏插入圖片描述
  2. 亂序事件中的 Watermark 現實情況下數據元素往往並不是按照其產生順序接入到 Flink 系統中進行處理,而頻繁 出現亂序或遲到的情況,這種情況就需要使用 Watermarks 來應對。比如下圖,設置延遲時間 t 爲 2
    在這裏插入圖片描述
    3.並行數據流中的 Watermark 在多並行度的情況下,Watermark 會有一個對齊機制,這個對齊機制會取所有 Channel 中最小的 Watermark。

關於WaterMark的詳細分析,在並行度paralism=1的情況下可查看大神李麥迪早前的文章,這裏說明一下結論:

  • Flink如何處理亂序?
    watermark+window機制。window中可以對input進行按照Event Time排序,使得完全按照Event Time發生的順序去處理數據,以達到處理亂序數據的目的。

  • Flink何時觸發window?
    對於late element太多的數據而言:1.Event Time < watermark時間
    對於out-of-order以及正常的數據而言:1. watermark時間 >= window_end_time; 2.在 [window_start_time,window_end_time)中有數據存在

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