Flink 解决乱序问题之WaterMarker

本文深入探讨了Flink中事件时间和水印(WaterMarker)的概念,解释了为什么在使用事件时间窗口时需要设置水印。水印是一种处理事件乱序的机制,通过允许一定延迟来确保窗口正确触发。文章通过实例说明了水印如何决定窗口触发时机,并强调了水印能部分解决乱序问题但无法完全避免。最后,给出了一个设置水印的代码示例,展示了水印如何应用于实际场景。
摘要由CSDN通过智能技术生成

前言

在前面的时间窗口中,我们初步使用了ProcesstimeWindow以及EventTimeWindow,我们注意到,在使用事件时间窗口的时候,系统提示我们要么设置WaterMarker,要么将时间处理模式切换为ProcessTime

image-20210524222812703

完整错误信息:

Caused by: java.lang.RuntimeException: Record has Long.MIN_VALUE timestamp (= no timestamp marker)
    . Is the time characteristic set to 'ProcessingTime', or did you forget to call 
    'DataStream.assignTimestampsAndWatermarks(...)'?

ProcessTime,我们已经知道了,就是数据在进入Flink,算子处理的时间,那么为什么在使用事件时间的时候要求我们必须进行额外的设置呢?接下来,咱们便瞧上一瞧。

(一)为什么需要WaterMarker

如果按照正常的时间窗口来划分

我们的事件时间窗口都是按照事件时间来触发计算的,如: [12:00:00 ~ 12:00:10) 的窗口,会随着第一个事件时间大于或等于12:00:10的元素来进行触发计算,一旦flink数据源接收到了这个数据,该窗口便会立即执行

比如我们现在设置了每十秒一次的滚动窗口,比如我们从MQ中的读取到的第一个事件时间为12:00:00,那么接下来开启的时间窗口开启与范围大致如下所示

image-20210526211516591

但是呢,可能我们生产端发送到MQ中本身就已经乱序了(由于网络啊、接口性能等等),导致我们读取的数据也是乱序的

例如:现在从MQ中读到的数据中的时间如下

image-20210526212524742

由于F 已经是00:11时间了,则会立即触发image-20210526212640513窗口计算,后续的数据 G H由于归属的窗口image-20210526212653341已经被执行,G H数据就会被丢弃,会被忽略计算。

上面这种情况呢,便是由于各种原因,导致数据乱序,进而产生了错误的计算结果;这种情况,在绝大多数场景下我们是不能接受的!

为了解决根据事件时间计算可能会产生这种问题,Flink 提供了WaterMarker机制,利用一定的延迟容忍,可一定程度上避免因消息乱序导致的错误计算或者数据丢失。

(二)什么是WaterMarker

为了处理事件时间,Flink首先需要知道事件本身的的时间戳,这意味着流中的每个元素都需要其携带事件时间;且时间属性需要为时间戳WaterMarker 是Flink提供的一种水印机制,实质就是根据事件元素的事件时间,进行处理后生成一个新的时间属性(也是时间戳),然后使用该水印进而更改窗口的计算触发时机

image-20211212184730787


既然WaterMarker可一定程度上避免因乱序问题导致的数据丢失,那么它到底是什么呢?

对于单流而言,会选择当前所有元素中最大的值timestamp作为watermark。如果新数据来到,无timestamp大于现有WaterMarker,则WaterMarker保持不变

ex:单数据流(或并行度为1的流)>>>设置WaterMarker

  • 单数据流情况下:WaterMarker=当前数据流中当前元素最大事件时间 - 最大允许的延迟时间或乱序时间

对于多流而言,会选择流中最小的watermark作为整个任务的watermark。如同短板效应,木桶里面最高水位线由最低的桶边决定。但是WaterMarker仍要保持上涨状态,如果一批新数据来到,取出最小事件时间小于当前WaterMarker,则WaterMarker保持不变

ex:单数据流>>keyBy() 变为多数据流>>>设置WaterMarker

ex:多并行度流>>>设置WaterMarker

  • 多数据流情况下:WaterMarker=当前数据流中元素最小事件时间 - 最大允许的延迟时间或乱序时间

watermark 会以广播的形式在算子之间进行传播,下游所有算子共享watermark。

