Flink Time和Watermark的理解

weixin_33758863發表於2019-02-14

Flink Time和Watermark的理解


1. Time

背景

在實際開發過程中,我們可能需要接入各種流資料來源,比如線上業務使用者點選流資料、監控系實時收集到的事件流資料、從感測器採集到的實時資料,等等,為了處理方便他們可能會寫入Kafka訊息中介軟體叢集中某個/某些topic中,或者選擇其它的緩衝/儲存系統。這些資料來源中資料元素具有固定的時間屬性,是在流資料處理系統之外的其它系統生成的。比如,上億使用者通過手機終端操作觸發生成的事件資料,都具有對應的事件時間;再特殊一點,可能我們希望回放(Replay)上一年手機終端使用者的歷史行為資料,與當前某個流資料集交叉分析才能夠得到支援某類業務的特定結果,這種情況下,基於資料所具有的事件時間進行處理,就具有很重要的意義了。
下面,我們先從Flink支援的3個與流資料處理相關的時間概念(Time Notion):ProcessTime、EventTime、IngestionTime。有些系統對時間概念的抽象有其它叫法,比如,Google Cloud Dataflow中稱為時間域(Time Domain)。在Flink中,基於不同的Time Notion來處理流資料,具有不同的意義和結果,所以瞭解這3個Time Notion非常關鍵。

Time Notion

我們先看下,Apache Flink官網文件給出的一張概念圖,非常形象地展示了Process Time、Event Time、Ingestion Time這三個時間分別所處的位置,如下圖所示:

6178553-879bcae80f14c1bd.png
Time

下面,分別對這3個Time Notion進行說明如下:

ProcessTime--事件被處理時當前系統的時間

Flink中有對資料處理的操作進行抽象,稱為Transformation Operator,而對於整個Dataflow的開始和結束分別對應著Source Operator和Sink Operator,這些Operator都是在Flink叢集系統所在的主機節點上,所以在基於ProcessTime的Notion進行與時間相關的資料處理時,資料處理依賴於Flink程式執行所在的主機節點系統時鐘(System Clock)。

因為我們關心的是資料處理時間(Process Time),比如進行Time Window操作,對Window的指派就是基於當前Operator所在主機節點的系統時鐘。也就是說,每次建立一個Window,計算Window對應的起始時間和結束時間都使用Process Time,它與外部進入的資料元素的事件時間無關。那麼,後續作用於Window的操作(Function)都是基於具有Process Time特性的Window進行的。

使用ProcessTime的場景,比如,我們需要對某個App應用的使用者行為進行實時統計分析與監控,由於使用者可能使用不同的終端裝置,這樣可能會造成資料並非是實時的(如使用者手機沒電,導致2小時以後才會將操作行為記錄批量上傳上來)。而此時,如果我們按照每分鐘的時間粒度做實時統計監控,那麼這些資料記錄延遲的太嚴重,如果為了等到這些記錄上傳上來(無法預測,具體什麼時間能獲取到這些資料)再做統計分析,對每分鐘之內的資料進行統計分析的結果恐怕要到幾個小時甚至幾天後才能計算並輸出結果,這不是我們所希望的。而且,資料處理系統可能也沒有這麼大的容量來處理海量資料的情況。結合業務需求,其實我們只需要每分鐘時間內進入的資料記錄,依賴當前資料處理系統的處理時間(Process Time)生成每分鐘的Window,指派資料記錄到指定Window並計算結果,這樣就不用考慮資料元素本身自帶的事件時間了。

EventTime--事件產生的時間,它通常由事件中的時間戳描述

流資料中的資料元素可能會具有不變的事件時間(Event Time)屬性,該事件時間是資料元素所代表的行為發生時就不會改變。最簡單的情況下,這也最容易理解:所有進入到Flink處理系統的流資料,都是在外部的其它系統中產生的,它們產生後具有了事件時間,經過傳輸後,進入到Flink處理系統,理論上(如果所有系統都具有相同系統時鐘)該事件時間對應的時間戳要早於進入到Flink處理系統中進行處理的時間戳,但實際應用中會出現資料記錄亂序、延遲到達等問題,這也是非常普遍的。

基於EventTime的Notion,處理資料的進度(Progress)依賴於資料本身,而不是當前Flink處理系統中Operator所在主機節點的系統時鐘。所以,需要有一種機制能夠控制資料處理的進度,比如一個基於事件時間的Time Window建立後,具體怎麼確定屬於該Window的資料元素都已經到達?如果確定都到達了,然後就可以對屬於這個Window的所有資料元素做滿足需要的處理(如彙總、分組等)。這就要用到WaterMark機制,它能夠衡量資料處理進度(表達資料到達的完整性)。

