Flink 中的時間和窗口

在批處理統計中,我們可以等待一批數據都到齊後,統一處理。但是在實時處理統計中,我們是來一條就得處理一條,那麼我們怎麼統計最近一段時間內的數據呢?引入“窗口”。

所謂的“窗口”,一般就是劃定的一段時間範圍,也就是“時間窗”;對在這範圍內的數據進行處理,就是所謂的窗口計算。所以窗口和時間往往是分不開的。

1.窗口

1.概念

Flink 是一種流式計算引擎,主要是來處理無界數據流的,數據源源不斷、無窮無盡。想要更加方便高效地處理無界流,一種方式就是將無限數據切割成有限的“數據塊”進行處理,這就是所謂的“窗口”(Window)

在Flink中,將流切割成有限大小的多個窗口;每個數據都會分發到對應的窗口中,當到達窗口結束時間時,就對每個窗口中收集的數據進行計算處理。

注意:Flink 中窗口並不是靜態準備好的,而是動態創建——當有落在這個窗口區間範圍的數據達到時,才創建對應的窗口。另外,這裏我們認爲到達窗口結束時間時,窗口就觸發計算並關閉,事實上“觸發計算”和“窗口關閉”兩個行爲也可以分開。

2.分類

1.按照驅動類型分

窗口本身是截取有界數據的一種方式,所以窗口一個非常重要的信息其實就是“怎樣截取數據”。換句話說,就是以什麼標準來開始和結束數據的截取,我們把它叫作窗口的“驅動類型”。

1.時間窗口(Time Window)

時間窗口以時間點來定義窗口的開始(start)和結束(end),所以截取出的就是某一時間段的數據。到達結束時間時,窗口不再收集數據,觸發計算輸出結果,並將窗口關閉銷燬。

2.計數窗口(Count Window)

計數窗口基於元素的個數來截取數據,到達固定的個數時就觸發計算並關閉窗口。每個窗口截取數據的個數,就是窗口的大小。

2.按照窗口分配數據的規則分類

根據分配數據的規則,窗口的具體實現可以分爲 4 類:滾動窗口(Tumbling Window)、滑動窗口(Sliding Window)、會話窗口(Session Window),以及全局窗口(Global Window)。

1.滾動窗口(Tumbling Windows)

滾動窗口有固定的大小,是一種對數據進行“均勻切片”的劃分方式。窗口之間沒有重疊,也不會有間隔,是“首尾相接”的狀態。這是最簡單的窗口形式,每個數據都會被分配到一個窗口,而且只會屬於一個窗口。

滾動窗口可以基於時間定義,也可以基於數據個數定義;需要的參數只有一個,就 是 窗口的大小 ( window size)。比如我們可以定義一個長度 爲1小時的滾動時間窗口,那麼每個小時就會進行一次統計;或者定義一個長度爲10的滾動計數窗口,就會每10個數進行一次統計。

滾動窗口應用非常廣泛,它可以對每個時間段做聚合統計,很多BI分析指標都可以用它來實現。

2.滑動窗口(Sliding Windows)

滑動窗口的大小也是固定的。但是窗口之間並不是首尾相接的,而是可以“錯開”一定的位置。

定義滑動窗口的參數有兩個:除去窗口大小(window size)之外,還有一個“滑動步長”(window slide),它其實就代表了窗口計算的頻率。窗口在結束時間觸發計算輸出結果,那麼滑動步長就代表了計算頻率。

當滑動步長小於窗口大小時,滑動窗口就會出現重 疊,這時數據也可能會被同時分配到多個窗口中。而具體的個數,就由窗口大小和滑動步長的比值(size/slide)來決定。

滾動窗口也可以看作是一種特殊的滑動窗口-窗口大小等於滑動步長(size = slide)。

滑動窗口適合計算結果更新頻率非常高的場景

3.會話窗口(Session Windows)

會話窗口,是基於“會話”(session)來來對數據進行分組的。會話窗口只能基於時間來定義。

