這一次帶你徹底搞懂 Flink Watermark

點擊上方“zhisheng”,選擇“設爲星標”

後臺回覆"666",獲取最新資源

https://daijiguo.blog.csdn.net/article/details/105706634

背景

我們知道,流處理從事件產生,到流經source,再到operator,中間是有一個過程和時間的。雖然大部分情況下,流到operator的數據都是按照事件產生的時間順序來的,但是也不排除由於網絡延遲等原因,導致亂序的產生,特別是使用kafka的話,多個分區的數據無法保證有序。那麼此時出現一個問題,一旦出現亂序,如果只根據 eventTime 決定 window 的運行,我們不能明確數據是否全部到位,又不能無限期的等下去,必須要有個機制來保證一個特定的時間後,必須觸發window去進行計算了。這個特別的機制,就是watermark

如圖中的 record 3 和 record 5 爲亂序數據,record 4 爲遲到數據,下文會介紹 Flink 是如何處理遲到數據的。

定義

watermark是一種特殊的時間戳,也是一種被插入到數據流的特殊的數據結構,用於表示eventTime小於watermark的事件已經全部落入到相應的窗口中,此時可進行窗口操作。

如圖是一個亂序流,窗口大小爲5。

w(5)表示eventTime < 5的所有數據均已落入相應窗口,window_end_time < =5的所有窗口都將進行計算;

w(10)表示表示eventTime < 10的所有數據均已落入相應窗口,5 < window_end_time < =10的所有窗口都將進行計算。

生成

1.生成時機

通常,在接收到source的數據後,應該立刻生成watermark;但是,也可以在source後應用簡單的map或者filter操作,再生成watermark。

1.生成方式

With Periodic Watermarks(常用)

週期性的生成watermark,週期默認是200ms,可通過env.getConfig().setAutoWatermarkInterval()進行修改。這種watermark生成方式需要實現AssignerWithPeriodicWatermarks接口,代碼如下:

DataStream<Tuple2<String, Long>> waterMarkStream = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {


            Long currentMaxTimestamp = 0L;
            final Long maxOutOfOrderness = 10000L;// 最大允許的延遲時間是10s


            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            /**
             * 定義生成watermark的邏輯
             * 默認200ms被調用一次
             */
            @Nullable
            @Override
            public Watermark getCurrentWatermark() {
                return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
            }


            //定義如何提取timestamp
            @Override
            public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
                long timestamp = element.f1;
                currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
                return timestamp;
            }
        });


2、With Punctuated Watermarks(不常用)

在滿足自定義條件時生成watermark,每一個元素都有機會判斷是否生成一個watermark。如果得到的watermark 不爲null並且比之前的大就注入流中。這種watermark生成方式需要實現AssignerWithPunctuatedWatermarks接口,使用方式如下:

DataStream<Tuple2<String, Long>> waterMarkStream = inputMap.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<Tuple2<String, Long>>(){


            @Override
            public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
                return element.f1;
            }


            @Nullable
            @Override
            public Watermark checkAndGetNextWatermark(Tuple2<String, Long> lastElement, long extractedTimestamp) {
                // 當時間戳爲偶數則生成,爲奇數不不生成
                return lastElement.f1 % 2 == 0 ? new Watermark(extractedTimestamp) : null;
            }
        });


更新規則

1.單並行度

watermark單調遞增,一直覆蓋較小的watermark

1.多並行度

每個分區都會維護和更新自己的watermark。某一時刻的watermark取所有分區中最小的那一個,詳情見watermark的傳播

傳播

Tasks 內部有一個 time services,維護 timers ,當接收到 watermark 時觸發。例如,一個窗口 operator 爲每一個活躍窗口在 time servive 註冊一個 timer,當event time大於窗口結束時間時,清除窗口狀態。

當 task 接收到 watermark 後,會執行以下操作:

task 根據 watermark 的時間戳,更新內部的 event_time clock。time service 區分出所有時間戳小於更新之後的 event_time 的 timers,對超時的 timer,task 執行回調函數觸發計算併發射數據。task 發射 watermark,時間戳爲更新之後的 event_time。

窗口觸發時機分析

下面以一些實驗對窗口的觸發時機進行分析

1.示例一

public class BoundedOutOfOrdernessGenerator implements AssignerWithPeriodicWatermarks<MyEvent> {


    private final long maxOutOfOrderness = 3000; // 3.0 seconds


    private long currentMaxTimestamp;


    @Override
    public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
        long timestamp = element.getCreationTime();
        currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
        return timestamp;
    }


    @Override
    public Watermark getCurrentWatermark() {
        // return the watermark as current highest timestamp minus the out-of-orderness bound
        // 以迄今爲止收到的最大時間戳來生成 watermark
        return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
    }
}


效果解析:

圖中是一個10s大小的窗口,10000~20000爲一個窗口。當 eventTime 爲 23000 的數據到來,生成的 watermark 的時間戳爲20000,>= window_end_time,會觸發窗口計算。

