(一)Flink WaterMark 详解及实例

3,727 阅读15分钟

Flink中时间概念

img

EventTime: 事件发生时间,是事件发生所在设备的当地时间,比如一个点击事件的时间发生时间,是用户点击操作所在的手机或电脑的时间。

IngestionTime:事件摄入时间,事件进入Flink的时间。

processTime:事件处理时间,事件被处理的时间,也就是由机器的系统时间来决定。

Flink流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,考虑其他时间属性。

Watermark解决乱序问题

我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。而乱序的产生,可以理解为数据到达的顺序和他的event-time排序不一致。导致这的原因有很多,比如延迟,消息积压,重试等等。特别是使用kafka的话,多个分区的数据无法保证有序。

在进行计算的时候,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark,watermark是用于处理乱序事件的。

Flink 流处理应用中常见的方案

聚合类的处理

Flink可以每来一个消息就处理一次,但是有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页,所以Flink引入了窗口概念。窗口的作用为了周期性的获取数据。就是把传入的原始数据流切分成多个buckets,所有计算都在单一的buckets中进行。

解决方案

聚合类处理带来了新的问题,比如乱序/延迟,其解决方案就是 Watermark / allowLateNess / sideOutPut 这一组合拳。

  1. Watermark 的作用是防止 数据乱序 / 指定时间内获取不到全部数据。

  2. allowLateNess 是将窗口关闭时间再延迟一段时间。

  3. sideOutPut 是最后兜底操作,当指定窗口已经彻底关闭后,就会把所有过期延迟数据放到侧输出流,让用户决定如何处理。

总结起来就是:Windows -----> Watermark -----> allowLateNess -----> sideOutPut

用Windows把流数据分块处理,用Watermark确定什么时候不再等待更早的数据/触发窗口进行计算,用allowLateNess 将窗口关闭时间再延迟一段时间。用sideOutPut 最后兜底把数据导出到其他地方。

WaterMark的设定

由于种种原因造成数据的乱序与延迟,可以设置WaterMark等待一定时间,而且会在下一个窗口触发前,计算好上一个延迟的窗口。

WaterMark设定方法有两种:

  1. Punctuated Watermark : 数据流中每一个递增的EventTime都会产生一个Watermark。在实际的生产中用Punctuated方式,在TPS很高的场景下,会产生大量的Watermark,在一定程度上造成了对下游算子造成压力。所以只有在实时性要求非常高的场景才会选择Punctuated的方式。

  2. Periodic Watermark : 默认200毫秒为一周期(或达到一定的记录条数)产生一个Watermark。在实际的生产中使用Periodic的方式必须结合时间和积累条数两个维度,周期性的产生Watermark,否则在极端情况下会有很大的延时

设置WaterMark的延迟时间结合业务场景进行设置,理论上不能无限等待迟到的数据,因为这样会导致程序不够实时。设置的太早又会错过才迟到不久的数据。

设置WaterMark步骤

  1. 设置StreamTime Characteristic为Event Time,即设置流式时间窗口(也可以称为流式时间特性)

  2. 创建的DataStreamSource调用 assignTimestampsAndWatermarks 方法,并设置WaterMark种类:AssignerWithPeriodicWatermarks / AssignerWithPunctuatedWatermarks 或者实现其接口。

  3. 重写 getCurrentWatermark 与 extractTimestamp 方法:

  • getCurrentWatermark方法:获取当前的水位线
  • extractTimestamp方法:提取数据流中的时间戳(必须显式的指定数据中的Event Time)

程序说明

  1. 使用Socket模拟接收数据;
  2. 设置WaterMark:在第一条数据进来时,指定第一条数据的时间戳后,比较该时间戳与当前 WaterMark的最大值,并将最大值设置为下一条数据的WaterMark,以此类推;
  3. 进行map基础转换,将String转换为Tuple2<String,String>,根据Key分组;
  4. 使用滚动Event Time窗口,将5秒内的同组数据,进行Fold拼接输出。

代码实现

数据顺序的场景模拟

package watermark;

import org.apache.flink.api.common.functions.FoldFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import java.text.SimpleDateFormat;

/**
 * @Author Natasha
 * @Description
 * @Date 2020/11/6 15:26
 **/

