Flink水位线

224 阅读7分钟

水位线

水位线:衡量事件时间(Event Time)进展的标记

水位线可以看作一条特殊的数据记录,它是插入到数据流中的一个标记点,主要内容就是一个时间戳,用来指示当前的事件时间。这个标记可以直接广播到下游而它插入流中的位置,就应该是在某个数据到来之后;这样就可以从这个数据中提取时间戳,作为当前水位线的时间戳了

①作用:作为衡量事件时间进展的标记;

②产生:从数据中提取时间戳,作为水位线的时间戳;

③特征:广播到下游,确保所有并行子任务都可以及时更新事件时间,进行窗口计算

水位线示意图:

每个事件产生的数据,都包含了一个时间戳,我们直接用一个整数表示。这里没有指定单位,可以理解为秒或者毫秒(方便起见,下面讲述统一认为是秒)。当产生于2 秒的数据到来之后,当前的事件时间就是 2 秒;在后面插入一个时间戳也为 2 秒的水位线,随着数据一起向下游流动。而当 5 秒产生的数据到来之后,同样在后面插入一个水位线,时间戳也为 5,当前的时钟就推进到了 5 秒。这样,如果出现下游有多个并行子任务的情形,我们只要将水位线广播出去,就可以通知到所有下游任务当前的时间进度了

有序流中的水位线:数据遵循先来后到的原则,因此可以每隔一段时间生成一次水位线;这时的水位线,其实就是有序流中的一个周期性出现的时间标记

对于水位线的周期性生成,周期时间是指处理时间(系统时间) ,而不是事件时间

有序流这种情况,只存在于理想状态下

乱序流中的水位线:数据的先后顺序不一致,所以在生成水位线时,要先判断一下时间戳是否比之前的大;只有数据的时间戳比当前时钟大,才能推动时钟前进,这时才插入水位线

如果考虑到大量数据同时到来的处理效率,我们同样可以周期性地生成水位线。这时只需要保存一下之前所有数据中的最大时间戳,需要插入水位线时,就直接以它作为时间戳生成新的水位线

水位线的特征:

⚫ 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据

⚫ 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展

⚫ 水位线是基于数据的时间戳生成的

⚫ 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进

⚫ 水位线可以通过设置延迟,来保证正确处理乱序数据

⚫ 一个水位线 Watermark(t),表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据

水位线生成策略

通过水位线的延迟避免漏掉数据;

延迟高:处理的实时性降低,可能为极数的迟到数据增加了很多不必要的延迟

延迟低:可能会导致窗口遗漏数据,计算结果不准确

生成水位线的API

assignTimestampsAndWatermarks():为流中的数据分配时间戳,并生成水位线来指示事件时间

DataStream<Event> stream = env.addSource(new ClickSource());
DataStream<Event> withTimestampsAndWatermarks = 
stream.assignTimestampsAndWatermarks(<watermark strategy>);

可以看到,该API需要一个watermark strategy作为参数,即水位线生成策略

WatermarkStrategy 中包含了一个“时间戳分配器”TimestampAssigner 和一个“水位线生成器”WatermarkGenerator

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

WatermarkGenerator:主要负责按照既定的方式,基于时间戳生成水位线。在WatermarkGenerator 接口中,主要又有两个方法:onEvent()和 onPeriodicEmit()

onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,以及允许发出水位线的一个 WatermarkOutput,可以基于事件做各种操作

onPeriodicEmit:周期性调用的方法,可以由 WatermarkOutput 发出水位线。周期时间为处理时间,可以调用环境配置的.setAutoWatermarkInterval()方法来设置,默认为200ms

env.getConfig().setAutoWatermarkInterval(60 * 1000L);

其返回值类型是一个SingleOutputStreamOperator,因此可以看做是一个转换算子;

Flink 内置水位线生成器

有序流:

stream.assignTimestampsAndWatermarks(
 WatermarkStrategy.<Event>forMonotonousTimestamps()
 .withTimestampAssigner(new SerializableTimestampAssigner<Event>() 
{
 @Override
 public long extractTimestamp(Event element, long recordTimestamp) 
{
 return element.timestamp;
 }
 })
);

