【白话Flink基础理论】Flink的窗口Windows机制—对有界流的处理

——wirte by 橙心橙意橙续缘,

前言

白话系列
————————————————————————————
也就是我在写作时完全不考虑写作方面的约束,完全把自己学到的东西、以及理由和所思考的东西等等都用大白话诉说出来,这样能够让信息最大化的从自己脑子里输出并且输入到有需要的同学的脑中。PS:较为专业的地方还是会用专业口语诉说,大家放心!

白话Flink系列
————————————————————————————
主要是记录本人(国内某985研究生)在Flink基础理论阶段学习的一些所学,更重要的是一些所思所想,所参考的视频资料或者博客以及文献资料均在文末放出.由于研究生期间的课题组和研究方向与Flink接轨较多,而且Flink的学习对于想进入大厂的同学们来说也是非常的赞,所以该系列文章会随着本人学习的深入来不断修改和完善,希望大家也可以多批评指正或者提出宝贵建议。

在Flink中的窗口Windows是什么?

在这里插入图片描述
将无界的流通过划分windows变成有界的流,也就是window在不同的时刻相当于是一个个的桶bucket,流数据满足这个桶的范围就被一一传入桶中,所以每个桶只会对桶内的数据,也就是窗口内的数据进行操作,而不会处理其他数据。

窗口类型

滚动窗口

在这里插入图片描述
不同用户的时间是统一的,每个数据来自有且仅有1个窗口,数据没有重叠,无缝连接,是特殊的滑动窗口

size确定1个滚动窗口

滑动窗口

在这里插入图片描述
窗口大小size一定,比滚动窗口更灵活,粒度优滑动间隔slide决定,滑动间隔=窗口大小时=滚动窗口

size+slide确定1个滑动窗口

会话窗口

在这里插入图片描述
数据与窗口对齐,有数据就有窗口,一段时间(gap间隔时间)没数据,窗口自动关闭。

会话窗口与其他窗口的最大区别就是窗口会很饱满,其他窗口可能造成有的窗口数据倾斜度高,即有的窗口数据多,有的窗口数据少

小结

滑动窗口和滚动窗口均可在时间数量两个维度进行滑动或者滚动。会话窗口由于其特性决定了它只能在时间层面。故窗口总共有3大类5小类.

特别的是,
——————————————————————————
2个窗口分界线上的数据是左闭右开,也就是会被包含在后续的窗口里。

windows API

API总览
在这里插入图片描述

創建窗口——窗口分配器window assigner

  • 时间窗口
    • 滚动时间窗口
      • TumblingProcessingTimeWindows/TumblingEventTimeWindows 滚动时间窗口分配器
      • 高层API:.timewindow(Time.seconds(15)) //Time size
      public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size) {
      	if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
      		return window(TumblingProcessingTimeWindows.of(size));
      	} else {
      		return window(TumblingEventTimeWindows.of(size));
      	}
      }
      
    • 滑动时间窗口
      • SlidingProcessingTimeWindows/SlidingEventTimeWindows 滑动时间窗口分配器
      • 高层API:.timewindow(Time.seconds(15),Time.seconds(5)) //Time size//Time slide
      public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size, Time slide) {
      		if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
      			return window(SlidingProcessingTimeWindows.of(size, slide));
      		} else {
      			return window(SlidingEventTimeWindows.of(size, slide));
      		}
      	}
      

可以看到Flink对于时间有2种定义,这个在后面时间语义部分探究.

	private SlidingProcessingTimeWindows(long size, long slide, long offset) {
		if (Math.abs(offset) >= slide || size <= 0) {
			throw new IllegalArgumentException("SlidingProcessingTimeWindows parameters must satisfy " +
				"abs(offset) < slide and size > 0");
		}

		this.size = size;
		this.slide = slide;
		this.offset = offset;
	}
	
	protected SlidingEventTimeWindows(long size, long slide, long offset) {
		if (Math.abs(offset) >= slide || size <= 0) {
			throw new IllegalArgumentException("SlidingEventTimeWindows parameters must satisfy " +
				"abs(offset) < slide and size > 0");
		}

		this.size = size;
		this.slide = slide;
		this.offset = offset;
	}