public class HelloWaterMark {
    public static void main(String[] args) throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");

        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        DataStream<String> dataStream = env.socketTextStream("127.0.0.1", 9999)
            .assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<String>() {
                long currentTimeStamp = 0l;
                long maxDelayAllowed = 0l;
                long currentWaterMark;
                @Override
                public Watermark getCurrentWatermark() {
                    currentWaterMark = currentTimeStamp - maxDelayAllowed;
                    return new Watermark(currentWaterMark);
                }
                @Override
                public long extractTimestamp(String s, long l) {
                    String[] arr = s.split(",");
                    long timeStamp = Long.parseLong(arr[1]);
                    currentTimeStamp = Math.max(timeStamp, currentTimeStamp);
                    System.out.println("Key:" + arr[0] + ",EventTime: " + sdf.format(timeStamp) + ",上一条数据的水位线: " + sdf.format(currentWaterMark));
                    return timeStamp;
                }
            });

        dataStream
            .map(new MapFunction<String, Tuple2<String, String>>() {
                @Override
                public Tuple2<String, String> map(String s) throws Exception {
                    return new Tuple2<>(s.split(",")[0], s.split(",")[1]);
                }
            })
            .keyBy(0)
            .window(TumblingEventTimeWindows.of(Time.seconds(5)))
            .fold("Start:", new FoldFunction<Tuple2<String, String>, String>() {
                @Override
                public String fold(String s, Tuple2<String, String> o) throws Exception {
                    return s + " - " + o.f1;
                }
            })
            .print();

        env.execute("WaterMark Test Demo");
    }
}

先启动netca监听9999端口,再启动Flink程序,并向端口监听终端输入以下内容:

HelloWaterMark,1553503185000
HelloWaterMark,1553503186000
HelloWaterMark,1553503187000
HelloWaterMark,1553503188000
HelloWaterMark,1553503189000
HelloWaterMark,1553503190000

Flink输出结果:

Key:HelloWaterMark,EventTime: 2019-03-25 16:39:45 , 上一条数据的水位线: 1970-01-01 08:00:00
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:46 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:47 , 上一条数据的水位线: 2019-03-25 16:39:46
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:48 , 上一条数据的水位线: 2019-03-25 16:39:47
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:49 , 上一条数据的水位线: 2019-03-25 16:39:48
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:50 , 上一条数据的水位线: 2019-03-25 16:39:49
3> Start: - 1553503185000 - 1553503186000 - 1553503187000 - 1553503188000 - 1553503189000

通过结果可以发现(可以自已打debug调试就很清楚了),Flink在指定WaterMark时,先调用extractTimestamp方法,再调用getCurrentWatermark方法, 所以打印信息中的WaterMark为上一条数据的WaterMark,并非当前的WaterMark。

数据乱序的场景模拟

上面的实例,Event Time的时间戳都是有序,现在来做一下数据乱序的场景模拟:

启动程序,在监听终端中输入数据,其中,在触发了第一个窗口计算后,又来了两条迟到数据hello,1553503187000,hello,1553503186000

HelloWaterMark,1553503185000
HelloWaterMark,1553503186000
HelloWaterMark,1553503187000
HelloWaterMark,1553503188000
HelloWaterMark,1553503189000
HelloWaterMark,1553503190000
HelloWaterMark,1553503187000
HelloWaterMark,1553503186000
HelloWaterMark,1553503191000
HelloWaterMark,1553503192000
HelloWaterMark,1553503193000
HelloWaterMark,1553503194000
HelloWaterMark,1553503195000

Flink结果:

Key:HelloWaterMark,EventTime: 2019-03-25 16:39:45 , 上一条数据的水位线: 1970-01-01 07:59:55
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:46 , 上一条数据的水位线: 2019-03-25 16:39:40
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:47 , 上一条数据的水位线: 2019-03-25 16:39:41
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:48 , 上一条数据的水位线: 2019-03-25 16:39:42
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:49 , 上一条数据的水位线: 2019-03-25 16:39:43
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:50 , 上一条数据的水位线: 2019-03-25 16:39:44
3> Start: - 1553503186000 - 1553503187000 - 1553503185000 - 1553503188000 - 1553503189000
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:47 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:46 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:51 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:52 , 上一条数据的水位线: 2019-03-25 16:39:46
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:53 , 上一条数据的水位线: 2019-03-25 16:39:47
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:54 , 上一条数据的水位线: 2019-03-25 16:39:48
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:55 , 上一条数据的水位线: 2019-03-25 16:39:49
3> Start: - 1553503190000 - 1553503193000 - 1553503192000 - 1553503194000 - 1553503191000
乱序时间的设置

