窗口
窗口是無界流處理程序的核心。窗口能夠將一個無界流切分成一個個有限大小的桶,以便進行計算。
窗口根據流的類型(keyed stream和non-keyed stream)分爲兩種,分別是keyed window和non-keyed window。它們的結構如下所示(方括號表示是可選的),可以看到,區別就是是否使用了keyBy。
窗口的聲明週期
簡而言之,當屬於此窗口的第一個元素到達窗口時此窗口才創建(created)。當時間(event or processing time)經過截止時間+用戶自定義的允許遲到時間(allowed lateness)時,此窗口移除(completely removed)。比如,指定的窗口策略是基於事件時間,時間間隔爲5分鐘,最大遲到時間爲1分鐘的非重疊窗口(滾動窗口),那麼當第一個攜帶的時間戳在12:00和12:05之間的元素到來時,flink就會創建一個12:00-12:05的窗口。然後當水印(watermark)經過12:06時,這個窗口就被移除了。
Flink保證只移除基於時間的窗口,而不刪除其他類型的窗口,例如全局窗口(global windows)(請參見窗口分配程序)。
此外,每個窗口都有一個觸發器(Triggers),以及一個函數(ProcessWindowFunction,ReduceFunction,AggregateFunction或FoldFunction) (參考 Window Functions) 。函數定義的是窗口內的計算邏輯,而觸發器定義的是窗口調用函數的條件。觸發策略可能類似於“當窗口中的元素數超過4時”或“當水印通過窗口末端時”。觸發器還可以決定在創建和移除窗口之間的任何時間清除窗口的內容。不過只能移除窗口內的元素,而不能移除窗口的元數據。也就是說,新的數據還是會源源不斷的進入窗口。
除此之外,還可以指定一個回收器(Evictors),它能夠在觸發器觸發後、函數調用之前或之後刪除窗口內的元素。
Keyed vs Non-Keyed Windows
第一件需要確定的事是你的流是否需要進行keyby分組,keyBy()方法會將流切分成邏輯上分組的流keyed stream。
- keyed stream:對於keyed stream來說,元素的任何一個屬性都可以用來分組(詳情)。keyed stream能夠在多個task中並行的執行窗口計算,因爲每個keyed stream都是獨立於其他流的。擁有相同key的元素會被髮送到同一個並行的task處理。
- non-keyed stream:對於non-keyed stream來說,不會將流切分成邏輯上獨立的多個流,所有的窗口邏輯都是在一個單一的task中處理的,也就是說並行度是1.
Window Assigners
在確定了是否對流進行分組之後,接下來要做的就是定義一個窗口分配器(Window Assigner)。
窗口分配器定義瞭如何將元素分配給窗口。窗口分配器是通過在window(…)(keyed stream)或windowAll()(non-keyed stream)中指定具體的WindowAssigner來完成的。
WindowAssigner負責將一個元素分配到一個或多個窗口。flink預定義了很多常用的窗口分配器(如 tumbling windows, sliding windows, session windows and global windows),以供直接使用。也可以通過繼承WindowAssigner
類來實現一個自定義的窗口分配器。所有內置的窗口分配器(除global window之外)都是基於時間(可以是處理時間或事件時間)來分配元素的。可以點這裏看一下處理時間和事件時間的區別,以及時間戳(timestamp)和水印(watermark)是如何生成的。
基於時間的窗口都有一個start timestamp(包含)和一個end timestamp(不包含),這個左開右閉的時間戳範圍描述了窗口的大小。在代碼中,flink通過TimeWindow的相關方法來查詢起止時間戳以及窗口最大時間戳(截止時間戳和最大時間戳的區別?)。
接下來,我們會介紹flink預定義的窗口分配器是如何工作的,以及在流處理程序中如何使用。下面的圖展示了每個窗口分配器的工作過程。紫色的圓點代表流中的元素,根據指定的key(下面的例子中的user1、user2、user3)將這些元素進行了分區。X軸代表處理時間(progress time)。
滾動窗口(Tumbling Windows)
滾動窗口分配器會將元素分配到一個指定大小的窗口中。滾動窗口的大小是固定的,元素是不重複的。
下面的代碼描述瞭如何使用滾動窗口:
DataStream<T> input = ...;
// 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)之類的指定。
從上面代碼中的最後一個窗口程序中可以看到,滾動窗口指定器還有一個可選的偏移量offset參數,該參數可用於更改窗口的對齊方式。例如,如果沒有偏移,每小時滾動的窗口將與epoch對齊,也就是說,您將得到諸如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)
滑動窗口分配器將元素分配給一個指定大小的窗口。與滾動窗口類似,通過window size參數來配置窗口大小。除此之外,還需要一個window slide參數來控制窗口的啓動頻率(也就是滑動間隔)。因此,如果滑動間隔小於窗口大小時,會出現重複,這時候一個元素就可能被分配到多個窗口中。
比如指定了滑動窗口大小爲10分鐘,滑動間隔爲5分鐘。那麼每5分鐘就會產生一個窗口,此窗口包含過去10分鐘的元素。
下面的代碼片段介紹瞭如何使用滑動窗口:
DataStream<T> input = ...;
// 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>);
會話窗口(Session Windows)
全局窗口(Global Windows)
窗口函數(Window Functions)
定義完窗口分配器之後,接下來需要指定每個窗口的計算邏輯。一旦當系統認爲一個窗口已經準備好處理數據後window function就開始負責每個窗口中元素的處理邏輯(see triggers for how Flink determines when a window is ready)。
window function可以是ReduceFunction
, AggregateFunction
, FoldFunction
or ProcessWindowFunction。前兩個函數可以更高效的執行,因爲對每個窗口來說,flink可以根據每個元素的到來進行遞增的聚合計算。而ProcessWindowFunction是在觸發窗口函數時時獲取包含整個窗口中所有元素的Iterable對象以及其他的窗口元數據,進而進行聚合操作的。因此ProcessWindowFunction需要在觸發前緩存窗口中的所有元素,效率比其他的窗口函數低。
不過我們可以將ProcessWindowFunction與ReduceFunction
, AggregateFunction
or ProcessWindowFunction組合使用,通過ProcessWindowFunction獲取窗口元數據,通過ReduceFunction
, AggregateFunction遞增的進行窗口聚合。
ReduceFunction
ReduceFunction
定義瞭如何將兩個輸入input元素聚合成相同類型type的輸出output元素。
flink使用ReduceFunction函數來遞增的聚合窗口中的元素。
可以通過類似如下方式來定義和使用ReduceFunction:
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.reduce(new ReduceFunction<Tuple2<String, Long>> {
public Tuple2<String, Long> reduce(Tuple2<String, Long> v1, Tuple2<String, Long> v2) {
return new Tuple2<>(v1.f0, v1.f1 + v2.f1);
}
});
上面的代碼會將窗口中所有元祖元素的第二個屬性進行累加操作。
AggregateFunction
AggregateFunction是ReduceFunction的一個通用版本,它有三種類型:輸入類型(IN)、累加器類型(ACC)和輸出類型(OUT)。輸入類型是輸入流中元素的類型,AggregateFunction有一個方法,用於向累加器添加一個輸入元素。該接口還具有創建初始累加器、將兩個累加器合併爲一個累加器以及從累加器提取輸出(類型爲OUT)的方法。我們將在下面的示例中看到這是如何工作的。
與ReduceFunction一樣,AggregateFunction也會遞增的對到達窗口的元素進行聚合操作。
下面描述瞭如何定義和使用AggregateFunction:
/**
* The accumulator is used to keep a running sum and a count. The {@code getResult} method
* computes the average.
*/
private static class AverageAggregate
implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> {
@Override
public Tuple2<Long, Long> createAccumulator() {
return new Tuple2<>(0L, 0L);
}
@Override
public Tuple2<Long, Long> add(Tuple2<String, Long> value, Tuple2<Long, Long> accumulator) {
return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
}
@Override
public Double getResult(Tuple2<Long, Long> accumulator) {
return ((double) accumulator.f0) / accumulator.f1;
}
@Override
public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
}
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.aggregate(new AverageAggregate());
上面的代碼會計算窗口中元素的第二個屬性的平均值。
FoldFunction
foldfunction定義瞭如何將窗口中的元素與一個輸出類型的元素進行組合。foldfunction會在每個元素進入window時被遞增的調用。進入窗口的第一個元素會與一個預定義的初始化的輸出類型的值進行組合。
一個foldfunction可以通過以下方式定義和使用:
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.fold("", new FoldFunction<Tuple2<String, Long>, String>> {
public String fold(String acc, Tuple2<String, Long> value) {
return acc + value.f1;
}
});
上面的代碼會將所有輸入元素的第二個屬性的值追加sppend到一個初始值爲空字符串的字符串。
Attention fold()
cannot be used with session windows or other mergeable windows.
ProcessWindowFunction
ProcessWindowFunction能夠獲取一個由窗口中所有元素組成的Iterable對象、一個能夠獲取time和state信息的Context對象,因此ProcessWindowFunction比其他的窗口函數更加靈活。但與此同時,帶來的是性能和資源的損耗,因爲元素不能增量的聚合,而是將所有元素緩存在內存中,直到觸發窗口函數。
ProcessWindowFunction類的結構如下:
public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> implements 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.
*/
public abstract void process(
KEY key,
Context context,
Iterable<IN> elements,
Collector<OUT> out) throws Exception;
/**
* The context holding window metadata.
*/
public abstract class Context implements java.io.Serializable {
/**
* Returns the window that is being evaluated.
*/
public abstract W window();
/** Returns the current processing time. */
public abstract long currentProcessingTime();
/** Returns the current event-time watermark. */
public abstract long currentWatermark();
/**
* State accessor for per-key and per-window state.
*
* <p><b>NOTE:</b>If you use per-window state you have to ensure that you clean it up
* by implementing {@link ProcessWindowFunction#clear(Context)}.
*/
public abstract KeyedStateStore windowState();
/**
* State accessor for per-key global state.
*/
public abstract KeyedStateStore globalState();
}
}
可以通過以下方式定義和使用一個ProcessWindowFunction:
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(t -> t.f0)
.timeWindow(Time.minutes(5))
.process(new MyProcessWindowFunction());
/* ... */
public class MyProcessWindowFunction
extends ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow> {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Long>> input, Collector<String> out) {
long count = 0;
for (Tuple2<String, Long> in: input) {
count++;
}
out.collect("Window: " + context.window() + "count: " + count);
}
}
上面的代碼統計了一個窗口中的元素數量,除此之外,窗口函數還將窗口信息添加到了輸出output中。
注意:使用ProcessWindowFunction來進行簡單的聚合操作,比如累加,是非常低效的。下一章將介紹如何組合使用ReduceFunction、
AggregateFunction
和ProcessWindowFunction來同時實現增量聚合和獲取
ProcessWindowFunction的附加信息。
ProcessWindowFunction with Incremental Aggregation
一個ProcessWindowFunction可以與
ReduceFunction或AggregateFunction或FoldFunction組合使用,這樣就能在元素進入窗口時進行增量聚合。當窗口關閉時,
ProcessWindowFunction還能夠提供聚合結果。這樣就能夠實現在訪問ProcessWindowFunction的附加窗口元信息的同時,以增量方式計算窗口。
注意:除了ProcessWindowFunction
之外,還可以使用 legacy WindowFunction來實現窗口增量聚合。
Incremental Window Aggregation with ReduceFunction
下面的示例展示瞭如何將ReduceFunction
和ProcessWindowFunction
組合使用,來返回窗口中最小的元素以及窗口的開始時間。
DataStream<SensorReading> input = ...;
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.reduce(new MyReduceFunction(), new MyProcessWindowFunction());
// Function definitions
private static class MyReduceFunction implements ReduceFunction<SensorReading> {
public SensorReading reduce(SensorReading r1, SensorReading r2) {
return r1.value() > r2.value() ? r2 : r1;
}
}
private static class MyProcessWindowFunction
extends ProcessWindowFunction<SensorReading, Tuple2<Long, SensorReading>, String, TimeWindow> {
public void process(String key,
Context context,
Iterable<SensorReading> minReadings,
Collector<Tuple2<Long, SensorReading>> out) {
SensorReading min = minReadings.iterator().next();
out.collect(new Tuple2<Long, SensorReading>(context.window().getStart(), min));
}
}
Incremental Window Aggregation with AggregateFunction
下面的示例描述瞭如何將一個增量聚合函數AggregateFunction與ProcessWindowFunction
組合使用,來計算窗口的平均值,並將平均值和key一起返回。
DataStream<Tuple2<String, Long>> input = ...;
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 {@code getResult} method
* computes the average.
*/
private static class AverageAggregate
implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> {
@Override
public Tuple2<Long, Long> createAccumulator() {
return new Tuple2<>(0L, 0L);
}
@Override
public Tuple2<Long, Long> add(Tuple2<String, Long> value, Tuple2<Long, Long> accumulator) {
return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
}
@Override
public Double getResult(Tuple2<Long, Long> accumulator) {
return ((double) accumulator.f0) / accumulator.f1;
}
@Override
public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
}
private static class MyProcessWindowFunction
extends ProcessWindowFunction<Double, Tuple2<String, Double>, String, TimeWindow> {
public void process(String key,
Context context,
Iterable<Double> averages,
Collector<Tuple2<String, Double>> out) {
Double average = averages.iterator().next();
out.collect(new Tuple2<>(key, average));
}
}
Incremental Window Aggregation with FoldFunction
下面的例子描述瞭如何將一個增量的FoldFunction與ProcessWindowFunction
組合使用,來獲取窗口中的元素數、key以及窗口結束時間。
DataStream<SensorReading> input = ...;
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.fold(new Tuple3<String, Long, Integer>("",0L, 0), new MyFoldFunction(), new MyProcessWindowFunction())
// Function definitions
private static class MyFoldFunction
implements FoldFunction<SensorReading, Tuple3<String, Long, Integer> > {
public Tuple3<String, Long, Integer> fold(Tuple3<String, Long, Integer> acc, SensorReading s) {
Integer cur = acc.getField(2);
acc.setField(cur + 1, 2);
return acc;
}
}
private static class MyProcessWindowFunction
extends ProcessWindowFunction<Tuple3<String, Long, Integer>, Tuple3<String, Long, Integer>, String, TimeWindow> {
public void process(String key,
Context context,
Iterable<Tuple3<String, Long, Integer>> counts,
Collector<Tuple3<String, Long, Integer>> out) {
Integer count = counts.iterator().next().getField(2);
out.collect(new Tuple3<String, Long, Integer>(key, context.window().getEnd(),count));
}
}
Using per-window state in ProcessWindowFunction
ProcessWindowFunction除了能夠獲取keyed state之外(像任何一個rich function那樣),還可以使用keyed state(作用域爲當前函數處理的窗口中的keyed state)。這個背景下,理解per-window state所指的window是什麼很重要。其中涉及到多個不同的window:
- 使用窗口算子時定義的窗口:比如定義一個大小爲1小時的滾動窗口,或一個大小爲2小時間隔爲1小時的滑動窗口。
- 一個指定key的窗口實例:比如一個key:value爲userid:xyz的從12:00到13:00的時間窗口。
Per-window state針對的就是後一種window。這意味着,如果我們處理的事件(元素)有1000種key,此時它們都進入了[12:00,13:00)這個時間窗口,那麼此時就有1000個window實例,這些window實例都擁有各自的keyed per-window state。
process()通過Context對象提供了兩種方法來分別獲取對應的state:
- globalState():獲取非窗口範圍的keyed state
- windowState():獲取窗口範圍的keyed state
當一個窗口有多次觸發操作或者需要用到之前窗口的狀態時,這個特性就很有用了。比如當需要對一個窗口中先到的數據和後到的數據使用不同的觸發器時,這時候就可能需要使用之前觸發器的信息或per-window state中的觸發器個數。
注意:當使用窗口狀態時,一個需要注意的地方是,當window被clear的時候,也需要清除狀態(在clear()方法中實現)。
WindowFunction (Legacy)
有些時候可以用WindowFunction代替ProcessWindowFunction。
WindowFunction是老版本的ProcessWindowFunction,提供較少的上下文信息,並且沒有一些高級功能,比如
per-window keyed state。這個接口在後面將會被廢除。
WindowFunction接口結構如下所示:
public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, 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.
*/
void apply(KEY key, W window, Iterable<IN> input, Collector<OUT> out) throws Exception;
}
可以通過以下方式使用:
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.apply(new MyWindowFunction());
觸發器(Triggers)
觸發器決定了一個窗口在什麼時候執行窗口函數。每個窗口分配器都有一個默認的觸發器。如果默認的觸發器無法滿足需求,還可以通過trigger(...)自定義觸發器。
觸發器接口提供了5個方法來應對不同的事件:
- onElement():每當一個元素進入窗口時,都會調用這個方法。
- onEventTime():當一個註冊的事件時間定時器被觸發時,就會調用這個方法。
- onProcessingTime():當一個註冊的處理時間定時器被觸發時,就會調用這個方法。
- onMerge():is relevant for stateful triggers and merges the states of two triggers when their corresponding windows merge, e.g. when using session windows.
- clear():當移除一個窗口時,會調用此方法。
對於上面這個5個方法來說,有兩點需要注意:
1)前三個通過返回TriggerResult來決定對調用事件執行什麼操作。操作可以是以下操作之一:
- CONTINUE:不做任何處理
- FIRE:觸發計算邏輯
- PURGE:清除window中的元素
- FIRE_AND_PURGE:觸發計算,然後清除窗口中的元素
2)這5個方法都可以用來註冊基於處理時間或基於事件時間的定時器,以便將來的操作。
觸發和清除(Fire and Purge)
一旦觸發器確定窗口已準備好進行處理,它就會觸發,即返回FIRE或FIRE_AND_PURGE。這是窗口算子emit當前窗口結果的信號。
當觸發器觸發時,既可以執行FIRE,也可以執行
FIRE_AND_PURGE。前者不會清除窗口內容,而後者會。默認情況下使用的是FIRE。
注意:清除操作只是簡單的移除窗口中的元素,並不會清除窗口的元數據信息以及觸發器的狀態數據。
窗口分配器的默認觸發器
窗口分配器的默認觸發器適用於許多用例。例如,所有的事件時間窗口都有一個默認的EventTimeTrigger觸發器,一旦水印通過窗口的末尾,這個觸發器就會觸發。
注意:GlobalWindow的默認觸發器是NeverTrigger,也就是永不觸發。因此在使用GlobalWindow時,需要自定義觸發器。
注意:當使用trigger()方法指定觸發器時,會覆蓋當前窗口默認的觸發器。比如,當你爲TumblingEventTimeWindows指定了CountTrigger作爲觸發器時,那麼窗口永遠都不會因爲時間而觸發,只會根據計數來觸發。這時,如果你想同時基於時間和計數來觸發窗口函數,那麼就需要自定義觸發器。
內建的和自定義的觸發器
flink自帶了一些觸發器:
- The (already mentioned)
EventTimeTrigger
fires based on the progress of event-time as measured by watermarks. - The
ProcessingTimeTrigger
fires based on processing time. - The
CountTrigger
fires once the number of elements in a window exceeds the given limit. - The
PurgingTrigger
takes as argument another trigger and transforms it into a purging one.
如果要自定義觸發器,需要繼承抽象類Trigger。
驅逐器(Evictors)
(未完待續。。。)