Flink中窗口的触发器、移除器、侧输出流

目录

1. 触发器(Trigger)

2. 移除器(Evictor)

3. 允许延迟(Allowed Lateness)

4. 将迟到的数据放入侧输出流


 

        对于一个窗口算子而言,窗口分配器和窗口函数是必不可少的。除此之外,Flink 还提供
了其他一些可选的 API ,让我们可以更加灵活地控制窗口行为。

1. 触发器(Trigger

        触发器主要是用来控制窗口什么时候触发计算。所谓的“触发计算”,本质上就是执行窗
口函数,所以可以认为是计算得到结果并输出的过程。
        基于 WindowedStream 调用 .trigger() 方法,就可以传入一个自定义的窗口触发器( Trigger )。
stream.keyBy(...)
.window(...)
.trigger(new MyTrigger())
        Trigger 是窗口算子的内部属性,每个窗口分配器( WindowAssigner )都会对应一个默认
的触发器;对于 Flink 内置的窗口类型,它们的触发器都已经做了实现。例如,所有事件时间
窗口,默认的触发器都是 EventTimeTrigger ;类似还有 ProcessingTimeTrigger CountTrigger
所以一般情况下是不需要自定义触发器的,不过我们依然有必要了解它的原理。
Trigger 是一个抽象类,自定义时必须实现下面四个抽象方法:
onElement(): 窗口中每到来一个元素,都会调用这个方法。
onEventTime(): 当注册的事件时间定时器触发时,将调用这个方法。
onProcessingTime (): 当注册的处理时间定时器触发时,将调用这个方法。
clear(): 当窗口关闭销毁时,调用这个方法。一般用来清除自定义的状态。
        可以看到,除了 clear() 比较像生命周期方法,其他三个方法其实都是对某种事件的响应。
onElement() 是对流中数据元素到来的响应;而另两个则是对时间的响应。这几个方法的参数中
都有一个“触发器上下文”( TriggerContext )对象,可以用来注册定时器回调( callback )。这
里提到的“定时器”( Timer ),其实就是我们设定的一个“闹钟”,代表未来某个时间点会执行
的事件;当时间进展到设定的值时,就会执行定义好的操作。很明显,对于时间窗口
TimeWindow )而言,就应该是在窗口的结束时间设定了一个定时器,这样到时间就可以触发
窗口的计算输出了。关于定时器的内容,我们在后面讲解处理函数( process function )时还会
提到。
        上面的前三个方法可以响应事件,那它们又是怎样跟窗口操作联系起来的呢?这就需要了
解一下它们的返回值。这三个方法返回类型都是 TriggerResult ,这是一个枚举类型( enum ),
其中定义了对窗口进行操作的四种类型。
CONTINUE(继续):什么都不做
FIRE(触发):触发计算,输出结果
PURGE(清除):清空窗口中的所有数据,销毁窗口
FIRE_AND_PURGE(触发并清除):触发计算输出结果,并清除窗口
        我们可以看到,Trigger 除了可以控制触发计算,还可以定义窗口什么时候关闭(销毁)。
上面的四种类型,其实也就是这两个操作交叉配对产生的结果。一般我们会认为,到了窗口的
结束时间,那么就会触发计算输出结果,然后关闭窗口——似乎这两个操作应该是同时发生的;
TriggerResult 的定义告诉我们,两者可以分开。稍后我们就会看到它们分开操作的场景。
        下面我们举一个例子。在日常业务场景中,我们经常会开比较大的窗口来计算每个窗口的