1.示例二

示例二相較於示例一,更換了watermark的計算方式

public class TimeLagWatermarkGenerator implements AssignerWithPeriodicWatermarks<MyEvent> {


    private final long maxTimeLag = 3000; // 3 seconds


    @Override
    public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
        return element.getCreationTime();
    }


    @Override
    public Watermark getCurrentWatermark() {
        // return the watermark as current time minus the maximum time lag
        return new Watermark(System.currentTimeMillis() - maxTimeLag);
    }
}

效果解析:

只是簡單的用當前系統時間減去最大延遲時間生成 Watermark ,當 Watermark 爲 20000時,>= 窗口的結束時間,會觸發10000~20000窗口計算。再當 eventTime 爲 19500 的數據到來,它本應該是屬於窗口 10000~20000窗口的,但這個窗口已經觸發計算了,所以此數據會被丟棄。

1.示例三

public class TumblingEventWindowExample {


    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);


        DataStream<String> socketStream = env.socketTextStream("localhost", 9999);
        DataStream<Tuple2<String, Long>> resultStream = socketStream
            // Time.seconds(3)有序的情況修改爲0
            .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<String>(Time.seconds(3)) {
                @Override
                public long extractTimestamp(String element) {
                    long eventTime = Long.parseLong(element.split(" ")[0]);
                    System.out.println(eventTime);
                    return eventTime;
                }
            })
            .map(new MapFunction<String, Tuple2<String, Long>>() {
                @Override
                public Tuple2<String, Long> map(String value) throws Exception {
                    return Tuple2.of(value.split(" ")[1], 1L);
                }
            })
            .keyBy(0)
            .window(TumblingEventTimeWindows.of(Time.seconds(10)))
            .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                @Override
                public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                    return new Tuple2<>(value1.f0, value1.f1 + value2.f1);
                }
            });
        resultStream.print();
        env.execute();
    }
}

運行程序之前,在本地啓動命令行監聽:

nc -l 9999

有序的情況下,watermark延遲時間爲0

miaowenting@miaowentingdeMacBook-Pro flink$ nc -l 9999
10000 a
11000 a
12000 b
13000 b
14000 a
19888 a
13000 a
20000 a  時間戳20000觸發第一個窗口計算,實際上19999也會觸發,因爲左閉右開的原則,20000這個時間戳並不會在第一個窗口計算,第一個窗口是[10000-20000),第二個窗口是[20000-30000),以此類推
11000 a
12000 b
21000 b
22000 a
29999 a  第一個窗口觸發計算後,後續來的11000,12000這兩條數據被拋棄,29999直接觸發窗口計算,並且本身也屬於第二個窗口,所以也參與計算了。

無序的情況下,watermark延遲時間爲3

miaowenting@miaowentingdeMacBook-Pro flink$ nc -l 9999
10000 a
11000 a
12000 b
20000 a  從數據中可以驗證,第一個窗口在20000的時候沒有觸發計算
21000 a
22000 b
23000 a  在23000的時候觸發計算,計算內容是第一個窗口[10000-20000),所以20000,21000,22000,23000屬於第二個窗口,沒有參與計算。
24000 a
29000 b
30000 a
22000 a
23000 a
33000 a  第二個窗口[20000-30000),它是在33000觸發計算,並且,遲到的數據22000,23000也被計算在內(如果這個數據在水印33000後到達,則會被拋棄),30000和33000是第三個窗口的數據,沒有計算

由數據落位圖可以看出,窗口是前開後閉的,20000和30000這兩個數據分別會落到[20000, 30000)和[30000, 40000)這兩個窗口;已經觸發過的窗口不會被再次觸發,即w(30000)不會再次觸發窗口[20000, 30000)

如何設置最大亂序時間

我們已知的BoundedOutOfOrdernessTimestampExtractor中 watermark的計算公式爲currentMaxTimestamp - maxOutOfOrderness,maxOutOfOrderness通過構造函數傳入。如何設置maxOutOfOrderness纔會比較合理呢?

如果maxOutOfOrderness設置的太小,而自身數據發送時由於網絡等原因導致亂序或者late太多,那麼最終的結果就是會有很多單條的數據在window中被觸發,數據的正確性太差,容錯性太低。對於嚴重亂序的數據,需要嚴格統計數據最大延遲時間,才能保證計算的數據準確。

如果maxOutOfOrderness延時設置太大,則當大部分時間都已落入所屬窗口時,flink遲遲不會進行窗口計算,影響數據的實時性;且由於在最大時間與watermark之間維護了很多未被觸發的窗口,會加重Flink作業的負擔。

總結:這個要結合自己的業務以及數據情況去設置。不是對eventTime要求特別嚴格的數據,儘量不要採用eventTime方式來處理,會有丟數據的風險。

延遲數據處理

1.定義