上面我们看到了timeWindow只有size和slide参数,其实对于底层的SlidingProcessingTimeWindows和SlidingEventTimeWindows窗口分配器来说,我们还可以通过.of方法设置偏移量,也就是我们不想让窗口从0开始这样,可以从offset开始时第一个桶这样,很好理解。[具体使用可以参照源码]

  • 计数窗口
    • 滚动计数窗口
      • GlobalWindows + trigger => 滚动计数窗口分配器
      • 高层API:.countWindow(15)
      public WindowedStream<T, KEY, GlobalWindow> countWindow(long size) {
      		return window(GlobalWindows.create()).trigger(PurgingTrigger.of(CountTrigger.of(size)));
      	}
      
    • 滑动计数窗口
      • GlobalWindows + evictor + trigger => 滑动计数窗口分配器
      • 高层API:.countWindow(15,5)
      public WindowedStream<T, KEY, GlobalWindow> countWindow(long size, long slide) {
      		return window(GlobalWindows.create())
      				.evictor(CountEvictor.of(size))
      				.trigger(CountTrigger.of(slide));
      	}
      

Flink底层实现计数窗口是通过GlobalWindows这个窗口分配器配合evictortrigger
Flink源码中是这样定义的。Use this if you want to use a {@link Trigger} and {@link Evictor} to do flexible, policy based windows.

  • 会话窗口
    • EventTimeSessionWindows 会话窗口分配器
    • 高层API .window(EventTimeSessionWindows.withGap(Time.minutes(10)))

可见会话窗口没有更高一层的API封装,直接是使用最原始的window算子,然后通过EventTimeSessionWindows这个MergingWindowAssigner来分配会话窗口。
Flink源码中这样定义MergingWindowAssigner:A WindowAssigner that can merge windows

小结

根据上面的介绍,在windowAPI 方面主要有这么四个。

  • window()
  • countWindow()
  • timeWindow()

以上3种API均位于keyedStream.java中定义

上述3种算子必须在keyBy()算子之后执行,把数据流从KeyedStream->WindowedStream

  • windowAll()

windowAll算子比较特殊,它定义在DataStream.java中,也就是说它不是对分组后的数据进行开窗,而是对所有的数据来进行操作。
Ps:一般很少这样做,所以在Flink底层也没有封装类似timeWindowAll()这样的算子,实现必须手动通过窗口分配器来分配。

窗口函数 windows Function

定义:在开窗口,对窗口内数据进行聚合操作的函数,将WindowedStream->SingleOutputStreamOperator(extends DataStream)

窗口函数是一种聚合操作,因为开窗的目的其实就是得到一组数据,那么对于这组数据只有对其进行相应的聚合操作,才是开窗的目的。

故,窗口函数是一定要有的。

  • 增量聚合函数
    • ReduceFunciton 实现自定义的聚合操作。 算子:reduce

      * @param <T> Type of the elements that this function processes.
      * 输入和输出的类型均为T
      public interface ReduceFunction<T> extends Function, Serializable
      
    • AggregateFunction 累加聚合,底层有包含1个累加器。 算子:aggregate

       * @param <IN>  The type of the values that are aggregated (input values)   输入类型
       * @param <ACC> The type of the accumulator (intermediate aggregate state). 累加器(上一次结果)类型
       * @param <OUT> The type of the aggregated result  输出类型
      
      public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable
      
    
    

类似流处理过程,来一条就计算,然后维持或更新一个状态。[监测到窗口完毕时可以直接输出当次的结果就是最终结果]

这里可以看出来类似max()、min()、sum()这些非常熟悉的聚合函数都属于ReduceFunction,也就是属于增量聚合。

区别与联系

  • Reducetion要求输入与输出的类型一致,而且当前所到数据和之前结果的聚合操作是固定的,也就是函数里设定好的。

  • AggregateFunction 就更加灵活了,出了输入输出类型可以自定义外,除了中间的聚合操作,还可以实现起始和结束时的额外操作。

  • 全窗口函数

    • ProcessWindowFunction 算子:process

       * @param <IN> The type of the input value.
       * @param <OUT> The type of the output value.
       * @param <KEY> The type of the key.    
       * @param <W> The type of {@code Window} that this window function can be applied on.
       */
      @PublicEvolving
      public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> extends AbstractRichFunction
      
    • WindowFunction 算子:apply

       * @param <IN> The type of the input value.
       * @param <OUT> The type of the output value.
       * @param <KEY> The type of the key.
       * @param <W> The type of {@code Window} that this window function can be applied on.
       */
      @Public
      public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, Serializable
      

类似批处理过程,先把整个窗口内的全部数据收集起来,收集完后在遍历数据进行计算。 [监测到窗口完毕,才开始计算,如果数据很多的话相比于增量聚合操作会很慢]

