Flink 提供了非常完善的窗口机制,窗口就是从Streaming到batch的一个桥梁,是 Flink 最大的亮点之一(其他的亮点包括消息乱序处理,和 checkpoint 机制等)。

窗口的生命周期

窗口的生命周期,就是创建和销毁。

  • 窗口创建:当第一个元素进入到窗口开始时间的时候,这个窗口就被创建了。
  • 窗口销毁:当时间(ProcessTime、EventTime或者 IngestionTime)越过了窗口的结束时间,再加上用户自定义的窗口延迟时间(allowed lateness),窗口就会被销毁。

每个窗口都会绑定一个触发器和一个执行函数。

  • 触发器(Trigger)定义了何时会触发窗口的执行函数的计算,比如在窗口元素数量大于等于4的时候,或者水位经过了窗口结束时间的时候。
  • 另外,每个窗口可以指定驱逐器(Evictor),它的作用是在触发器触发后,执行函数执行前,移除一些元素。

窗口的分类

经典的窗口划分为几类:时间窗口、事件窗口、会话窗口;其中时间窗口和事件窗口又可以划分为滚动窗口和滑动窗口;滚动窗口和滑动窗口的区别在于滚动窗口没有重叠,滑动窗口有重叠。

1. 时间窗口(Time Window)

1.1 滚动窗口(Tumbling Window)

// Stream of (userId, buyCnt)
 val buyCnts: DataStream[(Int, Int)] = ...val tumblingCnts: DataStream[(Int, Int)] = buyCnts
   // key stream by userId
   .keyBy(0) 
   // tumbling time window of 1 minute length
   .timeWindow(Time.minutes(1))
   // compute sum over buyCnt
   .sum(1)

1.2 滑动窗口(Sliding Window)

val slidingCnts: DataStream[(Int, Int)] = buyCnts
   .keyBy(0) 
   // sliding time window of 1 minute length and 30 secs trigger interval
   .timeWindow(Time.minutes(1), Time.seconds(30))
   .sum(1)

2. 事件窗口(Count Window)

2.1 滚动窗口(Tumbling Window)

// Stream of (userId, buyCnts)
 val buyCnts: DataStream[(Int, Int)] = ...val tumblingCnts: DataStream[(Int, Int)] = buyCnts
   // key stream by sensorId
   .keyBy(0)
   // tumbling count window of 100 elements size
   .countWindow(100)
   // compute the buyCnt sum 
   .sum(1)

2.2 滑动窗口(Sliding Window)

val slidingCnts: DataStream[(Int, Int)] = vehicleCnts
   .keyBy(0)
   // sliding count window of 100 elements size and 10 elements trigger interval
   .countWindow(100, 10)
   .sum(1)

3. 会话窗口(Session Window)

// Stream of (userId, buyCnts)
 val buyCnts: DataStream[(Int, Int)] = ...
   
 val sessionCnts: DataStream[(Int, Int)] = vehicleCnts
   .keyBy(0)
   // session window based on a 30 seconds session gap interval 
   .window(ProcessingTimeSessionWindows.withGap(Time.seconds(30)))
   .sum(1)

 

窗口函数

窗口函数主要分为两种,一种是增量计算,如reduceaggregate,一种是全量计算,如process。增量计算指的是窗口保存一份中间数据,每流入一个新元素,新元素与中间数据两两合一,生成新的中间数据,再保存到窗口中。全量计算指的是窗口先缓存该窗口所有元素,等到触发条件后对窗口内的全量元素执行计算。

1. ReduceFunction

使用reduce算子时,我们要重写一个ReduceFunction。ReduceFunction接受两个相同类型的输入,生成一个输出,即两两合一地进行汇总操作,生成一个同类型的新元素。在窗口上进行reduce的原理与之类似,只不过多了一个窗口状态数据,这个状态数据的数据类型和输入的数据类型是一致的,是之前两两计算的中间结果数据。当数据流中的新元素流入后,ReduceFunction将中间结果和新流入数据两两合一,生成新的数据替换之前的状态数据。

2. AggregateFunction

AggregateFunction也是一种增量计算窗口函数,也只保存了一个中间状态数据,但AggregateFunction使用起来更复杂一些。

public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable {
   // 在一次新的aggregate发起时,创建一个新的Accumulator,Accumulator是我们所说的中间状态数据,简称ACC
    // 这个函数一般在初始化时调用
    ACC createAccumulator();   // 当一个新元素流入时,将新元素与状态数据ACC合并,返回状态数据ACC
    ACC add(IN value, ACC accumulator);
   
    // 将两个ACC合并
    ACC merge(ACC a, ACC b);   // 将中间数据转成结果数据
    OUT getResult(ACC accumulator);}

输入类型是IN,输出类型是OUT,中间状态数据是ACC,这样复杂的设计主要是为了解决输入类型、中间状态和输出类型不一致的问题,同时ACC可以自定义,我们可以在ACC里构建我们想要的数据结构。比如我们要计算一个窗口内某个字段的平均值,那么ACC中要保存总和以及个数。

3. ProcessWindowFunction

与前两种方法不同,ProcessWindowFunction要对窗口内的全量数据都缓存。在Flink所有API中,process算子以及其对应的函数是最底层的实现,使用这些函数能够访问一些更加底层的数据,比如,直接操作状态等。

ProcessWindowFunction相比AggregateFunction和ReduceFunction的应用场景更广,能解决的问题也更复杂。但ProcessWindowFunction需要将窗口中所有元素作为状态存储起来,这将占用大量的存储资源,尤其是在数据量大窗口多的场景下,使用不慎可能导致整个程序宕机。

4. ProcessWindowFunction与增量计算相结合

当我们既想访问窗口里的元数据,又不想缓存窗口里的所有数据时,可以将ProcessWindowFunction与增量计算函数相reduce和aggregate结合。对于一个窗口来说,Flink先增量计算,窗口关闭前,将增量计算结果发送给ProcessWindowFunction作为输入再进行处理。