所謂延遲數據,即窗口已經因爲watermark進行了觸發,則在此之後如果還有數據進入窗口,則默認情況下不會對窗口進行再次觸發和聚合計算。要想在數據進入已經被觸發過的窗口後,還能繼續觸發窗口計算,則可以使用延遲數據處理機制。

1.觸發條件

延遲數據對窗口進行第二次(或多次)觸發的條件是 watermark < window_end_time + allowedLateness,只要滿足該條件,延遲數據已進入窗口就會觸發窗口計算。

1.示例

我們對“窗口觸發時機分析”這一章節中的示例三進行修改

public class TumblingEventWindowExample {


    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);
//        env.getConfig().setAutoWatermarkInterval(100);
        DataStream<String> socketStream = env.socketTextStream("localhost", 9999);
        DataStream<Tuple2<String, Long>> resultStream = socketStream
                .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<String>(Time.seconds(3)) {
                    @Override
                    public long extractTimestamp(String element) {
                        long eventTime = Long.parseLong(element.split(" ")[0]);
                        System.out.println(eventTime);
                        return eventTime;
                    }
                })
                .map(new MapFunction<String, Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> map(String value) throws Exception {
                        return Tuple2.of(value.split(" ")[1], 1L);
                    }
                })
                .keyBy(0)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .allowedLateness(Time.seconds(2)) // 允許延遲處理2秒
                .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                        return new Tuple2<>(value1.f0, value1.f1 + value2.f1);
                    }
                });
        resultStream.print();
        env.execute();
    }
}




djg@djgdeMacBook-Pro bin % nc -l 9999
10000 a
24000 a
11000 a
12000 a
25000 a
11000 a

當watermark爲21000時,觸發了[10000, 20000)窗口計算,由於設置了allowedLateness(Time.seconds(2))即允許兩秒延遲處理,watermark < window_end_time + lateTime公式得到滿足,因此隨後10000和12000進入窗口時,依然能觸發窗口計算;隨後watermark增加到22000,watermark < window_end_time + lateTime不再滿足,因此11000再次進入窗口時,窗口不再進行計算

延遲數據重定向

1.定義

遲到的元素也以使用側輸出(side output)特性被重定向到另外的一條流中去。

1.示例

流的返回值必須是SingleOutputStreamOperator,其是DataStream的子類。通過getSideOutput方法獲取延遲數據。可以將延遲數據重定向到其他流或者進行輸出。

public class TumblingEventWindowExample {


    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);
        DataStream<String> socketStream = env.socketTextStream("localhost", 9999);
        //保存被丟棄的數據
        OutputTag<Tuple2<String, Long>> outputTag = new OutputTag<Tuple2<String, Long>>("late-data"){};
        //注意,由於getSideOutput方法是SingleOutputStreamOperator子類中的特有方法,所以這裏的類型,不能使用它的父類dataStream。
        SingleOutputStreamOperator<Tuple2<String, Long>> resultStream = socketStream
                // Time.seconds(3)有序的情況修改爲0
                .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<String>(Time.seconds(3)) {
                    @Override
                    public long extractTimestamp(String element) {
                        long eventTime = Long.parseLong(element.split(" ")[0]);
                        System.out.println(eventTime);
                        return eventTime;
                    }
                })
                .map(new MapFunction<String, Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> map(String value) throws Exception {
                        return Tuple2.of(value.split(" ")[1], 1L);
                    }
                })
                .keyBy(0)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .sideOutputLateData(outputTag) // 收集延遲大於2s的數據
                .allowedLateness(Time.seconds(2)) //允許2s延遲
                .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                        return new Tuple2<>(value1.f0, value1.f1 + value2.f1);
                    }
                });
        resultStream.print();
        //把遲到的數據暫時打印到控制檯,實際中可以保存到其他存儲介質中
        DataStream<Tuple2<String, Long>> sideOutput = resultStream.getSideOutput(outputTag);
        sideOutput.print();
        env.execute();
    }
}


djg@djgdeMacBook-Pro bin % nc -l 9999
10000 a
25000 a
11000 a

當25000進入window時,watermark被更新到22000,觸發[10000, 20000)窗口進行計算;當延遲數據11000到達窗口時,由於不滿足watermark < window_end_time + lateTime,窗口無法被再次計算。但是11000會被收集,重定向到sideOutput流中,最終可以進行打印或輸出到其他介質

參考

https://blog.csdn.net/sghuu/article/details/103704415https://miaowenting.site/2019/10/19/Apache-Flink/




基於 Apache Flink 的實時監控告警系統
日誌收集Agent,陰暗潮溼的地底世界
2020 繼續踏踏實實的做好自己

END
關注我
公衆號(zhisheng)裏回覆 面經、ES、Flink、 Spring、Java、Kafka、監控 等關鍵字可以查看更多關鍵字對應的文章。你點的每個贊,我都認真當成了喜歡
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章