Flink之Event Time、Watermark的使用

728 阅读19分钟

前言

继上一篇Flink的state、checkpoint、savepoint之后,这一篇主要是演示Flink的Event Time、Watermark的使用。

到现在也写了好几篇了,感觉写的不太好,好像就平铺直述的感觉,可能还是没什么写这东西的天分,就当做是记录吧。如果有什么不足与问题,欢迎评论交流。

1.1 TimeWindow

像这种代码,大家应该都很熟悉了,但是主要是为了下面介绍process,做一个铺垫。这个其实是一种窗口,后面会详细介绍。

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

//每隔5秒统计最近10秒的数据
public class TimeWindowDemo {

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

        DataStreamSource<String> lc = env.socketTextStream("localhost", 8888);
        lc.flatMap(new FlatMapFunction<String, Tuple2<String,Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> collector) throws Exception {
                String[] split = value.split(",");
                for (String s : split) {
                    collector.collect(Tuple2.of(s,1));
                }
            }
        }).keyBy(0).timeWindow(timeWindow(Time.seconds(10),Time.seconds(5))).sum(1).print().setParallelism(1);

        env.execute("TimeWindowDemo");
    }

}

1.2 process

process 要求继承一个ProcessWindowFunction抽象类,并且要求实现process方法。

import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;

