Flink学习--EventTime和Watermarks案例实践

文章转载自:https://blog.csdn.net/xu47043...
作者:bigdata1024
侵删

watermarks的生成方式有两种:
1:With Periodic Watermarks:周期性的触发watermark的生成和发送。
2:With Punctuated Watermarks:基于某些事件触发watermark的生成和发送。

第一种方式比较常用,所以在这里我们使用第一种方式进行分析。
参考官网文档中With Periodic Watermarks的使用方法

代码中的extractTimestamp方法是从数据本身中提取EventTime。

getCurrentWatermar方法是获取当前水位线,利用currentMaxTimestamp - maxOutOfOrderness,这里的maxOutOfOrderness表示是允许数据的最大乱序时间。

所以在这里我们使用的话也实现接口AssignerWithPeriodicWatermarks。

1、实现watermark相关代码

1.1、程序说明
从socket模拟接收数据,然后使用map进行处理,后面再调用assignTimestampsAndWatermarks方法抽取timestamp并生成watermark。最后再调用window打印信息来验证window被触发的时机。

完整代码如下:


import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

import javax.annotation.Nullable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;


/**
 *
 * Watermark 案例
 *
 * Created by xuwei.tech.
 */
public class StreamingWindowWatermark1 {

    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 text = env.socketTextStream("192.168.59.133", port, "\n");

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