做一些只有在全局数据下才有意义的操作,比如说求中位数或者求均值等,显然如果采用增量计算,部分数据计算出来的不仅是无意义的,而且也难以获得整体的信息,比如求平均值时的个数,还得每次都维持1个数量状态,而全窗口函数便可以直接在最后使用API获取个数这样的全局信息

区别与联系

  • WindowFunction可以自定义输入和输出类型,还可以获取key和Window的信息,调用相应的具体的API即可。
  • ProcessWindowFunction可以获得Contenxt上下文信息,调用相应的具体API即可。

小结

  1. 虽然window操作看起来是将无界流转化为有界流,但其实说白了还是Stream,我们不如把它看作是一种分组操作,类似于KeyBy,只不过KeyBy是从键的角度进行分组,而window是从时间和数量的层面给每个分组画了个范围,但是在范围内它还是组啊,所以分组后当然要进行聚合。
  2. 全窗口函数虽然存在时延,但是它能够获得window的信息和key的信息或者上下文信息等,所以一般我们会把这两种函数结合起来使用
  3. 误区:增量聚合和全窗口是纯的实时和离线。原因:这两种操作都是在window完毕后才会输出最终的结果或者把结果发往下游,只不过一种的底层计算是增量形式的,也就是有数据到就计算,而另一种是数据来了只缓存不计算,等到最后才计算而已,当然对于每个分组来说都有1个结果,所以他们的输出形式还是keyedStream
  4. 误区:有很多个窗口。原因:在1条流上开窗后,窗口只有1个,只是因为在滚动或者滑动看起来好像有好多个窗口,其实从始至终只有1个窗口,只是在1次滚动或者滑动以后处理的数据发生了变化而已,每个窗口的输出都是基于当前窗口内的数据来进行的。

window高阶API

  • .trigger() 触发器

    定义windows什么时候关闭,触发计算并输出结果

  • .evictor() 移除器

    定义移除某些数据的逻辑,类似过滤功能

  • .allowedLateness(Time.seconds(50)) 允许处理迟到的数据

  • .sideOutputLateData(OutPutTag) 将迟到的数据放入侧输出流

  • .getSideOutput(OutPutTag) 获取侧输出流

触发器和移除器的功能使用可以参考Flink实现countWindows的原理

剩余3者适用于处理迟到数据的,迟到数据所采用的的时间语义一定是eventTime(数据创建的时间),而且一定是在时间窗口中,因为时间窗口的分配器我们在前面看到过有2种,一种是processTime的,一种是eventTime的,而我们又很容易误解从字面意思,将processTime理解为数据的产生时间,其实他是Flink系统处理数据的时间,这种显然是没有迟到概念的,因为你只要来了就符合我处理时间的这个,你不来就不符合我这个桶;而不像eventTime一样,按你的创建时间来说是我这个桶里的,但是我这个桶到点儿要关了,你怎么还不来呢,这就出现了迟到的概念。

对于迟到的数据

  • 首现,容忍它迟到一段时间,通过.allowedLateness(Time.seconds(50))来设置,这样的话就会在window结束时输出1次实时地结果,然后在50s后在输出一次修正后的结果。
  • 其次,如果还有数据没有到,我们就可以通过.sideOutputLateData(OutPutTag)将迟到数据放入侧输出流中
  • 最后,我们通过.getSideOutput(OutPutTag)获取设置的OutputTag的侧输出流,定义自己的逻辑将数据修正进去。
SingleOutputStreamOperator<SensorReading> minTempStream = dataStream.keyBy("id")
                .timeWindow(Time.seconds(15))
                .allowedLateness(Time.minutes(1))
                .sideOutputLateData(outputTag)
                .minBy("temperature");

        minTempStream.print("minTemp");
        minTempStream.getSideOutput(outputTag).print("late");

总结

总结部分主要在每一部分的小结中体现了,现在主要谈一下自己学习这部分的感受吧。

  • 可以看到这5类窗口,主要是涉及到时间的窗口比较麻烦,因为有高阶的迟到数据处理这一部分,同时这块也能引出后续的Flink时间语义,也就是Flink是如何规定其内部的时间的,又是怎么判断数据是否迟到的。
  • 其次就是,对于窗口函数这部分其实挺不好理解的,简单的可能比较好理解,单复杂的聚合操作可能得具体的项目实战才会有经验,而且像其中的一些Window信息的利用,Context上下文信息的利用,都需要我们对这2个类实现的1些API接口比较熟悉才可以
  • 通过这次的学习也告诉我,在学习高层API的同时结合底层实现部分的学习也很重要。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值