WaterMark帶有一個時間戳,假設為X,進入到資料處理系統中的資料元素具有事件時間,記為Y,如果Y<X,則所有的資料元素均已到達,可以計算並輸出結果。反過來說,可能更容易理解一些:要想觸發對當前Window中的資料元素進行計算,必須保證對所有進入到系統的資料元素,其事件時間Y>=X。如果資料元素的事件時間是有序的,那麼當出現一個資料元素的事件時間Y<X,則觸發對當前Window計算,並建立另一個新的Window來指派事件時間Y<X的資料元素到該新的Window中。

可以看到,有了WaterMark機制,對基於事件時間的流資料處理會變得特別靈活,可以根據實際業務需要選擇各種元件和處理策略。比如,上面我們說到,當Y<X則觸發當前Window計算,記為t1時刻,如果流資料元素是亂序的,經過一段時間,假設t2時刻有一個資料元素的事件時間Y>=X,這時該怎麼辦呢?如果t1時刻的Window已經不存在了,但我們還是希望新出現的亂序資料元素加入到t1時刻Window的計算中,這時可以實現自定義的Trigger來滿足各種業務場景的需要。

IngestionTime--事件進入Flink的時間

IngestionTime是資料進入到Flink流資料處理系統的時間,該時間依賴於Source Operator所在主機節點的系統時鐘,會為到達的資料記錄指派Ingestion Time。基於IngestionTime的Notion,存在多個Source Operator的情況下,每個Source Operator會使用自己本地系統時鐘指派Ingestion Time。後續基於時間相關的各種操作,都會使用資料記錄中的Ingestion Time。

與EventTime相比,IngestionTime不能處理亂序、延遲到達事件的應用場景,它也就不用必須指定如何生成WaterMark。

設定時間特性

Flink DataStream 程式的第一部分通常是設定基本時間特性。 該設定定義了資料流源的行為方式(例如:它們是否將分配時間戳),以及像 **KeyedStream.timeWindow(Time.seconds(30)) ** 這樣的視窗操作應該使用上面哪種時間概念。
以下示例顯示了一個 Flink 程式,該程式在每小時時間視窗中聚合事件。

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);

// 其他
// env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

DataStream<MyEvent> stream = env.addSource(new FlinkKafkaConsumer09<MyEvent>(topic, schema, props));

stream
    .keyBy( (event) -> event.getUser() )
    .timeWindow(Time.hours(1))
    .reduce( (a, b) -> a.add(b) )
    .addSink(...);

2. Watermark

Watermark的型別

EventTime和Watermarks
  • 在使用eventTime的時候如何處理亂序資料?

  • 我們知道,流處理從事件產生,到流經source,再到operator,中間是有一個過程和時間的。雖然大部分情況下,流到operator的資料都是按照事件產生的時間順序來的,但是也不排除由於網路延遲等原因,導致亂序的產生,特別是使用kafka的話,多個分割槽的資料無法保證有序。所以在進行window計算的時候,我們又不能無限期的等下去,必須要有個機制來保證一個特定的時間後,必須觸發window去進行計算了。這個特別的機制,就是watermark,watermark是用於處理亂序事件的。

  • watermark可以翻譯為水位線

有序的流的watermarks
6178553-0ccaac0264dbfdac.png
flink-021.png
無序的流的watermarks
6178553-56e7c7919d8d3846.png
flink-022.png
多並行度流的watermarks

注意:多並行度的情況下,watermark對齊會取所有channel最小的watermark

6178553-67833b6e3aaba076.png
flink-023.png

在Apache Flink中使用watermark的4個理解

當人們第一次使用Flink時,經常會對watermark感到困惑。但其實watermark並不複雜。讓我們通過一個簡單的例子來說明為什麼我們需要watermark,以及它的工作機制是什麼樣的。

在下文中的例子中,我們有一個帶有時間戳的事件流,但是由於某種原因它們並不是按順序到達的。圖中的數字代表事件發生的時間戳。第一個到達的事件發生在時間4,然後它後面跟著的是發生在更早時間(時間2)的事件,以此類推:

6178553-480c0f58ac5519e3.png
flink-020.png

注意這是一個按照事件時間處理的例子,這意味著時間戳反映的是事件發生的時間,而不是處理事件的時間。事件時間(Event-Time)處理的強大之處在於,無論是在處理實時的資料還是重新處理歷史的資料,基於事件時間建立的流計算應用都能保證結果是一樣的。

現在假設我們正在嘗試建立一個流計算排序運算元。也就是處理一個亂序到達的事件流,並按照事件時間的順序輸出事件。