public class TimeWindowDemo {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        //在控制台输入hive hbase
        DataStreamSource<String> lc = env.socketTextStream("localhost", 8888);
        lc.flatMap(new FlatMapFunction<String, Tuple2<String,Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> collector) throws Exception {
                String[] split = value.split(",");
                for (String s : split) {
                    collector.collect(Tuple2.of(s,1));
                }
            }
        }).keyBy(0).timeWindow(Time.seconds(10),Time.seconds(5))
                .process(new ProcessWindownImpl())
                .print().setParallelism(1);

        env.execute("TimeWindowDemo");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String,Integer>,Tuple2<String,Integer>, Tuple, TimeWindow> {
        FastDateFormat format = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
        /**
         * 当一个window触发计算的时候会调用这个方法
         * @param tuple key
         * @param context operator的上下文
         * @param elements 指定window的所有元素
         * @param out 用户输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Integer>> elements,
                            Collector<Tuple2<String, Integer>> out) {

            System.out.println("当前时间:"+ format.format(System.currentTimeMillis()));
            System.out.println("Window的处理时间:"+ format.format(context.currentProcessingTime()));
            System.out.println("Window的开始时间:"+ format.format(context.window().getStart()));
            System.out.println("Window的结束时间:"+ format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Integer> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }

}

结果,可以看到这里的窗口打印的时间,是5秒产生一个窗口,。每一个窗口都有当时的数据,并且进行了打印。

当前时间: 18:22:40
Window的处理时间: 18:22:40
Window的开始时间: 18:22:30
Window的结束时间: 18:22:40
(hive,1)
当前时间: 18:22:45
当前时间: 18:22:45
Window的处理时间: 18:22:45
Window的处理时间: 18:22:45
Window的开始时间: 18:22:35
Window的开始时间: 18:22:35
Window的结束时间: 18:22:45
Window的结束时间: 18:22:45
(hive,1)
(hbase,1)

根据5秒一个窗口,那么每隔5秒生成一个窗口,统计10秒,其实就是窗口的范围大小,开始时间和接受时间相差10秒,处理时间等于结束时间,

1.3 Time的种类

看图可知,一共有3种时间,数据从产生-> 进入队列 -> 进入Flink -> 进入窗口 -> ...

  • Event Time: 就是事件产生的时间,比如订单的生成时间,运单的创建时间。
  • Ingestion Time: 就是事件进入到Flink的时间。
  • processing Time: 事件被处理的时的当前系统的时间

既然如此,那我们就需要考虑一个问题,当我们针对业务进行统计的时候,例如统计1个小时内,一共产生了多少条订单,那我们应该使用什么样的时间呢?

如果消息都是有序的,那么我们可能什么都不需要做,但是真实情况并不是这样的,考虑到消息在很可能因为网络或者其他原因,导致无序、延迟、甚至丢失,那么我们就需要根据不同情况,做不同的调整,丢失的话,我们可能在程序里面不太好处理,但是无序、以及一定程度的延时,我们是可以通过Flink提供的一些方法,来进行挽救的。

1.3.1 当窗口数据有序

我们将之前的代码改造一下,模拟发送数据,先看一下图,感受一下。

import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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 java.util.concurrent.TimeUnit;

public class WindowDemo {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //使用自定义source 来模拟产生数据
        DataStreamSource<String> lc = env.addSource(new SourceFunction<String>() {

            FastDateFormat dateFormat = FastDateFormat.getInstance("HH:mm:ss");

            @Override
            public void run(SourceContext<String> out) throws Exception {
                // 我们控制一下 秒数是10的倍数的时候 进行发送
                String currTime = String.valueOf(System.currentTimeMillis());
                while (Integer.valueOf(currTime.substring(currTime.length() - 4)) > 100) {
                    currTime = String.valueOf(System.currentTimeMillis());
                    continue;
                }
                System.out.println("开始发送事件的时间:" + dateFormat.format(System.currentTimeMillis()));

                //第一个窗口 生成2条数据
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));

                // 睡眠 5秒发送一个   第二个窗口
                TimeUnit.SECONDS.sleep(5);
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));

                // 睡眠7秒 在发送一个  第三个窗口
                TimeUnit.SECONDS.sleep(7);
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));

                TimeUnit.SECONDS.sleep(300);

            }

            @Override
            public void cancel() {

            }
        });
        lc.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> collector) throws Exception {
                String[] split = value.split(",");
                for (String s : split) {
                    collector.collect(Tuple2.of(s, 1));
                }
            }
        }).keyBy(0).timeWindow(Time.seconds(10), Time.seconds(5))
                .process(new ProcessWindownImpl())
                .print().setParallelism(1);

        env.execute("WindowDemo");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String, Integer>, Tuple2<String, Integer>, Tuple, TimeWindow> {
        //这是一个线程安全的时间格式化器
        FastDateFormat format = FastDateFormat.getInstance("HH:mm:ss");

        /**
         * 当一个window触发计算的时候会调用这个方法
         *
         * @param tuple    key
         * @param context  operator的上下文
         * @param elements 指定window的所有元素
         * @param out      输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Integer>> elements,
                            Collector<Tuple2<String, Integer>> out) {

//            System.out.println("当前时间:" + format.format(System.currentTimeMillis()));
//            System.out.println("Window的处理时间:" + format.format(context.currentProcessingTime()));
//            System.out.println("Window的开始时间:" + format.format(context.window().getStart()));
//            System.out.println("Window的结束时间:" + format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Integer> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }

}

运行结果 可以看到 结果和我们的图,是是一致的

开始发送事件的时间:10:57:50
//第一个窗口
(hadoop,2)
(10:57:50,2)
(10:57:55,1)
//第二个窗口
(hadoop,3)
(10:57:50,2)
//第三个窗口
(hadoop,2)
(10:58:02,1)
(10:57:55,1)

像这种有序的情况,我们什么都不需要做,然后总会有不正常的时候,

1.3.2 当窗口数据无序

看图

package juejinDemo.window;

import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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 java.util.concurrent.TimeUnit;

/**
 * 无序
 */
public class WindowDemo2 {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //使用自定义source 来模拟产生数据
        DataStreamSource<String> lc = env.addSource(new SourceFunction<String>() {

            FastDateFormat dateFormat = FastDateFormat.getInstance("HH:mm:ss");

            @Override
            public void run(SourceContext<String> out) throws Exception {
                // 我们控制一下 秒数是10的倍数的时候 进行发送
                String currTime = String.valueOf(System.currentTimeMillis());
                while (Integer.valueOf(currTime.substring(currTime.length() - 4)) > 100) {
                    currTime = String.valueOf(System.currentTimeMillis());
                    continue;
                }
                System.out.println("开始发送事件的时间:" + dateFormat.format(System.currentTimeMillis()));

                //第一个窗口 生成2条数据
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));

                //这个事件产生了,但是由于网络原因 没有发送出去,这里需要一点想象空间。
                String event = "hadoop," + dateFormat.format(System.currentTimeMillis());

                // 睡眠 5秒发送一个   第二个窗口
                TimeUnit.SECONDS.sleep(5);
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));
                //在第二个窗口的这个时候  之前的数据终于发送出去了
                out.collect(event);

                // 睡眠7秒 在发送一个  第三个窗口
                TimeUnit.SECONDS.sleep(7);
                out.collect("hadoop," + dateFormat.format(System.currentTimeMillis()));

                TimeUnit.SECONDS.sleep(300);

            }

            @Override
            public void cancel() {

            }
        });
        lc.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> collector) throws Exception {
                String[] split = value.split(",");
                for (String s : split) {
                    collector.collect(Tuple2.of(s, 1));
                }
            }
        }).keyBy(0).timeWindow(Time.seconds(10), Time.seconds(5))
                .process(new ProcessWindownImpl())
                .print().setParallelism(1);

        env.execute("WindowDemo2");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String, Integer>, Tuple2<String, Integer>, Tuple, TimeWindow> {
        //这是一个线程安全的时间格式化器
        FastDateFormat format = FastDateFormat.getInstance("HH:mm:ss");

        /**
         * 当一个window触发计算的时候会调用这个方法
         *
         * @param tuple    key
         * @param context  operator的上下文
         * @param elements 指定window的所有元素
         * @param out      输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Integer>> elements,
                            Collector<Tuple2<String, Integer>> out) {

//            System.out.println("当前时间:" + format.format(System.currentTimeMillis()));
//            System.out.println("Window的处理时间:" + format.format(context.currentProcessingTime()));
//            System.out.println("Window的开始时间:" + format.format(context.window().getStart()));
//            System.out.println("Window的结束时间:" + format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Integer> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }

}

结果(由于输出的问题,有时候会没有我下面这样子,这么整齐,其实我的也没有这么一眼就能看出效果,我排列了一下),可以看到 数据的结果和图片显示一致。

开始发送事件的时间:11:17:30
//窗口1
(hadoop,1)
(11:17:30,1)
//窗口2
(hadoop,3)
(11:17:35,1)
(11:17:30,2)
//窗口3
(hadoop,3)
(11:17:42,1)
(11:17:30,1)
(11:17:35,1)

可以看到,这个时候数据就已经乱了,第一个窗口的数据由于延迟到达,导致第3个窗口的数据多了一条,这个时候做的统计肯定是不准确的。

1.3.3 使用Event Time 处理无序

原理就是根据事件的时间,与窗口的时间进行判断,判断是否属于这个窗口。看图

import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.functions.FlatMapFunction;
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.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
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.util.concurrent.TimeUnit;

/**
 * 处理无序
 */
public class WindowDemo3 {

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

        //1.设置时间类型
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        //使用自定义source 来模拟产生数据
        DataStreamSource<String> lc = env.addSource(new SourceFunctionImpl());
        lc.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Long>> collector) throws Exception {
                String[] split = value.split(",");
                //这里我们将时间放进去
                if (split != null) {
                    collector.collect(Tuple2.of(split[0], Long.valueOf(split[1])));
                }
            }
        }).assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarksImpl()).keyBy(0)
                .timeWindow(Time.seconds(10), Time.seconds(5))
                .process(new ProcessWindownImpl())
                .print().setParallelism(1);

        env.execute("WindowDemo3");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String, Long>, Tuple2<String, Integer>, Tuple, TimeWindow> {
        //这是一个线程安全的时间格式化器
        FastDateFormat format = FastDateFormat.getInstance("HH:mm:ss");

        /**
         * 当一个window触发计算的时候会调用这个方法
         *
         * @param tuple    key
         * @param context  operator的上下文
         * @param elements 指定window的所有元素
         * @param out      输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Long>> elements,
                            Collector<Tuple2<String, Integer>> out) {

//            System.out.println("当前时间:" + format.format(System.currentTimeMillis()));
//            System.out.println("Window的处理时间:" + format.format(context.currentProcessingTime()));
//            System.out.println("Window的开始时间:" + format.format(context.window().getStart()));
//            System.out.println("Window的结束时间:" + format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Long> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }
    //自定义的source
    public static class SourceFunctionImpl implements SourceFunction<String> {
        FastDateFormat dateFormat = FastDateFormat.getInstance("HH:mm:ss");

        @Override
        public void run(SourceContext<String> out) throws Exception {
            // 我们控制一下 秒数是10的倍数的时候 进行发送
            String currTime = String.valueOf(System.currentTimeMillis());
            while (Integer.valueOf(currTime.substring(currTime.length() - 4)) > 100) {
                currTime = String.valueOf(System.currentTimeMillis());
                continue;
            }
            System.out.println("开始发送事件的时间:" + dateFormat.format(System.currentTimeMillis()));

            //第一个窗口 生成2条数据
            out.collect("hadoop," + System.currentTimeMillis());

            //这个事件产生了,但是由于网络原因 没有发送出去,这里需要一点想象空间。
            String event = "hadoop," + System.currentTimeMillis();


            //为了更好的看到效果 设置长一点
            // 睡眠 7秒发送一个   第二个窗口
            TimeUnit.SECONDS.sleep(7);
            out.collect("hadoop," + System.currentTimeMillis());
            //在第二个窗口的这个时候  之前的数据终于发送出去了
            out.collect(event);

            // 睡眠7秒 在发送一个  第三个窗口
            TimeUnit.SECONDS.sleep(7);
            out.collect("hadoop," + System.currentTimeMillis());

            TimeUnit.SECONDS.sleep(300);

        }

        @Override
        public void cancel() {

        }
    }

    /**
     * 泛形是输入的数据类型
     *
     */
    public static class  AssignerWithPeriodicWatermarksImpl implements AssignerWithPeriodicWatermarks<Tuple2<String,Long>> {

        //水位 后面再说
        @Nullable
        @Override
        public Watermark getCurrentWatermark() {
            return new Watermark(System.currentTimeMillis());
        }
        //从event 里面获取事件事件
        @Override
        public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
            return element.f1;
        }
    }

}