會話窗口中,最重要的參數就是會話的超時時間,也就是兩個會話窗口之間的最小距離。如果相鄰兩個數據到來的時間間隔(Gap)小於指定的大小(size),那說明還在保持會話,它們就屬於同一個窗口;如果gap大於size,那麼新來的數據就應該屬於新的會話窗口,而前一個窗口就應該關閉了。

會話窗口的長度不固定,起始和結束時間也是不確定的,各個分區之間窗口沒有任何關聯。會話窗口之間一定 是不會重疊的,而且會留有至少爲size的間隔(session gap)。

在一些類似保持會話的場景下,可以使用會話窗口來進行數據的處理統計。

4.全局窗口(Global Windows)

“全局窗口”,這種窗口全局有效,會把相同key的所有數據都分配到同一個窗口中。這種窗口沒有結束的時候,默認是不會做觸發計算的。如果希望它能對數據進行計算處理,還需要自定義“觸發器”(Trigger)。

全局窗口沒有結束的時間點,所以一般在希望做更加靈活的窗口處理時自定義使用。Flink中的計數窗口(Count Window),底層就是用全局窗口實現的。

3.窗口 API 概覽

1.按鍵分區(Keyed)和非按鍵分區(Non-Keyed)

在定義窗口操作之前,首先需要確定,到底是基於按鍵分區(Keyed)的數據流
KeyedStream 來開窗,還是直接在沒有按鍵分區的 DataStream 上開窗。也就是說,在調用窗口算子之前,是否有 keyBy 操作。

1.按鍵分區窗口(Keyed Windows)

經過按鍵分區 keyBy 操作後,數據流會按照 key 被分爲多條邏輯流(logical streams),這就是 KeyedStream。基於 KeyedStream進行窗口操作時,窗口計算會在多個並行子任務上同時執行。相同 key 的數據會被髮送到同一個並行子任務,而窗口操作會基於每個 key 進行單獨的處理。所以可以認爲,每個 key 上都定義了一組窗口,各自獨立地進行統計計算。

在代碼實現上,我們需要先對 DataStream 調用.keyBy()進行按鍵分區,然後再調用.window()定義窗口。

stream.keyBy(...)
 .window(...)
2.非按鍵分區(Non-Keyed Windows)

如果沒有進行 keyBy,那麼原始的 DataStream 就不會分成多條邏輯流。這時窗口邏輯只能在一個任務(task)上執行,就相當於並行度變成了 1。

在代碼中,直接基於 DataStream 調用.windowAll()定義窗口。

stream.windowAll(...)

注意:對於非按鍵分區的窗口操作,手動調大窗口算子的並行度也是無效的,windowAll本身就是一個非並行的操作。

2.代碼中窗口 API 的調用

窗口操作主要有兩個部分:窗口分配器(Window Assigners)和窗口函數(Window Functions)。

stream.keyBy(<key selector>)
 .window(<window assigner>)
 .aggregate(<window function>)

其 中.window()方法需要傳入一個窗口分配器,它指明瞭窗口的類型;而後面的.aggregate()方法傳入一個窗口函數作爲參數,它用來定義窗口具體的處理邏輯。窗口分配器有各種形式,而窗口函數的調用方法也不只.aggregate()一種,

4.窗口分配器

定義窗口分配器(Window Assigners)是構建窗口算子的第一步,它的作用就是定義數據應該被“分配”到哪個窗口。所以可以說,窗口分配器其實就是在指定窗口的類型。

窗口分配器最通用的定義方式,就是調用.window()方法。這個方法需要傳入一個WindowAssigner 作爲參數,返回 WindowedStream。如果是非按鍵分區窗口,那麼直接調用.windowAll()方法,同樣傳入一個 WindowAssigner,返回的是 AllWindowedStream。

窗口按照驅動類型可以分成時間窗口和計數窗口,而按照具體的分配規則,又有滾動窗口、滑動窗口、會話窗口、全局窗口四種。除去需要自定義的全局窗口外,其他常用的類型Flink 中都給出了內置的分配器實現,我們可以方便地調用實現各種需求。

