一、Time
在 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,因爲我們要根據日誌的生成時間進行統計。
二、Window
1 、Window 概述
streaming 流式計算是一種被設計用於處理無限數據集的數據處理引擎,而無限數據集是指一種不斷增長的本質上無限的數據集,而 window 是一種切割無限數據爲有限塊進行處理的手段。
Window 是無限數據流處理的核心, Window 將一個無限的 stream 拆分成有限大小的” buckets”桶,我們可以在這些桶上做計算操作。
2 Window 類型
Window 可以分成兩類:
CountWindow: 按照指定的數據條數生成一個 Window,與時間無關。
TimeWindow:按照時間生成 Window。
對於 TimeWindow,可以根據窗口實現原理的不同分成三類: 滾動窗口(TumblingWindow) 、 滑動窗口(Sliding Window) 和會話窗口(Session Window) 。
1. 滾動窗口(Tumbling Windows)
將數據依據固定的窗口長度對數據進行切片。
特點: 時間對齊,窗口長度固定,沒有重疊。
適用場景:適合做 BI 統計等(做每個時間段的聚合計算)。
滾動窗口分配器將每個元素分配到一個指定窗口大小的窗口中,滾動窗口有一個固定的大小,並且不會出現重疊。例如: 如果你指定了一個 5 分鐘大小的滾動窗口, 窗口的創建如下圖所示:
2. 滑動窗口(Sliding Windows)
滑動窗口是固定窗口的更廣義的一種形式,滑動窗口由固定的窗口長度和滑動間隔組成。
特點: 時間對齊,窗口長度固定,有重疊。
適用場景:對最近一個時間段內的統計(求某接口最近 5min 的失敗率來決定是否要報警)。
滑動窗口分配器將元素分配到固定長度的窗口中,與滾動窗口類似,窗口的大小由窗口大小參數來配置,另一個窗口滑動參數控制滑動窗口開始的頻率。因此,滑動窗口如果滑動參數小於窗口大小的話,窗口是可以重疊的,在這種情況下元素會被分配到多個窗口中。
例如,你有 10 分鐘的窗口和 5 分鐘的滑動,那麼每個窗口中 5 分鐘的窗口裏包含着上個 10 分鐘產生的數據,如下圖所示:
3. 會話窗口(Session Windows)
由一系列事件組合一個指定時間長度的 timeout 間隙組成,類似於 web 應用的session,也就是一段時間沒有接收到新數據就會生成新的窗口。
特點: 時間無對齊。
session 窗口分配器通過 session 活動來對元素進行分組, session 窗口跟滾動窗口和滑動窗口相比,不會有重疊和固定的開始時間和結束時間的情況,相反, 當它在一個固定的時間週期內不再收到元素,即非活動間隔產生,那個這個窗口就會關
閉。一個 session 窗口通過一個 session 間隔來配置,這個 session 間隔定義了非活躍週期的長度,當這個非活躍週期產生,那麼當前的 session 將關閉並且後續的元素將被分配到新的 session 窗口中去
三、Window API
6.3.1 CountWindow
CountWindow 根據窗口中相同 key 元素的數量來觸發執行,執行時只計算元素數量達到窗口大小的 key 對應的結果。
注意: CountWindow 的 window_size 指的是相同 Key 的元素的個數,不是輸入的所有元素的總數。
1 、滾動窗口
默認的 CountWindow 是一個滾動窗口,只需要指定窗口大小即可,當元素數量達到窗口大小時,就會觸發窗口的執行。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item.split(" ")(0), item.split("
")(1).toLong)).keyBy(0)
// 引入滾動窗口
// 這裏的 5 指的是 5 個相同 key 的元素計算一次
val streamWindow = streamKeyBy.countWindow(5)
// 執行聚合操作
val streamReduce = streamWindow.reduce(
(item1, item2) => (item1._1, item1._2 + item2._2)
)
// 將聚合數據寫入文件
streamReduce.print()
// 執行程序
env.execute("TumblingWindow")
2 滑動窗口
滑動窗口和滾動窗口的函數名是完全一致的,只是在傳參數時需要傳入兩個參數,一個是 window_size,一個是 sliding_size。
下面代碼中的 sliding_size 設置爲了 2,也就是說,每收到兩個相同 key 的數據就計算一次,每一次計算的 window 範圍是 5 個元素。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item.split(" ")(0), item.split("
")(1).toLong)).keyBy(0)
// 引入滾動窗口
// 當相同 key 的元素個數達到 2 個時,觸發窗口計算,計算的窗口範圍爲 5
val streamWindow = streamKeyBy.countWindow(5,2)
// 執行聚合操作
val streamReduce = streamWindow.reduce(
(item1, item2) => (item1._1, item1._2 + item2._2)
)
// 將聚合數據寫入文件
streamReduce.print()
// 執行程序
env.execute("TumblingWindow")
}
6.3.2 TimeWindow
TimeWindow 是將指定時間範圍內的所有數據組成一個 window,一次對一個window 裏面的所有數據進行計算。
1. 滾動窗口
Flink 默認的時間窗口根據 Processing Time 進行窗口的劃分,將 Flink 獲取到的數據根據進入 Flink 的時間劃分到不同的窗口中。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item, 1)).keyBy(0)
// 引入時間窗口
val streamWindow = streamKeyBy.timeWindow(Time.seconds(5))
// 執行聚合操作
val streamReduce = streamWindow.reduce(
(item1, item2) => (item1._1, item1._2 + item2._2)
)
// 將聚合數據寫入文件
streamReduce.print()
// 執行程序
env.execute("TumblingWindow")
時間間隔可以通過 Time.milliseconds(x), Time.seconds(x), Time.minutes(x)等其中的一個來指定。
2. 滑動窗口(SlidingEventTimeWindows)
滑動窗口和滾動窗口的函數名是完全一致的,只是在傳參數時需要傳入兩個參數,一個是 window_size,一個是 sliding_size。
下面代碼中的 sliding_size 設置爲了 2s,也就是說,窗口每 2s 就計算一次,每一次計算的 window 範圍是 5s 內的所有元素。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item, 1)).keyBy(0)
// 引入滾動窗口
val streamWindow = streamKeyBy.timeWindow(Time.seconds(5), Time.seconds(2))
// 執行聚合操作
val streamReduce = streamWindow.reduce(
(item1, item2) => (item1._1, item1._2 + item2._2)
)
// 將聚合數據寫入文件
streamReduce.print()
// 執行程序
env.execute("TumblingWindow")
6.3.3 Window Reduce
WindowedStream → DataStream:給 window 賦一個 reduce 功能的函數,並返回一個聚合的結果。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item, 1)).keyBy(0)
// 引入時間窗口
val streamWindow = streamKeyBy.timeWindow(Time.seconds(5))
// 執行聚合操作
val streamReduce = streamWindow.reduce(
(item1, item2) => (item1._1, item1._2 + item2._2)
)
// 將聚合數據寫入文件
streamReduce.print()
// 執行程序
env.execute("TumblingWindow")
6.3.4 Window Fold
WindowedStream → DataStream:給窗口賦一個 fold 功能的函數,並返回一個 fold 後的結果。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111,'\n',3)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item, 1)).keyBy(0)
// 引入滾動窗口
val streamWindow = streamKeyBy.timeWindow(Time.seconds(5))
// 執行 fold 操作
val streamFold = streamWindow.fold(100){
(begin, item) =>
begin + item._2
}
// 將聚合數據寫入文件
streamFold.print()
// 執行程序
env.execute("TumblingWindow")
6.3.5 Aggregation on Window
WindowedStream → DataStream:對一個 window 內的所有元素做聚合操作。min 和 minBy 的區別是 min 返回的是最小值,而 minBy 返回的是包含最小值字段的元素(同樣的原理適用於 max 和 maxBy)。
// 獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 創建 SocketSource
val stream = env.socketTextStream("localhost", 11111)
// 對 stream 進行處理並按 key 聚合
val streamKeyBy = stream.map(item => (item.split(" ")(0), item.split(" ")(1))).keyBy(0)
// 引入滾動窗口
val streamWindow = streamKeyBy.timeWindow(Time.seconds(5))
// 執行聚合操作
val streamMax = streamWindow.max(1)
// 將聚合數據寫入文件
streamMax.print()
// 執行程序
env.execute("TumblingWindow")