为了解决上面的问题,我们允许Flink处理延迟以5秒内的迟到数据:

//修改最大乱序时间
long maxDelayAllowed = 5000l;

Flink输出结果:

Key:HelloWaterMark,EventTime: 2019-03-25 16:39:45 , 上一条数据的水位线: 1970-01-01 07:59:55
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:46 , 上一条数据的水位线: 2019-03-25 16:39:40
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:47 , 上一条数据的水位线: 2019-03-25 16:39:41
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:48 , 上一条数据的水位线: 2019-03-25 16:39:42
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:49 , 上一条数据的水位线: 2019-03-25 16:39:43
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:50 , 上一条数据的水位线: 2019-03-25 16:39:44
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:47 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:46 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:51 , 上一条数据的水位线: 2019-03-25 16:39:45
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:52 , 上一条数据的水位线: 2019-03-25 16:39:46
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:53 , 上一条数据的水位线: 2019-03-25 16:39:47
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:54 , 上一条数据的水位线: 2019-03-25 16:39:48
Key:HelloWaterMark,EventTime: 2019-03-25 16:39:55 , 上一条数据的水位线: 2019-03-25 16:39:49
3> Start: - 1553503185000 - 1553503186000 - 1553503187000 - 1553503188000 - 1553503189000 - 1553503187000 - 1553503186000

结论

window的触发要符合以下几个条件:

  1. watermark时间 >= window_end_time
  2. 在[window_start_time,window_end_time)中有数据存在

同时满足了以上2个条件,window才会触发。

代码分析

水位线为0秒延迟的案例分析

long maxDelayAllowed = 0l;
.window(TumblingEventTimeWindows.of(Time.seconds(5)));
EventTimeWatermarkWindowStartTimeWindowEndTime备注
2019-03-25 16:39:452019-03-25 16:39:452019-03-25 16:39:452019-03-25 16:39:50窗口时间为5s,延迟时间为0s
2019-03-25 16:39:462019-03-25 16:39:46......
2019-03-25 16:39:472019-03-25 16:39:47......
2019-03-25 16:39:482019-03-25 16:39:48
2019-03-25 16:39:492019-03-25 16:39:49
2019-03-25 16:39:502019-03-25 16:39:50当watermark时间 >= window_end_time时触发第一个窗口为5s的fold计算:Start: - 1553503185000 - 1553503186000 - 1553503187000 - 1553503188000 - 1553503189000

水位线为5秒延迟的案例分析

long maxDelayAllowed = 5000l;
.window(TumblingEventTimeWindows.of(Time.seconds(5)));
EventTimeWatermarkWindowStartTimeWindowEndTime备注
2019-03-25 16:39:452019-03-25 16:39:402019-03-25 16:39:452019-03-25 16:39:50窗口时间为5s,延迟时间为5s
2019-03-25 16:39:462019-03-25 16:39:41......
2019-03-25 16:39:472019-03-25 16:39:42......
2019-03-25 16:39:482019-03-25 16:39:43
2019-03-25 16:39:492019-03-25 16:39:44
2019-03-25 16:39:502019-03-25 16:39:45
2019-03-25 16:39:462019-03-25 16:39:45
2019-03-25 16:39:472019-03-25 16:39:45
2019-03-25 16:39:512019-03-25 16:39:46
2019-03-25 16:39:522019-03-25 16:39:47
2019-03-25 16:39:532019-03-25 16:39:48
2019-03-25 16:39:542019-03-25 16:39:49
2019-03-25 16:39:552019-03-25 16:39:50当watermark时间 >= window_end_time时触发第一个窗口为5s的fold计算:Start: - 1553503186000 - 1553503188000 - 1553503186000 - 1553503187000 - 1553503185000 - 1553503189000 - 1553503187000;于是延迟的两条数据我们也收到了。

可以看到,设置了最大允许乱序时间后,WaterMark要比原来低5秒,可以对延迟5秒内的数据进行处理,窗口的触发条件也同样会往后延迟。

github

本文章中相关代码样例已上传 github :github.com/ShawnVanorG…