1.時間窗口

時間窗口是最常用的窗口類型,又可以細分爲滾動、滑動和會話三種。

1.滾動處理時間窗口

窗口分配器由類 TumblingProcessingTimeWindows 提供,需要調用它的靜態方法.of()。

stream.keyBy(...)
 .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
 .aggregate(...)

這裏.of()方法需要傳入一個 Time 類型的參數 size,表示滾動窗口的大小,我們這裏創建了一個長度爲 5 秒的滾動窗口。

另外,.of()還有一個重載方法,可以傳入兩個 Time 類型的參數:size 和 offset。第一個參數當然還是窗口大小,第二個參數則表示窗口起始點的偏移量。

2.滑動處理時間窗口

窗口分配器由類 SlidingProcessingTimeWindows 提供,同樣需要調用它的靜態方法.of()。

stream.keyBy(...)
 .window(SlidingProcessingTimeWindows.of(Time.seconds(10) ,
Time.seconds(5)))
 .aggregate(...)

這裏.of()方法需要傳入兩個 Time 類型的參數:size 和 slide,前者表示滑動窗口的大小,後者表示滑動窗口的滑動步長。我們這裏創建了一個長度爲 10 秒、滑動步長爲 5 秒的滑動窗口。

滑動窗口同樣可以追加第三個參數,用於指定窗口起始點的偏移量,用法與滾動窗口完全一致。

3.處理時間會話窗口

窗口分配器由類 ProcessingTimeSessionWindows 提供,需要調用它的靜態方法.withGap()或者.withDynamicGap()。

stream.keyBy(...)
 .window(ProcessingTimeSessionWindows.withGap(Time.seconds(10
)))
 .aggregate(...)

這裏.withGap()方法需要傳入一個 Time類型的參數 size,表示會話的超時時間,也就是最小間隔 session gap。我們這裏創建了靜態會話超時時間爲 10 秒的會話窗口。

另外,還可以調用 withDynamicGap()方法定義 session gap 的動態提取邏輯。

4.滾動事件時間窗口

窗口分配器由類 TumblingEventTimeWindows提供,用法與滾動處理事件窗口完全一致。

stream.keyBy(...)
 .window(TumblingEventTimeWindows.of(Time.seconds(5)))
 .aggregate(...)
5.滑動事件時間窗口

窗口分配器由類 SlidingEventTimeWindows 提供,用法與滑動處理事件窗口完全一致。

stream.keyBy(...)
 .window(SlidingEventTimeWindows.of(Time.seconds(10) ,
Time.seconds(5)))
 .aggregate(...)
6.事件時間會話窗口

窗口分配器由類 EventTimeSessionWindows 提供,用法與處理事件會話窗口完全一致。

stream.keyBy(...)
 .window(EventTimeSessionWindows.withGap(Time.seconds(10)))
 .aggregate(...)

2.計數窗口

計數窗口概念非常簡單,本身底層是基於全局窗口(Global Window)實現的。Flink 爲我們提供了非常方便的接口:直接調用.countWindow()方法。根據分配規則的不同,又可以分爲滾動計數窗口和滑動計數窗口兩類,下面我們就來看它們的具體實現。

1.滾動計數窗口

滾動計數窗口只需要傳入一個長整型的參數 size,表示窗口的大小。

stream.keyBy(...)
 .countWindow(10)

我們定義了一個長度爲 10 的滾動計數窗口,當窗口中元素數量達到 10 的時候,就會觸發計算執行並關閉窗口。

2.滑動計數窗口

與滾動計數窗口類似,不過需要在.countWindow()調用時傳入兩個參數:size 和 slide,前者表示窗口大小,後者表示滑動步長。

stream.keyBy(...)
 .countWindow(10,3)

我們定義了一個長度爲 10、滑動步長爲 3 的滑動計數窗口。每個窗口統計 10個數據,每隔 3 個數據就統計輸出一次結果。

3.全局窗口

