Application Development / Streaming (DataStream API) / Operators / Windows
Windows
Windows是處理無限流的核心。Windows將流拆分爲有限大小的“桶”,我們可以在其上應用計算。本文檔重點介紹如何在Flink中執行窗口,以及程序員如何從其提供的函數中獲益最大化。
窗口Flink程序的一般結構如下所示。第一個片段指的是被Keys化的流,而第二個片段指的是非Keys化的流。正如你所見,唯一的區別是keyBy(...)
調用Keys化的流那麼window(…)成爲非Key化的數據流的windowAll(…)。這也將作爲頁面其餘部分的路線圖。
Keyed Windows
- stream
.keyBy(...) <- keyed versus non-keyed windows
.window(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
Non-Keyed Windows
- stream
.windowAll(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
在上面,方括號([…])中的命令是可選的。這表明Flink允許您以多種不同方式自定義窗口邏輯,以便最適合您的需求。
目錄
-
- Tumbling Windows
- Sliding Windows
- Session Windows
- Global Windows
-
- ReduceFunction
- AggregateFunction
- FoldFunction
- ProcessWindowFunction
- ProcessWindowFunction with Incremental Aggregation
- Using per-window state in ProcessWindowFunction
- WindowFunction (Legacy)
-
- Fire and Purge
- Default Triggers of WindowAssigners
- Built-in and Custom Triggers
-
- Getting late data as a side output
- Late elements considerations
-
- Interaction of watermarks and windows
- Consecutive windowed operations
Window 生命週期
簡而言之,只要應該屬於此窗口的第一個數據元到達就會創建一個窗口,當時間(事件或處理時間)超過其結束時間戳加上用戶指定的允許延遲時,窗口將被完全刪除(請參閱允許的延遲)。Flink保證僅刪除基於時間的窗口而不是其他類型,例如全局窗口(請參閱窗口分配器)。例如,使用基於事件時間的窗口策略,每隔5分鐘創建一個非重疊(或翻滾)的窗口並允許延遲1分鐘,Flink將創建一個新窗口,用於間隔12:00和12:05當具有落入此間隔的時間戳的第一個數據元到達時,並且當水印通過12:06時間戳時它將刪除它。
此外,每個窗口都有 Trigger(參見 Triggers)和一個函數(ProcessWindowFunction,ReduceFunction,AggregateFunction 或 FoldFunction)(見Window Functions)連接到它。該函數將包含要應用於窗口內容的計算,而 Trigger 指定的窗口被認爲準備好應用該函數的條件。觸發策略可能類似於“當窗口中的數據元數量大於4”時,或“當水印通過窗口結束時”。觸發器還可以決定在創建和刪除之間的任何時間清除窗口的內容。在這種情況下,清除僅指窗口中的數據元,而不是窗口元數據。這意味着仍然可以將新數據添加到該窗口。
除了上述內容之外,您還可以指定一個 Evictor(參見 Evictors),它可以在觸發器觸發後以及應用函數之前 and/or 之後從窗口中刪除數據元。
在下文中,我們將詳細介紹上述每個組件。在轉到可選部分之前,我們從上面代碼段中的必需部分開始(請參閱Keyed vs Non-Keyed Windows,Window Assigner和 Window Function))。
被 Keys 化與非被 Keys 化 Windows 對比
首先要說明的是你的流應該被 keys化 還是不 keys 化。必須在定義窗口之前完成此算子操作。使用 keyBy(…) 將你的無限流分成邏輯被 Key 化的數據流。如果 keyBy(…) 未調用,則表示您的流不是被Keys化的。
對於被Key化的數據流,可以將傳入事件的任何屬性用作鍵(更多詳細信息查看here)。具有被 Key 化的數據流將允許您的窗口計算由多個任務並行執行,因爲每個邏輯被 Key 化的數據流可以獨立於其餘任務進行處理。引用相同Keys的所有數據元將被髮送到同一個並行任務(the same parallel task)。
在非 Key 化的數據流的情況下,您的原始流將不會被拆分爲多個邏輯流,並且所有窗口邏輯將由單個任務執行,即並行度爲1。
窗口分配器(Window Assigners)
指定您的流是否已 kyes 化後,下一步是定義一個窗口分配器。窗口分配器定義如何將數據元分配給窗口。這是通過在 window(…)(對於被 Keys 化的流)或 windowAll() (對於非被 Keys 化流)調用中指定所選擇的 WindowAssigner 來完成的。
WindowAssigner 負責將每個傳入元素分配給一個或多個窗口。Flink 帶有預定義的窗口分配器用於最常見的用例,即翻滾窗口, 滑動窗口,會話窗口和全局窗口。您還可以通過擴展 WindowAssigner 類來實現自定義窗口分配器。所有內置窗口分配器(全局窗口除外)都根據時間爲窗口分配數據元,這可以是處理時間或事件時間。請查看我們關於 event time的部分,瞭解處理時間和事件時間之間的差異以及時間戳和水印的生成方式。
基於時間的窗口具有開始時間戳(包括)和結束時間戳(不包括),它們一起描述窗口的大小。在代碼中,Flink在使用基於時間的窗口時使用 TimeWindow,該窗口具有查詢開始和結束時間戳的方法 ,以及返回給定窗口的最大允許時間戳的附加方法 maxTimestamp()。
在下文中,我們將展示 Flink 的預定義窗口分配器如何工作以及如何在 DataStream 程序中使用它們。下圖顯示了每個分配者的工作情況。紫色圓圈表示流的數據元,這些數據元由某個鍵(在這種情況下是用戶1,用戶2和用戶3)劃分。x軸顯示時間的進度。
翻滾的Windows(Tumbling Windows)
翻滾窗口分配器將每個元素分配給指定窗口大小的窗口。翻滾窗口具有固定的尺寸,不重疊。例如,如果指定大小爲5分鐘的翻滾窗口,則將評估當前窗口,並且每五分鐘將啓動一個新窗口,如下圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ucu7RQXC-1573525418455)(https://ci.apache.org/projects/flink/flink-docs-release-1.8/fig/tumbling-windows.svg)]
以下代碼段顯示瞭如何使用翻滾窗口。
val input: DataStream[T] = ...
// tumbling event-time windows
input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.<windowed transformation>(<window function>)
// tumbling processing-time windows
input
.keyBy(<key selector>)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.<windowed transformation>(<window function>)
// daily tumbling event-time windows offset by -8 hours.
input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
.<windowed transformation>(<window function>)
可以使用Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等指定時間間隔。
如上一個示例所示,翻滾窗口分配器還採用可選的偏移參數,可用於更改窗口的對齊方式。 例如,沒有偏移,每小時翻滾窗口與時期對齊,即你將獲得 1:00:00.000 - 1:59:59.999, 2:00:00.000 - 2:59:59.999 等窗口。 如果你想改變它,你可以給出一個偏移量。 如果偏移15分鐘,你會得到 1:15:00.000 - 2:14:59.999, 2:15:00.000 - 3:14:59.999 等。偏移的一個重要用例是調整窗口到時區 UTC-0以外的。 例如,在中國,您必須指定Time.hours(-8)的偏移量。
Sliding Windows
滑動窗口分配器將元素分配給固定長度的窗口。 與翻滾窗口分配器類似,窗口大小由窗口大小參數配置。 附加的窗口滑動參數控制滑動窗口的啓動頻率。 因此,如果滑動小於窗口大小,則滑動窗口會重疊。 在這種情況下,元素被分配給多個窗口。
例如,您可以將大小爲10分鐘的窗口滑動5分鐘。 有了這個,你每隔5分鐘就會得到一個窗口,其中包含過去10分鐘內到達的事件,如下圖所示。
以下代碼段顯示瞭如何使用滑動窗口。
val input: DataStream[T] = ...
// sliding event-time windows
input
.keyBy(<key selector>)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>)
// sliding processing-time windows
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>)
// sliding processing-time windows offset by -8 hours
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
.<windowed transformation>(<window function>)
可以使用Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等指定時間間隔。
如上一個示例所示,滑動窗口分配器還採用可選的偏移參數,該參數可用於更改窗口的對齊方式。 例如,沒有偏移每小時窗口滑動30分鐘與時期對齊,也就是說你會得到 1:00:00.000 - 1:59:59.999, 1:30:00.000 - 2:29:59.999 等窗口等等。 如果你想改變它,你可以給出一個偏移量。 如果偏移15分鐘,你會得到 1:15:00.000 - 2:14:59.999, 1:45:00.000 - 2:44:59.999 等。偏移的一個重要用例是調整窗口到時區 UTC-0以外的。 例如,在中國,您必須指定Time.hours(-8)的偏移量。
Session Windows
會話窗口分配器按活動會話對元素進行分組。 會話窗口不重疊,沒有固定的開始和結束時間,與翻滾窗口和滑動窗口相反。 相反,當會話窗口在一段時間內沒有接收到元素時,即當發生不活動的間隙時,會話窗口關閉。 會話窗口分配器可以配置靜態會話間隙或會話間隙提取器功能,該功能定義不活動時間段的長度。 當此期限到期時,當前會話將關閉,後續元素將分配給新的會話窗口。
以下代碼段顯示瞭如何使用會話窗口。
val input: DataStream[T] = ...
// event-time session windows with static gap
input
.keyBy(<key selector>)
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
.<windowed transformation>(<window function>)
// event-time session windows with dynamic gap
input
.keyBy(<key selector>)
.window(EventTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
override def extract(element: String): Long = {
// determine and return session gap
}
}))
.<windowed transformation>(<window function>)
// processing-time session windows with static gap
input
.keyBy(<key selector>)
.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
.<windowed transformation>(<window function>)
// processing-time session windows with dynamic gap
input
.keyBy(<key selector>)
.window(DynamicProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
override def extract(element: String): Long = {
// determine and return session gap
}
}))
.<windowed transformation>(<window function>)
靜態間隙可以使用Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等來指定。
通過實現SessionWindowTimeGapExtractor接口指定動態間隙。
注意 由於會話窗口沒有固定的開始和結束,因此它們的評估方式與翻滾和滑動窗口不同。 在內部,會話窗口操作算子爲每個到達的記錄創建一個新窗口,如果它們彼此之間的距離比定義的間隙更接近,則將窗口合併在一起。 爲了可合併,會話窗口運算符需要合併觸發器和合並窗口函數,例如ReduceFunction,AggregateFunction或ProcessWindowFunction(FoldFunction無法合併。)
Global Windows
全局窗口分配器將具有相同鍵的所有元素分配給同一個全局窗口。 此窗口方案僅在您還指定自定義觸發器時纔有用。 否則,將不執行任何計算,因爲全局窗口沒有我們可以處理聚合元素的自然結束。
以下代碼段顯示瞭如何使用全局窗口。
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(GlobalWindows.create())
.<windowed transformation>(<window function>)
Window Functions
定義窗口分配器後,我們需要指定要在每個窗口上執行的計算。 這是窗口函數的職責,窗口函數用於在系統確定窗口準備好進行處理後處理每個(可能是keys化的)窗口的元素(請參閱Flink如何確定窗口準備就緒的觸發器)。
窗口函數可以是ReduceFunction
,AggregateFunction
,FoldFunction
或ProcessWindowFunction
之一。 前兩個可以更有效地執行(參見State Size部分),因爲Flink可以在每個窗口到達時遞增地聚合它們的元素。 ProcessWindowFunction
獲取窗口中包含的所有元素的Iterable以及有關元素所屬窗口的其他元信息。
使用ProcessWindowFunction
的窗口轉換不能像其他情況一樣有效地執行,因爲Flink必須在調用函數之前在內部緩衝窗口的所有元素。 這可以通過將ProcessWindowFunction
與ReduceFunction
,AggregateFunction
或FoldFunction
結合使用來獲得窗口元素的增量聚合和ProcessWindowFunction
接收的其他窗口元數據。 我們將查看每個變換的示例。
ReduceFunction
ReduceFunction 指定如何組合輸入中的兩個元素以生成相同類型的輸出元素。 Flink使用ReduceFunction逐步聚合窗口的元素。
可以像這樣定義和使用ReduceFunction:
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.reduce { (v1, v2) => (v1._1, v1._2 + v2._2) }
上面的示例總結了窗口中所有元素的元組的第二個字段。
AggregateFunction
AggregateFunction 是 ReduceFunction 的通用版本,有三種類型:輸入類型(IN),累加器類型(ACC)和輸出類型(OUT)。 輸入類型是輸入流中元素的類型,AggregateFunction具有將一個輸入元素添加到累加器的方法。 該接口還具有用於創建初始累加器的方法,用於將兩個累加器合併到一個累加器中以及用於從累加器提取輸出(類型OUT)的方法。 我們將在下面的示例中看到它的工作原理。
與 ReduceFunction 相同,Flink將在窗口到達時遞增地聚合窗口的輸入元素。
可以像這樣定義和使用AggregateFunction:
/**
* The accumulator is used to keep a running sum and a count. The [getResult] method
* computes the average.
*/
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
override def createAccumulator() = (0L, 0L)
override def add(value: (String, Long), accumulator: (Long, Long)) =
(accumulator._1 + value._2, accumulator._2 + 1L)
override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2
override def merge(a: (Long, Long), b: (Long, Long)) =
(a._1 + b._1, a._2 + b._2)
}
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.aggregate(new AverageAggregate)
上面的示例計算窗口中元素的第二個字段的平均值。
FoldFunction
FoldFunction 指定窗口的輸入元素如何與輸出類型的元素組合。 對於添加到窗口的每個元素和當前輸出值,將逐步調用FoldFunction。 第一個元素與輸出類型的預定義初始值組合。
可以像這樣定義和使用FoldFunction:
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.fold("") { (acc, v) => acc + v._2 }
上面的示例將所有輸入Long值附加到最初爲空的String。
注意 fold()不能與session windows
或其他mergeable windows
一起使用。
ProcessWindowFunction
ProcessWindowFunction 獲取包含窗口所有元素的Iterable,以及可訪問時間和狀態信息的Context對象,這使其能夠提供比其他窗口函數更多的靈活性。 這是以性能和資源消耗爲代價的,因爲元素不能以遞增方式聚合,而是需要在內部進行緩衝,直到認爲窗口已準備好進行處理。
ProcessWindowFunction的簽名如下:
abstract class ProcessWindowFunction[IN, OUT, KEY, W <: Window] extends Function {
/**
* Evaluates the window and outputs none or several elements.
*
* @param key The key for which this window is evaluated.
* @param context The context in which the window is being evaluated.
* @param elements The elements in the window being evaluated.
* @param out A collector for emitting elements.
* @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
def process(
key: KEY,
context: Context,
elements: Iterable[IN],
out: Collector[OUT])
/**
* The context holding window metadata
*/
abstract class Context {
/**
* Returns the window that is being evaluated.
*/
def window: W
/**
* Returns the current processing time.
*/
def currentProcessingTime: Long
/**
* Returns the current event-time watermark.
*/
def currentWatermark: Long
/**
* State accessor for per-key and per-window state.
*/
def windowState: KeyedStateStore
/**
* State accessor for per-key global state.
*/
def globalState: KeyedStateStore
}
}
注意 關鍵參數是通過爲keyBy()調用指定的 KeySelector 提取的key。 在 tuple-index 鍵或字符串字段引用的情況下,此鍵類型始終爲Tuple,您必須手動將其轉換爲正確大小的元組以提取鍵字段。
可以像這樣定義和使用ProcessWindowFunction:
val input: DataStream[(String, Long)] = ...
input
.keyBy(_._1)
.timeWindow(Time.minutes(5))
.process(new MyProcessWindowFunction())
/* ... */
class MyProcessWindowFunction extends ProcessWindowFunction[(String, Long), String, String, TimeWindow] {
def process(key: String, context: Context, input: Iterable[(String, Long)], out: Collector[String]): () = {
var count = 0L
for (in <- input) {
count = count + 1
}
out.collect(s"Window ${context.window} count: $count")
}
}
該示例顯示了一個ProcessWindowFunction,用於計算窗口中的元素。 此外,窗口功能將有關窗口的信息添加到輸出。
注意 請注意使用 ProcessWindowFunction 進行簡單的聚合(例如count)效率非常低。 下一節將介紹如何將 ReduceFunction 或AggregateFunction 與 ProcessWindowFunction 結合使用,以獲取增量聚合和 ProcessWindowFunction 的添加信息。
ProcessWindowFunction with Incremental Aggregation
ProcessWindowFunction 可以與 ReduceFunction,AggregateFunction 或 FoldFunction結合使用,以便在元素到達窗口時遞增聚合元素。 關閉窗口時,將爲 ProcessWindowFunction 提供聚合結果。 這允許它在訪問 ProcessWindowFunction 的附加窗口元信息的同時遞增地計算窗口。
注意 您還可以使用舊版 WindowFunction 而不是 ProcessWindowFunction 進行增量窗口聚合。
Incremental Window Aggregation with ReduceFunction
以下示例顯示瞭如何將增量 ReduceFunction 與 ProcessWindowFunction 結合以返回窗口中的最小事件以及窗口的開始時間。
val input: DataStream[SensorReading] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.reduce(
(r1: SensorReading, r2: SensorReading) => { if (r1.value > r2.value) r2 else r1 },
( key: String,
context: ProcessWindowFunction[_, _, _, TimeWindow]#Context,
minReadings: Iterable[SensorReading],
out: Collector[(Long, SensorReading)] ) =>
{
val min = minReadings.iterator.next()
out.collect((context.window.getStart, min))
}
)
Incremental Window Aggregation with AggregateFunction
以下示例顯示如何將增量 AggregateFunction 與 ProcessWindowFunction 結合以計算平均值,並同時發出鍵和窗口以及平均值。
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.aggregate(new AverageAggregate(), new MyProcessWindowFunction())
// Function definitions
/**
* The accumulator is used to keep a running sum and a count. The [getResult] method
* computes the average.
*/
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
override def createAccumulator() = (0L, 0L)
override def add(value: (String, Long), accumulator: (Long, Long)) =
(accumulator._1 + value._2, accumulator._2 + 1L)
override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2
override def merge(a: (Long, Long), b: (Long, Long)) =
(a._1 + b._1, a._2 + b._2)
}
class MyProcessWindowFunction extends ProcessWindowFunction[Double, (String, Double), String, TimeWindow] {
def process(key: String, context: Context, averages: Iterable[Double], out: Collector[(String, Double)]): () = {
val average = averages.iterator.next()
out.collect((key, average))
}
}
Incremental Window Aggregation with FoldFunction
以下示例顯示如何將增量 FoldFunction 與 ProcessWindowFunction 結合以提取窗口中的事件數並返回窗口的鍵和結束時間。
val input: DataStream[SensorReading] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.fold (
("", 0L, 0),
(acc: (String, Long, Int), r: SensorReading) => { ("", 0L, acc._3 + 1) },
( key: String,
window: TimeWindow,
counts: Iterable[(String, Long, Int)],
out: Collector[(String, Long, Int)] ) =>
{
val count = counts.iterator.next()
out.collect((key, window.getEnd, count._3))
}
)
Using per-window state in ProcessWindowFunction
除了訪問kes化狀態(任何富函數可以)之外,ProcessWindowFunction 還可以使用kes化狀態,該kes化狀態的作用域是函數當前正在處理的窗口。 在這種情況下,瞭解每個窗口狀態所指的窗口是很重要的。 涉及不同的“窗口”:
- 指定窗口操作時定義的窗口:這可能是1小時的翻滾窗口或一小時滑動的2小時長度的滑動窗口。
- 給定鍵的已定義窗口的實際實例:對於user-id xyz,這可能是從12:00到13:00的時間窗口。 這基於窗口定義,並且將基於作業當前正在處理的鍵的數量以及基於事件落入的時隙而存在許多窗口。
每窗口狀態與最近兩者相關聯。 這意味着如果我們處理1000個不同鍵的事件,並且所有這些事件的事件當前都落入[12:00,13:00]時間窗口,那麼將有1000個窗口實例,每個窗口實例都有自己的Keys化的每窗口狀態。
在Context對象上有兩個方法,process()調用接收它們允許訪問兩種類型的狀態:
- globalState(), 允許訪問未限定爲窗口的鍵控狀態
- windowState(), 它允許訪問也限定在窗口範圍內的鍵控狀態
如果您預計同一窗口會發生多次觸發,則此功能非常有用,如果您遲到的數據或者您有自定義觸發器進行推測性早期觸發時可能會發生這種情況。 在這種情況下,您將存儲有關先前 firing 的信息或每個窗口狀態的 firing 次數。
使用窗口狀態時,清除窗口時清除該狀態也很重要。 這應該在clear()
方法中發生。
WindowFunction (Legacy)
在某些可以使用 ProcessWindowFunction 的地方,您也可以使用 WindowFunction。 這是 ProcessWindowFunction 的舊版本,它提供較少的上下文信息,並且沒有一些高級功能,例如 per-window 鍵控狀態。 此接口將在某個時候棄用。
WindowFunction的簽名如下所示:
trait WindowFunction[IN, OUT, KEY, W <: Window] extends Function with Serializable {
/**
* Evaluates the window and outputs none or several elements.
*
* @param key The key for which this window is evaluated.
* @param window The window that is being evaluated.
* @param input The elements in the window being evaluated.
* @param out A collector for emitting elements.
* @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
def apply(key: KEY, window: W, input: Iterable[IN], out: Collector[OUT])
}
可以像如下使用:
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.apply(new MyWindowFunction())
Triggers
觸發器確定何時一個窗(由窗口分配器形成)是被window函數
準備好處理的。 每個WindowAssigner
都帶有一個默認觸發器。 如果默認觸發器不符合您的需要,您可以使用trigger(...)
指定自定義觸發器。
觸發器接口有五種方法允許觸發器對不同的事件做出反應:
- 爲添加到窗口的每個元素調用
onElement()
方法。 - 當註冊的 event-time 計時器觸發時,將調用
onEventTime()
方法。 - 當註冊的 processing-time 計時器觸發時,將調用
onProcessingTime()
方法。 onMerge()
方法與有狀態觸發器相關,並在它們相應的窗口合併時合併兩個觸發器的狀態,例如: 使用會話窗口時。- 最後
clear()
方法執行刪除相應窗口時所需的任何操作。
關於上述方法需要注意兩點:
1)前三個解決如何通過返回TriggerResult來對其調用事件進行操作。 該操作可以是以下之一:
- CONTINUE:什麼都不做,
- FIRE:觸發計算,
- PURGE:清除窗口中的元素,和
- FIRE_AND_PURGE:觸發計算並在之後清除窗口中的元素。
2)這些方法中的任何一種都可用於爲將來的操作註冊處理或event-time計時器。
Fire and Purge
一旦觸發器確定window已準備好進行處理,它就會觸發,即返回 FIRE 或 FIRE_AND_PURGE。 這是窗口 operator 發出當前窗口結果的信號。 給定一個帶有ProcessWindowFunction
的窗口,所有元素都傳遞給ProcessWindowFunction(可能之後將他們交給一個evictor)。 具有ReduceFunction,AggregateFunction或FoldFunction的Windows只會急切地發出聚合的結果。
當觸發器觸發時,它可以是 FIRE 或 FIRE_AND_PURGE。 當 FIRE 保留窗口內容時,FIRE_AND_PURGE會刪除其內容。 默認情況下,預先實現的觸發器只需FIRE而不會清除窗口狀態。
注意 清除將簡單地刪除窗口的內容,並將保留有關窗口和任何觸發狀態的任何潛在元信息。
Default Triggers of WindowAssigners
WindowAssigner 的默認觸發器適用於許多用例。 例如,所有事件時窗口分配器都將EventTimeTrigger作爲默認觸發器。 一旦 watermark 通過窗口的末端,該觸發器就會觸發。
注意 GlobalWindow的默認觸發器是NeverTrigger,它永遠不會觸發。 因此,在使用GlobalWindow時,您始終必須定義自定義觸發器。
注意 通過使用trigger()
指定觸發器,您將覆蓋WindowAssigner的默認觸發器。 例如,如果爲TumblingEventTimeWindows指定CountTrigger,則不會再根據時間進度獲取窗口,而只能按count計數。 現在,如果你想根據時間和數量做出反應,你必須編寫自己的自定義觸發器。
Built-in and Custom Triggers
Flink附帶了一些內置觸發器。
- EventTimeTrigger(已經提到過)根據watermark 測量的事件時間進度觸發。
- ProcessingTimeTrigger根據處理時間觸發。
- 一旦窗口中的元素數量超過給定限制,CountTrigger就會觸發。
- PurgingTrigger將另一個觸發器作爲參數,並將其轉換爲清除觸發器。
如果需要實現自定義觸發器,則應該檢查抽象Trigger類。請注意,API仍在不斷髮展,可能會在Flink的未來版本中發生變化。
Evictors(逐出器)
除了WindowAssigner和Trigger之外,Flink的窗口模型還允許指定可選的Evictor。 這可以使用evictor(...)
方法完成(在本文檔的開頭顯示)。 Evictors
可以在觸發器觸發後以及在應用窗口函數之前 and/or 之後從窗口中移除元素。 爲此,Evictor接口有兩種方法:
/**
* 可選地 evicts 元素. windowing function後調用.
*
* @param elements 窗格中當前的元素.
* @param size 窗格中當前的元素數.
* @param window The {@link Window}
* @param evictorContext Evictor的上下文
*/
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/**
* 可選地 evicts 元素. windowing function後調用.
*
* @param elements 窗格中當前的元素.
* @param size 窗格中當前的元素數.
* @param window The {@link Window}
* @param evictorContext Evictor的上下文
*/
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
evictBefore()
包含要在窗口函數之前應用的eviction邏輯,而evictAfter()
包含要在窗口函數之後應用的邏輯。 在應用窗口函數之前被逐出的元素將不會被它處理。
Flink附帶三個預先實施的evictors。 這些是:
- CountEvictor:從窗口保持用戶指定數量的元素,並從窗口緩衝區的開頭丟棄剩餘的元素。
- DeltaEvictor:採用DeltaFunction和閾值,計算窗口緩衝區中最後一個元素與其餘每個元素之間的差值,並刪除delta大於或等於閾值的值。
- TimeEvictor:將參數作爲一個間隔(以毫秒爲單位),對於給定的窗口,它查找其元素中的最大時間戳max_ts,並刪除時間戳小於
max_ts - interval
的所有元素。
默認 情況下,所有預先實現的evictors
在窗口函數之前應用它們的邏輯。
注意 指定Evictors
會阻止任何預聚合,因爲在應用計算之前,必須將窗口的所有元素傳遞給Evictors
。
注意 Flink不保證窗口內元素的順序。 這意味着儘管Evictors
可以從窗口的開頭移除元素,但這些元素不一定是首先到達或最後到達的元素。
Allowed Lateness
當正處理 event-time 窗口時,可能會發生元素遲到的情況,即 Flink 用於跟蹤事件時間進度的 watermark 已經超過元素所屬的窗口的結束時間戳。 查看event time,特別是late elements,以便更全面地討論Flink如何處理活動時間。
默認情況下,當 watermark 超過窗口末尾時,會刪除延遲元素。 但是,Flink允許爲窗口運算符指定最大允許延遲。允許延遲指定元素在被刪除之前可以延遲多少時間,並且其默認值爲0。在水印已經過了窗口結束但在它通過窗口結束加上允許的延遲之後到達的元素, 仍然被添加到窗口中。 根據所使用的觸發器,延遲但未丟棄的元素可能會導致窗口再次觸發。 EventTimeTrigger就是這種情況。
爲了使這項工作,Flink保持窗口的狀態,直到他們允許的延遲到期。 一旦發生這種情況,Flink將刪除窗口並刪除其狀態,如Window Lifecycle部分中所述。
默認 情況下,允許的延遲設置爲0。也就是說,到達水印後面的元素將被刪除。
您可以指定允許的延遲,如下所示:
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.<windowed transformation>(<window function>)
注意當使用GlobalWindows窗口分配器時,沒有數據被認爲是遲到的,因爲全局窗口的結束時間戳是Long.MAX_VALUE
。
Getting late data as a side output
使用Flink的side output功能,您可以獲得最近丟棄的數據流。
首先需要在窗口化流上使用sideOutputLateData(OutputTag)
指定要獲取延遲數據。 然後,您可以在窗口操作的結果上獲取側輸出流:
val lateOutputTag = OutputTag[T]("late-data")
val input: DataStream[T] = ...
val result = input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.sideOutputLateData(lateOutputTag)
.<windowed transformation>(<window function>)
val lateStream = result.getSideOutput(lateOutputTag)
Late elements considerations
當指定允許的延遲大於0時,在水印通過窗口結束後保留窗口及其內容。 在這些情況下,當一個遲到但未落下的元素到達時,它可能觸發另一個窗口的發出。 這些發出被稱爲後期射擊,因爲它們是由遲到事件觸發的,與主要射擊相反,後者是窗口的第一次發射。 在會話窗口的情況下,後期發出可以進一步導致窗口的合併,因爲它們可以“橋接”兩個預先存在的未合併窗口之間的間隙。
注意 您應該知道,後期觸發發出的元素應被視爲先前計算的更新結果,即您的數據流將包含同一計算的多個結果。 根據您的應用程序,您需要考慮這些重複的結果或對其進行重複數據刪除。
Working with window results
operations 保留在結果元素中,因此如果要保留有關窗口的元信息,則必須在ProcessWindowFunction
的結果元素中手動編碼該信息。 在結果元素上設置的唯一相關信息是元素時間戳。 這被設置爲已處理窗口的最大允許時間戳,即結束時間戳-1,因爲窗口結束時間戳是獨佔的。 請注意,事件時間窗口和處理時間窗口都是如此。 即在窗口化操作元素之後總是具有時間戳,但是這可以是event-time時間戳或processing-time時間戳。 對於processing-time窗口,這沒有特別的含義,但對於事件時間窗口,這與watermark與窗口交互的方式一起使得能夠以相同的窗口大小進行連續的窗口操作。 在看了 watermark 如何與窗口交互後,我們將介紹這一點。
Interaction of watermarks and windows
在繼續本節之前,您可能需要查看有關event time and watermarks的部分。
當watermark到達窗口操作符時,會觸發兩件事:
- watermark觸發計算所有窗口,其中最大時間戳(即結束時間戳-1)小於新watermark
- watermark被轉發(按原樣)到下游操作
直觀地,watermark “刷出”任何窗口,一旦接收到該watermark,將在下游操作中被認爲是遲到的。
Consecutive windowed operations
如前所述,計算窗口結果的時間戳的方式以及watermark與窗口交互的方式允許將連續的窗口操作串聯在一起。 當您想要執行兩個連續的窗口操作時,這可能很有用,您希望使用不同的鍵,但仍希望來自同一上游窗口的元素最終位於同一下游窗口中。 考慮這個例子:
val input: DataStream[Int] = ...
val resultsPerKey = input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce(new Summer())
val globalResults = resultsPerKey
.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new TopKWindowFunction())
在該示例中,來自第一操作的時間窗口[0,5]的結果也將在隨後的窗口化operation中的時間窗口[0,5]中結束。 這允許計算每個鍵的和,然後在第二個operation中計算同一窗口內的前k個元素。
Useful state size considerations
Windows可以在很長一段時間內(例如幾天,幾周或幾個月)定義,因此可以累積非常大的狀態。 在估算窗口計算的存儲要求時,需要記住幾條規則:
- Flink爲每個窗口創建一個每個元素的副本。 鑑於此,翻滾窗口保留每個元素的一個副本(一個元素恰好屬於一個窗口,除非它被延遲)。 相反,滑動窗口會創建每個元素的幾個,如Window Assigners 部分中所述。 因此,尺寸爲1天且滑動1秒的滑動窗口可能不是一個好主意。
- ReduceFunction,AggregateFunction和FoldFunction可以顯着降低存儲要求,因爲它們急切地聚合元素並且每個窗口只存儲一個值。 相反,只需使用ProcessWindowFunction就需要累積所有元素。
- 使用Evictor可以防止 pre-aggregation,因爲在應用計算之前,窗口的所有元素都必須通過逐出器傳遞(參見Evictors)。
Watermark 的理解
Watermark是用來處理 eventtime 中數據的亂序問題。是Eventtime處理進度的一個標誌。通常又是需要結合window來實現。如下圖,事件到達Flink是一個亂序的,圖中的數字表示時間戳。
從圖中可以看到時間戳爲9的事件後到達了,當處理時按照事件發生的時間處理,這裏顯然就有問題了。爲了保證基於事件事件在處理實時的數據還是重新處理歷史的數據時都能保證結果一致,需要一些額外的處理,這時就需要用到 Watermark 了。
例如在處理上面數據時,第一個爲7,地二個是11,這時就輸出結果嗎,這個不好判斷了,當數據時亂序到達,可能有些事件會晚到達,誰都不確定事件7和事件11中間的事件是否存在,是否已經全部到達,或者什麼時候到達。那麼我們只能等待,等待就會有緩存,然後必然就會產生延遲。那麼等待多久,如果沒有限制,那麼有可能存在一直等下去,這樣程序一直不敢輸出結果了,Watermark 正是限定這個等待時間的,表示早於這個時間的所有事件已全部到達,可以將計算結果輸出了。