理解1

資料流中的第一個元素的時間是4,但是我們不能直接將它作為排序後資料流的第一個元素並輸出它。因為資料是亂序到達的,也許有一個更早發生的資料還沒有到達。事實上,我們能預見一些這個流的未來,也就是我們的排序運算元至少要等到2這條資料的到達再輸出結果。

有快取,就必然有延遲。

理解2

如果我們做錯了,我們可能會永遠等待下去。首先,我們的應用程式從看到時間4的資料,然後看到時間2的資料。是否會有一個比時間2更早的資料到達呢?也許會,也許不會。我們可以一直等下去,但可能永遠看不到1。

最終,我們必須勇敢地輸出 2 作為排序流的第一個結果

理解3

我們需要的是某種策略,它定義了對於任何帶時間戳的事件流,何時停止等待更早資料的到來。

這正是 watermark 的作用,他們定義了何時不再等待更早的資料。

Flink中的事件時間處理依賴於一種特殊的帶時間戳的元素,成為watermark,它們會由資料來源或是watermark生成器插入資料流中。具有時間戳t的watermark可以被理解為斷言了所有時間戳小於或等於t的事件都(在某種合理的概率上)已經到達了。

注:此處原文是“小於”,譯者認為應該是 “小於或等於”,因為 Flink 原始碼中採用的是 “小於或等於” 的機制。

何時我們的排序運算元應該停止等待,然後將事件2作為首個元素輸出?答案是當收到時間戳為2(或更大)的watermark時。

理解4

我們可以設想不同的策略來生成watermark。

我們知道每個事件都會延遲一段時間才到達,而這些延遲差異會比較大,所以有些事件會比其他事件延遲更多。一種簡單的方法是假設這些延遲不會超過某個最大值。Flink 把這種策略稱作 “有界無序生成策略”(bounded-out-of-orderness)。當然也有很多更復雜的方式去生成watermark,但是對於大多數應用來說,固定延遲的方式已經足夠了。

如果想要構建一個類似排序的流應用,可以使用Flink的ProcessFunction。它提供了對事件時間計時器(基於watermark觸發回撥)的訪問,還提供了可以用來快取資料的託管狀態介面。

Watermark案例

1.watermarks的生成方式
  • 通常,在接收到source的資料後,應該立刻生成watermark;但是,也可以在source後,應用簡單的map或者filter操作後,再生成watermark。

  • 注意:如果指定多次watermark,後面指定的會覆蓋前面的值。

  • 生成方式

  • With Periodic Watermarks

    • 週期性的觸發watermark的生成和傳送,預設是100ms

    • 每隔N秒自動向流裡注入一個WATERMARK

    • 時間間隔由ExecutionConfig.setAutoWatermarkInterval 決定.

    • 每次呼叫getCurrentWatermark 方法, 如果得到的WATERMARK

    • 不為空並且比之前的大就注入流中

    • 可以定義一個最大允許亂序的時間,這種比較常用

    • 實現AssignerWithPeriodicWatermarks介面

  • With Punctuated Watermarks

    • 基於某些事件觸發watermark的生成和傳送

    • 基於事件向流裡注入一個WATERMARK,每一個元素都有機會判斷是否生成一個WATERMARK.

    • 如果得到的WATERMARK 不為空並且比之前的大就注入流中

    • 實現AssignerWithPunctuatedWatermarks介面

2.watermark和window案例

這裡寫了一個watermark&window的flink程式,從socket讀取資料
程式碼:

public class StreamingWindowWatermark {

    private static final Logger log = LoggerFactory.getLogger(StreamingWindowWatermark.class);

    public static void main(String[] args) throws Exception {
        //定義socket的埠號
        int port = 9000;
        //獲取執行環境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //設定使用eventtime,預設是使用processtime
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);


        //設定並行度為1,預設並行度是當前機器的cpu數量
        env.setParallelism(1);

        //連線socket獲取輸入的資料
        DataStream<String> text = env.socketTextStream("zzy", port, "\n");

        //解析輸入的資料,每行資料按逗號分隔
        DataStream<Tuple2<String, Long>> inputMap = text.map(new MapFunction<String, Tuple2<String, Long>>() {
            @Override
            public Tuple2<String, Long> map(String value) throws Exception {
                String[] arr = value.split(",");
                return new Tuple2<>(arr[0], Long.parseLong(arr[1]));
            }
        });

