Window意為視窗。在流處理系統中資料來源源不斷流入到系統,我們可以逐條處理流入的資料,也可以按一定規則一次處理流中的多條資料。當處理資料時程式需要知道什麼時候開始處理、處理哪些資料。視窗提供了這樣一種依據,決定了資料何時開始處理。
Flink內建Window
Flink有3個內建Window
以事件數量驅動的Count Window
以會話間隔驅動的Session Window
以時間驅動的Time Window
本文圍繞這3個內建視窗展開討論,我們首先了解這3個視窗在執行時產生的現象,最後再討論它們的實現原理。
Count Window
計數視窗,採用事件數量作為視窗處理依據。計數視窗分為滾動和滑動兩類,使用keyedStream.countWindow實現計數視窗定義。
Tumbling Count Window 滾動計數視窗
例子:以使用者分組,當每位使用者有3次付款事件時計算一次該使用者付款總金額。下圖中“訊息A、B、C、D”代表4位不同使用者,我們以A、B、C、D分組並計算金額。
/** 每3個事件,計算視窗內資料 */
keyedStream.countWindow(3);
複製程式碼
Sliding Count Window 滑動計數視窗
例子:一位使用者每3次付款事件計算最近4次付款事件總金額。
/** 每3個事件,計算最近4個事件訊息 */
keyedStream.countWindow(4,3);
複製程式碼
Session Window
會話視窗,採用會話持續時長作為視窗處理依據。設定指定的會話持續時長時間,在這段時間中不再出現會話則認為超出會話時長。
例子:每隻股票超過2秒沒有交易事件時計算視窗內交易總金額。下圖中“訊息A、訊息B”代表兩隻不同的股票。
/** 會話持續2秒。當超過2秒不再出現會話認為會話結束 */
keyedStream.window(ProcessingTimeSessionWindows.withGap(Time.seconds(2)))複製程式碼
Time Window
時間視窗,採用時間作為視窗處理依據。時間窗分為滾動和滑動兩類,使用keyedStream.timeWindow實現時間窗定義。
Tumbling Time Window 滾動時間視窗:
/** 每1分鐘,計算視窗資料 */
keyedStream.timeWindow(Time.minutes(1));複製程式碼
Sliding Time Window 滑動時間視窗:
/** 每半分鐘,計算最近1分鐘視窗資料 */
keyedStream.timeWindow(Time.minutes(1), Time.seconds(30));
複製程式碼
Flink Window元件
Flink Window使用3個元件協同實現了內建的3個視窗。通過對這3個元件不同的組合,可以滿足許多場景的視窗定義。
WindowAssigner元件為資料分配視窗、Trigger元件決定如何處理視窗中的資料、藉助Evictor元件實現靈活清理視窗中資料時機。
WindowAssigner
當有資料流入到Window Operator時需要按照一定規則將資料分配給視窗,WindowAssigner為資料分配視窗。下面程式碼片段是WindowAssigner部分定義,assignWindows方法定義返回的結果是一個集合,也就是說資料允許被分配到多個視窗中。
/*** WindowAssigner關鍵介面定義 ***/
public abstract class WindowAssigner<T, W extends Window> implements Serializable {
/** 分配資料到視窗集合並返回 */
public abstract Collection<W> assignWindows(T element, long timestamp, WindowAssignerContext context);
}複製程式碼
Flink針對不同視窗型別實現了相應的WindowAssigner。Flink 1.7.0繼承關係如下圖
Trigger
/** Trigger關鍵介面定義 */
public abstract class Trigger<T, W extends Window> implements Serializable {
/*** 新的資料進入視窗時觸發 ***/
public abstract TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx) throws Exception;
/*** 處理時間計數器觸發 ***/
public abstract TriggerResult onProcessingTime(long time, W window, TriggerContext ctx) throws Exception;
/*** 事件時間計數器觸發 ***/
public abstract TriggerResult onEventTime(long time, W window, TriggerContext ctx) throws Exception;
}
複製程式碼
當有資料流入Window Operator時會觸發onElement方法、當處理時間和事件時間生效時會觸發onProcessingTime和onEventTime方法。每個觸發動作的返回結果用TriggerResult定義。
Trigger觸發運算後返回處理結果,處理結果使用TriggerResult列舉表示。
public enum TriggerResult {
CONTINUE,FIRE,PURGE,FIRE_AND_PURGE;
}複製程式碼
Flink的內建視窗(Counter、Session、Time)有自己的觸發器實現。下表為不同視窗使用的觸發器。
Evictor
Evictor驅逐者,如果定義了Evictor當執行視窗處理前會刪除視窗內指定資料再交給視窗處理,或等視窗執行處理後再刪除視窗中指定資料。
public interface Evictor<T, W extends Window> extends Serializable {
/** 在視窗處理前刪除資料 */
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/** 在視窗處理後刪除資料 */
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
}複製程式碼
實現原理
通過KeyedStream可以直接建立Count Window和Time Window。他們最終都是基於window(WindowAssigner)方法建立,在window方法中建立WindowedStream例項,引數使用當前的KeyedStream物件和指定的WindowAssigner。
/** 依據WindowAssigner例項化WindowedStream */
public <W extends Window> WindowedStream<T, KEY, W> window(WindowAssigner<? super T, W> assigner) {
return new WindowedStream<>(this, assigner);
}複製程式碼
/** WindowedStream構造器 */
public WindowedStream(KeyedStream<T, K> input, WindowAssigner<? super T, W> windowAssigner) {
this.input = input;
this.windowAssigner = windowAssigner;
this.trigger = windowAssigner.getDefaultTrigger(input.getExecutionEnvironment());
}
複製程式碼
構造器執行完畢後,WindowedStream建立完成。構造器中初始化了3個屬性。預設情況下trigger屬性使用WindowAssigner提供的DefaultTrigger作為初始值。
同時,WindowedStream提供了trigger方法用來覆蓋預設的trigger。Flink內建的計數視窗就使用windowedStream.trigger方法覆蓋了預設的trigger。
public WindowedStream<T, K, W> trigger(Trigger<? super T, ? super W> trigger) {
if (windowAssigner instanceof MergingWindowAssigner && !trigger.canMerge()) {
throw new UnsupportedOperationException();
}
if (windowAssigner instanceof BaseAlignedWindowAssigner) {
throw new UnsupportedOperationException();
}
this.trigger = trigger;
return this;
}複製程式碼
在WindowedStream中還有一個比較重要的屬性evictor,可以通過evictor方法設定。
public WindowedStream<T, K, W> evictor(Evictor<? super T, ? super W> evictor) {
if (windowAssigner instanceof BaseAlignedWindowAssigner) {
throw new UnsupportedOperationException();
}
this.evictor = evictor;
return this;
}複製程式碼
WindowedStream實現中根據evictor屬性是否空(null == evictor)決定是建立WindowOperator還是EvictingWindowOperator。EvictingWindowOperator繼承自WindowOperator,它主要擴充套件了evictor屬性以及相關的邏輯處理。
public class EvictingWindowOperator extends WindowOperator {
private final Evictor evictor;
}複製程式碼
Evictor定義了清理資料的時機。在EvictingWindowOperator的emitWindowContents方法中,實現了清理資料邏輯呼叫。這也是EvictingWindowOperator與WindowOperator的主要區別。「在WindowOperator中壓根就沒有evictor的概念」
private void emitWindowContents(W window, Iterable<StreamRecord<IN>> contents, ListState<StreamRecord<IN>> windowState) throws Exception {
/** Window處理前資料清理 */
evictorContext.evictBefore(recordsWithTimestamp, Iterables.size(recordsWithTimestamp));
/** Window處理 */
userFunction.process(triggerContext.key, triggerContext.window, processContext, projectedContents, timestampedCollector);
/** Window處理後資料清理 */
evictorContext.evictAfter(recordsWithTimestamp, Iterables.size(recordsWithTimestamp));
}
複製程式碼
Count Window API
下面程式碼片段是KeyedStream提供建立Count Window的API。
/** 滾動計數視窗 */
public WindowedStream<T, KEY, GlobalWindow> countWindow(long size) {
return window(GlobalWindows.create()).trigger(PurgingTrigger.of(CountTrigger.of(size)));
}
/** 滑動計數視窗 */
public WindowedStream<T, KEY, GlobalWindow> countWindow(long size, long slide) {
return window(GlobalWindows.create())
.evictor(CountEvictor.of(size))
.trigger(CountTrigger.of(slide));
}
複製程式碼
滾動計數視窗與滑動計數視窗有幾個差異
入參不同
滑動視窗使用了evictor元件
兩者使用的trigger元件不同
下面我們對這幾點差異做深入分析,看一看他們是如何影響滾動計數視窗和滑動計數視窗的。
/** GlobalWindows是一個WindowAssigner實現,這裡只展示實現assignWindows的程式碼片段 */
public class GlobalWindows extends WindowAssigner<Object, GlobalWindow> {
/** 返回一個GlobalWindow */
public Collection<GlobalWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
return Collections.singletonList(GlobalWindow.get());
}
}
複製程式碼
GlobalWindow繼承了Window,表示為一個視窗。對外提供get()方法返回GlobalWindow例項,並且是個全域性單例。所以當使用GlobalWindows作為WindowAssigner時,所有資料將被分配到一個視窗中。
/** GlobalWindow是一個Window */
public class GlobalWindow extends Window {
private static final GlobalWindow INSTANCE = new GlobalWindow();
/** 永遠返回GlobalWindow單例 */
public static GlobalWindow get() {
return INSTANCE;
}
}
複製程式碼
PurgingTrigger是一個代理模式的Trigger實現,在計數視窗中PurgingTrigger代理了CountTrigger。
/** PurgingTrigger代理的Trigger */
private Trigger<T, W> nestedTrigger;
/** PurgingTrigger私有構造器 */
private PurgingTrigger(Trigger<T, W> nestedTrigger) {
this.nestedTrigger = nestedTrigger;
}
/** 為代理的Trigger構造一個PurgingTrigger例項 */
public static <T, W extends Window> PurgingTrigger<T, W> of(Trigger<T, W> nestedTrigger) {
return new PurgingTrigger<>(nestedTrigger);
}
複製程式碼
在這裡比較一下PurgingTrigger.onElement和CountTrigger.onElement方法實現,幫助理解PurgingTrigger的作用。
/** CountTrigger實現 */
public TriggerResult onElement(Object element, long timestamp, W window, TriggerContext ctx) throws Exception {
ReducingState<Long> count = ctx.getPartitionedState(stateDesc);
count.add(1L);
if (count.get() >= maxCount) {
count.clear();
return TriggerResult.FIRE;
}
return TriggerResult.CONTINUE;
}
/** PurgingTrigger實現 */
public TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx) throws Exception {
TriggerResult triggerResult = nestedTrigger.onElement(element, timestamp, window, ctx);
return triggerResult.isFire() ? TriggerResult.FIRE_AND_PURGE : triggerResult;
}
複製程式碼
在CountTrigger實現中,當事件流入視窗後計數+1,之後比較視窗中事件數是否大於設定的最大數量,一旦大於最大數量返回FIRE。也就是說只處理視窗資料,不做清理。
在PurgingTrigger實現中,依賴CountTrigger的處理邏輯,但區別在於當CounterTrigger返回FIRE時PurgingTrigger返回FIRE_AND_PURGE。也就是說不僅處理視窗資料,還做資料清理。通過這種方式實現了滾動計數視窗資料不重疊。
滑動計數視窗依賴Evictor元件在視窗處理前清除了指定數量以外的資料,再交給視窗處理。通過這種方式實現了視窗計算最近指定次數的事件數量。
Time Window API
下面程式碼片段是KeyedStream中提供建立Time Window的API。
/** 建立滾動時間視窗 */
public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size) {
if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
return window(TumblingProcessingTimeWindows.of(size));
} else {
return window(TumblingEventTimeWindows.of(size));
}
}
/** 建立滑動時間視窗 */
public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size, Time slide) {
if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
return window(SlidingProcessingTimeWindows.of(size, slide));
} else {
return window(SlidingEventTimeWindows.of(size, slide));
}
}
複製程式碼
建立TimeWindow時會根據Flink應用當前時間型別environment.getStreamTimeCharacteristic()來決定使用哪個WindowAssigner建立視窗。
Flink對時間分成了3類。處理時間、攝入時間、事件時間。使用TimeCharacteristic列舉定義。
public enum TimeCharacteristic {
/** 處理時間 */
ProcessingTime,
/** 攝入時間 */
IngestionTime,
/** 事件時間 */
EventTime
}
複製程式碼
對於Flink的3個時間概念,我們目前只需要瞭解
處理時間(TimeCharacteristic.ProcessingTime)就是執行Flink環境的系統時鐘產生的時間
事件時間(TimeCharacteristic.EventTime)是業務上產生的時間,由資料自身攜帶
攝入時間(TimeCharacteristic.IngestionTime)是資料進入到Flink的時間,它在底層實現上與事件時間相同。
下面的表格中展示了視窗型別和時間型別對應的WindowAssigner的實現類
我們以一個TumblingProcessingTimeWindows和一個SlidingEventTimeWindows為例,討論它的實現原理。
public static TumblingProcessingTimeWindows of(Time size) {
return new TumblingProcessingTimeWindows(size.toMilliseconds(), 0);
}
public static TumblingProcessingTimeWindows of(Time size, Time offset) {
return new TumblingProcessingTimeWindows(size.toMilliseconds(), offset.toMilliseconds());
}
複製程式碼
不管使用哪種方式初始化TumblingProcessingTimeWindows,最終都會呼叫同一個構造方法初始化,構造方法初始化size和offset兩個屬性。
/** TumblingProcessingTimeWindows構造器 */
private TumblingProcessingTimeWindows(long size, long offset) {
if (offset < 0 || offset >= size) {
throw new IllegalArgumentException();
}
this.size = size;
this.offset = offset;
}
複製程式碼
TumblingProcessingTimeWindows是一個WindowAssigner,所以它實現了assignWindows方法來為流入的資料分配視窗。
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
final long now = context.getCurrentProcessingTime();
long start = TimeWindow.getWindowStartWithOffset(now, offset, size);
return Collections.singletonList(new TimeWindow(start, start + size));
}
複製程式碼
第一步assignWindows首先獲得系統當前時間戳,context.getCurrentProcessingTime();最終實現實際是呼叫System.currentTimeMillis()。
第二步執行TimeWindow.getWindowStartWithOffset(now, offset, size);這個方法根據當前時間、偏移量、設定的間隔時間最終計算視窗起始時間。
第三步根據起始時間和結束時間建立一個新的視窗new TimeWindow(start, start + size)並返回。
比如,希望每10秒處理一次視窗資料keyedStream.timeWindow(Time.seconds(10))。當資料來源源不斷的流入Window Operator時,它會按10秒切割一個時間窗。
我們假設資料在2019年1月1日 12:00:07到達,那麼視窗以下面方式切割(請注意,視窗是左閉右開)。
Window[2019年1月1日 12:00:00, 2019年1月1日 12:00:10)複製程式碼
如果在2019年1月1日 12:10:09又一條資料到達,視窗是這樣的
Window[2019年1月1日 12:10:00, 2019年1月1日 12:10:10)複製程式碼
如果我們希望從第15秒開始,每過1分鐘計算一次視窗資料,這種場景需要用到offset。基於處理時間的滾動視窗可以這樣寫
keyedStream.window(TumblingProcessingTimeWindows.of(Time.minutes(1), Time.seconds(15)))複製程式碼
我們假設資料從2019年1月1日 12:00:14到達,那麼視窗以下面方式切割
Window[2019年1月1日 11:59:15, 2019年1月1日 12:00:15)複製程式碼
如果在2019年1月1日 12:00:16又一資料到達,那麼視窗以下面方式切割
Window[2019年1月1日 12:00:15, 2019年1月1日 12:01:15)複製程式碼
TumblingProcessingTimeWindows.assignWindows方法每次都會返回一個新的視窗,也就是說視窗是不重疊的。但因為TimeWindow實現了equals方法,所以通過計算後start, start + size相同的資料,在邏輯上是同一個視窗。
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TimeWindow window = (TimeWindow) o;
return end == window.end && start == window.start;
}
複製程式碼
public static SlidingEventTimeWindows of(Time size, Time slide) {
return new SlidingEventTimeWindows(size.toMilliseconds(), slide.toMilliseconds(), 0);
}
public static SlidingEventTimeWindows of(Time size, Time slide, Time offset) {
return new SlidingEventTimeWindows(size.toMilliseconds(), slide.toMilliseconds(),offset.toMilliseconds() % slide.toMilliseconds());
}複製程式碼
同樣,不管使用哪種方式初始化SlidingEventTimeWindows,最終都會呼叫同一個構造方法初始化,構造方法初始化三個屬性size、slide和offset。
protected SlidingEventTimeWindows(long size, long slide, long offset) {
if (offset < 0 || offset >= slide || size <= 0) {
throw new IllegalArgumentException();
}
this.size = size;
this.slide = slide;
this.offset = offset;
}複製程式碼
SlidingEventTimeWindows是一個WindowAssigner,所以它實現了assignWindows方法來為流入的資料分配視窗。
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
if (timestamp > Long.MIN_VALUE) {
List<TimeWindow> windows = new ArrayList<>((int) (size / slide));
long lastStart = TimeWindow.getWindowStartWithOffset(timestamp, offset, slide);
for (long start = lastStart; start > timestamp - size;start -= slide) {
windows.add(new TimeWindow(start, start + size));
}
return windows;
} else {
throw new RuntimeException();
}
}複製程式碼
與基於處理時間的WindowAssigner不同,基於事件時間的WindowAssigner不依賴於系統時間,而是依賴於資料本身的事件時間。在assignWindows方法中第二個引數timestamp就是資料的事件時間。
第一步assignWindows方法會先初始化一個List<TimeWindow>,大小是size / slide。這個集合用來存放時間窗物件並作為返回結果。
第二步執行TimeWindow.getWindowStartWithOffset(timestamp, offset, slide);計算視窗起始時間。
第三步根據事件時間、滑動大小和視窗大小計算並生成資料能落入的視窗new TimeWindow(start, start + size),最後加入到List集合並返回。「因為是滑動視窗一個資料可能落在多個視窗」
比如,希望每5秒滑動一次處理最近10秒視窗資料keyedStream.timeWindow(Time.seconds(10), Time.seconds(5))。當資料來源源不斷流入Window Operator時,會按10秒切割一個時間窗,5秒滾動一次。
我們假設一條付費事件資料付費時間是2019年1月1日 17:11:24,那麼這個付費資料將落到下面兩個視窗中(請注意,視窗是左閉右開)。
Window[2019年1月1日 17:11:20, 2019年1月1日 17:11:30)
Window[2019年1月1日 17:11:15, 2019年1月1日 17:11:25)複製程式碼
TumblingProcessingTimeWindows使用ProcessingTimeTrigger作為預設Trigger。ProcessingTimeTrigger在onElement的策略是永遠返回CONTINUE,也就是說它不會因為資料的流入觸發視窗計算和清理。在返回CONTINUE前呼叫registerProcessingTimeTimer(window.maxTimestamp());註冊一個定時器,並且邏輯相同視窗只註冊一次,事件所在視窗的結束時間與系統當前時間差決定了定時器多久後觸發。
ScheduledThreadPoolExecutor.schedule(new TriggerTask(), timeEndTime - systemTime, TimeUnit.MILLISECONDS);
複製程式碼
定時器一旦觸發會回撥Trigger的onProcessingTime方法。ProcessingTimeTrigger中實現的onProcessingTime直接返回FIRE。也就是說系統時間大於等於視窗最大時間時,通過回撥方式觸發視窗計算。但因為返回的是FIRE只是觸發了視窗計算,並沒有做清除。
SlidingEventTimeWindows使用EventTimeTrigger作為預設Trigger。事件時間、攝入時間與處理時間在時間概念上有一點不同,處理時間處理依賴的是系統時鐘生成的時間,而事件時間和攝入時間依賴的是Watermark(水印)。我們現在只需要知道水印是一個時間戳,可以由Flink以固定的時間間隔發出,或由開發人員根據業務自定義。水印用來衡量處理程式的時間進展。
EventTimeTrigger的onElement方法中比較視窗的結束時間與當前水印時間,如果視窗結束時間已小於或等於當前水印時間立即返回FIRE。
「個人理解這是由於時間差問題導致的視窗時間小於或等於當前水印時間,正常情況下如果視窗結束時間已經小於水印時間則資料不會被處理,也不會呼叫onElement」
如果視窗結束時間大於當前水印時間,呼叫registerEventTimeTimer(window.maxTimestamp())註冊一個事件後直接返回CONTINUE。EventTime註冊事件沒有使用Scheduled,因為它依賴水印時間。所以在註冊時將邏輯相同的時間窗封裝為一個特定物件新增到一個排重佇列,並且相同視窗物件只新增一次。
上面提到水印是以固定時間間隔發出或由開發人員自定義的,Flink處理水印時從排重佇列頭獲取一個時間窗物件與水印時間戳比較,一旦視窗時間小於或等於水印時間回撥trigger的onEventTime。
EventTimeTrigger中onEventTime並不是直接返回FIRE,而是判斷視窗結束時間與獲取的時間窗物件時間做比較,僅當時間相同時才返回FIRE,其他情況返回CONTINUE。「個人理解這麼做是為了滿足滑動視窗的需求,因為滑動視窗在排重佇列中存在兩個不同的物件,而兩個視窗物件的時間可能同時滿足回撥條件」
Flink內建Time Window實現沒有使用Evictor。
Session Window API
KeyedStream中沒有為Session Window提供類似Count Windown和Time Window一樣能直接使用的API。我們可以使用window(WindowAssigner assigner)建立Session Window。
比如建立一個基於處理時間,時間間隔為2秒的SessionWindow可以這樣實現
keyedStream.window(ProcessingTimeSessionWindows.withGap(Time.seconds(2)))複製程式碼
MergingWindowAssigner繼承了WindowAssigner,所以它具備分配時間窗的能力。MergingWindowAssigner自身是一個可以merge的Window,它的內部定義了一個mergeWindows抽象方法以及merge時的回撥定義。
public abstract void mergeWindows(Collection<W> windows, MergeCallback<W> callback);
public interface MergeCallback<W> {
void merge(Collection<W> toBeMerged, W mergeResult);
}複製程式碼
我們以ProcessingTimeSessionWindows為例介紹Session Window。ProcessingTimeSessionWindows提供了一個靜態方法用來初始化ProcessingTimeSessionWindows
public static ProcessingTimeSessionWindows withGap(Time size) {
return new ProcessingTimeSessionWindows(size.toMilliseconds());
}複製程式碼
靜態方法withGap接收一個時間引數,用來描述時間間隔。並呼叫構造方法將時間間隔賦值給sessionTimeout屬性。
protected ProcessingTimeSessionWindows(long sessionTimeout) {
if (sessionTimeout <= 0) {
throw new IllegalArgumentException();
}
this.sessionTimeout = sessionTimeout;
}複製程式碼
ProcessingTimeSessionWindows是一個WindowAssigner,所以它實現了資料分配視窗的能力。
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
long currentProcessingTime = context.getCurrentProcessingTime();
return Collections.singletonList(new TimeWindow(currentProcessingTime, currentProcessingTime + sessionTimeout));
}複製程式碼
ProcessingTimeSessionWindows會為每個資料都分配一個新的時間視窗。由於是基於處理時間,所以視窗的起始時間就是系統當前時間,而結束時間是系統當前時間+設定的時間間隔。通過起始時間和結束時間確定了視窗的時間範圍。
public Trigger<Object, TimeWindow> getDefaultTrigger(StreamExecutionEnvironment env) {
return ProcessingTimeTrigger.create();
}複製程式碼
ProcessingTimeTrigger在基於處理時間的Time Window介紹過,它通過註冊、onProcessorTime回撥方式觸發視窗計算,這裡不再討論。
public void mergeWindows(Collection<TimeWindow> windows, MergeCallback<TimeWindow> c) {
TimeWindow.mergeWindows(windows, c);
}複製程式碼
Session Window處理流程大致是這樣
使用WindowAssigner為流入的資料分配視窗
Merge視窗,將存在交集的視窗合併,取最小時間和最大時間作為視窗的起始和關閉。假設有兩條資料流入系統後,通過WindowAssigner分配的視窗分別是
資料A:Window[2019年1月1日 10:00:00, 2019年1月1日 10:20:00)
資料B:Window[2019年1月1日 10:05:00, 2019年1月1日 10:25:00)
經過合併後,使用資料A的起始時間和資料B的結束時間作為節點,視窗時間變為了
[2019年1月1日 10:00:00, 2019年1月1日 10:25:00)執行Trigger.onMerge,為合併後的視窗註冊回撥事件
移除其他註冊的回撥事件
Window State合併
開始處理資料,執行Trigger.onElement
…後續與其他Window處理一樣
可以看到,Session Window與Time Window類似,通過註冊回撥方式觸發資料處理。但不同的是Session Window通過不斷為新流入的資料做Merge操作來改變回撥時間點,以實現Session Window的特性。
總結
Window Operator建立
Window處理流程由WindowOperator或EvictingWindowOperator控制,他們的關係及區別體現在以下幾點
EvictingWindowOperator繼承自WindowOperator,所以EvictingWindowOperator是一個WindowOperator,具備WindowOperator的特性。
清理視窗資料的機制不同,EvictingWindowOperator內部依賴Evictor元件,而WindowOperator內部不使用Evictor。這也導致它們兩個Operator初始化時的差異
MergeWindow特殊處理
可以合併視窗的WindowAssigner會繼承MergingWindowAssigner。當資料流入Window Operator後,根據WindowAssigner是否為一個MergingWindowAssigner決定了處理流程。
視窗生命週期
Flink內建的視窗生命週期是不同的,下表描述了他們直接的差異
側路輸出
當Flink應用採用EventTime作為時間機制時,Window不會處理延遲到達的資料,也就是說不處理在水印時間戳之前的資料。Flink提供了一個SideOutput機制可以處理這些延遲到達的資料。通過WindowedStream.sideOutputLateData方法實現側路輸出。自定義視窗
Flink內建視窗利用WindowAssigner、Trigger、Evictor3個元件的相互組合實現了多種非常強大的功能,我們也可以嘗試通過元件實現一個自定義的Window。由於篇幅原因,自定義視窗下篇再細聊。
作者:TalkingData 史天舒