调用WatermarkStrategy.forMonotonousTimestamps()方法就可以实现

调用.withTimestampAssigner()方法,将数据中的 timestamp 字段提取出来,作为时间戳分配给数据元素;然后用内置的有序流水位线生成器构造出了生成策略

乱序流:

env.addSource(new ClickSource())
 // 插入水位线的逻辑
.assignTimestampsAndWatermarks(
 // 针对乱序流插入水位线,延迟时间设置为 5s
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
 // 抽取时间戳的逻辑
 @Override
 public long extractTimestamp(Event element, long recordTimestamp) {
 return element.timestamp;
 }
 })
 )
 .print();

调用 WatermarkStrategy. forBoundedOutOfOrderness()方法实现;

需要传入一个 maxOutOfOrderness 参数,表示“最大乱序程度”,它表示数据流中乱序数据时间戳的最大差值;如果我们能确定乱序程度,那么设置对应时间长度的延迟,就可以等到所有的乱序数据了;

注意:

乱序流中生成的水位线真正的时间戳,其实是 当前最大时间戳 – 延迟时间 – 1,这里的单位是毫秒。为什么要减 1 毫秒呢?我们可以回想一下水位线的特点:时间戳为 t 的水位线,表示时间戳≤t 的数据全部到齐,不会再来了。如果考虑有序流,也就是延迟时间为 0 的情况,那么时间戳为 7 秒的数据到来时,之后其实是还有可能继续来 7 秒的数据的;所以生成的水位线不是 7 秒,而是 6 秒 999 毫秒,7 秒的数据还可以继续来、

从BoundedOutOfOrdernessWatermarks源码中也可以看出:

自定义水位线生成策略

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

通过onEvent()观察判断输入的事件,而在 onPeriodicEmit()里发出水位线

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

        env
                .addSource(new ClickSource())
                .assignTimestampsAndWatermarks(new CustomWatermarkStrategy())
                .print();

        env.execute();
    }

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

        @Override
        //水位线生成器
        public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new CustomPeriodicGenerator();
        }
    }

    //自定义水位线生成器
    public static class CustomPeriodicGenerator implements WatermarkGenerator<Event> {
        private Long delayTime = 5000L; // 延迟时间
        private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳

        @Override
        public void onEvent(Event 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));
        }
    }
}

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

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

public class CustomPunctuatedGenerator implements WatermarkGenerator<Event> {
 @Override
 public void onEvent(Event r, long eventTimestamp, WatermarkOutput output) {
// 只有在遇到特定的 itemId 时,才发出水位线
 if (r.user.equals("Mary")) {
 output.emitWatermark(new Watermark(r.timestamp - 1));
 }
 }
 @Override
 public void onPeriodicEmit(WatermarkOutput output) {
 // 不需要做任何事情,因为我们在 onEvent 方法中发射了水位线
 }
}

3.在自定义数据源中发送水位线:

    public static class ClickSourceWithWatermark implements SourceFunction<Event> {
        private boolean running = true;
        @Override
        public void run(SourceContext<Event> sourceContext) throws Exception {
            Random random = new Random();
            String[] userArr = {"Mary", "Bob", "Alice"};
            String[] urlArr  = {"./home", "./cart", "./prod?id=1"};
            while (running) {
                long currTs = Calendar.getInstance().getTimeInMillis(); // 毫秒时间戳
                String username = userArr[random.nextInt(userArr.length)];
                String url      = urlArr[random.nextInt(urlArr.length)];
                Event event = new Event(username, url, currTs);
                // 使用collectWithTimestamp方法将数据发送出去,并指明数据中的时间戳的字段
                sourceContext.collectWithTimestamp(event, event.timestamp);
                // 发送水位线
                sourceContext.emitWatermark(new Watermark(event.timestamp - 1L));
                Thread.sleep(1000L);
            }
        }

        @Override
        public void cancel() {
            running = false;
        }
    }

适合用于编写Flink 的测试程序

任务间水位线的传递

上游并行子任务发来不同的水位线,当前任务会为每一个分区设置一个“分区水位线”(Partition Watermark),这是一个分区时钟;而当前任务自己的时钟,就是所有分区时钟里最小的那个