        //抽取timestamp和生成watermark
        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的邏輯,比當前最大時間戳晚10s
             * 預設100ms被呼叫一次
             */
            @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);
                //設定多並行度時獲取執行緒id
                long id = Thread.currentThread().getId();
                log.info("extractTimestamp=======>" + ",currentThreadId:" + id + ",key:" + element.f0 + ",eventtime:[" + element.f1 + "|" + sdf.format(element.f1) + "]," +
                        "currentMaxTimestamp:[" + currentMaxTimestamp + "|" +
                        sdf.format(currentMaxTimestamp) + "],watermark:[" + getCurrentWatermark().getTimestamp() + "|" + sdf.format(getCurrentWatermark().getTimestamp()) + "]");
//                System.out.println("currentThreadId:" + id + ",key:" + element.f0 + ",eventtime:[" + element.f1 + "|" + sdf.format(element.f1) + "],currentMaxTimestamp:[" + currentMaxTimestamp + "|" +
//                        sdf.format(currentMaxTimestamp) + "],watermark:[" + getCurrentWatermark().getTimestamp() + "|" + sdf.format(getCurrentWatermark().getTimestamp()) + "]");
                return timestamp;
            }
        });

        DataStream<String> window = waterMarkStream.keyBy(0)//分組
                .window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照訊息的EventTime分配視窗,和呼叫TimeWindow效果一樣
                .apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
                    /**
                     * 對window內的資料進行排序,保證資料的順序
                     * @param tuple
                     * @param window
                     * @param input
                     * @param out
                     * @throws Exception
                     */
                    @Override
                    public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<String> out) throws Exception {
                        String key = tuple.toString();
                        List<Long> arrarList = new ArrayList<Long>();
                        Iterator<Tuple2<String, Long>> it = input.iterator();
                        while (it.hasNext()) {
                            Tuple2<String, Long> next = it.next();
                            //時間戳放到了arrarList裡
                            arrarList.add(next.f1);
                        }
                        Collections.sort(arrarList);
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                        String result = key + "," + arrarList.size() + "," + sdf.format(arrarList.get(0)) + "," + sdf.format(arrarList.get(arrarList.size() - 1))
                                + "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
                        out.collect(result);
                    }
                });
        //測試-把結果列印到控制檯即可
        window.print();

        //注意:因為flink是懶載入的,所以必須呼叫execute方法,上面的程式碼才會執行
        env.execute("eventtime-watermark");

    }
}

啟動程式StreamingWindowWatermark

列印日誌:

6178553-edfaf8f4c0f067e8.png
flink-024.png
2019-02-14 11:57:36,715 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [org.apache.flink.streaming.api.functions.source.SocketTextStreamFunction] [INFO] - Connecting to server socket zzy:9000
2019-02-14 11:57:36,741 [Window(TumblingEventTimeWindows(3000), EventTimeTrigger, WindowFunction$3) -> Sink: Print to Std. Out (1/1)] [org.apache.flink.runtime.state.heap.HeapKeyedStateBackend] [INFO] - Initializing heap keyed state backend with stream factory.

首先,我們開啟socket,輸入第一條資料,資料格式是(id,時間戳):

➜  /data nc -l 9000
0001,1550116440000

輸出如下:

6178553-75f87ce2df0d8a30.png
flink-025.png
019-02-14 11:58:48,690 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116440000|2019-02-14 11:54:00.000],currentMaxTimestamp:[1550116440000|2019-02-14 11:54:00.000],watermark:[1550116430000|2019-02-14 11:53:50.000]

彙總下表:

6178553-c6ad8186a7532a6c.png
flink-026.png

此時,wartermark的時間按照邏輯,已經落後於currentMaxTimestamp10秒了。
我們繼續輸入:
0001,1550116444000
輸出內容如下:

2019-02-14 12:08:25,474 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116444000|2019-02-14 11:54:04.000],currentMaxTimestamp:[1550116444000|2019-02-14 11:54:04.000],watermark:[1550116434000|2019-02-14 11:53:54.000]

再次彙總表:

6178553-fe8141b5fd0d26cc.png
flink-027.png

繼續輸入:
0001,1550116450000

輸出內容如下:

2019-02-14 14:30:27,480 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116450000|2019-02-14 11:54:10.000],currentMaxTimestamp:[1550116450000|2019-02-14 11:54:10.000],watermark:[1550116440000|2019-02-14 11:54:00.000]

彙總下表:

6178553-a313ba4a83fed067.png
flink-028.png

到這裡,window仍然沒有被觸發,此時watermark的時間已經等於了第一條資料的Event Time了。那麼window到底什麼時候被觸發呢?我們再次輸入:
0001,1550116451000
輸出內容如下:

2019-02-14 14:36:01,479 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116451000|2019-02-14 11:54:11.000],currentMaxTimestamp:[1550116451000|2019-02-14 11:54:11.000],watermark:[1550116441000|2019-02-14 11:54:01.000]