结果,可以看到,第三个窗口的数据,正常了,但是第一个窗口的数据,由于延迟,根本没有进去,导致计算不准确。

(hadoop,1)
(hadoop,3)
(hadoop,2)

2.1 Watermark

2.1.1 使用WaterMark机制解决无序

其实就是让这个窗口再等一等,就好像出去参加旅游团一样,虽然约定了9点出发,但是总有人会迟到,那么就会等一等,等待这些迟到的人到达,所以数据的迟到,我们也可以等一下,在这个迟到的范围内,只要数据到达,那么数据就可以被收集到。

/**
 * watermark 解决无序
 */
public class WindowDemo4 {

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

        //1.设置时间类型
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        //使用自定义source 来模拟产生数据
        DataStreamSource<String> lc = env.addSource(new SourceFunctionImpl());
        lc.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Long>> collector) throws Exception {
                String[] split = value.split(",");
                //这里我们将时间放进去
                if (split != null) {
                    collector.collect(Tuple2.of(split[0], Long.valueOf(split[1])));
                }
            }
        }).assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarksImpl()).keyBy(0)
                .timeWindow(Time.seconds(10), Time.seconds(5))
                .process(new ProcessWindownImpl())
                .print().setParallelism(1);

        env.execute("WindowDemo4");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String, Long>, Tuple2<String, Integer>, Tuple, TimeWindow> {
        //这是一个线程安全的时间格式化器
        FastDateFormat format = FastDateFormat.getInstance("HH:mm:ss");

        /**
         * 当一个window触发计算的时候会调用这个方法
         *
         * @param tuple    key
         * @param context  operator的上下文
         * @param elements 指定window的所有元素
         * @param out      输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Long>> elements,
                            Collector<Tuple2<String, Integer>> out) {

//            System.out.println("当前时间:" + format.format(System.currentTimeMillis()));
//            System.out.println("Window的处理时间:" + format.format(context.currentProcessingTime()));
//            System.out.println("Window的开始时间:" + format.format(context.window().getStart()));
//            System.out.println("Window的结束时间:" + format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Long> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }

    //自定义的source
    public static class SourceFunctionImpl implements SourceFunction<String> {
        FastDateFormat dateFormat = FastDateFormat.getInstance("HH:mm:ss");

        @Override
        public void run(SourceContext<String> out) throws Exception {
            // 我们控制一下 秒数是10的倍数的时候 进行发送
            String currTime = String.valueOf(System.currentTimeMillis());
            while (Integer.valueOf(currTime.substring(currTime.length() - 4)) > 100) {
                currTime = String.valueOf(System.currentTimeMillis());
                continue;
            }
            System.out.println("开始发送事件的时间:" + dateFormat.format(System.currentTimeMillis()));

            //第一个窗口 生成2条数据
            out.collect("hadoop," + System.currentTimeMillis());

            //这个事件产生了,但是由于网络原因 没有发送出去,这里需要一点想象空间。
            String event = "hadoop," + System.currentTimeMillis();


            //为了更好的看到效果 设置长一点
            // 睡眠 7秒发送一个   第二个窗口
            TimeUnit.SECONDS.sleep(7);
            out.collect("hadoop," + System.currentTimeMillis());
            //在第二个窗口的这个时候  之前的数据终于发送出去了
            out.collect(event);

            // 睡眠7秒 在发送一个  第三个窗口
            TimeUnit.SECONDS.sleep(7);
            out.collect("hadoop," + System.currentTimeMillis());

            TimeUnit.SECONDS.sleep(300);

        }

        @Override
        public void cancel() {

        }
    }

    /**
     * 泛形是输入的数据类型
     */
    public static class AssignerWithPeriodicWatermarksImpl implements AssignerWithPeriodicWatermarks<Tuple2<String, Long>> {

        //我们这是水位的时间减少 10 秒
        @Nullable
        @Override
        public Watermark getCurrentWatermark() {
            return new Watermark(System.currentTimeMillis() - 10000);
        }

        //从event 里面获取事件事件
        @Override
        public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
            return element.f1;
        }
    }

}

