flink水位线

1. 时间语义

​ 有两个非常重要的时间概念:数据的产生时间(数据自带的创建时间)和处理时间(执行处理操作的机器的系统时间)。

​ 另外还有一个摄入时间,指的是数据进入flink数据流的时间,也就是source 算子读入数据的时间。

​ 一般以事件时间为基准,比如我们统计PV、UV 等指标,我们就需要以事件时间为基准。且flink的时间处理默认就是事件时间。

2. 水位线

1. 什么是水位线

​ 水位线可以看做是插入到数据流中的一个标记点,主要内容就是一个时间戳,用来指示当前的事件时间(实际就是用来度量事件时间的)。而它插入流中的位置,就应该是在某个数据到来之后;这样就可以从这个数据中提取时间戳,作为当前水位线的时间戳了。

​ 水位线是是数据流中的一部分,随着数据一起流动,在不同任务之间传输。

​ 并不是每条数据都会生成水位线。水位线也是一条数据,是流数据的一部分,watermark是一个全局的值,不是某一个key下的值,所以即使不是同一个key的数据,其warmark也会增加。

1. 有序流中的水位线

​ 在有序数据流的情况下,如果每条数据都插入一个水位线会浪费大量的无用功,所以一般会每隔一段时间生成一个水位线,这个水位线的时间戳,就是当前最新数据的时间戳。

2. 乱序流中的水位线

​ 乱序是指数据的先后顺序不一致,主要就是基于数据的产生时间而言。插入新的水位线时,要先判断一下时间戳是否比之前的大,否则就不再生成新的水位线。也就是只有数据的时间戳比当前时钟大,才能推动时钟前进,这时才插入水位线。

​ 考虑大量数据的情况,也可以周期性插入水位线。这时要保存一下之前所有数据中的最大时间戳。

关于迟到的数据:

问题:上面操作可以定义出一个事件时钟,但是有一个问题就是无法处理迟到的数据。比如当9s的数据到达之后,我们就直接将时钟推到了9s;如果有一个窗口的结束时间是0-9s,这时候窗口就应该关闭、将收集到的数据计算输出结果。但事实上由于乱序的数据,可能时间戳为7s、8s的数据在9s之后才到来,按照上面的逻辑会导致丢失这部分数据。

解决办法:等上2s,也就是用当前已有数据最大时间戳 减去2s(这个时间需要自己根据数据的情况动态设置)。这样9s的数据到了之后,时间钟进展到7s;必须等待11s的数据到达之后,事件时钟才会进展到9s。修改后水位线如下:

3. 水位线的特点

​ 水位线代表了当前的事件时间时钟,而且可以在数据的时间戳基础上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。水位线用来保证窗口处理结果的正确性。

  • 水位线是插入到数据流中的一个标记,可以认为是一条特殊的数据
  • 水位线的主要内容是基于数据的时间戳生成的一个时间戳,用来表示当前事件时间的进展
  • 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
  • 水位线可以通过设置延迟,来保证正确处理乱序数据
  • 一个水位线Watermark(t), 表示在当前流中事件事件已经达到了事件戳t,这代表t之前的所有数据都已经到齐了,之后流中不会出现时间戳 <= t 的数据

2. 如何生成水位线