彙總如下:

6178553-97a0af2ad7ef5396.png
flink-029.png

可以看到window仍然沒有觸發,此時,我們的資料已經發到2019-02-14 11:54:11.000了,最早的資料已經過去了11秒了,還沒有開始計算。那是不是要等到13(10+3)秒過去了,才開始觸發window呢?答案是否定的。
我們再次增加1秒,輸入:
0001,1550116452000
輸出內容如下:

2019-02-14 14:40:50,332 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116452000|2019-02-14 11:54:12.000],currentMaxTimestamp:[1550116452000|2019-02-14 11:54:12.000],watermark:[1550116442000|2019-02-14 11:54:02.000]

彙總如下:

6178553-968c663c0cc40e59.png
flink-030.png

Window依舊沒有觸發
我們再次增加1s,輸入:
0001,1550116453000
輸出內容如下:

2019-02-14 14:51:10,020 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116453000|2019-02-14 11:54:13.000],currentMaxTimestamp:[1550116453000|2019-02-14 11:54:13.000],watermark:[1550116443000|2019-02-14 11:54:03.000]
(0001),1,2019-02-14 11:54:00.000,2019-02-14 11:54:00.000,2019-02-14 11:54:00.000,2019-02-14 11:54:03.000

可以看到觸發了window操作,列印資料到控制檯了

6178553-3f61042b9993b150.png
flink-031.png
String result = key + "," + arrarList.size() + "," + sdf.format(arrarList.get(0)) + "," + sdf.format(arrarList.get(arrarList.size() - 1))
        + "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
out.collect(result);

彙總如下:

6178553-da850c6bb27ea3b6.png
flink-032.png

到這裡,我們做一個說明:
window的觸發機制,是先按照自然時間將window劃分,如果window大小是3秒,那麼1分鐘內會把window劃分為如下的形式(注意window是左閉右開的):

[00:00:00,00:00:03)
[00:00:03,00:00:06)
...
[00:00:57,00:01:00)

如果window大小是10秒,則window會被分為如下的形式:

[00:00:00,00:00:10)
[00:00:10,00:00:20)
...
[00:00:50,00:01:00)

window的設定無關資料本身,而是系統定義好了的。

輸入的資料中,根據自身的Event Time,將資料劃分到不同的window中,如果window中有資料,則當watermark時間>=Event Time時,就符合了window觸發的條件了,最終決定window觸發,還是由資料本身的Event Time所屬的window中的window_end_time決定。

上面的測試中,最後一條資料到達後,其水位線已經升至19:34:24秒,正好是最早的一條記錄所在window的window_end_time,所以window就被觸發了。

為了驗證window的觸發機制,我們繼續輸入資料:
0001,1550116455000
輸出內容如下:

2019-02-14 15:00:58,535 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116455000|2019-02-14 11:54:15.000],currentMaxTimestamp:[1550116455000|2019-02-14 11:54:15.000],watermark:[1550116445000|2019-02-14 11:54:05.000]
6178553-d5cb73f5596c9225.png
flink-033.png

彙總表:

6178553-c22ee26a75348665.png
flink-034.png

此時,watermark時間雖然已經達到了第二條資料的時間,但是由於其沒有達到第二條資料所在window的結束時間,所以window並沒有被觸發。那麼,第二條資料所在的window時間是:
[2019/2/14 11:54:03, 2019/2/14 11:54:06)
也就是說,我們必須輸入一個11:54:06秒的資料,第二條資料所在的window才會被觸發。
我們繼續輸入:
0001,1550116456000

輸出如下:

2019-02-14 15:07:48,879 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116456000|2019-02-14 11:54:16.000],currentMaxTimestamp:[1550116456000|2019-02-14 11:54:16.000],watermark:[1550116446000|2019-02-14 11:54:06.000]
(0001),1,2019-02-14 11:54:04.000,2019-02-14 11:54:04.000,2019-02-14 11:54:03.000,2019-02-14 11:54:06.000
6178553-06ce50ef2745247d.png
flink-035.png

可以看到是有觸發windows操作的

彙總:

6178553-ae91e555d400cecf.png
flink-036.png

下面劃重點了
watermark觸發條件
此時,我們已經看到,window的觸發要符合以下幾個條件:

  • 1、watermark時間 >= window_end_time

  • 2、在[window_start_time,window_end_time)中有資料存在

同時滿足了以上2個條件,window才會觸發。

而且,這裡要強調一點,watermark是一個全域性的值,不是某一個key下的值,所以即使不是同一個key的資料,其warmark也會增加,例如:
0002,1550116458000
輸出如下:

2019-02-14 15:22:04,219 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:39,key:0002,eventtime:[1550116458000|2019-02-14 11:54:18.000],currentMaxTimestamp:[1550116458000|2019-02-14 11:54:18.000],watermark:[1550116448000|2019-02-14 11:54:08.000]

我們看到,currentMaxTimestamp也增加到2019-02-14 11:54:08.000了。

watermark+window處理亂序

我們上面的測試,資料都是按照時間順序遞增的,現在,我們輸入一些亂序的(late)資料,看看watermark結合window機制,是如何處理亂序的。

輸入:

0001,1550116440000
0001,1550116441000
0001,1550116442000
0001,1550116443000
0001,1550116444000
0001,1550116445000
0001,1550116446000
0001,1550116450000
0001,1550116451000
0001,1550116452000
0001,1550116453000
0001,1550116456000
0001,1550116460000
0001,1550116461000
0001,1550116462000
0001,1550116464000

輸出如下:

2019-02-14 15:34:49,469 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116452000|2019-02-14 11:54:12.000],currentMaxTimestamp:[1550116452000|2019-02-14 11:54:12.000],watermark:[1550116442000|2019-02-14 11:54:02.000]
2019-02-14 15:34:50,276 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116453000|2019-02-14 11:54:13.000],currentMaxTimestamp:[1550116453000|2019-02-14 11:54:13.000],watermark:[1550116443000|2019-02-14 11:54:03.000]
(0001),3,2019-02-14 11:54:00.000,2019-02-14 11:54:02.000,2019-02-14 11:54:00.000,2019-02-14 11:54:03.000
2019-02-14 15:35:05,916 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116456000|2019-02-14 11:54:16.000],currentMaxTimestamp:[1550116456000|2019-02-14 11:54:16.000],watermark:[1550116446000|2019-02-14 11:54:06.000]
(0001),3,2019-02-14 11:54:03.000,2019-02-14 11:54:05.000,2019-02-14 11:54:03.000,2019-02-14 11:54:06.000
2019-02-14 15:35:17,804 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116460000|2019-02-14 11:54:20.000],currentMaxTimestamp:[1550116460000|2019-02-14 11:54:20.000],watermark:[1550116450000|2019-02-14 11:54:10.000]
2019-02-14 15:35:17,804 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116461000|2019-02-14 11:54:21.000],currentMaxTimestamp:[1550116461000|2019-02-14 11:54:21.000],watermark:[1550116451000|2019-02-14 11:54:11.000]
2019-02-14 15:35:17,804 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116462000|2019-02-14 11:54:22.000],currentMaxTimestamp:[1550116462000|2019-02-14 11:54:22.000],watermark:[1550116452000|2019-02-14 11:54:12.000]
(0001),1,2019-02-14 11:54:06.000,2019-02-14 11:54:06.000,2019-02-14 11:54:06.000,2019-02-14 11:54:09.000
(0001),2,2019-02-14 11:54:10.000,2019-02-14 11:54:11.000,2019-02-14 11:54:09.000,2019-02-14 11:54:12.000
2019-02-14 15:35:48,356 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116464000|2019-02-14 11:54:24.000],currentMaxTimestamp:[1550116464000|2019-02-14 11:54:24.000],watermark:[1550116454000|2019-02-14 11:54:14.000]

再輸入:
0001,1550116454000
輸出如下:

2019-02-14 15:40:41,051 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116454000|2019-02-14 11:54:14.000],currentMaxTimestamp:[1550116464000|2019-02-14 11:54:24.000],watermark:[1550116454000|2019-02-14 11:54:14.000]

彙總:

6178553-7f9c9af2d278bdc5.png
flink-037.png

可以看到,雖然我們輸入了一個2019/2/14 11:54:14的資料,但是currentMaxTimestamp和watermark都沒變。
此時,按照我們上面提到的公式:

  • 1、watermark時間 >= window_end_time

  • 2、在[window_start_time,window_end_time)中有資料存在

那如果我們再次輸入一條2019/2/14 11:54:25的資料,此時watermark時間會升高到19:34:33,這時的window一定就會觸發了,我們試一試:
輸入:
0001,1550116465000
輸出如下:

2019-02-14 15:48:07,322 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark] [INFO] - extractTimestamp=======>,currentThreadId:37,key:0001,eventtime:[1550116465000|2019-02-14 11:54:25.000],currentMaxTimestamp:[1550116465000|2019-02-14 11:54:25.000],watermark:[1550116455000|2019-02-14 11:54:15.000]
(0001),3,2019-02-14 11:54:12.000,2019-02-14 11:54:14.000,2019-02-14 11:54:12.000,2019-02-14 11:54:15.000

