FLink-12-Flink Window窗口相关概念-allowLateness机制&窗口分类&Trigger&Evictor
窗口计算 API
使用事件时间机制的窗口之前,必须要在前置算子中生成waterMark,然后窗口才起作用,不然窗口设置了无效,没有触发机制。处理时间机制的窗口,可以不关注。
1.窗口(Window)概念
窗口,就是把无界的数据流,依据一定规则划分成一段一段的有界数据流来计算; 既然划分成有界数据段,通常都是为了"聚合";
滚动窗口 等价于 滑动窗口的一个特例(窗口长度=滑动步长)
窗口的分类维度:
1.按时间维度划分 :滚动窗口、滑动窗口、回话session窗口
2.按照数据条数划分:滚动窗口、滑动窗口
3.按照上游算子是否为keyedStream类型算子划分:NonKeyedWindow 和 KeyedWindow。
Keyedwindow 重要特性:任何一个窗口,都绑定在自己所属的 key 上;不同 key 的数据肯定不会划分 到相同窗口中去!
2.allowLateness机制
- flink中有允许迟到机制(allowLateness机制),当一个窗口已经触发完毕后,又来了一条数据,正好在之前刚刚计算过的那个桶里面,这时候如果有允许迟到机制,那么刚刚计算完毕的那个桶还会保留一段时间(这个时间就是迟到机制里面设置的时间),在这个时间里面,此条数据会被被继续放入刚计算完毕的那个桶里面,等到设置的时间完毕后,再触发一次窗口计算,此时的值,就会由3变成4。
- 具体代码案例如下:
- 样例数据中最后一条是迟到数据,然后触发完窗口后,又重新触发了一次窗口
- 程序运算结果如下,统计结果又3变为4;
- 允许迟到时间为2秒,这两秒的解释:当数据流中事件时间到达了上一个窗口的最大值+2S之前,也就是waterMark中记录的时间,如果这2秒内一直有数据过来,waterMark还没有更新到容错时间的最大值,不管来多少条数据都会一直触发之前窗口的逻辑进行计算;如果waterMark时间更新到容错时间之后,符合之前窗口时间的迟到数据就不会被触发了。这就是允许迟到的最大容错时间。
- 需要注意的是:都是以waterMark中记录的时间为标准,来判断容错时间是多少,如果waterMark一直不变,容错时间内的数据会一直被触发。
- waterMark中设置的容错时间和allowlateness设置的容错时间,对应的窗口触发的频率不同,前者是触发一次窗口,后者来多少条数据,触发多少次窗口。
- allowedLatenss(2) 的解释:如果waterMark(此刻的事件时间)推进到了A窗口结束点后2秒,如果还来A窗口的数据,就算迟到,就不会再去触发A窗口的计算逻辑,而是输出到迟到的侧流中。
- 样例数据中最后一条是迟到数据,然后触发完窗口后,又重新触发了一次窗口
- 具体代码如下:
package com.yang.flink.window;
import com.yang.flink.vo.EventBean2;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
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.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.*;
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 org.apache.flink.util.OutputTag;
import java.time.Duration;
public class WindowApiAllowedLatenessDemo3 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
// 1,e01,3000,pg02
DataStreamSource<String> source = env.socketTextStream("localhost", 9999);
SingleOutputStreamOperator<Tuple2<EventBean2,Integer>> beanStream = source.map(s -> {
String[] split = s.split(",");
EventBean2 bean = new EventBean2(Long.parseLong(split[0]), split[1], Long.parseLong(split[2]), split[3], Integer.parseInt(split[4]));
return Tuple2.of(bean,1);
}).returns(new TypeHint<Tuple2<EventBean2, Integer>>() {})
.assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<EventBean2,Integer>>forBoundedOutOfOrderness(Duration.ofMillis(0))
.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<EventBean2,Integer>>() {
@Override
public long extractTimestamp(Tuple2<EventBean2,Integer> element, long recordTimestamp) {
return element.f0.getTimeStamp();
}
}));
OutputTag<Tuple2<EventBean2,Integer>> lateDataOutputTag = new OutputTag<>("late_data", TypeInformation.of(new TypeHint<Tuple2<EventBean2, Integer>>() {}));
SingleOutputStreamOperator<String> sumResult = beanStream.keyBy(tp -> tp.f0.getGuid())
.window(TumblingEventTimeWindows.of(Time.seconds(10))) // 事件时间滚动窗口,窗口长度为10
.allowedLateness(Time.seconds(2)) // 允许迟到2s
.sideOutputLateData(lateDataOutputTag) // 迟到超过允许时限的数据,输出到该“outputtag”所标记的测流
/*.sum("f1")*/
.apply(new WindowFunction<Tuple2<EventBean2, Integer>, String, Long, TimeWindow>() {
@Override
public void apply(Long aLong, TimeWindow window, Iterable<Tuple2<EventBean2, Integer>> input, Collector<String> out) throws Exception {
int count = 0;
for (Tuple2<EventBean2, Integer> eventBean2IntegerTuple2 : input) {
count ++;
}
out.collect(window.getStart()+":"+ window.getEnd()+","+count);
}
});
DataStream<Tuple2<EventBean2, Integer>> lateDataSideStream = sumResult.getSideOutput(lateDataOutputTag);
sumResult.print("主流结果");
lateDataSideStream.print("迟到数据");
env.execute();
}
}
3.Flink乱序数据处理相关总结
- Flink乱序相关总结:
- 1.小乱序,利用waterMark的容错时间来解决
- 2.中乱序,利用窗口允许迟到机制 allowLateness机制
- 3.大乱序,利用窗口中的 迟到数据侧流输出机制 sideOutputLateData
- 符合怎样条件标准的数据才是输出到“迟到侧流”中?
- 超过了waterMark和allowLateness的容错机制的数据,都会被丢弃到侧流里面。
4.滚动聚合算子和全窗口聚合算子的区别
窗口聚合算子,整体上分为两类
- 增量滚动聚合算子,如 min、max、minBy、maxBy、sum、reduce、aggregate
- 全量聚合算子,如 apply、process
- 两个聚合算子各有优缺点:
- 1.增量滚动聚合算子的优点:数据在内存中占用 容量少,只存储最终结果。
- 2.全量聚合算子的优点:灵活度高,可支持拓展的功能多。
- 3.两个算子的选择,需要根据用户的需求来进行适当的选择。
5.窗口指派的API
- 滚动聚合函数,在写代码的时候,每次只能拿到一条数据,和当前的累加器数据进行操作。
- keyedWindow算子,底层逻辑的核心要点:窗口是和自己的Key绑定的,不会把不同的key的数据放到同一个窗口中。
- apply全窗口算子和process窗口聚合算子的区别,process窗口继承了rich丰富类,里面的方法比apply全窗口算子多一些,与之前process系列的一样。
- 滚动窗口以及滑动窗口的选择需要根据业务需求来进行选择
- flink中默认的序列化器,不是用jdk的serializerable,默认使用的avro序列化器。
/**
* NonKeyed 窗口,全局窗口
*/
// 处理时间语义,滚动窗口
source.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5)));
// 处理时间语义,滑动窗口
source.windowAll(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(1)));
// 事件时间语义,滚动窗口
source.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));
// 事件时间语义,滑动窗口
source.windowAll(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(1)));
// 计数滚动窗口
source.countWindowAll(100);
// 计数滑动窗口
source.countWindowAll(100,20);
/**
* Keyed 窗口
*/
KeyedStream<String, String> keyedStream = source.keyBy(s -> s);
// 处理时间语义,滚动窗口
keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(5)));
// 处理时间语义,滑动窗口
keyedStream.window(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(1)));
// 事件时间语义,滚动窗口
keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
// 事件时间语义,滑动窗口 keyedStream.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(1)));
// 计数滚动窗口
keyedStream.countWindow(1000);
// 计数滑动窗口
keyedStream.countWindow(1000,100);
6.窗口的触发机制 Trigger
触发的机制:数据达到的时候会去检查,时间事件触发的时候会去检查
7.Evictor 窗口数据移除机制
-
窗口触发前,或者触发后,对窗口中的数据移除的机制;
-
算子在调用Trigger后,发现满足触发条件时:
- 就会先去调用Evictor的evictBefore方法 来进行移除,
- 然后进行计算;
- 计算完成后,还回去再调用Evictor的evictAfter()方法,来进行数据清理。
-
自定义窗口触发机制和自定义窗口移除机制具体代码案例如下:
package com.yang.flink.window;
import com.yang.flink.vo.EventBean2;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.java.tuple.Tuple2;
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.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.evictors.Evictor;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.triggers.Trigger;
import org.apache.flink.streaming.api.windowing.triggers.TriggerResult;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.streaming.runtime.operators.windowing.TimestampedValue;
import org.apache.flink.util.Collector;
import java.time.Duration;
import java.util.Iterator;
/**
* window窗口API- Triger触发器 和 Evictor窗口移除机制
* 产品需求:统计最近10S,每个用户出现的次数
* 除了在正常的10S窗口时间到达时触发窗口,还有另外一个特别的需求:
* 遇到用户的行为是e0x,马上触发一次窗口计算,
* 并且每次触发时不要包含其中的e0x这条数据
*
* 测试数据
* 1,e01,10000,p01,10 [10,20)
* 1,e02,11000,p02,20
* 1,e02,12000,p03,40
* 1,e0x,13000,p03,40 ==> 这里会触发一次窗口
* 1,e04,16000,p05,50
* 1,e03,20000,p02,10 [20,30) ==> 时间到达也会触发一次窗口
*/
public class WindowApiTrigerEvictorDemo4 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//获取原始数据
DataStreamSource<String> streamSource = env.socketTextStream("hadoop102", 9999);
//将数据样例:1,e01,10000,p01,10,进行拆分后封装为对象
SingleOutputStreamOperator<Tuple2<EventBean2, Integer>> beanStream = streamSource.map(bean -> {
String[] arr = bean.split(",");
EventBean2 eventBean2 = new EventBean2(Long.parseLong(arr[0]), arr[1], Long.parseLong(arr[2]), arr[3], Integer.parseInt(arr[4]));
return Tuple2.of(eventBean2, 1);
}).returns(new TypeHint<Tuple2<EventBean2, Integer>>() {
});
//给beanStream流,设置waterMark并指定事件时间是哪一个字段
beanStream.assignTimestampsAndWatermarks(WatermarkStrategy
.<Tuple2<EventBean2, Integer>>forBoundedOutOfOrderness(Duration.ofMillis(5000))
.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<EventBean2, Integer>>() {
@Override
public long extractTimestamp(Tuple2<EventBean2, Integer> element, long recordTimestamp) {
return element.f0.getTimeStamp();
}
})
);
//数据按照用户guid进行keyby分组,并开窗计算数据,按照需求选择窗口类型为滚动窗口,开窗时间为10S
beanStream.keyBy(data ->data.f0.getGuid())
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
//按照需求要求:遇到用户的行为是e0x,马上触发一次窗口计算,设置自定义触发器Trigeer
.trigger(MyEventTimeTrigger.create())
//按照需求要求:每次触发时不要包含其中的e0x这条数据,设置自定义移除机制Evictor
.evictor(MyTimeEvictor.of(Time.seconds(10)))
//按照需求要求:统计最近10S,每个用户出现的次数,使用全量窗口机制apply,来进行数据聚合
.apply(new WindowFunction<Tuple2<EventBean2, Integer>, String, Long, TimeWindow>() {
@Override
public void apply(Long aLong, TimeWindow window, Iterable<Tuple2<EventBean2, Integer>> input, Collector<String> out) throws Exception {
int count = 0;
for (Tuple2<EventBean2, Integer> eventBean2IntegerTuple2 : input) {
count++;
}
out.collect("window_start:"+window.getStart()+","+ "window_end:"+window.getEnd()+",该用户出现的次数:"+count);
}
});
env.execute();
}
}
class MyTimeEvictor implements Evictor<Object, TimeWindow> {
private static final long serialVersionUID = 1L;
private final long windowSize;
private final boolean doEvictAfter;
public MyTimeEvictor(long windowSize) {
this.windowSize = windowSize;
this.doEvictAfter = false;
}
public MyTimeEvictor(long windowSize, boolean doEvictAfter) {
this.windowSize = windowSize;
this.doEvictAfter = doEvictAfter;
}
/**
* 窗口触发前,调用
*/
@Override
public void evictBefore(
Iterable<TimestampedValue<Object>> elements, int size, TimeWindow window, EvictorContext ctx) {
if (!doEvictAfter) {
evict(elements, size, ctx);
}
}
/**
* 窗口触发后,调用
*/
@Override
public void evictAfter(
Iterable<TimestampedValue<Object>> elements, int size, TimeWindow window, EvictorContext ctx) {
if (doEvictAfter) {
evict(elements, size, ctx);
}
}
/**
* 元素移除的核心逻辑
*/
private void evict(Iterable<TimestampedValue<Object>> elements, int size, EvictorContext ctx) {
if (!hasTimestamp(elements)) {
return;
}
long currentTime = getMaxTimestamp(elements);
long evictCutoff = currentTime - windowSize;
for (Iterator<TimestampedValue<Object>> iterator = elements.iterator();
iterator.hasNext(); ) {
TimestampedValue<Object> record = iterator.next();
Tuple2<EventBean2,Integer> tuple = (Tuple2<EventBean2, Integer>) record.getValue();
// 加了一个条件: 数据的eventId=e0x,也移除
if (record.getTimestamp() <= evictCutoff || tuple.f0.getEventId().equals("e0x")) {
iterator.remove();
}
}
}
private boolean hasTimestamp(Iterable<TimestampedValue<Object>> elements) {
Iterator<TimestampedValue<Object>> it = elements.iterator();
if (it.hasNext()) {
return it.next().hasTimestamp();
}
return false;
}
/**
* 用于计算移除的时间截止点
*/
private long getMaxTimestamp(Iterable<TimestampedValue<Object>> elements) {
long currentTime = Long.MIN_VALUE;
for (Iterator<TimestampedValue<Object>> iterator = elements.iterator();
iterator.hasNext(); ) {
TimestampedValue<Object> record = iterator.next();
currentTime = Math.max(currentTime, record.getTimestamp());
}
return currentTime;
}
public static MyTimeEvictor of(Time windowSize) {
return new MyTimeEvictor(windowSize.toMilliseconds());
}
}
class MyEventTimeTrigger extends Trigger<Tuple2<EventBean2, Integer>, TimeWindow>{
private MyEventTimeTrigger() {}
/**
* 来一条数据时,需要检查watermark是否已经越过窗口结束点需要触发
* 需求:遇到用户的行为是e0x,马上触发一次窗口计算
*/
@Override
public TriggerResult onElement(Tuple2<EventBean2, Integer> element, long timestamp, TimeWindow window, TriggerContext ctx) throws Exception {
// 如果窗口结束点 <= 当前的watermark
if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
// if the watermark is already past the window fire immediately
return TriggerResult.FIRE;
} else {
// 注册定时器,定时器的触发时间为: 窗口的结束点时间
ctx.registerEventTimeTimer(window.maxTimestamp());
// 判断,当前数据的用户行为事件id是否等于e0x,如是,则触发
if("e0x".equals(element.f0.getEventId())) return TriggerResult.FIRE;
return TriggerResult.CONTINUE;
}
}
@Override
public TriggerResult onEventTime(long time, TimeWindow window, TriggerContext ctx) {
return time == window.maxTimestamp() ? TriggerResult.FIRE : TriggerResult.CONTINUE;
}
@Override
public TriggerResult onProcessingTime(long time, TimeWindow window, TriggerContext ctx)
throws Exception {
return TriggerResult.CONTINUE;
}
@Override
public void clear(TimeWindow window, TriggerContext ctx) throws Exception {
ctx.deleteEventTimeTimer(window.maxTimestamp());
}
@Override
public boolean canMerge() {
return true;
}
@Override
public void onMerge(TimeWindow window, OnMergeContext ctx) {
// only register a timer if the watermark is not yet past the end of the merged window
// this is in line with the logic in onElement(). If the watermark is past the end of
// the window onElement() will fire and setting a timer here would fire the window twice.
long windowMaxTimestamp = window.maxTimestamp();
if (windowMaxTimestamp > ctx.getCurrentWatermark()) {
ctx.registerEventTimeTimer(windowMaxTimestamp);
}
}
@Override
public String toString() {
return "EventTimeTrigger()";
}
/**
* Creates an event-time trigger that fires once the watermark passes the end of the window.
*
* <p>Once the trigger fires all elements are discarded. Elements that arrive late immediately
* trigger window evaluation with just this one element.
*/
public static MyEventTimeTrigger create() {
return new MyEventTimeTrigger();
}
}