​ 在flink的dataStream 中,有一个单独用于生成水位线的方法,assignTimestampsAndWatermarks 用于为流中的数据分配时间戳,并生成水位线来指示事件时间。

    public SingleOutputStreamOperator<T> assignTimestampsAndWatermarks(
            WatermarkStrategy<T> watermarkStrategy) {

​ 该方法接收的是一个水位策略WatermarkStrategy。WatermarkStrategy 包含一个时间戳分配器 TimestampAssigner 和 一个水位线生成器 WatermarkGenerator。

  1. 时间戳分配器

org.apache.flink.api.common.eventtime.TimestampAssigner

@Public
@FunctionalInterface
public interface TimestampAssigner<T> {

    /**
     * The value that is passed to {@link #extractTimestamp} when there is no previous timestamp
     * attached to the record.
     */
    long NO_TIMESTAMP = Long.MIN_VALUE;

    /**
     * Assigns a timestamp to an element, in milliseconds since the Epoch. This is independent of
     * any particular time zone or calendar.
     *
     * <p>The method is passed the previously assigned timestamp of the element. That previous
     * timestamp may have been assigned from a previous assigner. If the element did not carry a
     * timestamp before, this value is {@link #NO_TIMESTAMP} (= {@code Long.MIN_VALUE}: {@value
     * Long#MIN_VALUE}).
     *
     * @param element The element that the timestamp will be assigned to.
     * @param recordTimestamp The current internal timestamp of the element, or a negative value, if
     *     no timestamp has been assigned yet.
     * @return The new timestamp.
     */
    long extractTimestamp(T element, long recordTimestamp);
}

​ 主要负责从流数据元素的某个字段提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础。

  1. 水位线生成器

org.apache.flink.api.common.eventtime.WatermarkGenerator

public interface WatermarkGenerator<T> {

    /**
     * 每个数据到来都会调用
     */
    void onEvent(T event, long eventTimestamp, WatermarkOutput output);

    /**
     * 周期性调用的方法,可以由WatermarkOutput 发出水位线,周期时间为处理时间,默认为200ms。 可以通过executionEnvironment.getConfig().setAutoWatermarkInterval()设置
     */
    void onPeriodicEmit(WatermarkOutput output);
}

​ 主要负责按照既定的方式,基于时间戳生成水位线。

1. 内置水位线

​ 使用内置的有序和无序水位线。

package cn.qz.time;

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
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.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 java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class WatermarkTest {

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

        DataStreamSource<MyEvent> dataStreamSource = executionEnvironment.fromElements(
                new MyEvent("zs", "/user", 1000L),
                new MyEvent("zs", "/order", 1100L),
                new MyEvent("zs", "/product?id=1", 1200L),
                new MyEvent("ls", "/user", 1200L),
                new MyEvent("ls", "/product", 2000L),
                new MyEvent("ww", "/product", 4000L),
                new MyEvent("ww", "/order", 6000L),
                new MyEvent("zl", "/order", 10000L)
        );

        // 有序流
        /*dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
                    @Override
                    public long extractTimestamp(MyEvent element, long recordTimestamp) {
                        *//**
         * 时间戳和水位线的单位都必须是毫秒
         *//*
                        return element.getTimestamp();
                    }
                }));*/
        // 无序流(且延迟时间是5s)
        dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofMillis(5))
                .withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
                    @Override
                    public long extractTimestamp(MyEvent element, long recordTimestamp) {
                        return element.getTimestamp();
                    }
                }))
                // 根据user分组,开窗统计
                .keyBy(data -> data.user)
                .window(TumblingEventTimeWindows.of(Time.seconds(1)))
                .process(new WatermarkTestResult())
                .print();
        /**
         * 事实上。 有序流的水位线生成器本质和乱序流是一样的,相当于延迟设为0的乱序流水线生成器
         * forMonotonousTimestamps
         * forBoundedOutOfOrderness
         * @see AscendingTimestampsWatermarks
         */

        executionEnvironment.execute();
    }

    // 自定义处理窗口函数,输出当前的水位线和窗口信息以及每个窗口的数据信息
    public static class WatermarkTestResult extends ProcessWindowFunction<MyEvent, String, String, TimeWindow> {
        @Override
        public void process(String s, Context context, Iterable<MyEvent> elements, Collector<String> out) throws Exception {
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            Long currentWatermark = context.currentWatermark();
            Long count = elements.spliterator().getExactSizeIfKnown();

            // 收集元素, 然后汇总到结果集
            List<String> result = new ArrayList<>();
            Iterator<MyEvent> iterator = elements.iterator();
            while (iterator.hasNext()) {
                result.add(iterator.next().toString());
            }
            out.collect("窗口" + start + " ~ " + end + "中共有" + count + "个元素,窗口闭合计算时,水位线处于:" + currentWatermark + " result: " + result);
        }
    }
}

结果:

3> 窗口4000 ~ 5000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ww, url=/product, timestamp=4000)]
6> 窗口10000 ~ 11000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=zl, url=/order, timestamp=10000)]
1> 窗口1000 ~ 2000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ls, url=/user, timestamp=1200)]
3> 窗口6000 ~ 7000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ww, url=/order, timestamp=6000)]
7> 窗口1000 ~ 2000中共有3个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=zs, url=/user, timestamp=1000), MyEvent(user=zs, url=/order, timestamp=1100), MyEvent(user=zs, url=/product?id=1, timestamp=1200)]
1> 窗口2000 ~ 3000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ls, url=/product, timestamp=2000)]