如果在程序里面收到了一个 Long.MAX_VALUE 这个数值的 watermark,就表示对应的那一条流的一个部分不会再有数据发过来了,它相当于就是一个终止的一个标志。

ex:原本根据事件时间 十秒滚动窗口 现在设置了最大允许的延迟时间或乱序时间为5秒,我们的事件时间窗口就不会按照原本事件元素中的时间列来进行触发窗口计算了,而是会按照WaterMarker来触发窗口计算!

(三)设置WaterMarker后,窗口如何触发

1.窗口中存在事件元素(空窗口不会触发)

image-20210526214724251

2.WaterMarker大于等于窗口结束时间

image-20210526214831385

(四)WaterMarker触发详解

例如,现在我们有了一个[12:00:00-12:00:10)的时间窗口,现在事件如下图所示顺序ABCDEF…到达

image-20210526220911245

在未设置WaterMarker的情况下,当元素C到达的时候,便会触发窗口[12:00:00-12:00:10)进行计算,因为C元素的时间已经满足大于等于窗口[12:00:00-12:00:10)的结束时间12:00:10了。

此时呢,该窗口中仅会有AB两个元素,如果从事件时间划分来讲,D、F应也属于窗口[12:00:00-12:00:10)内的元素,但是由于C提前到来(D F 延迟了)触发了计算,因Flink时间窗口的生命周期限制,[12:00:00-12:00:10)这个窗口,再也不会打开。

其最后结果将是 导致D、F再无可归属窗口,D、F因此成了无用数据,最终会被抛弃掉!


当我们引入了WaterMarker水印机制,设置了延迟时间后,事件时间窗口的触发时机由事件时间决定变为了由WaterMarker决定

比如我们现在设置了窗口允许数据最大延迟时间为5s

image-20210527214605234

A数据到达:WaterMarker=max{12:00:01}-5 =11:59:56 < 窗口[12:00:00-12:00:10)的结束时间12:00:10,不会触发计算