结果,可以看到,数据终于正确了,这里还有一个问题,就如旅游一样,迟到的人,我们等了1个小时还没有到,那么我们只能出发了,那这些迟到太久的人怎么办?,当然是记录下来,然后退钱呗。具体如何记录迟到太久的数据,下面演示。

开始发送事件的时间:12:24:20
(hadoop,2)
(hadoop,3)
(hadoop,2)

2.1.2 WaterMark的定义

流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因,导致乱序的产生,特别是使用kafka的话,多个分区的数据无法保证有序。所以在进行window计算的时候,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark,watermark是用于处理乱序事件的。watermark可以翻译为水位线

有序的流的watermarks

无序的流的watermarks

多并行度流的watermarks

简单来讲就是,多个并行度中,取最小的watermark,单并行度中去最大的watermark。

2.1.3 配置waterMark产生的周期

默认是每200毫秒产生一次
env.getConfig().setAutoWatermarkInterval(1000);

2.1.4 窗口的计算与WaterMark的关系

import org.apache.commons.lang.time.FastDateFormat;
import org.apache.flink.api.common.functions.FlatMapFunction;
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.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
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;

/**
 * watermark 解决无序
 */
public class WindowDemo5 {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        //1.设置时间类型
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        //设置水位产生的周期
        env.getConfig().setAutoWatermarkInterval(1000);

