全網最詳細Flink之Watermark機制

一、Flink之Watermark

在上一篇文章中我們介紹了窗口相關的內容,那麼問題來了,假如我們實時處理蒸漂亮同學的行爲,結果蒸漂亮恰好網絡異常,本來我們窗口設置的5秒一算,而她剛纔的行爲恰巧屬於上一個5秒窗口A計算的,但是網絡異常後使得她的這次行爲數據進入了到了下一個5秒B中計算。那麼我們的計算是不是就存在了問題!!所以這時我們就需要去了解下咱們的Watermark了,當然爲了理解的更清晰會再舉例介紹!

1.1 基本概念之是什麼

推遲窗口觸發的時間,實現方式:通過當前窗口中最大的eventTime-延遲時間所得到的Watermark與窗口原始觸發時間進行對比,當Watermark大於窗口原始觸發時間時則觸發窗口執行!!!我們知道,流處理從事件產生,到流經source,再到operator,中間是有一個過程和時間的,雖然大部分情況下,流到operator的數據都是按照事件產生的時間順序來的,但是也不排除由於網絡、分佈式等原因,導致亂序的產生,所謂亂序,就是指Flink接收到的事件的先後順序不是嚴格按照事件的Event Time順序排列的。

在這裏插入圖片描述
那麼此時出現一個問題,一旦出現亂序,如果只根據eventTime決定window的運行,我們不能明確數據是否全部到位,但又不能無限期的等下去,此時必須要有個機制來保證一個特定的時間後,必須觸發window去進行計算了,這個特別的機制,就是Watermark

  • Watermark是一種衡量Event Time進展的機制
  • Watermark是用於處理亂序事件的,而正確的處理亂序事件,通常用Watermark機制結合window來實現。
  • 數據流中的Watermark用於表示timestamp小於Watermark的數據,都已經到達了,因此,window的執行也是由Watermark觸發的。
  • Watermark可以理解成一個延遲觸發機制,我們可以設置Watermark的延時時長t,每次系統會校驗已經到達的數據中最大的maxEventTime,然後認定eventTime小於maxEventTime - t的所有數據都已經到達,如果有窗口的停止時間等於maxEventTime – t,那麼這個窗口被觸發執行。
    有序流的Watermarker如下圖所示:(Watermark設置爲0)

在這裏插入圖片描述
亂序流的Watermarker如下圖所示:(Watermark設置爲2)
在這裏插入圖片描述
當Flink接收到數據時,會按照一定的規則去生成Watermark,這條Watermark就等於當前所有到達數據中的maxEventTime - 延遲時長,也就是說,Watermark是由數據攜帶的,一旦數據攜帶的Watermark比當前未觸發的窗口的停止時間要晚,那麼就會觸發相應窗口的執行由於Watermark是由數據攜帶的,因此,如果運行過程中無法獲取新的數據,那麼沒有被觸發的窗口將永遠都不被觸發。
上圖中,我們設置的允許最大延遲到達時間爲2s,所以時間戳爲7s的事件對應的Watermark是5s,時間戳爲12s的事件的Watermark是10s,如果我們的窗口1是1s5s,窗口2是6s10s,那麼時間戳爲7s的事件到達時的Watermarker恰好觸發窗口1,時間戳爲12s的事件到達時的Watermark恰好觸發窗口2。

Watermark 就是觸發前一窗口的“關窗時間”,一旦觸發關門那麼以當前時刻爲準在窗口範圍內的所有所有數據都會收入窗中。只要沒有達到水位那麼不管現實中的時間推進了多久都不會觸發關窗。

1.2 Watermark的引入

watermark的引入很簡單,對於亂序數據,最常見的引用方式如下:

dataStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.milliseconds(1000)) {
 override def extractTimestamp(element: SensorReading): Long = {
   element.timestamp * 1000
 }
} )

Event Time的使用一定要指定數據源中的時間戳。否則程序無法知道事件的事件時間是什麼(數據源裏的數據沒有時間戳的話,就只能使用Processing Time了)。
我們看到上面的例子中創建了一個看起來有點複雜的類,這個類實現的其實就是分配時間戳的接口。Flink暴露了TimestampAssigner接口供我們實現,使我們可以自定義如何從事件數據中抽取時間戳。

val env = StreamExecutionEnvironment.getExecutionEnvironment

// 從調用時刻開始給env創建的每一個stream追加時間特性
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

val readings: DataStream[SensorReading] = env
.addSource(new SensorSource)
.assignTimestampsAndWatermarks(new MyAssigner())

MyAssigner有兩種類型

  • AssignerWithPeriodicWatermarks
  • AssignerWithPunctuatedWatermarks

以上兩個接口都繼承自TimestampAssigner。

1.2.1 定期水位線(Assigner with periodic watermarks)