B数据到达:WaterMarker=max{12:00:05,12:00:01}-5=12:00:0 [12:00:00-12:00:10)12:00:10`,不会触发计算

C据到达:WaterMarker=max{12:00:11,12:00:05,12:00:01}-5=12:00:06 < 窗口[12:00:00-12:00:10)的结束时间12:00:10,不会触发计算

D据到达:WaterMarker=max{12:00:08,12:00:11,12:00:05,12:00:01}-5=12:00:06 < 窗口[12:00:00-12:00:10)的结束时间12:00:10,不会触发计算

直到有一个事件时间-5>=窗口[12:00:00-12:00:10)的结束时间12:00:10才会触发窗口[12:00:00-12:00:10)计算!

G数据据到达:WaterMarker=max{12:00:15,12:00:09,12:00:08,12:00:11,12:00:05,12:00:01}-5=12:00:10 等于 窗口[12:00:00-12:00:10)的结束时间12:00:10,此时窗口[12:00:00-12:00:10) 的归属元素有A、B、D、F,窗口存在元素,会触发计算!!!!

注意点:

注意点1

WaterMarker只是决定了窗口的触发时机,并非可以改变元素归属的窗口(事件应归属的窗口是由事件本身的事件时间决定的),例如 上方元素 C、G 虽然根据设置的延迟时间可能触发窗口[12:00:00-12:00:10)计算,但其本身时间不归属于窗口之内,因为窗口[12:00:00-12:00:10) 中永远不会有大于等于12:00:10的元素存在

注意点2

WaterMarker可以在一定程度上解决事件乱序问题,但严重的乱序问题依然无法解决!

image-20210527215856036

例如,在事件G后又来了事件H(乱序比较严重!!因为前边元素都12:00:15了,自己反而才12:00:07) 如果设置的时允许最大延迟时间为5S,元素G依然满足触发窗口[12:00:00-12:00:10)计算销毁,后来的元素H便再无可归属的时间窗口了,所以H仍然会丢失!!

基于这种情况,如果想避免H数据丢失怎么办呢?我们可以让G无法触发窗口[12:00:00-12:00:10)就行,即允许延迟时间再设置大一点,比如允许延迟10S…延迟10S后,至少有了事件时间为12:00:20的事件到来才会触发计算了,这样H就不会丢了,,,,但如此设置,又避免不了更更延迟的数据,比如延迟了20s的数据…所以说,WaterMarker只能在一定程度上解决乱序问题!!

当然以上情况,我们可以结合侧位输出来收集更为延迟的数据,避免延迟数据丢失(这个后边会讲),但是!事件时间延迟,主要问题在生产端!如果真要解决这个问题,应该从生产事件的生产端入手,而非极力依托于计算框架!!


(五)WaterMarker案例演示

示例代码,设置一个十秒滚动的事件时间窗口,且允许数据最大延迟为5S

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
        // 准备数据
        DataStreamSource<Location> locationSource = env.addSource(new LocationSource());
        // 设置水位线 允许延迟为5秒
        SingleOutputStreamOperator<Location> watermarks = locationSource.
                assignTimestampsAndWatermarks(WatermarkStrategy.
                        <Location>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                        // 设置事件时间为devTime属性
                        .withTimestampAssigner((event, timestamp) -> event.getDevTime()));
        // 时间滚动窗口 十秒计算一次
        WindowedStream<Location, Integer, TimeWindow> window = watermarks.keyBy(Location::getVehicleId)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)));
        SingleOutputStreamOperator<String> source = window.apply(new AlarmCalcWindow());
        source.printToErr("系统时间 " + System.currentTimeMillis() + " 输出数据:");
        env.execute();
    }

注意:因为我这里是自定义source,并souce实现的是SourceFunction,此数据源不支持多并行度,且我设置WaterMarker在keyBy()算子之前,则我这里的数据流为单数据里,因此,当前job的WaterMarker为当前事件流中元素最大事件时间 - 最大允许的延迟时间或乱序时间

image-20210527222926265

如上图所示,我们的定位事件目前处于一个乱序状态(定位事件并未从小到大排序输出)

上图中 ① 是时间窗口[2021-05-27 22:12:53-2021-05-27 22:13:03) 被触发计算时窗口内的数据

为什么 ①时间窗口开始时间是2021-05-27 22:12:53? 因为Flnk接受到的第一个元素事件时间便是它;因为窗口大小为10s,则结束时间为2021-05-27 22:13:03

① 窗口直到数据image-20210527223642153到来才触发了计算!

我们可以按照(四)WaterMarker 触发详解进行推理

第一个元素 devTime=16221247738242021-05-27 22:12:53 ,那么此时WaterMarker=2021-05-27 22:12:53-5=2021-05-27 22:12:48 小于窗口结束时间2021-05-27 22:13:03 不会触发计算

直到devTime=16221247998632021-05-27 22:13:19,那么此时WaterMarker=2021-05-27 22:13:19-5=2021-05-27 22:13:14 大于窗口结束时间2021-05-27 22:13:03,且该窗口内有归属元素,因此触发了计算…

上图 中② 是时间窗口[2021-05-27 22:13:03-2021-05-27 22:13:13) 被触发计算时窗口内的数据

触发逻辑同理①

. . . . . . .

可能有小伙伴想问了,你设置了WaterMarker了,到时避免了一定数据丢失了,但看你控制台还是打印的时间是乱序的呀!

拜托!您在窗口那个迭代器中先按时间事件排个序,再计算,不就可以保证此次事件是有序的了吗???

附上完整demo

package com.leilei;

import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSON;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
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.WindowFunction;
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.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Random;

/**
 * @author lei
 * @version 1.0
 * @date 2021/3/16 22:22
 * @desc flink 使用 watermaker 解决一定程度(自定义允许事件延迟的时间)乱序问题
 */
public class Flink_Water_Maker_1 {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
        // 准备数据
        DataStreamSource<Location> locationSource = env.addSource(new LocationSource());
        // 设置水位线 允许延迟为5秒
        SingleOutputStreamOperator<Location> watermarks = locationSource.
                assignTimestampsAndWatermarks(WatermarkStrategy.
                        <Location>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                        // 设置事件时间为devTime属性
                        .withTimestampAssigner((event, timestamp) -> event.getDevTime()));
        // 时间滚动窗口 十秒计算一次
        WindowedStream<Location, Integer, TimeWindow> window = watermarks.keyBy(Location::getVehicleId)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)));
        SingleOutputStreamOperator<String> source = window.apply(new AlarmCalcWindow());
        source.printToErr("系统时间 " + System.currentTimeMillis() + " 输出数据:");
        env.execute();
    }

    public static class LocationSource implements SourceFunction<Location> {
        Boolean flag = true;

        @Override
        public void run(SourceContext<Location> ctx) throws Exception {
            Random random = new Random();
            while (flag) {
                int vehicleId = 1;
                Location location = Location.builder()
                        .vehicleId(vehicleId)
                        .plate("川A000" + vehicleId)
                        .color("黄")
                        .date(Integer.parseInt(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)))
                        .gpsSpeed(RandomUtil.randomInt(90, 100))
                        .limitSpeed(RandomUtil.randomInt(88, 95))
                        .devTime(System.currentTimeMillis() - RandomUtil.randomInt(5, 30) * 1000)
                        .build();
                ctx.collect(location);
                System.out.println("初始数据:" + location);
                Thread.sleep(2000);

            }
        }

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

    /**
     * 自定义窗口
     */
    public static class AlarmCalcWindow implements WindowFunction<Location, String, Integer, TimeWindow> {
        @Override
        public void apply(Integer key, TimeWindow window, Iterable<Location> input, Collector<String> out) {
            //todo 迭代器元素根据时间排序
            //System.out.println("当前窗口Key=" + key+" 窗口开始时间:"+window.getStart()+" 窗口结束时间:"+window.getEnd());
            for (Location location : input) {
                out.collect(JSON.toJSONString(location));
            }
        }
    }


}


(六)WaterMarker总结

1、WaterMarker 是Flink提供的一种水印机制,实质就是根据事件元素的事件时间(必须为时间戳),进行处理后生成一个新的时间属性(也是时间戳),然后使用该水印进而更改窗口的计算触发时机

image-20211212184730787

2、WaterMarker值最终决定属性

  • 单数据流情况下:WaterMarker=当前事件流中元素最大事件时间 - 最大允许的延迟时间或乱序时间

  • 多数据流情况下:WaterMarker=当前当前事件流中元素最小事件时间 - 最大允许的延迟时间或乱序时间(木桶短板效应)

3、watermark是一个全局的值,不是某一个key下的值,所以即使不是同一个key的数据,其watermark也会不断增加

4、WaterMarker可以在一定程度上解决事件乱序问题,但严重的乱序问题依然无法解决!

5、不可过度依赖WaterMarker帮助我们解决乱序问题,如果发生过多乱序问题应注重检查生产数据的生产端问题!

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Flink 中,乱序问题指的是事件流在处理过程中不按照时间顺序到达。为了解决乱序问题,可以考虑以下几种方法: 1. Watermark 和 Event Time:使用 Event Time 概念来处理事件流。Flink 提供了 Watermark 机制来标记事件的事件时间,通过设置适当的 Watermark 值,可以告知 Flink 在某个时间点之后不再有新的事件到达。这样可以在处理事件时,根据事件时间来进行排序和处理。 2. 乱序窗口(Out-of-Order Windows):Flink 提供了乱序窗口的支持,可以按照事件的事件时间和乱序时间进行窗口的划分和计算。通过设置窗口的允许乱序时间,可以在一定程度上容忍事件的乱序到达。 3. 重排序缓冲区(Reordering Buffer):在某些场景下,可以使用重排序缓冲区来缓冲乱序的事件,并根据事件时间进行排序后再进行处理。这种方式需要维护一个缓冲区,并设置合适的缓冲时间窗口。 4. 侧输出流(Side Outputs):通过将乱序事件发送到侧输出流,可以将乱序事件和正常顺序的事件分开处理。这样可以针对乱序事件使用不同的处理逻辑。 5. 使用状态(State):在 Flink 中使用状态来保存事件的状态信息,可以在乱序事件到达时,根据事件的事件时间和当前状态进行排序和处理。 以上方法可以根据具体的业务需求和场景选择适合的解决方案。在实际应用中,可能需要结合多种方法来处理乱序问题
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值