        //使用自定义source 来模拟产生数据
        DataStreamSource<String> lc = env.socketTextStream("localhost",8888);
        lc.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Long>> collector) throws Exception {
                String[] split = value.split(",");
                //这里我们将时间放进去
                if (split != null) {
                    collector.collect(Tuple2.of(split[0],System.currentTimeMillis()));
                }
            }
        }).assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarksImpl()).keyBy(0)
                .timeWindow(Time.seconds(5), Time.seconds(5))
                .process(new ProcessWindownImpl())
                .print().setParallelism(1);

        env.execute("WindowDemo5");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String, Long>, Tuple2<String, Integer>, Tuple, TimeWindow> {
        //这是一个线程安全的时间格式化器
        FastDateFormat format = FastDateFormat.getInstance("HH:mm:ss");

        /**
         * 当一个window触发计算的时候会调用这个方法
         *
         * @param tuple    key
         * @param context  operator的上下文
         * @param elements 指定window的所有元素
         * @param out      输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Long>> elements,
                            Collector<Tuple2<String, Integer>> out) {

            System.out.println("当前时间:" + format.format(System.currentTimeMillis()));
            System.out.println("Window的处理时间:" + format.format(context.currentProcessingTime()));
            System.out.println("Window的开始时间:" + format.format(context.window().getStart()));
            System.out.println("Window的结束时间:" + format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Long> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }

    /**
     * 泛形是输入的数据类型
     */
    public static class AssignerWithPeriodicWatermarksImpl implements AssignerWithPeriodicWatermarks<Tuple2<String, Long>> {

        FastDateFormat dateFormat = FastDateFormat.getInstance("HH:mm:ss");;

        private long currentMaxEventTime = 0L;
        private long maxOutOfOrderness = 10000;

        public AssignerWithPeriodicWatermarksImpl() {
        }

        public AssignerWithPeriodicWatermarksImpl(long maxOutOfOrderness) {
            this.maxOutOfOrderness = maxOutOfOrderness;
        }

        //我们这是水位的时间减少 10 秒
        @Nullable
        @Override
        public Watermark getCurrentWatermark() {
            long watermark = System.currentTimeMillis();
            System.out.println("水位产生---" + dateFormat.format(watermark));
            return new Watermark(watermark - maxOutOfOrderness);
        }

        //从event 里面获取事件事件  这里需要修改一下,
        @Override
        public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
            long currentElementEventTime = element.f1;

            //这里是为了防止乱序的时候,可能中间的数据的时间,会覆盖最新的数据的事件时间
            
            currentMaxEventTime = Math.max(currentMaxEventTime, currentElementEventTime);
            return currentElementEventTime;
        }
    }

}

