文章目录
(一)前言-回顾WaterMaker
在前边的文章中,我们已经将了WaterMaker
的实质与使用WaterMaker
来解决一定程度上因乱序问题造成的数据丢失
快速跳转:Flink时间窗口-WaterMaker
Ex:如果设置基于事件时间的10s滚动窗口,且数据来源是如下所示
未设置WaterMaker
情况:
元素C触发窗口[12:00:00-12:00:10)
执行,导致后续的D、F、H丢失
未设置WaterMaker
延迟时间为5s情况:
元素G触发窗口[12:00:00-12:00:10)
执行,导致后续的H丢失丢失
总结:WaterMaker可以在一定程度上(由允许延迟时间设置与数据滞后性决定)解决事件乱序问题,但严重的乱序问题依然无法解决!
可以看到,我们无论如何设置WaterMaker,由于数据滞后粒度的不确定性,或多或少仍会造成数据丢失,那么这个时候呢,如何解决呢?一共有两种方案!
- 排查数据生产端消息滞后原因(治本)
- 使用
allowd-lateness
设置WaterMaker后窗口触发时机?
watermaker
大于等于end-of-window(窗口结束时间)
设置WaterMaker后,什么情况下数据会被丢弃或者说不会被计算
计算job设置了watermaker
,但未设置allowedLateness
情况下(默认值为0),假设某条延迟数据属于某个窗口,但是watermark
大于等于了窗口的结束时间且触发过该窗口计算了,则该条数据会被丢弃;
(二)前言-回顾窗口生命周期
一旦应属于该窗口的第一个元素到达,就会创建一个窗口,并且当时间(事件时间、处理时间、摄入时间)超过其结束时间戳(end-of-time)(如果设置了WaterMaker,WaterMaker触发窗口执行后)且计算逻辑执行后 ,该窗口将被完全删除,Flink只会删除基于时间的窗口。
Ex:采用基于处理时间的开窗策略,该策略每5分钟创建一次不重叠(或翻滚)的窗口
现在第一个元素的处理时间为2021-05-26:12:00,那么Flink会创建一个2021-05-26 12:00 - 2021-05-26 12:05
的时间窗口,当处理时间大于等于12:05时,便会触发该窗口进行计算,且计算后将此窗口删除,后续在无法打开2021-05-26 12:00 - 2021-05-26 12:05
这个窗口
时间窗口默认trigger是默认的EventTimeTrigger,其决定了一个时间窗口什么时候被触发(无WaterMaker则根据事件时间,有WaterMaker则根据WaterMaker)
(三)Allowed-Lateness的作用
因为processingTime
不会存在延时的情况,故而AllowedLateness
只针对eventTime
有效。
默认情况下,当watermark
大于等于某窗口的end-of-window
(结束时间)时便会触发window计算,激活window计算结束之后,再有之前的数据到达时,这些数据会被删除。
为了避免在设置了WaterMaker后,仍有些迟到的数据被删除,因此产生了allowedLateness,通过使用allowedLateness来延迟销毁窗口,允许有一段时间(也是以event time来衡量)来等待之前的数据到达,以便再次处理这些数据!!!!
上方的再次处理是什么意思呢?就是将延迟数据扔进之前的窗口,且将该窗口再次执行计算!
如果设置了allowedLateness,当延迟数据来到时会再次触发之前窗口(延迟数据事件时间归属的窗口)的计算,而之前触发的数据,会buffer起来,直到watermark超过end-of-window + allowedLateness的时间,窗口的数据及元数据信息才会被删除。
(四)WaterMaker与Allowed-Lateness区别
- watermark 通过时间戳来控制窗口触发时机,主要是为了解决数据乱序到达的问题
- allowedLateness 用来控制窗口的销毁时间,解决窗口计算销毁后,延迟数据到来,被丢弃的问题
- 我们需要将二者结合使用,才可进一步保证延迟数据的不丢失,以及进一步保证窗口计算的准确性(来的数据更全了)
(五)Allowed-Lateness使用实战
示例:开启一个基于事件时间的,10s滚动窗口,且设置WaterMaker,容忍5s内的数据,在设置allowedLateness 允许延迟时间为10s
//设置水位线 允许延迟为5秒
SingleOutputStreamOperator<Location> watermarks = locationSource.
assignTimestampsAndWatermarks(WatermarkStrategy.
//水位线延迟时间设为5 即接受5秒钟内的延迟数据
<Location>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getDevTime()));
//时间滚动窗口 十秒计算一次
SingleOutputStreamOperator<String> resultStream = watermarks.keyBy(Location::getVehicleId)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
// allowedLateness允许延迟时间
.allowedLateness(Time.seconds(5))
.apply(new AlarmCalcWindow());
resultStream.print();
如此设置,执行示例如下:
解释:
前提:单数据流未KeyBy前设置了WaterMaker,允许延迟时间为5s,故此WaterMaker=当前事件流最大数据时间时间-设置延迟时间(5s)
上示例:仅设置了WaterMaker,元素G到来(此时waterMaker=12:00:15-5=12:00:10),触发窗口计算后,窗口被销毁…H数据丢失
下示例:设置了WaterMaker,且设置了Allowed-lateness时间为5s,元素G到来(此时waterMaker=12:00:15-5=12:00:10),触发窗口计算,计算完成后,窗口并不会销毁,因为设置了Allowed-lateness,此时WaterMaker不满足窗口销毁条件,WaterMaker需要大于窗口结束时间+Allowed-lateness延迟允许时间,(即WaterMaker需要为12:00:10+5=12:00:15时)窗口才会移除,说白了,就是需要一个事件时间为12:00:20的数据到来,才会移除掉[12:00:00-12:00:10)窗口,事件时间为12:00:21元素到来之前,只要有一个事件事时间归属在[12:00:00-12:00:10)窗口,都会拿去到之前该窗口中的元素缓存,重新触发计算!!
代码执行结果展示
上图代码执行结果示例中,我们可以看到,窗口[1622968770000,1622968780000)
进行了多次的开启计算操作!且能清晰的看到后边开启的窗口获取到了上一次该窗口计算时的元素,验证了(当延迟数据来到时会再次触发之前窗口(延迟数据事件时间归属的窗口)的计算,而之前触发的数据,会buffer起来)
细心的小伙伴可能已经注意到了,我print sink输出后,还打印出了 31 32 33 34…这些数字,这些数字是什么呢?
这些是,我定义在时间窗口中的状态-键控状态MapState
随着延迟数据的到来,窗口的不断重复开启,而我们的算子状态也在不停的累计,而且如果窗口像我一样,一条一条转换后输出,未作聚合处理,也意味着,我们sink的数据也有重复的!这一特点,需要格外的注意!!!!
(六)Allowed-Lateness总结与注意事项
-
Flink提供了allowedLateness方法,allowedLateness只针对Event Time有效,在WaterMaker一定程度允许延迟数据的情况下,进一步了处理乱序乱序数据的问题!
-
allowedLateness主要是改变了窗口的销毁时机与对上次该窗口数据做一个缓存操作,但这可能使得窗口再次(多次)被触发,相当于对前一次窗口的窗口的不断修正(累加计算或者累加撤回计算);
-
注意再次触发窗口时,状态值会累加,要考虑state在计算时的去重问题。
-
注意再次触发窗口时,同一个key的同一个window结果可能被sink多次(触发多少次补偿计算则会输出多少次sink),因此sink接收端需要注意去重问题
(7)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.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
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.RichWindowFunction;
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;
/**
* @author lei
* @version 1.0
* @date 2021/3/17 20:49
* @desc flink 使用 watermaker水位线 +allowedLateness 双重延迟数据保护策略
*/
public class AllowedLateness {
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.
//水位线延迟时间设为5 即接受5秒钟内的延迟数据
<Location>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getDevTime()));
//时间滚动窗口 十秒计算一次
SingleOutputStreamOperator<String> resultStream = watermarks.keyBy(Location::getVehicleId)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
// allowedLateness允许延迟时间
.allowedLateness(Time.seconds(5))
.apply(new AlarmCalcWindow());
resultStream.print();
env.execute();
}
public static class LocationSource implements SourceFunction<Location> {
Boolean flag = true;
@Override
public void run(SourceContext<Location> ctx) throws Exception {
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);
Thread.sleep(2000);
}
}
@Override
public void cancel() {
flag = false;
}
}
/**
* 自定义窗口
*/
public static class AlarmCalcWindow extends RichWindowFunction<Location, String, Integer, TimeWindow> {
MapState<String, Integer> mapState;
@Override
public void apply(Integer key, TimeWindow window, Iterable<Location> input, Collector<String> out) throws Exception {
System.out.println(String.format("窗口执行--开始时间:%s-------结束时间%s", window.getStart(), window.getEnd()));
//todo 迭代器元素根据时间排序
for (Location location : input) {
String s = location.getPlate() + location.getColor();
Integer value = mapState.get(s);
if (value == null) {
mapState.put(s, 1);
} else {
mapState.put(s, mapState.get(s) + 1);
}
out.collect(JSON.toJSONString(location));
System.out.println(mapState.get(location.getPlate() + location.getColor()));
}
}
@Override
public void open(Configuration parameters) {
mapState = getRuntimeContext().getMapState(new MapStateDescriptor<>("test",
TypeInformation.of(String.class), TypeInformation.of(Integer.class)));
}
}
}