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

本文详细解释了ApacheFlink中窗口算子的触发器、移除器以及如何处理迟到数据,包括自定义触发器的原理、默认触发器如EventTimeTrigger和CountTrigger,以及AllowedLateness用于设定迟到数据的处理策略和sideOutputLateData用于将迟到数据放入侧输出流的机制。
摘要由CSDN通过智能技术生成

目录

1. 触发器(Trigger)

2. 移除器(Evictor)

3. 允许延迟(Allowed Lateness)

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


 

Trigger 是窗口算子的内部属性,每个窗口分配器( WindowAssigner )都会对应一个默认 的触发器;对于 Flink 内置的窗口类型,它们的触发器都已经做了实现。例如,所有事件时间 窗口,默认的触发器都是 EventTimeTrigger ;类似还有 ProcessingTimeTrigger 和 CountTrigger 。

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

1. 触发器(Trigger)

        触发器主要是用来控制窗口什么时候触发计算。所谓的“触发计算”,本质上就是执行窗
口函数,所以可以认为是计算得到结果并输出的过程。
        基于 WindowedStream 调用 .trigger() 方法,就可以传入一个自定义的窗口触发器( Trigger )。

   
   
  1. stream.keyBy(...)
  2. .window(...)
  3. .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 秒钟触发一次窗口的计算。

    
    
  1. import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
  2. import org.apache.flink.api.common.eventtime.WatermarkStrategy;
  3. import org.apache.flink.api.common.state.ValueState;
  4. import org.apache.flink.api.common.state.ValueStateDescriptor;
  5. import org.apache.flink.api.common.typeinfo.Types;
  6. import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
  7. import
  8. org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
  9. import
  10. org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
  11. import org.apache.flink.streaming.api.windowing.time.Time;
  12. import org.apache.flink.streaming.api.windowing.triggers.Trigger;
  13. import org.apache.flink.streaming.api.windowing.triggers.TriggerResult;
  14. import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
  15. import org.apache.flink.util.Collector;
  16. public class TriggerExample {
  17. public static void main (String[] args) throws Exception {
  18. StreamExecutionEnvironment env =
  19. StreamExecutionEnvironment.getExecutionEnvironment();
  20. env.setParallelism( 1);
  21. env
  22. .addSource( new ClickSource())
  23. .assignTimestampsAndWatermarks(
  24. WatermarkStrategy.<Event>forMonotonousTimestamps()
  25. .withTimestampAssigner( new
  26. SerializableTimestampAssigner<Event>() {
  27. @Override
  28. public long extractTimestamp (Event event, long l) {
  29. return event.timestamp;
  30. }
  31. })
  32. )
  33. .keyBy(r -> r.url)
  34. .window(TumblingEventTimeWindows.of(Time.seconds( 10)))
  35. .trigger( new MyTrigger())
  36. .process( new WindowResult())
  37. .print();
  38. env.execute();
  39. }
  40. public static class WindowResult extends ProcessWindowFunction<Event,
  41. UrlViewCount, String, TimeWindow> {
  42. @Override
  43. public void process (String s, Context context, Iterable<Event> iterable,
  44. Collector<UrlViewCount> collector) throws Exception {
  45. collector.collect(
  46. new UrlViewCount(
  47. s,
  48. // 获取迭代器中的元素个数
  49. iterable.spliterator().getExactSizeIfKnown(),
  50. context.window().getStart(),
  51. context.window().getEnd()
  52. )
  53. );
  54. }
  55. }
  56. public static class MyTrigger extends Trigger<Event, TimeWindow> {
  57. @Override
  58. public TriggerResult onElement (Event event, long l, TimeWindow timeWindow,
  59. TriggerContext triggerContext) throws Exception {
  60. ValueState<Boolean> isFirstEvent =
  61. triggerContext.getPartitionedState(
  62. new ValueStateDescriptor<Boolean>( "first-event",
  63. Types.BOOLEAN)
  64. );
  65. if (isFirstEvent.value() == null) {
  66. for ( long i = timeWindow.getStart(); i < timeWindow.getEnd(); i =
  67. i + 1000L) {
  68. triggerContext.registerEventTimeTimer(i);
  69. }
  70. isFirstEvent.update( true);
  71. }
  72. return TriggerResult.CONTINUE;
  73. }
  74. @Override
  75. public TriggerResult onEventTime (long l, TimeWindow timeWindow,
  76. TriggerContext triggerContext) throws Exception {
  77. return TriggerResult.FIRE;
  78. }
  79. @Override
  80. public TriggerResult onProcessingTime (long l, TimeWindow timeWindow,
  81. TriggerContext triggerContext) throws Exception {
  82. return TriggerResult.CONTINUE;
  83. }
  84. @Override
  85. public void clear (TimeWindow timeWindow, TriggerContext triggerContext)
  86. throws Exception {
  87. ValueState<Boolean> isFirstEvent =
  88. triggerContext.getPartitionedState(
  89. new ValueStateDescriptor<Boolean>( "first-event",
  90. Types.BOOLEAN)
  91. );
  92. isFirstEvent.clear();
  93. }
  94. }
  95. }
输出结果如下:
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 是一个接口,不同的窗口类型都有各自预实
现的移除器。
 

   
   
  1. stream.keyBy(...)
  2. .window(...)
  3. .evictor( new MyEvictor())

Evictor 接口定义了两个方法:

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

3. 允许延迟(Allowed Lateness)

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

   
   
  1. stream.keyBy(...)
  2. .window(TumblingEventTimeWindows.of(Time.hours( 1)))
  3. .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 的类型与流中数据类型相同。
 

   
   
  1. DataStream<Event> stream = env.addSource(...);
  2. OutputTag<Event> outputTag = new OutputTag<Event>( "late") {};
  3. stream.keyBy(...)
  4. .window(TumblingEventTimeWindows.of(Time.hours( 1)))
  5. .sideOutputLateData(outputTag)

        将迟到数据放入侧输出流之后,还应该可以将它提取出来。基于窗口处理完成之后的
DataStream ,调用 .getSideOutput() 方法,传入对应的输出标签,就可以获取到迟到数据所在的
流了。
 

   
   
  1. SingleOutputStreamOperator<AggResult> winAggStream = stream.keyBy(...)
  2. .window(TumblingEventTimeWindows.of(Time.hours( 1)))
  3. .sideOutputLateData(outputTag)
  4. .aggregate( new MyAggregateFunction())
  5. DataStream<Event> lateStream = winAggStream.getSideOutput(outputTag);

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值