也可以强行将所有数据分到一个组:(keyBy 直接返回一个固定的值即可)

package cn.qz.time;

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
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.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 java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class WatermarkTest {

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

        DataStreamSource<MyEvent> dataStreamSource = executionEnvironment.fromElements(
                new MyEvent("zs", "/user", 1000L),
                new MyEvent("zs", "/order", 1100L),
                new MyEvent("zs", "/product?id=1", 1200L),
                new MyEvent("ls", "/user", 1200L),
                new MyEvent("ls", "/product", 2000L),
                new MyEvent("ww", "/product", 4000L),
                new MyEvent("ww", "/order", 6000L),
                new MyEvent("zl", "/order", 10000L)
        );

        // 有序流
        /*dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
                    @Override
                    public long extractTimestamp(MyEvent element, long recordTimestamp) {
                        *//**
         * 时间戳和水位线的单位都必须是毫秒
         *//*
                        return element.getTimestamp();
                    }
                }));*/
        // 无序流(且延迟时间是5s)
        dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofMillis(5))
                .withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
                    @Override
                    public long extractTimestamp(MyEvent element, long recordTimestamp) {
                        return element.getTimestamp();
                    }
                }))
                // 根据user分组,开窗统计
                .keyBy(data -> true)
                .window(TumblingEventTimeWindows.of(Time.seconds(1)))
                .process(new WatermarkTestResult())
                .print();
        /**
         * 事实上。 有序流的水位线生成器本质和乱序流是一样的,相当于延迟设为0的乱序流水线生成器
         * forMonotonousTimestamps
         * forBoundedOutOfOrderness
         * @see AscendingTimestampsWatermarks
         */

        executionEnvironment.execute();
    }

    // 自定义处理窗口函数,输出当前的水位线和窗口信息以及每个窗口的数据信息
    public static class WatermarkTestResult extends ProcessWindowFunction<MyEvent, String, Boolean, TimeWindow> {
        @Override
        public void process(Boolean s, Context context, Iterable<MyEvent> elements, Collector<String> out) throws Exception {
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            Long currentWatermark = context.currentWatermark();
            Long count = elements.spliterator().getExactSizeIfKnown();

            // 收集元素, 然后汇总到结果集
            List<String> result = new ArrayList<>();
            Iterator<MyEvent> iterator = elements.iterator();
            while (iterator.hasNext()) {
                result.add(iterator.next().toString());
            }
            out.collect("窗口" + start + " ~ " + end + "中共有" + count + "个元素,窗口闭合计算时,水位线处于:" + currentWatermark + " result: " + result);
        }
    }
}

​ 这里注意: 乱序流中的水位线真正的时间戳,其实是 当前最大时间戳-延迟时间-1,单位毫秒。 减一的原因:

考虑时间戳为t的水位线,标识时间戳<=t 的数据全部到齐,不会再来了。考虑有序流延迟为0的情况,时间戳为7s的时间到来时,之后其实还有可能有7s的数据,所以生成水位线的水位线不是7s,而是6秒999毫秒,7s的数据还可以继续来。参考: org.apache.flink.api.common.eventtime.BoundedOutOfOrdernessWatermarks#onPeriodicEmit

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
    }

补充一下自己的处理窗口函数:org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction

@PublicEvolving
public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window>
        extends AbstractRichFunction {

    private static final long serialVersionUID = 1L;

    /**
     * Evaluates the window and outputs none or several elements.
     *
     * @param key The key for which this window is evaluated.
     * @param context The context in which the window is being evaluated.
     * @param elements The elements in the window being evaluated.
     * @param out A collector for emitting elements.
     * @throws Exception The function may throw exceptions to fail the program and trigger recovery.
     */
    public abstract void process(
            KEY key, Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception;

    /**
     * Deletes any state in the {@code Context} when the Window expires (the watermark passes its
     * {@code maxTimestamp} + {@code allowedLateness}).
     *
     * @param context The context to which the window is being evaluated
     * @throws Exception The function may throw exceptions to fail the program and trigger recovery.
     */
    public void clear(Context context) throws Exception {}

    /** The context holding window metadata. */
    public abstract class Context implements java.io.Serializable {
        /** Returns the window that is being evaluated. */
        public abstract W window();

        /** Returns the current processing time. */
        public abstract long currentProcessingTime();

        /** Returns the current event-time watermark. */
        public abstract long currentWatermark();

        /**
         * State accessor for per-key and per-window state.
         *
         * <p><b>NOTE:</b>If you use per-window state you have to ensure that you clean it up by
         * implementing {@link ProcessWindowFunction#clear(Context)}.
         */
        public abstract KeyedStateStore windowState();

        /** State accessor for per-key global state. */
        public abstract KeyedStateStore globalState();

        /**
         * Emits a record to the side output identified by the {@link OutputTag}.
         *
         * @param outputTag the {@code OutputTag} that identifies the side output to emit to.
         * @param value The record to emit.
         */
        public abstract <X> void output(OutputTag<X> outputTag, X value);
    }
}

2. 自定义水位线

​ 内置的一般够用了,有时候业务逻辑复杂这时对水位线生成的逻辑有更高的要求,就必须自定义实现水位线策略。

​ 时间戳分配器都大同小异,指定字段提取时间戳就可以了;而不同策略的关键在于WatermarkGenerator 水位生成器的实现,有两种方式,一种是周期性(Periodic), 另一种是断点式的Punctuated。

(1)周期性水位线生成器

​ 周期性生成器一般是通过onEvent() 观察判断输入的事件,而在onPeriodicEmit() 里发出水位线。

package cn.qz.time;

import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.eventtime.*;
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.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 java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class CustomerWatermarkTest {

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

        DataStreamSource<MyEvent> dataStreamSource = executionEnvironment.fromElements(
                new MyEvent("zs", "/user", 1000L),
                new MyEvent("zs", "/order", 1100L),
                new MyEvent("zs", "/product?id=1", 1200L),
                new MyEvent("ls", "/user", 1200L),
                new MyEvent("ls", "/product", 2000L),
                new MyEvent("ww", "/product", 4000L),
                new MyEvent("ww", "/order", 6000L),
                new MyEvent("zl", "/order", 10000L)
        );

        dataStreamSource.assignTimestampsAndWatermarks(new CustomWatermarkStrategy())
                // 根据user分组,开窗统计
                .keyBy(data -> data.user)
                .window(TumblingEventTimeWindows.of(Time.seconds(1)))
                .process(new WatermarkTestResult())
                .print();
        /**
         * 事实上。 有序流的水位线生成器本质和乱序流是一样的,相当于延迟设为0的乱序流水线生成器
         * forMonotonousTimestamps
         * forBoundedOutOfOrderness
         * @see AscendingTimestampsWatermarks
         */

        executionEnvironment.execute();
    }

    public static class CustomWatermarkStrategy implements WatermarkStrategy<MyEvent> {
        @Override
        public TimestampAssigner<MyEvent> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<MyEvent>() {
                @Override
                public long extractTimestamp(MyEvent element, long recordTimestamp) {
                    return element.timestamp; // 告诉程序数据源里的时间戳是哪一个字段
                }
            };
        }

        @Override
        public WatermarkGenerator<MyEvent> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new CustomPeriodicGenerator();
        }
    }

    public static class CustomPeriodicGenerator implements WatermarkGenerator<MyEvent> {

        private Long delayTime = 5000L; // 延迟时间

        private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳

        @Override
        public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
            // 每来一条数据就调用一次
            maxTs = Math.max(event.timestamp, maxTs); // 更新最大时间戳
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 发射水位线,默认200ms调用一次
            output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
        }
    }

    /**
     * ProcessWindowFunction 依次为: <IN, OUT, KEY, W extends Window>
     */
    private static class WatermarkTestResult extends ProcessWindowFunction<MyEvent, String, String, TimeWindow> {
        @Override
        public void process(String s, Context context, Iterable<MyEvent> elements, Collector<String> out) throws Exception {
            Long start = context.window().getStart();
            Long end = context.window().getEnd();
            Long currentWatermark = context.currentWatermark();
            Long count = elements.spliterator().getExactSizeIfKnown();

            // 收集元素, 然后汇总到结果集
            List<String> result = new ArrayList<>();
            Iterator<MyEvent> iterator = elements.iterator();
            while (iterator.hasNext()) {
                result.add(iterator.next().toString());
            }
            out.collect("窗口" + start + " ~ " + end + "中共有" + count + "个元素,窗口闭合计算时,水位线处于:" + currentWatermark + " result: " + result);
        }
    }
}

