窗口算子WindowOperator是窗口機制的底層實現,它幾乎會牽扯到所有窗口相關的知識點,因此相對複雜。本文將以由面及點的方式來分析WindowOperator的實現。首先,我們來看一下對於最常見的時間窗口(包含處理時間和事件時間)其執行示意圖:
上圖中,左側從左往右爲事件流的方向。方框代表事件,事件流中夾雜着的豎直虛線代表水印,Flink通過水印分配器(TimestampsAndPeriodicWatermarksOperator和TimestampsAndPunctuatedWatermarksOperator這兩個算子)向事件流中注入水印。元素在streaming dataflow引擎中流動到WindowOperator時,會被分爲兩撥,分別是普通事件和水印。
如果是普通的事件,則會調用processElement方法(上圖虛線框中的三個圓圈中的一個)進行處理,在processElement方法中,首先會利用窗口分配器爲當前接收到的元素分配窗口,接着會調用觸發器的onElement方法進行逐元素觸發。對於時間相關的觸發器,通常會註冊事件時間或者處理時間定時器,這些定時器會被存儲在WindowOperator的處理時間定時器隊列和水印定時器隊列中(見圖中虛線框中上下兩個圓柱體),如果觸發的結果是FIRE,則對窗口進行計算。
如果是水印(事件時間場景),則方法processWatermark將會被調用,它將會處理水印定時器隊列中的定時器。如果時間戳滿足條件,則利用觸發器的onEventTime方法進行處理。
而對於處理時間的場景,WindowOperator將自身實現爲一個基於處理時間的觸發器,以觸發trigger方法來消費處理時間定時器隊列中的定時器滿足條件則會調用窗口觸發器的onProcessingTime,根據觸發結果判斷是否對窗口進行計算。
以上是WindowOperator的常規流程最簡單的表述,事實上其邏輯要複雜得多。我們首先分解掉幾個內部核心對象,上圖中我們看到有兩個隊列:分別是水印定時器隊列和處理時間定時器隊列。這裏的定時器是什麼?它有什麼作用呢?接下來我們就來看看它的定義——WindowOperator的內部類Timer。Timer是所有時間窗口執行的基礎,它其實是一個上下文對象,封裝了三個屬性:
timestamp:觸發器觸發的時間戳; key:當前元素所歸屬的分組的鍵; window:當前元素所屬窗口;
在我們講解窗口觸發器時,我們曾提及過觸發器上下文對象,它作爲process系列方法參數。在WindowOperator內部我們終於看到了對該上下文對象接口的實現——Context,它主要提供了三種類型的方法:
提供狀態存儲與訪問; 定時器的註冊與刪除; 窗口觸發器process系列方法的包裝;
在註冊定時器時,會新建定時器對象並將其加入到定時器隊列中。等到時間相關的處理方法(processWatermark和trigger)被觸發調用,則會從定時器隊列中消費定時器對象並調用窗口觸發器,然後根據觸發結果來判斷是否觸動窗口的計算。我們選擇事件時間的處理方法processWatermark進行分析(處理時間的處理方法trigger跟其類似):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
|
以上方法雖然冗長但流程還算清晰,其中的fire方法用於對窗口進行計算,它會調用內部窗口函數(即InternalWindowFunction,它包裝了WindowFunction)的apply方法。
而isCleanupTime和cleanup這對方法主要涉及到窗口的清理。如果當前窗口是時間窗口,且窗口的時間到達了清理時間,則會進行清理窗口清理。那麼清理時間如何判斷呢?Flink是通過窗口的最大時間戳屬性結合允許延遲的時間聯合計算的:
1 2 3 4 5 6 7 8 |
|
求出清理時間後會與定時器註冊的時間進行對比,如果兩者相等則布爾條件爲真,否則爲假:
1 2 3 4 |
|
下面我們來看一下清理方法主要做了哪些事情:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
關於窗口清理,其實三大處理方法(processElement\/processWatermark\/trigger)都會進行判斷,如果滿足條件則清理。而真正註冊清理定時器的邏輯在processElement中,它會調用registerCleanupTimer方法:
1 2 3 4 5 6 7 8 9 10 |
|
從上面的代碼段可知:清理定時器跟普通定時器是一樣的。
如果沒有延遲,對於事件時間和處理時間而言,也許它們的窗口清理不一定是由清理定時器觸發。因爲在事件時間觸發器和處理時間觸發器中,它們註冊的定時器對應的時間點就是窗口的最大時間戳。由於這些定時器在隊列中一般排在清理定時器之前,所以這些定時器會優先於清理定時器得到執行(優先觸發窗口的清理)。而這裏的registerCleanupTimer方法,是一般化的清理機制,針對所有類型的窗口都適用,並確保窗口一定會得到清理。而對於剛剛提到的這種情況,重複的“清理”定時器並不會產生負作用。
WindowOperator還有一個繼承者:EvictingWindowOperator,該算子在常規的窗口算子上支持了元素驅逐器(見上圖中大虛線框內部的小虛線長方形)。EvictingWindowOperator特別的地方主要在於其fire的實現——在進行窗口計算之前會預先對符合驅逐條件的元素進行剔除,具體實現見如下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在最終調用窗口計算的apply方法之前,會先計算要驅逐的元素個數,然後跳過這些元素並且跳過的都是從首個元素開始的連續個元素(這一點在之前我們分析窗口元素驅逐器是也曾提及過)。
這裏採用了Guava類庫的FluentIterable幫助類,它擴展了Iterable接口並提供了非常豐富的擴展API。