可以看到觸發了window操作,列印了2019/2/14 11:54:14這條資料

彙總:

6178553-ed8cf6df8ebb0bdb.png
flink-038.png

上邊的結果,已經表明,對於out-of-order的資料,Flink可以通過watermark機制結合window的操作,來處理一定範圍內的亂序資料。那麼對於“遲到”太多的資料,Flink是怎麼處理的呢?

late element的處理

執行程式碼:StreamingWindowWatermark2

public class StreamingWindowWatermark2 {

    private static final Logger log = LoggerFactory.getLogger(StreamingWindowWatermark2.class);


    public static void main(String[] args) throws Exception {
        //定義socket的埠號
        int port = 9000;
        //獲取執行環境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //設定使用eventtime,預設是使用processtime
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        //設定並行度為1,預設並行度是當前機器的cpu數量
        env.setParallelism(1);

        //連線socket獲取輸入的資料
        DataStream<String> text = env.socketTextStream("zzy", port, "\n");

        //解析輸入的資料
        DataStream<Tuple2<String, Long>> inputMap = text.map(new MapFunction<String, Tuple2<String, Long>>() {
            @Override
            public Tuple2<String, Long> map(String value) throws Exception {
                String[] arr = value.split(",");
                return new Tuple2<>(arr[0], Long.parseLong(arr[1]));
            }
        });

        //抽取timestamp和生成watermark
        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的邏輯
             * 預設100ms被呼叫一次
             */
            @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);
                log.info("key:"+element.f0+",eventtime:["+element.f1+"|"+sdf.format(element.f1)+"],currentMaxTimestamp:["+currentMaxTimestamp+"|"+
                        sdf.format(currentMaxTimestamp)+"],watermark:["+getCurrentWatermark().getTimestamp()+"|"+sdf.format(getCurrentWatermark().getTimestamp())+"]");

//                System.out.println("key:"+element.f0+",eventtime:["+element.f1+"|"+sdf.format(element.f1)+"],currentMaxTimestamp:["+currentMaxTimestamp+"|"+
//                        sdf.format(currentMaxTimestamp)+"],watermark:["+getCurrentWatermark().getTimestamp()+"|"+sdf.format(getCurrentWatermark().getTimestamp())+"]");
                return timestamp;
            }
        });

        //儲存被丟棄的資料
        OutputTag<Tuple2<String, Long>> outputTag = new OutputTag<Tuple2<String, Long>>("late-data"){};
        //注意,由於getSideOutput方法是SingleOutputStreamOperator子類中的特有方法,所以這裡的型別,不能使用它的父類dataStream。
        SingleOutputStreamOperator<String> window = waterMarkStream.keyBy(0)
                .window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照訊息的EventTime分配視窗,和呼叫TimeWindow效果一樣
                //.allowedLateness(Time.seconds(2))//允許資料遲到2秒--延遲時間
                .sideOutputLateData(outputTag)
                .apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
                    /**
                     * 對window內的資料進行排序,保證資料的順序
                     * @param tuple
                     * @param window
                     * @param input
                     * @param out
                     * @throws Exception
                     */
                    @Override
                    public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<String> out) throws Exception {
                        String key = tuple.toString();
                        List<Long> arrarList = new ArrayList<Long>();
                        Iterator<Tuple2<String, Long>> it = input.iterator();
                        while (it.hasNext()) {
                            Tuple2<String, Long> next = it.next();
                            arrarList.add(next.f1);
                        }
                        Collections.sort(arrarList);
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                        String result = "key:" + key + ",size:" + arrarList.size() + "," + sdf.format(arrarList.get(0)) + "," + sdf.format(arrarList.get(arrarList.size() - 1))
                                + "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
                        out.collect(result);
                    }
                });
        //window.getSideOutput獲取遲到的資料,把遲到的資料暫時列印到控制檯,實際中可以儲存到其他儲存介質中
        DataStream<Tuple2<String, Long>> sideOutput = window.getSideOutput(outputTag);
        sideOutput.flatMap(new FlatMapFunction<Tuple2<String,Long>, Tuple2<String,String>>() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            @Override
            public void flatMap(Tuple2<String, Long> stringLongTuple2, Collector<Tuple2<String, String>> collector) throws Exception {
                collector.collect(new Tuple2<>(stringLongTuple2.f0,"eventtime:" + stringLongTuple2.f1 + "|"
                + sdf.format(stringLongTuple2.f1)));
            }
        }).print();
//        sideOutput.print();
        //測試-把結果列印到控制檯即可
        window.print();

        //注意:因為flink是懶載入的,所以必須呼叫execute方法,上面的程式碼才會執行
        env.execute("eventtime-watermark-late-data");

    }
}