(2)断点式水位线生成器

​ 断点式水位线会不停地检测onEvent()中的事件,当发现带有水位线信息的特殊事件时,就立即发出水位线。一般不会通过onPeriodicEmit()发出水位线。

3. 水位线的传递

​ 水位线是数据流中插入的一个标记,用来表示时间的进展,它会随着数据一起在任务间传递。

​ 如果是直通式(forward)的传输,数据和水位线都是按照本身的顺序依次传递、依次处理的。一旦水位线到达了算子任务,这个子任务就会将它内部的时钟设为这个水位线的时间戳。

​ 对于重分区(redistributing)模式,一个任务有可能来自不同分区上游子任务的数据。而不同分区的子任务时钟并不同步,比如某个任务收到上游两个任务不同的水位线分别为5s、7s。水位线的本质:当前时间之前的数据都已经到齐了,会取短板的为准,处理5s之前的数据。

​ 用下图理解其水位线传递规则:

上游四个并行子任务,下游有三个并行子任务,所以会向三个分区发出水位线。

(1)当前任务自己的时钟就是所有分区时钟里最小的那个。

(2)传来4的时候,更新对应分区的时钟,然后然后对比四个分区最小的时钟是3,于是当当前的事件时钟更新到3,并且广播给所有下游子任务

(3)收到第二份分区的7,然后找到最小的还是3,没变化就不更新时钟

(4)收到第三个分区的6,对比最小是4,然后更新自己的时钟为4,并且向所有下游子任务广播自己的时钟

4. 总结

1.水位线在事件事件的世界里承担了时钟的角色;并且插入在数据流同数据一起流向下游子任务

2.水位线的默认计算公式:水位线=观察到的最大事件时间-最大延迟时间-1毫秒

3.在数据流开始之前,flink会插入一个大小是负无穷大(-Long.MAX_VALUE)的水位线;数据流结束时,插入一个正无穷大(Long.MAX_VALUE)的水位线,以保证所有的窗口闭合以及所有的定时器都被处罚。

总结:

  1. 核心概念

1.flink中对时间的处理满足前开后闭,包含前面不包含后面。且窗口时间、事件时间的单位都是ms。

2.时间语义:事件时间(时间自带的时间戳)、处理时间(当前处理事件的服务器的时间)、摄入时间(进入Fflink流的时间)。时间用于窗口函数中基于时间的窗口函数。可以参考: org.apache.flink.streaming.api.TimeCharacteristic

3.窗口:处理无界流的关键,时间窗口有开始时间(start_time)和结束时间(end_time), 这个时间是系统时间。可以理解为用窗口将数据划分成定长的buckets分开计算。

4.水位线:可以理解为是插入数据流的一条特殊数据,不存在与每条数据。水位线在不断的变化,一旦大于某个window的end_time,就会触发此window的计算,水位线就是用来触发window计算的。

  1. 用水位线处理乱序流的流程

​ 假设设置的10s的时间窗口(window),那么0-10s,10-20s 都是一个窗口。以0-10为例子,0为开始时间、10位结束时间。假设四条数据的事件时间分别为: A(8)B(12.5)C(9)D(13.5)。 设置水位线延迟时间为事件时间减去3.5s。下面分析四条数据到达的时候其过程:

A:watermark = max{8}-3.5=4.5, 不会触发计算

B:watermark = max{8, 12.5}-3.5=9 不会触发计算

C:watermark = max{12.5, 9}-3.5=9 不会触发计算

D:watermark = max{12.5, 13.5}-3.5=10,触发计算。会将AC都计算进去,因为他们都小于10。

posted @ 2022-08-02 22:44  QiaoZhi  阅读(1690)  评论(0编辑  收藏  举报