全局窗口是計數窗口的底層實現,一般在需要自定義窗口時使用。它的定義同樣是直接調用.window(),分配器由 GlobalWindows 類提供。

stream.keyBy(...)
 .window(GlobalWindows.create());

需要注意使用全局窗口,必須自行定義觸發器才能實現窗口計算,否則起不到任何作用。

        // 1.指定窗口分配器,指定用 哪一種窗口 --- 時間 || 計數 ? 滾動 || 滑動 || 會話 ?
        //  1.1 沒有keyBy的窗口:窗口內的 所有數據 進入同一個 子任務, 並行度只能爲1
        // keyedStream.windowAll()

        // 1.2 有keyBy的窗口:每個key上都定義了一組窗口,各自獨立的進行統計計算

        // 基於時間的
        keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10))); //滾動窗口,窗口長度10s
        keyedStream.window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(2)));  //滑動窗口,窗口長度10s,滑動步長2s
        keyedStream.window(ProcessingTimeSessionWindows.withGap(Time.seconds(5)));  // 會話窗口,超時間隔5s

        // 基於計數的
        keyedStream.countWindow(5); //滾動窗口,窗口長度=5個元素
        keyedStream.countWindow(5,2);   //滑動窗口,窗口長度=5個元素,滑動步長=2個元素
        keyedStream.window(GlobalWindows.create()); // 全局窗口,計數窗口的底層就是用的這個,需要自定義的時候纔會用

5.窗口函數

定義了窗口分配器,我們只是知道了數據屬於哪個窗口,可以將數據收集起來了;至於收集起來到底要做什麼,其實還完全沒有頭緒。所以在窗口分配器之後,必須再接上一個定義窗口如何進行計算的操作,這就是所謂的“窗口函數”(window functions)。

窗口函數定義了要對窗口中收集的數據做的計算操作,根據處理的方式可以分爲兩類:增量聚合函數和全窗口函數。

1.增量聚合函數(ReduceFunction / AggregateFunction)

窗口將數據收集起來,最基本的處理操作當然就是進行聚合。我們可以每來一個數據就在之前結果上聚合一次,這就是“增量聚合”。
典型的增量聚合函數有兩個:ReduceFunction 和 AggregateFunction。

1.歸約函數(ReduceFunction)
        KeyedStream<String, String> keyedStream = socketDS.keyBy(item -> item.split(",")[0]);
        // 1.窗口分配器
        WindowedStream<String, String, TimeWindow> window = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
        // 2.指定窗口函數,窗口內數據的計算邏輯
        // 增量聚合 - 來一條數據 計算一條數據 窗口觸發的時候輸出計算結果
        // 增量聚合 reduce
        //  1.相同key的第一條數據來的時候,不會調用reduce方法
        //  2.增量聚合,來一條數據,就會計算一次,但是不會輸出
        //  3.在窗口觸發的時候,纔會輸出窗口的最終計算結果
        SingleOutputStreamOperator<String> reduce = window.reduce(new ReduceFunction<String>() {
            @Override
            public String reduce(String value1, String value2) throws Exception {
                Integer temp = Integer.valueOf(value1) + Integer.valueOf(value2);
                return temp.toString();
            }
        });

        reduce.print();
2.聚合函數(AggregateFunction)

上面的規約函數 ReduceFunction 可以解決大多數歸約聚合的問題,但是這個接口有一個限制,就是聚合狀態的類型、輸出結果的類型都必須和輸入數據類型一樣

Flink Window API 中的 aggregate 就突破了這個限制,可以定義更加靈活的窗口聚合操作。這個方法需要傳入一個 AggregateFunction 的實現類作爲參數。

AggregateFunction 可以看作是 ReduceFunction 的通用版本,這裏有三種類型:輸入類型(IN)、累加器類型(ACC)和輸出類型(OUT)。輸入類型 IN 就是輸入流中元素的數據類型;累加器類型 ACC 則是我們進行聚合的中間狀態類型;而輸出類型當然就是最終計算結果的類型了。

6.其他 API

2.時間語義

3.水位線(Watermark)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章