        //抽取timestamp和生成watermark
        DataStream> waterMarkStream = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks>() {

            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 element, long previousElementTimestamp) {
                long timestamp = element.f1;
                currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
                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;
            }
        });

        //分组,聚合
        DataStream window = waterMarkStream.keyBy(0)
                .window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照消息的EventTime分配窗口,和调用TimeWindow效果一样
                .apply(new WindowFunction, String, Tuple, TimeWindow>() {
                    /**
                     * 对window内的数据进行排序,保证数据的顺序
                     * @param tuple
                     * @param window
                     * @param input
                     * @param out
                     * @throws Exception
                     */
                    @Override
                    public void apply(Tuple tuple, TimeWindow window, Iterable> input, Collector out) throws Exception {
                        String key = tuple.toString();
                        List arrarList = new ArrayList();
                        Iterator> it = input.iterator();
                        while (it.hasNext()) {
                            Tuple2 next = it.next();
                            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");

    }



}

1.2、程序详解

  1. 接收socket数据。
  2. 将每行数据按照逗号分隔,每行数据调用map转换成tuple类型。其中tuple中的第一个元素代表具体的数据,第二行代表数据的eventtime。
  3. 抽取timestamp,生成watermar,允许的最大乱序时间是10s,并打印(key,eventtime,currentMaxTimestamp,watermark)等信息
  4. 分组聚合,window窗口大小为3秒,输出(key,窗口内元素个数,窗口内最早元素的时间,窗口内最晚元素的时间,窗口自身开始时间,窗口自身结束时间)。
2、通过数据跟踪watermark的时间

在这里重点查看watermark和timestamp的时间,通过数据的输出来确定window的触发时机。
首先我们开启socker,输入第一条数据

0001,1538359882000

输出的内容如下:
image.png
此时,wartermark的时间,已经落后于currentMaxTimestamp10秒了。我们继续输入

0001,1538359886000

此时,输入内容如下:
image.png
继续输入:

0001,1538359892000

image.png
到这里,window仍然没有被触发,此时watermark的时间已经等于了第一条数据的Event Time了。那么window到底什么时候被触发呢?
我们再次输入:

0001,1538359893000

输出内容如下:

汇总如下:
image.png

window仍然没有触发,此时,我们的数据已经发到2018-10-01 10:11:33.000了,根据eventtime来算,最早的数据已经过去了11秒了,window还没有开始计算,那到底什么时候会触发window呢?

我们再次增加1秒,输入:

0001,1538359894000

image.png
到这里,我们做一个说明:
window的触发机制,是先按照自然时间将window划分,如果window大小是3秒,那么1分钟内会把window划分为如下的形式【左闭右开】:

[00:00:00,00:00:03)
[00:00:03,00:00:06)
[00:00:06,00:00:09)
[00:00:09,00:00:12)
[00:00:12,00:00:15)
[00:00:15,00:00:18)
[00:00:18,00:00:21)
[00:00:21,00:00:24)
[00:00:24,00:00:27)
[00:00:27,00:00:30)
[00:00:30,00:00:33)
[00:00:33,00:00:36)
[00:00:36,00:00:39)
[00:00:39,00:00:42)
[00:00:42,00:00:45)
[00:00:45,00:00:48)
[00:00:48,00:00:51)
[00:00:51,00:00:54)
[00:00:54,00:00:57)
[00:00:57,00:01:00)

window的设定无关数据本身,而是系统定义好了的。
输入的数据中,根据自身的Event Time,将数据划分到不同的window中,如果window中有数据,则当watermark时间>=Event Time时,就符合了window触发的条件了,最终决定window触发,还是由数据本身的Event Time所属的window中的window_end_time决定。
上面的测试中,最后一条数据到达后,其水位线已经升至10:11:24秒,正好是最早的一条记录所在window的window_end_time,所以window就被触发了。

为了验证window的触发机制,我们继续输入数据:

0001,1538359896000

输出:

汇总如下:
image.png
此时,watermark时间虽然已经达到了第二条数据的时间,但是由于其没有达到第二条数据所在window的结束时间,所以window并没有被触发。那么,第二条数据所在的window时间是:

[00:00:24,00:00:27)

也就是说,我们必须输入一个10:11:27秒的数据,第二条数据所在的window才会被触发。我们继续输入:

输出:

image.png

此时,我们已经看到,window的触发要符合以下几个条件:

**1、watermark时间 >= window_end_time
2、在[window_start_time,window_end_time)区间中有数据存在,注意是左闭右开的区间**

同时满足了以上2个条件,window才会触发。

3、watermark+window处理乱序数据
我们上面的测试,数据都是按照时间顺序递增的,现在,我们输入一些乱序的(late)数据,看看watermark结合window机制,是如何处理乱序的。
输入两行数据

0001,1538359899000
0001,1538359891000

输出:

汇总如下:
image.png

可以看到,虽然我们输入了一个10:11:31的数据,但是currentMaxTimestamp和watermark都没变。此时,按照我们上面提到的公式:
1、watermark时间 >= window_end_time
2、在[window_start_time,window_end_time)中有数据存在

watermark时间(10:11:29) < window_end_time(10:11:33),因此不能触发window。

那如果我们再次输入一条10:11:43的数据,此时watermark时间会升高到10:11:33,这时的window一定就会触发了,我们试一试:
输入:

0001,1538359903000


输出:


汇总如下:

image.png

这里,我么看到,窗口中有2个数据,10:11:31和10:11:32,但是没有10:11:33的数据,原因是窗口是一个前闭后开的区间,10:11:33的数据是属于[10:11:33,10:11:36)的窗口的。

Flink应该如何设置最大乱序时间 ?
这个要结合自己的业务以及数据情况去设置。如果maxOutOfOrderness设置的太小,而自身数据发送时由于网络等原因导致乱序或者late太多,那么最终的结果就是会有很多单条的数据在window中被触发,数据的正确性影响太大。
对于严重乱序的数据,需要严格统计数据最大延迟时间,才能保证计算的数据准确,延时设置太小会影响数据准确性,延时设置太大不仅影响数据的实时性,更加会加重Flink作业的负担,不是对eventTime要求特别严格的数据,尽量不要采用eventTime方式来处理,会有丢数据的风险。

上边的结果,已经表明,对于out-of-order的数据,Flink可以通过watermark机制结合window的操作,来处理一定范围内的乱序数据。那么对于“迟到(late element)”太多的数据,Flink是怎么处理的呢?

4:late element(延迟数据)的处理
延迟数据的三种处理方案

4.1:丢弃(默认)
输入:

0001,1538359890000
0001,1538359903000

输出:
image.png
注意:此时watermark是2018-10-01 10:11:33.000
下面我们再输入几个eventtime小于watermark的时间
输入:【输入了三行内容】

0001,1538359890000
0001,1538359891000
0001,1538359892000

输出:

注意:此时并没有触发window。因为输入的数据所在的窗口已经执行过了,flink默认对这些迟到的数据的处理方案就是丢弃。

4.2:allowedLateness 指定允许数据延迟的时间

在某些情况下,我们希望对迟到的数据再提供一个宽容的时间。
Flink提供了allowedLateness方法可以实现对迟到的数据设置一个延迟时间,在指定延迟时间内到达的数据还是可以触发window执行的。
修改代码:


下面我们来验证一下:

输入:【输入两行内容】

0001,1538359890000
0001,1538359903000

输出:
image.png
此时watermark是2018-10-01 10:11:33.000
那么现在我们输入几条eventtime

输入:【输入三行内容】

0001,1538359890000
0001,1538359891000
0001,1538359892000

输出:

在这里看到每条数据都触发了window执行。

汇总:
image.png
我们再输入一条数据,把water调整到10:11:34

输入:

汇总如下:
image.png
此时,把water上升到了10:11:34,我们再输入几条eventtime输入:

0001,1538359890000
0001,1538359891000
0001,1538359892000

输出:

发现输入的三行数据都触发了window的执行。

我们再输入一条数据,把water调整到10:11:35

输入:

0001,1538359905000

输出:

此时,watermark上升到了10:11:35
我们再输入几条eventtime

输入:

0001,1538359890000
0001,1538359891000
0001,1538359892000

输出:

发现这几条数据都没有触发window。

分析:
当watemark等于10:11:33的时候,正好是window_end_time,所以会触发[10:11:30~10:11:33) 的window执行。
当窗口执行过后,我们输入[10:11:30~10:11:33) window内的数据会发现window是可以被触发的。
当watemark提升到10:11:34的时候,我们输入[10:11:30~10:11:33)window内的数据会发现window也是可以被触发的。
当watemark提升到10:11:35的时候,我们输入[10:11:30~10:11:33)window内的数据会发现window不会被触发了。
由于我们在前面设置了allowedLateness(Time.seconds(2)),可以允许延迟在2s内的数据继续触发window执行。
所以当watermark是10:11:34的时候可以触发window,但是10:11:35的时候就不行了。

总结:
对于此窗口而言,允许2秒的迟到数据,即第一次触发是在watermark >=window_end_time时
第二次(或多次)触发的条件是watermark < window_end_time + allowedLateness时间内,这个窗口有late数据到达时。

解释:
当watermark等于10:11:34的时候,我们输入eventtime为10:11:30、10:11:31、10:11:32的数据的时候,是可以触发的,因为这些数据的window_end_time都是10:11:33,也就是10:11:34<10:11:33+2 为true。
但是当watermark等于10:11:35的时候,我们再输入eventtime为10:11:30、10:11:31、10:11:32的数据的时候,这些数据的window_end_time都是10:11:33,此时,10:11:35<10:11:33+2 为false了。所以最终这些数据迟到的时间太久了,就不会再触发window执行了。

4.3:sideOutputLateData 收集迟到的数据

通过sideOutputLateData 可以把迟到的数据统一收集,统计存储,方便后期排查问题。
需要先调整代码:

我们来输入一些数据验证一下
输入:

0001,1538359890000
0001,1538359903000

此时,window被触发执行了,此时watermark是10:11:33
下面我们再输入几个eventtime小于watermark的数据测试一下
输入:

0001,1538359890000
0001,1538359891000
0001,1538359892000

输出:

此时,针对这几条迟到的数据,都通过sideOutputLateData保存到了outputTag中。

你可能感兴趣的:(flink,大数据,实时计算)