前言
在前面的时间窗口中,我们初步使用了ProcesstimeWindow
以及EventTimeWindow
,我们注意到,在使用事件时间窗口的时候,系统提示我们要么设置WaterMarker
,要么将时间处理模式切换为ProcessTime
完整错误信息:
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
,那么接下来开启的时间窗口开启与范围大致如下所示
但是呢,可能我们生产端发送到MQ中本身就已经乱序了(由于网络啊、接口性能等等),导致我们读取的数据也是乱序的
例如:现在从MQ中读到的数据中的时间如下
由于F 已经是00:11
时间了,则会立即触发窗口计算,后续的数据 G H由于归属的窗口
已经被执行,G H数据就会被丢弃,会被忽略计算。
上面这种情况呢,便是由于各种原因,导致数据乱序,进而产生了错误的计算结果;这种情况,在绝大多数场景下我们是不能接受的!
为了解决根据事件时间计算可能会产生这种问题,Flink 提供了WaterMarker机制,利用一定的延迟容忍,可一定程度上避免因消息乱序导致的错误计算或者数据丢失。
(二)什么是WaterMarker
①
为了处理事件时间,Flink首先需要知道事件本身的的时间戳,这意味着流中的每个元素都需要其携带事件时间;且时间属性需要为时间戳
,WaterMarker
是Flink提供的一种水印机制,实质就是根据事件元素的事件时间,进行处理后生成一个新的时间属性(也是时间戳),然后使用该水印进而更改窗口的计算触发时机
②
既然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.窗口中存在事件元素(空窗口不会触发)
2.WaterMarker大于等于窗口结束时间
(四)WaterMarker触发详解
例如,现在我们有了一个[12:00:00-12:00:10)
的时间窗口,现在事件如下图所示顺序ABCDEF…到达
在未设置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
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可以在一定程度上解决事件乱序问题,但严重的乱序问题依然无法解决!
例如,在事件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为当前事件流中元素最大事件时间 - 最大允许的延迟时间或乱序时间
如上图所示,我们的定位事件目前处于一个乱序状态(定位事件并未从小到大排序输出)
上图中 ① 是时间窗口[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
① 窗口直到数据到来才触发了计算!
我们可以按照(四)WaterMarker 触发详解进行推理
第一个元素 devTime=1622124773824
即2021-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=1622124799863
即2021-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提供的一种水印机制,实质就是根据事件元素的事件时间(必须为时间戳),进行处理后生成一个新的时间属性(也是时间戳),然后使用该水印进而更改窗口的计算触发时机
2、WaterMarker值最终决定属性
-
单数据流情况下:WaterMarker=当前事件流中元素最大事件时间 - 最大允许的延迟时间或乱序时间
-
多数据流情况下:WaterMarker=当前当前事件流中元素最小事件时间 - 最大允许的延迟时间或乱序时间(木桶短板效应)
3、watermark是一个全局的值,不是某一个key下的值,所以即使不是同一个key的数据,其watermark也会不断增加
4、WaterMarker可以在一定程度上解决事件乱序问题,但严重的乱序问题依然无法解决!
5、不可过度依赖WaterMarker帮助我们解决乱序问题,如果发生过多乱序问题应注重检查生产数据的生产端问题!