pv 或者 uv 等数据。但窗口开的太大,会使我们看到计算结果的时间间隔变长。所以我们可以
使用触发器,来隔一段时间触发一次窗口计算。我们在代码中计算了每个 url 10 秒滚动窗
口的 pv 指标,然后设置了触发器,每隔 1 秒钟触发一次窗口的计算。
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import 
org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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.triggers.Trigger;
import org.apache.flink.streaming.api.windowing.triggers.TriggerResult;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
public class TriggerExample {
 public static void main(String[] args) throws Exception {
 StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
 env.setParallelism(1);
 env
 .addSource(new ClickSource())
 .assignTimestampsAndWatermarks(
 WatermarkStrategy.<Event>forMonotonousTimestamps()
 .withTimestampAssigner(new 
SerializableTimestampAssigner<Event>() {
 @Override
 public long extractTimestamp(Event event, long l) {
 return event.timestamp;
 }
 })
 )
 .keyBy(r -> r.url)
 .window(TumblingEventTimeWindows.of(Time.seconds(10)))
 .trigger(new MyTrigger())
 .process(new WindowResult())
 .print();
 env.execute();
 }
 public static class WindowResult extends ProcessWindowFunction<Event, 
UrlViewCount, String, TimeWindow> {
 @Override
 public void process(String s, Context context, Iterable<Event> iterable, 
Collector<UrlViewCount> collector) throws Exception {
 collector.collect(
 new UrlViewCount(
 s,
 // 获取迭代器中的元素个数
 iterable.spliterator().getExactSizeIfKnown(),
 context.window().getStart(),
 context.window().getEnd()
 )
 );
 }
 }
 public static class MyTrigger extends Trigger<Event, TimeWindow> {
 @Override
 public TriggerResult onElement(Event event, long l, TimeWindow timeWindow, 
TriggerContext triggerContext) throws Exception {
 ValueState<Boolean> isFirstEvent = 
triggerContext.getPartitionedState(
 new ValueStateDescriptor<Boolean>("first-event", 
Types.BOOLEAN)
 );
 if (isFirstEvent.value() == null) {
 for (long i = timeWindow.getStart(); i < timeWindow.getEnd(); i = 
i + 1000L) {
 triggerContext.registerEventTimeTimer(i);
 }
 isFirstEvent.update(true);
 }
 return TriggerResult.CONTINUE;
 }
 @Override
 public TriggerResult onEventTime(long l, TimeWindow timeWindow, 
TriggerContext triggerContext) throws Exception {
 return TriggerResult.FIRE;
 }
 @Override
 public TriggerResult onProcessingTime(long l, TimeWindow timeWindow, 
TriggerContext triggerContext) throws Exception {
 return TriggerResult.CONTINUE;
 }
 @Override
 public void clear(TimeWindow timeWindow, TriggerContext triggerContext) 
throws Exception {
 ValueState<Boolean> isFirstEvent = 
triggerContext.getPartitionedState(
 new ValueStateDescriptor<Boolean>("first-event", 
Types.BOOLEAN)
 );
 isFirstEvent.clear();
 }
 }
}
输出结果如下:
UrlViewCount{url='./prod?id=1', count=1, windowStart=2021-07-01 14:44:10.0,
windowEnd=2021-07-01 14:44:20.0}
172 173
UrlViewCount{url='./prod?id=1', count=1, windowStart=2021-07-01 14:44:10.0,
windowEnd=2021-07-01 14:44:20.0}
UrlViewCount{url='./prod?id=1', count=1, windowStart=2021-07-01 14:44:10.0,
windowEnd=2021-07-01 14:44:20.0}
UrlViewCount{url='./prod?id=1', count=1, windowStart=2021-07-01 14:44:10.0,
windowEnd=2021-07-01 14:44:20.0}