结果如下,想完全描述清楚,比较麻烦,我这边就简单演示一下,然后直接解释。

为什么说使用Watermark结合EventTime,可以解决窗口统计时的数据的乱序,因为,窗口触发的时需要满足2个条件,

  1. watermark >= window_end_time(窗口结束时间).
  2. 在窗口的区间中有数据存在,左闭右开的区间。通过获取事件时间,确保数据属于当前窗口
水位产生---16:59:24
水位产生---16:59:25
当前时间:16:59:25
Window的处理时间:16:59:25
Window的开始时间:16:59:20
Window的结束时间:16:59:25
(hadoop,2)
水位产生---16:59:26
水位产生---16:59:27
水位产生---16:59:28
水位产生---16:59:29
水位产生---16:59:30
水位产生---16:59:31
水位产生---16:59:32
水位产生---16:59:33
水位产生---16:59:34
水位产生---16:59:35
当前时间:16:59:35
Window的处理时间:16:59:35
Window的开始时间:16:59:30
Window的结束时间:16:59:35
(hadoop,2)
水位产生---16:59:36
水位产生---16:59:37

2.1.5 处理迟到太多的数据

之前上面就讲过,对于迟到太久的数据,我们要进行另外的处理,因为窗口不能一直等待。

  • 直接丢弃,这个是默认的处理方式,也就是不做任何处理
  • allowedLateness 指定允许数据延迟的时间
//这个其实就是 当正常窗口(包含水位)触发之后,如果在配置的时间内,还有数据过来,那么,就会再一次触发一次计算。
.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarksImpl(10)).keyBy(0)
.timeWindow(Time.seconds(5), Time.seconds(5))
.allowedLateness(Time.seconds(2))
.process(new ProcessWindownImpl())
.print().setParallelism(1);  
  • sideOutputLateData 收集迟到的数据

我这里就不跑了,有兴趣可以自己测试一下,收集迟到的数据,在最后对一些结果数据进行修正。

/**
 * 收集迟到太久的数据
 */
public class SideOutputLateDataDemo {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        //1.设置时间类型
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        //设置水位产生的周期
        env.getConfig().setAutoWatermarkInterval(1000);

        // 保存迟到的,会被丢弃的数据
        OutputTag<Tuple2<String, Long>> sideOutputLateData =
                new OutputTag<Tuple2<String, Long>>("late-date"){};

