时间语义:flink中有三种时间,分别是
(1)EventTime:即事件时间,或者说是数据本身所携带的时间,这个是非常常用的
(2)IngestionTime:数据进入时间,即数据进入flink的时间
(3)ProcessingTime:计算时间,即该数据在被flink计算处理的时间

Flink默认采用ProcessingTime,但是大部分开发中,我们都是以EventTime进行开发,使用EventTime开发时,由于分布式、网络延迟等原因,会出现数据乱序现象。如下图所示

flink for system_time as of 系统时间 flink时间语义解释_ide

而这种乱序数据会导致我们的数据计算错误,假如我们定义窗口大小为5,那么第一个窗口应该是[0,1,2,3,4],当 5 进入后应该立即关闭第一个窗口,开启第二个窗口,这是正常情况下的处理。

但如果是乱序数据,当[1,4]进来后, 5 就进入数据流了,此时如果窗口立即关闭并进行数据计算,那么得到的结果肯定是错误结果。

为了解决这个问题,Flink推出了watermark机制。

1. watermark机制 

1.1 什么是watermark机制

正如上面所述,watermark机制是为了解决时间乱序数据导致的计算结果错误问题,其实简单来说,watermark机制的思想就是:虽然单条数据之间的时间顺序无法保证,但我们可以保证批量数据(或者说窗口)之间的时间顺序,即上一批数据的最大时间戳一定小于下一批数据的最小时间戳。如下图所示

flink for system_time as of 系统时间 flink时间语义解释_ide_02

一个 watermark 本质上就代表了这个窗口内所有数据的最大 timestamp 数值,表示以后到来的数据已经再也没有小于或 等于这个时间的了。当窗口设置watermark后,一旦窗口到达了watermark后,窗口就会关闭,并立即进行一次计算,得到计算结果。

举个例子:

数据的时间戳进入顺序为:1 3 5 7 2 4 6 8 9

假设我们设置窗口宽度为 5 ,watermark的计算规则是延迟 3 ,第一个窗口的左右边界应该是[1-5],也即是说当watermark为大于5的时候,立即关闭第一个窗口进行计算。

(1)1 进入第一个窗口,计算watermark 1-3=-2,那么当前的watermark变为-2,-2小于5,所以第一个窗口不关闭。
(2)2 进入第一个窗口,计算watermark 3-3=0,那么当前的watermark变为0,0小于5,所以第一个窗口不关闭。
(3)5 进入第一个窗口,计算watermark 5-3=2,那么当前的watermark变为2,2小于5,所以第一个窗口不关闭。
(4)7 进入第二个窗口(按照窗口宽度划分),计算watermark 7-3=4,那么当前的watermark变为4,4小于5,所以第一个窗口不关闭。
(5)2 进入第一个窗口,计算watermark 2-3=-1,-1小于当前的watermark=4,,所以watermark不变,第一个窗口不关闭。
(6)6 进入第二个窗口,计算watermark 6-3=3,3小于当前的watermark=4,,所以watermark不变,第一个窗口不关闭。
(7)8 进入第二个窗口,计算watermark 8-3=5,5大于当前的watermark=4,,所以watermark变为5,第一个窗口不关闭。
(8)9 进入第二个窗口,计算waterm

ark 9-3=6,6大于当前的watermark=5,所以watermark变为6,超过了第一个窗口的边界,所以第一个窗立即关闭并处理窗口内数据。

通过上面的例子,你可以简单理解为其实watermark机制就是将窗口关闭和数据处理计算的时间推迟了,好等待那些迟到得到数据进入窗口进行计算,并没有什么特别复杂的东西。就好像电影《摔跤吧,爸爸》,电影里两个女儿为了偷懒调整了爸爸的闹钟时间(闹钟时间就是watermark)的做法,本来8点时间一到(到达窗口关闭时间),就应该起床锻炼(窗口关闭,数据计算),但如果将闹钟时间往后调两个小时(设置watermark,延迟2小时),即使实际上8点到了(原本窗口的最大时间戳已经进入窗口),也还可以多睡两个小时(不触发关闭),等到实际上10点,闹钟时间8点的时候,在起床(watermark到达,窗口关闭)。

但注意,watermark机制可不是和上一篇文章所说的window api中的allowedLateness()方法一样,虽然最终效果一样。

watermark机制是延迟了窗口的关闭和处理计算时间,但是allowedLateness()方法只延迟了窗口关闭时间,并没有延迟数据处理计算时间,到了时间点,窗口仍然会触发一次计算操作,并且将计算结果传递给下游算子任务,而watermark机制则不会,这点是最重要的区别。

当然,如果有一部分数据还是在我们设定的watermark之后到达,仍然无法解决数据计算的准确度问题,在这种情况下,可以通过采用方法allowedLateness()方法来使窗口延迟关闭,先输出一次近似结果,再进行一次等待,如果等待时间结束后还有这个窗口的数据进入,那么就只能通过侧输出流来进行二次处理,这三步保证基本可以解决数据准确度问题。

watermark设置的过小会造成很多迟到数据无法参与运算导致结果错误,但如果watermark设置的过大,又会造成数据处理延迟问题,所以,如何平衡数据处理结果延迟与数据处理准确度的问题,得由自己依据业务情况衡量解决。