2. 移除器(Evictor

        移除器主要用来定义移除某些数据的逻辑。基于 WindowedStream 调用 .evictor() 方法,就
可以传入一个自定义的移除器( Evictor )。 Evictor 是一个接口,不同的窗口类型都有各自预实
现的移除器。
 
stream.keyBy(...)
.window(...)
.evictor(new MyEvictor())

Evictor 接口定义了两个方法:

evictBefore():定义执行窗口函数之前的移除数据操作
evictAfter():定义执行窗口函数之后的以处数据操作
        默认情况下,预实现的移除器都是在执行窗口函数(window fucntions )之前移除数据的。

3. 允许延迟(Allowed Lateness

        在事件时间语义下,窗口中可能会出现数据迟到的情况。这是因为在乱序流中,水位线
watermark )并不一定能保证时间戳更早的所有数据不会再来。当水位线已经到达窗口结束时
间时,窗口会触发计算并输出结果,这时一般也就要销毁窗口了;如果窗口关闭之后,又有本
属于窗口内的数据姗姗来迟,默认情况下就会被丢弃。这也很好理解:窗口触发计算就像发车,
如果要赶的车已经开走了,又不能坐其他的车(保证分配窗口的正确性),那就只好放弃坐班
车了。
        不过在多数情况下,直接丢弃数据也会导致统计结果不准确,我们还是希望该上车的人都
能上来。为了解决迟到数据的问题, Flink 提供了一个特殊的接口,可以为窗口算子设置一个
“允许的最大延迟”( Allowed Lateness )。也就是说,我们可以设定允许延迟一段时间,在这段
时间内,窗口不会销毁,继续到来的数据依然可以进入窗口中并触发计算。 直到水位线推进到
了 窗口结束时间 + 延迟时间,才真正将窗口的内容清空,正式关闭窗口。
        基于 WindowedStream 调用 .allowedLateness() 方法,传入一个 Time 类型的延迟时间,就可
以表示允许这段时间内的延迟数据。
 
stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.allowedLateness(Time.minutes(1))

        比如上面的代码中,我们定义了 1 小时的滚动窗口,并设置了允许 1 分钟的延迟数据。也

就是说,在不考虑水位线延迟的情况下,对于 8 ~9 点的窗口,本来应该是水位线到达 9
整就触发计算并关闭窗口;现在允许延迟 1 分钟,那么 9 点整就只是触发一次计算并输出结果,
并不会关窗。后续到达的数据,只要属于 8 ~9 点窗口,依然可以在之前统计的基础上继续
叠加,并且再次输出一个更新后的结果。直到水位线到达了 9 点零 1 分,这时就真正清空状态、
关闭窗口,之后再来的迟到数据就会被丢弃了。
        从这里我们就可以看到,窗口的触发计算(Fire )和清除( Purge )操作确实可以分开。不
过在默认情况下,允许的延迟是 0 ,这样一旦水位线到达了窗口结束时间就会触发计算并清除
窗口,两个操作看起来就是同时发生了。当窗口被清除(关闭)之后,再来的数据就会被丢弃。
 

4. 将迟到的数据放入侧输出流

        我们自然会想到,即使可以设置窗口的延迟时间,终归还是有限的,后续的数据还是会被
丢弃。如果不想丢弃任何一个数据,又该怎么做呢?
        Flink 还提供了另外一种方式处理迟到数据。我们可以将未收入窗口的迟到数据,放入“侧
输出流”( side output )进行另外的处理。所谓的侧输出流,相当于是数据流的一个“分支”,
这个流中单独放置那些错过了该上的车、本该被丢弃的数据。
        基于 WindowedStream 调用 .sideOutputLateData() 方法,就可以实现这个功能。方法需要
传入一个“输出标签”( OutputTag ),用来标记分支的迟到数据流。因为保存的就是流中的原
始数据,所以 OutputTag 的类型与流中数据类型相同。
 
DataStream<Event> stream = env.addSource(...);
OutputTag<Event> outputTag = new OutputTag<Event>("late") {};
stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.sideOutputLateData(outputTag)

        将迟到数据放入侧输出流之后,还应该可以将它提取出来。基于窗口处理完成之后的
DataStream ,调用 .getSideOutput() 方法,传入对应的输出标签,就可以获取到迟到数据所在的
流了。
 
SingleOutputStreamOperator<AggResult> winAggStream = stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.sideOutputLateData(outputTag)
.aggregate(new MyAggregateFunction())
DataStream<Event> lateStream = winAggStream.getSideOutput(outputTag);

        这里注意,getSideOutput() SingleOutputStreamOperator 的方法,获取到的侧输出流数据
类型应该和 OutputTag 指定的类型一致,与窗口聚合之后流中的数据类型可以不同。

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flink输出是指在一个算子输出多个数据的机制,可以理解为将一个数据分成多个数据输出输出可以用于将某些特殊情况下的数据分离出来,便于后续的处理。例如,当输入的数据不符合某个条件时,将其输出输出进行特殊处理。 在Flink输出是通过使用OutputTag实现的。OutputTag是一个泛型类,用于定义输出的类型。在算子,可以通过调用processElement方法将数据输出输出,如下所示: ```java // 定义输出的OutputTag OutputTag<String> outputTag = new OutputTag<String>("side-output"){}; // 定义处理数据的算子 public class MyProcessFunction extends ProcessFunction<Integer, String> { @Override public void processElement(Integer value, Context ctx, Collector<String> out) throws Exception { if (value % 2 == 0) { // 将偶数输出到主数据 out.collect(value.toString()); } else { // 将奇数输出输出 ctx.output(outputTag, "odd: " + value.toString()); } } } // 获取输出 SingleOutputStreamOperator<String> mainDataStream = inputStream.process(new MyProcessFunction()); DataStream<String> sideOutputStream = mainDataStream.getSideOutput(outputTag); ``` 在上述代码,ProcessFunction通过调用ctx.output方法将数据输出输出。在最后通过调用getSideOutput方法获取输出。 注意:输出的数据类型必须和输出的OutputTag定义的类型一致。否则会出现类型转换异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值