        //使用自定义source 来模拟产生数据
        DataStreamSource<String> lc = env.socketTextStream("localhost",8888);
        SingleOutputStreamOperator<Tuple2<String, Integer>> result = lc.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Long>> collector) throws Exception {
                String[] split = value.split(",");
                //这里我们将时间放进去
                if (split != null) {
                    collector.collect(Tuple2.of(split[0], System.currentTimeMillis()));
                }
            }
        }).assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarksImpl(10)).keyBy(0)
                .timeWindow(Time.seconds(5), Time.seconds(5))
//                .allowedLateness(Time.seconds(2))
                .sideOutputLateData(sideOutputLateData)
                .process(new ProcessWindownImpl());

        result.print().setParallelism(1);
        DataStream<Tuple2<String, Long>> sideOutput = result.getSideOutput(sideOutputLateData);
        SingleOutputStreamOperator<Object> map = sideOutput.map(new MapFunction<Tuple2<String, Long>, Object>() {
            @Override
            public Object map(Tuple2<String, Long> stringLongTuple2) throws Exception {
                //这里对迟到太久的数据作相应的操作
                return stringLongTuple2;
            }
        });
        map.print();

        env.execute("sideOutputLateDataDemo");
    }

    /**
     * IN, OUT, KEY, W
     * IN:输入的数据类型
     * OUT:输出的数据类型
     * Key:key的数据类型(在Flink里面,String用Tuple表示)
     * W:Window的数据类型
     */
    public static class ProcessWindownImpl extends ProcessWindowFunction<Tuple2<String, Long>, Tuple2<String, Integer>, Tuple, TimeWindow> {
        //这是一个线程安全的时间格式化器
        FastDateFormat format = FastDateFormat.getInstance("HH:mm:ss");

        /**
         * 当一个window触发计算的时候会调用这个方法
         *
         * @param tuple    key
         * @param context  operator的上下文
         * @param elements 指定window的所有元素
         * @param out      输出
         */
        @Override
        public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Long>> elements,
                            Collector<Tuple2<String, Integer>> out) {

            System.out.println("当前时间:" + format.format(System.currentTimeMillis()));
            System.out.println("Window的处理时间:" + format.format(context.currentProcessingTime()));
            System.out.println("Window的开始时间:" + format.format(context.window().getStart()));
            System.out.println("Window的结束时间:" + format.format(context.window().getEnd()));

            int sum = 0;
            for (Tuple2<String, Long> ele : elements) {
                sum += 1;
            }
            // 输出单词出现的次数
            out.collect(Tuple2.of(tuple.getField(0), sum));

        }
    }

    /**
     * 泛形是输入的数据类型
     */
    public static class AssignerWithPeriodicWatermarksImpl implements AssignerWithPeriodicWatermarks<Tuple2<String, Long>> {

        FastDateFormat dateFormat = FastDateFormat.getInstance("HH:mm:ss");;

        private long currentMaxEventTime = 0L;
        private long maxOutOfOrderness = 10000;

        public AssignerWithPeriodicWatermarksImpl() {
        }

        public AssignerWithPeriodicWatermarksImpl(long maxOutOfOrderness) {
            this.maxOutOfOrderness = maxOutOfOrderness;
        }

        //我们这是水位的时间减少 10 秒
        @Nullable
        @Override
        public Watermark getCurrentWatermark() {
            long watermark = System.currentTimeMillis() - maxOutOfOrderness;
            System.out.println("水位产生---" + dateFormat.format(watermark));
            return new Watermark(watermark);
        }

        //从event 里面获取事件事件  这里需要修改一下,
        @Override
        public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
            long currentElementEventTime = element.f1;

            //这里是为了防止乱序的时候,可能中间的数据的时间,会覆盖最新的数据的事件时间

            currentMaxEventTime = Math.max(currentMaxEventTime, currentElementEventTime);
            return currentElementEventTime;
        }
    }

}

最后

关于Event Time、Watermark的使用的就介绍玩了,之前还打算把窗口也丢进来,然后偷个懒就放弃了,下一篇Flink之窗口的使用