上面講述了根據從事件數據中去獲取時間戳設置水位線,但存在的問題是沒有達到水位線時不管現實中的時間推進了多久都不會觸發關窗,所以接下來我們就來介紹下定期水位線(Periodic Watermark)按照固定時間間隔生成新的水位線,不管是否有新的消息抵達,水位線提升的時間間隔是由用戶設置的,在兩次水位線提升時隔內會有一部分消息流入,用戶可以根據這部分數據來計算出新的水位線。舉個例子,最簡單的水位線算法就是取目前爲止最大的事件時間,然而這種方式比較暴力,對亂序事件的容忍程度比較低,容易出現大量遲到事件。

應用定期水位線需要實現AssignerWithPeriodicWatermarks API,以下是 Flink 官網提供的定期水位線的實現例子。

class BoundedOutOfOrdernessGenerator extends AssignerWithPeriodicWatermarks[MyEvent] {
    val maxOutOfOrderness = 3500L; // 3.5 seconds
    var currentMaxTimestamp: Long;
    override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = {
        val timestamp = element.getCreationTime()
        currentMaxTimestamp = max(timestamp, currentMaxTimestamp)
        timestamp;
    }
    override def getCurrentWatermark(): Watermark = {
        // return the watermark as current highest timestamp minus the out-of-orderness bound
        new Watermark(currentMaxTimestamp - maxOutOfOrderness);
    }
}

其中extractTimestamp用於從消息中提取事件時間,而getCurrentWatermark用於生成新的水位線,新的水位線只有大於當前水位線纔是有效的。每個窗口都會有該類的一個實例,因此可以利用實例的成員變量保存狀態,比如上例中的當前最大時間戳

注:週期性的(一定時間間隔或者達到一定的記錄條數)產生一個Watermark。在實際的生產中Periodic的方式必須結合時間和積累條數兩個維度繼續週期性產生Watermark,否則在極端情況下會有很大的延時。

1.2.2 標點水位線(Assigner with punctuated watermarks)

標點水位線(Punctuated Watermark)通過數據流中某些特殊標記事件來觸發新水位線的生成。這種方式下窗口的觸發與時間無關,而是決定於何時收到標記事件。
應用標點水位線需要實現AssignerWithPunctuatedWatermarks API,以下是 Flink 官網提供的標點水位線的實現例子。

class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[MyEvent] {
    override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = {
        element.getCreationTime
    }
    override def checkAndGetNextWatermark(lastElement: MyEvent, extractedTimestamp: Long): Watermark = {
        if (element.hasWatermarkMarker()) new Watermark(extractedTimestamp) else null
    }
}

其中extractTimestamp用於從消息中提取事件時間,checkAndGetNextWatermark用於檢查事件是否標點事件,若是則生成新的水位線。不同於定期水位線定時調用getCurrentWatermark,標點水位線是每接受一個事件就需要調用checkAndGetNextWatermark,若返回值非 null 且新水位線大於當前水位線,則觸發窗口計算

注:數據流中每一個遞增的EventTime都會產生一個Watermark。在實際的生產中Punctuated方式在TPS很高的場景下會產生大量的Watermark在一定程度上對下游算子造成壓力,所以只有在實時性要求非常高的場景纔會選擇Punctuated的方式進行Watermark的生成

1.3 遲到事件

雖說水位線表明着早於它的事件不應該再出現,但是上如上文所講,接收到水位線以前的的消息是不可避免的,這就是所謂的遲到事件。實際上遲到事件是亂序事件的特例,和一般亂序事件不同的是它們的亂序程度超出了水位線的預計,導致窗口在它們到達之前已經關閉。
遲到事件出現時窗口已經關閉併產出了計算結果,因此處理的方法有3種:

  • 重新激活已經關閉的窗口並重新計算以修正結果。
  • 將遲到事件收集起來另外處理。
  • 將遲到事件視爲錯誤消息並丟棄。

Flink 默認的處理方式是第3種直接丟棄,其他兩種方式分別使用Side Output和Allowed Lateness。

  • Side Output機制可以將遲到事件單獨放入一個數據流分支,這會作爲 window 計算結果的副產品,以便用戶獲取並對其進行特殊處理。
  • Allowed Lateness機制允許用戶設置一個允許的最大遲到時長。Flink 會再窗口關閉後一直保存窗口的狀態直至超過允許遲到時長,這期間的遲到事件不會被丟棄,而是默認會觸發窗口重新計算。因爲保存窗口狀態需要額外內存,並且如果窗口計算使用了 ProcessWindowFunction API 還可能使得每個遲到事件觸發一次窗口的全量計算,代價比較大,所以允許遲到時長不宜設得太長,遲到事件也不宜過多,否則應該考慮降低水位線提高的速度或者調整算法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章