我們輸入一個亂序很多的資料來測試下:
輸入:

➜  /data nc -l 9000
0001,1550116440000
0001,1550116443000
0001,1550116444000
0001,1550116445000
0001,1550116446000
0001,1550116450000
0001,1550116451000
0001,1550116452000
0001,1550116453000
0001,1550116441000
0001,1550116454000
0001,1550116455000
0001,1550116455000
0001,1550116457000
0001,1550116458000

輸出如下:

2019-02-14 16:34:27,881 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116455000|2019-02-14 11:54:15.000],currentMaxTimestamp:[1550116455000|2019-02-14 11:54:15.000],watermark:[1550116445000|2019-02-14 11:54:05.000]
2019-02-14 16:34:27,881 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116455000|2019-02-14 11:54:15.000],currentMaxTimestamp:[1550116455000|2019-02-14 11:54:15.000],watermark:[1550116445000|2019-02-14 11:54:05.000]
2019-02-14 16:34:27,882 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116457000|2019-02-14 11:54:17.000],currentMaxTimestamp:[1550116457000|2019-02-14 11:54:17.000],watermark:[1550116447000|2019-02-14 11:54:07.000]
key:(0001),size:3,2019-02-14 11:54:03.000,2019-02-14 11:54:05.000,2019-02-14 11:54:03.000,2019-02-14 11:54:06.000
2019-02-14 16:34:28,420 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116458000|2019-02-14 11:54:18.000],currentMaxTimestamp:[1550116458000|2019-02-14 11:54:18.000],watermark:[1550116448000|2019-02-14 11:54:08.000]

輸入資料:
0001,1550116447000
0001,1550116446000

輸出如下:

2019-02-14 16:35:25,902 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116447000|2019-02-14 11:54:07.000],currentMaxTimestamp:[1550116458000|2019-02-14 11:54:18.000],watermark:[1550116448000|2019-02-14 11:54:08.000]
2019-02-14 16:39:11,450 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116446000|2019-02-14 11:54:06.000],currentMaxTimestamp:[1550116458000|2019-02-14 11:54:18.000],watermark:[1550116448000|2019-02-14 11:54:08.000]
6178553-897907e54aac31b8.png
flink-039.png

沒有觸發window
550116446000|2019-02-14 11:54:06.000 對應的window是
[2019-02-14 11:54:06.000, 2019-02-14 11:54:09.000)

而現在的watermark是2019-02-14 11:54:08.000 比2019-02-14 11:54:09.000小,輸入eventtime是1550116445000|2019-02-14 11:54:05.000的事件
輸入:
0001,1550116445000
輸出:

2019-02-14 16:40:14,721 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116445000|2019-02-14 11:54:05.000],currentMaxTimestamp:[1550116458000|2019-02-14 11:54:18.000],watermark:[1550116448000|2019-02-14 11:54:08.000]
(0001,eventtime:1550116445000|2019-02-14 11:54:05.000)
6178553-543b0ed5b1bc42b0.png
flink-040.png

我們輸入資料:
0001,1550116444000
輸出:

2019-02-14 16:47:38,607 [Source: Socket Stream -> Map -> Timestamps/Watermarks (1/1)] [xuwei.tech.streaming.watermark.StreamingWindowWatermark2] [INFO] - key:0001,eventtime:[1550116444000|2019-02-14 11:54:04.000],currentMaxTimestamp:[1550116458000|2019-02-14 11:54:18.000],watermark:[1550116448000|2019-02-14 11:54:08.000]
(0001,eventtime:1550116444000|2019-02-14 11:54:04.000)

可以看出來是有觸發window的

6178553-01389d4627acad72.png
flink-041.png

總結

  • 1.Flink如何處理亂序?
    watermark+window機制,window中可以對input進行按照Event Time排序,使得完全按照Event Time發生的順序去處理資料,以達到處理亂序資料的目的。

    1. Flink何時觸發window?
    • 1、watermark時間 >= window_end_time(對於out-of-order以及正常的資料而言)

    • 2、在[window_start_time,window_end_time)中有資料存在

  • 3.Flink應該如何設定最大亂序時間?
    這個要結合自己的業務以及資料情況去設定。如果maxOutOfOrderness設定的太小,而自身資料傳送時由於網路等原因導致亂序或者late太多,那麼最終的結果就是會有很多單條的資料在window中被觸發,資料的正確性影響太大。

參考:

http://shiyanjun.cn/archives/1785.html

http://wuchong.me/blog/2018/11/18/flink-tips-watermarks-in-apache-flink-made-easy/

相關文章