Flink中时间概念
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 这一组合拳。
-
Watermark 的作用是防止 数据乱序 / 指定时间内获取不到全部数据。
-
allowLateNess 是将窗口关闭时间再延迟一段时间。
-
sideOutPut 是最后兜底操作,当指定窗口已经彻底关闭后,就会把所有过期延迟数据放到侧输出流,让用户决定如何处理。
总结起来就是:Windows -----> Watermark -----> allowLateNess -----> sideOutPut
用Windows把流数据分块处理,用Watermark确定什么时候不再等待更早的数据/触发窗口进行计算,用allowLateNess 将窗口关闭时间再延迟一段时间。用sideOutPut 最后兜底把数据导出到其他地方。
WaterMark的设定
由于种种原因造成数据的乱序与延迟,可以设置WaterMark等待一定时间,而且会在下一个窗口触发前,计算好上一个延迟的窗口。
WaterMark设定方法有两种:
-
Punctuated Watermark : 数据流中每一个递增的EventTime都会产生一个Watermark。在实际的生产中用Punctuated方式,在TPS很高的场景下,会产生大量的Watermark,在一定程度上造成了对下游算子造成压力。所以只有在实时性要求非常高的场景才会选择Punctuated的方式。
-
Periodic Watermark : 默认200毫秒为一周期(或达到一定的记录条数)产生一个Watermark。在实际的生产中使用Periodic的方式必须结合时间和积累条数两个维度,周期性的产生Watermark,否则在极端情况下会有很大的延时
设置WaterMark的延迟时间结合业务场景进行设置,理论上不能无限等待迟到的数据,因为这样会导致程序不够实时。设置的太早又会错过才迟到不久的数据。
设置WaterMark步骤
-
设置StreamTime Characteristic为Event Time,即设置流式时间窗口(也可以称为流式时间特性)
-
创建的DataStreamSource调用 assignTimestampsAndWatermarks 方法,并设置WaterMark种类:AssignerWithPeriodicWatermarks / AssignerWithPunctuatedWatermarks 或者实现其接口。
-
重写 getCurrentWatermark 与 extractTimestamp 方法:
- getCurrentWatermark方法:获取当前的水位线
- extractTimestamp方法:提取数据流中的时间戳(必须显式的指定数据中的Event Time)
程序说明
- 使用Socket模拟接收数据;
- 设置WaterMark:在第一条数据进来时,指定第一条数据的时间戳后,比较该时间戳与当前 WaterMark的最大值,并将最大值设置为下一条数据的WaterMark,以此类推;
- 进行map基础转换,将String转换为Tuple2<String,String>,根据Key分组;
- 使用滚动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的触发要符合以下几个条件:
- watermark时间 >= window_end_time
- 在[window_start_time,window_end_time)中有数据存在
同时满足了以上2个条件,window才会触发。
代码分析
水位线为0秒延迟的案例分析
long maxDelayAllowed = 0l;
.window(TumblingEventTimeWindows.of(Time.seconds(5)));
EventTime | Watermark | WindowStartTime | WindowEndTime | 备注 |
---|---|---|---|---|
2019-03-25 16:39:45 | 2019-03-25 16:39:45 | 2019-03-25 16:39:45 | 2019-03-25 16:39:50 | 窗口时间为5s,延迟时间为0s |
2019-03-25 16:39:46 | 2019-03-25 16:39:46 | ... | ... | |
2019-03-25 16:39:47 | 2019-03-25 16:39:47 | ... | ... | |
2019-03-25 16:39:48 | 2019-03-25 16:39:48 | |||
2019-03-25 16:39:49 | 2019-03-25 16:39:49 | |||
2019-03-25 16:39:50 | 2019-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)));
EventTime | Watermark | WindowStartTime | WindowEndTime | 备注 |
---|---|---|---|---|
2019-03-25 16:39:45 | 2019-03-25 16:39:40 | 2019-03-25 16:39:45 | 2019-03-25 16:39:50 | 窗口时间为5s,延迟时间为5s |
2019-03-25 16:39:46 | 2019-03-25 16:39:41 | ... | ... | |
2019-03-25 16:39:47 | 2019-03-25 16:39:42 | ... | ... | |
2019-03-25 16:39:48 | 2019-03-25 16:39:43 | |||
2019-03-25 16:39:49 | 2019-03-25 16:39:44 | |||
2019-03-25 16:39:50 | 2019-03-25 16:39:45 | |||
2019-03-25 16:39:46 | 2019-03-25 16:39:45 | |||
2019-03-25 16:39:47 | 2019-03-25 16:39:45 | |||
2019-03-25 16:39:51 | 2019-03-25 16:39:46 | |||
2019-03-25 16:39:52 | 2019-03-25 16:39:47 | |||
2019-03-25 16:39:53 | 2019-03-25 16:39:48 | |||
2019-03-25 16:39:54 | 2019-03-25 16:39:49 | |||
2019-03-25 16:39:55 | 2019-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…