1.2 watermark的特点

(1)watermark 本身是一条特殊的数据记录,它是由进入Fink的数据中的时间戳所计算得到的,代表着当前流处理的时间进度。
(2)watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退。
(3)watermark 与数据中所携带的时间戳相关。 

1.3 watermark的上下游传播

watermark会在整条数据流中以广播的形式自上游向下游进行传播,其传播的方式简单概括来讲就是:单输入取其大,多输入取其小。

单输入取其大:对于同一个上游子任务发送的watermark,支取最大的作为watermark。

多输入取其小:由于并行运行的原因,同一个算子任务会有多个线程运行,每个线程内部的watermark自然也不一样,所以,下游算子任务会收到来自上游的多个watermark,此时下游的算子任务就要从这些watermark中取最小的作为当前算子任务的最小watermark。

下图比较形象的描述了这个过程

flink for system_time as of 系统时间 flink时间语义解释_数据_03

1.4 watermark相关代码API

(1)自定义SourceFunction实现数据时间戳提取和watermark生成

我们在SourceFunction接口中定义run方法的逻辑代码时,可以通过调用参数sourceContext对象的 collectWithTimestamp() 方法发送一条数据,其中第一个参数就是我们要发送的数据,第二个参数就是这个数据所对应的时间戳;

而调用sourceContext对象的emitWatermark() 方法可以提交一条 watermark,表示接下来不会再有时间戳小于等于这个数值记录。

public interface SourceFunction<T> extends Function, Serializable {
    void run(SourceFunction.SourceContext<T> sourceContext) throws Exception;

    void cancel();
    
    public interface SourceContext<T> {
        void collect(T var1);
        
        void collectWithTimestamp(T var1, long var2);
        
        void emitWatermark(Watermark var1);

        void markAsTemporarilyIdle();

        Object getCheckpointLock();

        void close();
    }
}
public class MySourceFunction implements SourceFunction<String> {

        @Override
        public void run(SourceContext<String> sourceContext) throws Exception {
            //数据时间戳提取
            sourceContext.collectWithTimestamp("test", 1L);
            //生成Watermark
            sourceContext.emitWatermark(new Watermark(1L));
        }

        @Override
        public void cancel() {

        }
    }

(2)assignTimestampsAndWatermarks()方法指定数据时间戳提取与watermark生成

此方法属于DataStream API, 调用这个方法,能够接收不同的 timestamp 和 watermark 的生成器。watermark 的生成器分为两类

第一类是定时生成器,即每隔一段时间就调用生成器生成watermark,需要自己实现AssignerWithPeriodicWatermarks接口来定义时间戳提取逻辑和watermark生成生成规则。Flink提供了一个该接口的实现类BoundedOutOfOrdernessTimestampExtractor,可以参考该类的具体实现。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //首先,必须要设置为事件时间
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        //如果是周期性的生成Watermark,可以自己设置这个周期时间,单位是ms,默认是200ms
        env.getConfig().setAutoWatermarkInterval(1000);

        //设置为事件时间后,需要指定提取时间字段,并且设置watermark
        DataStreamSource<String> streamSource = env.socketTextStream("192.168.127.128", 9999);

        SingleOutputStreamOperator<TestBean> map = streamSource.map(new MyMapper());


        //该方法用于提取数据中的时间戳,BoundedOutOfOrdernessTimestampExtractor表示是一个有界无序数据流的时间戳提取器
        //参数表示最大乱序程度,或者说延迟等待时间
        map.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<TestBean>(Time.seconds(10)) {
                    @Override
                    public long extractTimestamp(TestBean testBean) {
                        return testBean.getTimestamp();
                    }
                });


        //自定义AssignerWithPeriodicWatermarks
        map.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<TestBean>() {

            /**
             * @Author
             * @Description //TODO 在该方法内定义Watermark生成逻辑
             * @param
             * @Return org.apache.flink.streaming.api.watermark.Watermark
             * @Modified By:
             */
            @Nullable
            @Override
            public Watermark getCurrentWatermark() {
                return null;
            }

            /**
             * @Author
             * @Description //TODO 数据时间戳提取逻辑
             * @Date
             * @param testBean 传入的当前数据
             * @param  l  上一个数据时间戳
             * @Return long 返回生成的数据时间戳
             * @Modified By:
             */
            @Override
            public long extractTimestamp(TestBean testBean, long l) {
                return 0;
            }
        });

第二类是根据一些在流处理数据流中的数据生成的,由数据驱动,需要实现AssignerWithPunctuatedWatermarks接口。

map.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<TestBean>() {

            /**
             * @Author dinggang
             * @Description //TODO 在该方法内基于数据时间戳生成Watermark
             * @param testBean 上一个数据
             * @param  l  数据的时间戳
             * @Return org.apache.flink.streaming.api.watermark.Watermark
             * @Modified By:
             */
            @Nullable
            @Override
            public Watermark checkAndGetNextWatermark(TestBean testBean, long l) {
                return null;
            }

            //时间戳提取逻辑
            @Override
            public long extractTimestamp(TestBean testBean, long l) {
                return 0;
            }
        });