Flink之窗口

Table of Contents

概述

窗口的种类

滚动窗口(Tumbling Windows)

滑动窗口(Sliding Windows)

会话窗口(Session Windows)

全局窗口(Global Windows)

窗口函数

ReduceFunction

AggregateFunction

FoldFunction

ProcessWindowFunction

具有增量聚合功能的底层窗口函数

在ProcessWindowFunction中使用窗口状态

触发器

触发与清除

默认触发器和自定义触发器

剔除器

如何处理迟到的事件元素? 

将迟到元素从侧输出流输出

迟到元素处理注意事项

窗口计算后还可以做什么?

watermarks对窗口的作用 

连续的窗口操作

如何估计窗口存储大小? 


概述

Windows是处理无限流的核心。是一种切割无限数据为有限块进行处理的手段。Windows将流分成有限大小的“buckets”,我们可以基于每个块的数据进行计算。

Flink的窗口程序一般结构如下所示。第一个代码段指的是键控流窗口,第二个代码段指的是非键控流窗口。两者唯一的区别是keyBy(...)之后调用window(...)的称为成为键控流;DataStream直接调用windowAll(...)的称为非键控流。

Keyed Windows

stream
       .keyBy(...)               <-  keyed versus non-keyed windows
       .window(...)              <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/fold/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

Non-Keyed Windows

stream
       .windowAll(...)           <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/fold/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"
  • trigger() —— 触发器,定义 window 什么时候关闭,触发计算并输出结果
  • evitor() —— 移除器(剔除器),定义移除某些数据的逻辑
  • allowedLateness() —— 允许处理迟到的数据
  • sideOutputLateData() —— 将迟到的数据放入侧输出流
  • getSideOutput() —— 获取侧输出流

方括号([…])中的命令是可选的。这表明Flink允许以多种不同方式自定义窗口逻辑,从而达到最适合的操作。

两者的区别:键控流按key将流在逻辑上分为多个流,不同的逻辑流之间都可以独立于其他逻辑流进行处理,即并行度有多个。而非键控流不会拆分为多个逻辑流,所以窗口逻辑将由单个任务执行,即并行度为1。

窗口的种类

滚动窗口(Tumbling Windows)

滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,窗口的创建如下图所示:

代码实现:

val input: DataStream[T] = ...

// tumbling event-time windows
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>)

// tumbling processing-time windows
input
    .keyBy(<key selector>)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>)

// daily tumbling event-time windows offset by -8 hours.
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
    .<windowed transformation>(<window function>)

时间间隔可以通过Time.milliseconds(x)Time.seconds(x), Time.minutes(x)等指定。

如最后一个示例所示,滚动窗口还有一个可选参数offset,该参数可用于更改窗口的对齐方式。例如,如果没有偏移,则每小时滚动窗口与自然时间对齐,即1:00:00.000 - 1:59:59.999,2:00:00.000 - 2:59:59.999依此类推。如果要更改,可以提供一个偏移量。如设置15分钟的偏移量,则窗口时间为 1:15:00.000 - 2:14:59.999,2:15:00.000 - 3:14:59.999。偏移量最常见的用法是修正UTC时区等。例如,在中国则可以指定偏移量Time.hours(-8)。

滑动窗口(Sliding Windows)

滑动窗口将每个元素分配给固定长度的窗口。与滚动窗口类似,窗口大小由窗口大小参数window size配置。另一个窗口滑动参数控制滑动窗口的滑动步长。因此,如果滑动步长小于窗口大小,则滑动窗口可能重叠。在这种情况下,同一个元素会被分配给多个窗口。

例如,有10分钟大小的窗口,滑动5分钟。这样,每5分钟会得到一个窗口,其中包含在下图所示的最后10分钟内到达的事件。

代码示例:

val input: DataStream[T] = ...

// sliding event-time windows
input
    .keyBy(<key selector>)
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>)

// sliding processing-time windows
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>)

// sliding processing-time windows offset by -8 hours
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
    .<windowed transformation>(<window function>)

滑动窗口与滚动窗口一样可以设置对其方式。

会话窗口(Session Windows)

会话窗口按活动的会话对元素进行分组。与滚动窗口和滑动窗口相比,会话窗口不重叠,也没有固定的开始和结束时间。相反,当会话窗口在一段时间内没有接收元素时,它将关闭,即当一段时间没有活动时,就会生成一个窗口。session窗口通过session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个间隔内没有收到新元素,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。

代码示例:

val input: DataStream[T] = ...

// event-time session windows with static gap
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>)

// event-time session windows with dynamic gap
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
      override def extract(element: String): Long = {
        // determine and return session gap
      }
    }))
    .<windowed transformation>(<window function>)

// processing-time session windows with static gap
input
    .keyBy(<key selector>)
    .window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>)


// processing-time session windows with dynamic gap
input
    .keyBy(<key selector>)
    .window(DynamicProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
      override def extract(element: String): Long = {
        // determine and return session gap
      }
    }))
    .<windowed transformation>(<window function>)

静态间隔可以通过使用中的一个来指定Time.milliseconds(x)Time.seconds(x), Time.minutes(x),等。

动态间隔是通过实现SessionWindowTimeGapExtractor接口指定。

注意:由于会话窗口没有固定的开始和结束,因此它们的计算方式与滚动和滑动窗口不同。会话窗口为每个到达的记录创建一个新窗口,如果窗口之间的距离比定义的间隔更小,则将它们合并在一起。为了能够合并,会话窗口操作符需要一个合并触发器和一个合并窗口函数,例如ReduceFunction、AggregateFunction或ProcessWindowFunction(FoldFunction不能合并)

全局窗口(Global Windows)

将具有相同键的所有元素分配给同一个全局窗口。只有在指定了自定义触发器时,此窗口方案才有用。否则,将不会执行任何计算,因为全局窗口是没有终止的。

全局窗口的应用场景几乎是没有的。

代码示例:

val input: DataStream[T] = ...

input
    .keyBy(<key selector>)
    .window(GlobalWindows.create())
    .<windowed transformation>(<window function>)

窗口函数

ReduceFunction

ReduceFunction指定如何组合输入中的两个元素以生成同一类型的输出元素。Flink使用ReduceFunction递增聚合窗口元素。

ReduceFunction的定义和使用方法如下:

val input: DataStream[(String, Long)] = ...

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .reduce { (v1, v2) => (v1._1, v1._2 + v2._2) }

上面的例子聚合了窗口中所有元素元组的第二个字段。其中v1代表之前做过的所有操作得出的元素,v2代表当前元素。

AggregateFunction

AggregateFunction比ReduceFunction的应用范围更广(但是一般不会用到,reduce就足够了),它有三个参数:输入类型(IN)、累加器类型(ACC)和输出类型(OUT)。输入类型是输入流中元素的类型,AggregateFunction有一个方法将一个输入元素添加到累加器。该接口还具有创建初始累加器、将两个累加器合并为一个累加器以及从累加器中提取输出(OUT类型)的方法。我们将在下面的示例中看到这是如何工作的。

与ReduceFunction相同,Flink将在窗口的输入元素到达时递增地聚合它们。

AggregateFunction的定义和使用方法如下:

/**
 * The accumulator is used to keep a running sum and a count. The [getResult] method
 * computes the average.
 */
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
  override def createAccumulator() = (0L, 0L)

  override def add(value: (String, Long), accumulator: (Long, Long)) =
    (accumulator._1 + value._2, accumulator._2 + 1L)

  override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2

  override def merge(a: (Long, Long), b: (Long, Long)) =
    (a._1 + b._1, a._2 + b._2)
}

val input: DataStream[(String, Long)] = ...

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .aggregate(new AverageAggregate)

上述代码计算窗口中元素第二个字段的平均值。

FoldFunction

FoldFunction指定如何将窗口的输入元素与输出类型的元素组合。对添加到窗口的每个元素和当前输出值增量调用FoldFunction。与ReduceFunction类似。区别在于FoldFunction需要预设一个初始值,reduce不用。

val input: DataStream[(String, Long)] = ...

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .fold("") { (acc, v) => acc + v._2 }

上面的示例将所有输入的值追加到最初为空的String上。

注意:fold()不能与会话窗口或其他可合并窗口一起使用。

ProcessWindowFunction

ProcessWindowFunction是一个底层函数,获取一个包含窗口所有元素的Iterable,以及一个可以访问时间和状态信息的上下文对象,这使它能够比其他窗口函数更加灵活。但这是以性能和资源消耗为代价的,因为元素不能增量聚合,而是需要在内部缓冲,直到窗口被认为可以处理为止。

ProcessWindowFunction的结构如下:

abstract class ProcessWindowFunction[IN, OUT, KEY, W <: Window] extends Function {

  /**
    * Evaluates the window and outputs none or several elements.
    *
    * @param key      The key for which this window is evaluated.
    * @param context  The context in which the window is being evaluated.
    * @param elements The elements in the window being evaluated.
    * @param out      A collector for emitting elements.
    * @throws Exception The function may throw exceptions to fail the program and trigger recovery.
    */
  def process(
      key: KEY,
      context: Context,
      elements: Iterable[IN],
      out: Collector[OUT])

  /**
    * The context holding window metadata
    */
  abstract class Context {
    /**
      * Returns the window that is being evaluated.
      */
    def window: W

    /**
      * Returns the current processing time.
      */
    def currentProcessingTime: Long

    /**
      * Returns the current event-time watermark.
      */
    def currentWatermark: Long

    /**
      * State accessor for per-key and per-window state.
      */
    def windowState: KeyedStateStore

    /**
      * State accessor for per-key global state.
      */
    def globalState: KeyedStateStore
  }

}

注意:如果key是元组的key,或者是一个字符串的某个字段,那么这个key的类型总是二元组,需要手动设置其元组类型,将其转换为大小正确的元组。

ProcessWindowFunction的定义和使用方法如下:

val input: DataStream[(String, Long)] = ...

input
  .keyBy(_._1)
  .timeWindow(Time.minutes(5))
  .process(new MyProcessWindowFunction())

/* ... */

class MyProcessWindowFunction extends ProcessWindowFunction[(String, Long), String, String, TimeWindow] {

  def process(key: String, context: Context, input: Iterable[(String, Long)], out: Collector[String]) = {
    var count = 0L
    for (in <- input) {
      count = count + 1
    }
    out.collect(s"Window ${context.window} count: $count")
  }
}

该示例展示了使用一个processWindow函数,该函数统计了窗口中元素的个数。并输出有关窗口的信息。

注意,对简单聚合(如count)使用ProcessWindowFunction是非常低效的。下面介绍如何将ReduceFunction或AggregateFunction与ProcessWindowFunction组合以获取ProcessWindowFunction的增量聚合和添加的信息。

具有增量聚合功能的底层窗口函数

ProcessWindowFunction可以与ReduceFunction、AggregateFunction或FoldFunction组合,在元素到达窗口时增量聚合元素。当窗口关闭时,ProcessWindowFunction将返回聚合结果。这使得它可以在访问窗口的上下文信息的同时进行增量计算。

提示:一般进行增量聚合计算的情况,都是直接使用增量聚合函数,很少会和ProcessWindowFunction合起来用。

下面的示例演示如何将增量ReduceFunction与ProcessWindowFunction组合,以返回窗口中的最小事件以及窗口的开始时间。

val input: DataStream[SensorReading] = ...

input
  .keyBy(<key selector>)
  .timeWindow(<duration>)
  .reduce(
    (r1: SensorReading, r2: SensorReading) => { if (r1.value > r2.value) r2 else r1 },
    ( key: String,
      context: ProcessWindowFunction[_, _, _, TimeWindow]#Context,
      minReadings: Iterable[SensorReading],
      out: Collector[(Long, SensorReading)] ) =>
      {
        val min = minReadings.iterator.next()
        out.collect((context.window.getStart, min))
      }
  )

下面的示例演示了如何将AggregateFunction与ProcessWindowFunction组合起来计算平均值,并同时输出键和窗口以及平均值。

val input: DataStream[(String, Long)] = ...

input
  .keyBy(<key selector>)
  .timeWindow(<duration>)
  .aggregate(new AverageAggregate(), new MyProcessWindowFunction())

// Function definitions

/**
 * The accumulator is used to keep a running sum and a count. The [getResult] method
 * computes the average.
 */
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
  override def createAccumulator() = (0L, 0L)

  override def add(value: (String, Long), accumulator: (Long, Long)) =
    (accumulator._1 + value._2, accumulator._2 + 1L)

  override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2

  override def merge(a: (Long, Long), b: (Long, Long)) =
    (a._1 + b._1, a._2 + b._2)
}

class MyProcessWindowFunction extends ProcessWindowFunction[Double, (String, Double), String, TimeWindow] {

  def process(key: String, context: Context, averages: Iterable[Double], out: Collector[(String, Double)]) = {
    val average = averages.iterator.next()
    out.collect((key, average))
  }
}

在ProcessWindowFunction中使用窗口状态

除了访问键控状态(任何富函数都可以)之外,ProcessWindowFunction还可以使用键控状态,与富函数不同的是,ProcessWindowFunction的状态称为Per-window State,即其作用域仅为函数当前正在处理的窗口内部。

Per-window State在ProcessWindowFunction中分为两类:

  • globeState:全局状态,窗口中的keyed state数据不限定在某个窗口中。
  • windowState:窗口状态,窗口中的keyed state数据限定在固定的窗口中。

如果同一窗口会多次触发(如event-time触发器加上允许最大延迟时间,则有肯触发多次计算),则此功能很有用。例如可以存储每次窗口触发的次数以及最新一次触发的信息,为下一次窗口触发提供逻辑处理信息。使用Per-window State数据时要及时清理状态数据,可以覆写,调用ProcessWindowFunction的clear()完成状态数据的清理。

触发器

触发器决定窗口何时将数据交给窗口函数处理。每个窗口都有一个默认触发器。如果默认触发器不符合业务需要,也可以使用自定义的触发器。

触发器接口有五种方法:

  • onElement() :每个元素被添加到窗口时,都会触发此方法。
  • onEventTime(): 当注册一个event-time 定时器时会被调用。
  • onProcessingTime(): 当注册一个processing-time 定时器时被调用。
  • onMerge(): 与状态性触发器相关,两个窗口合并时,合并两个触发器的状态,如使用session window时,窗口会进行合并,此时调用该方法。
  • clear() :窗口关闭时触发,用于做一些清理工作。

关于上述方法,需要注意两件事:

1) 前三个函数通过返回TriggerResult来决定如何处理它们的调用事件。操作可以是以下操作之一:

  • CONTINUE:什么都不做
  • FIRE:触发计算
  • PURGE:清除窗口中的元素
  • FIRE_AND_PURGE:触发计算,然后清除窗口中的元素。

2) 这些方法中的任何一个都可以用于为将来的操作注册processing-time 定时器或event-time定时器。

触发与清除

一旦一个触发器认为一个窗口已经可以进行处理,它将触发并返回FIRE或者FIRE_AND_PURGE。这意味着当前窗口马上就要触发计算,并将元素发送给计算方法。如一个带有ProcessWindowFunction的窗口,当触发器触发fire后,所有元素都被传递给ProcessWindowFunction(如果有剔除器,则先经过剔除器)。

如果窗口的计算函数时ReduceFunction、AggregateFunction或FoldFunction,则只发出它们聚合的结果,因为在窗口内部已经由这些预聚合方法进行ji's。

触发器触发的方式有两种:FIRE或者FIRE_AND_PURGE。如果是FIRE的话,将保留window中的内容,FIRE_AND_PURGE则会清除window的内容。默认情况下,触发器使用的是FIRE。

注意:清除操作仅清除window的内容,但会保留窗口的元数据信息和触发器状态。

默认触发器和自定义触发器

WindowAssigner的默认触发器适用于各种用例。例如,所有事件时间窗口分配程序都有一个event time trigger作为默认触发器。一旦watermark 大于窗口的endtime,那么这个触发器就会触发。

注意:全局窗口的默认触发器是一个永不触发的触发器。因此,使用GlobalWindow时,如果需要用到触发器,就必须自定义触发器。

注意:通过使用trigger()指定触发器后,将覆盖WindowAssigner的默认触发器。例如,如果为TumblingEventTimeWindows指定CountTrigger,则不再根据时间进度而仅按count触发窗口,两者不同时生效。所以,如果想同时基于时间和计数触发窗口,就必须编写自定义触发器。

flink的默认触发器有四种:

EventTimeTrigger:时间时间触发器,根据watermark触发。

ProcessingTimeTrigger:处理时间触发器,根据元素在被处理的那台机器的系统时间触发。

CountTrigger:计数触发器,根据元素的个数触发。

PurgingTrigger :清除触发器,将另一个触发器作为参数,将其转换为清除触发器,即在原有触发器的基础上,添加清除窗口内容功能。

如果需要自定义触发器的话,则需要实现Trigger抽象类 。但是该API仍在开发中,在未来的flink版本中可能会有变化。

剔除器

Flink的窗口还可以使用剔除器,用于从窗口中移除元素。使用exictor(…)方法调用(如本文开头所示)。剔除器作用在触发器触发之后,且窗口函数执行之前或之后。逐出器接口有两个方法:

/**
 * Optionally evicts elements. Called before windowing function.
 *
 * @param elements The elements currently in the pane.
 * @param size The current number of elements in the pane.
 * @param window The {@link Window}
 * @param evictorContext The context for the Evictor
 */
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);

/**
 * Optionally evicts elements. Called after windowing function.
 *
 * @param elements The elements currently in the pane.
 * @param size The current number of elements in the pane.
 * @param window The {@link Window}
 * @param evictorContext The context for the Evictor
 */
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);

evictBefore():用于在窗口函数执行之前剔除元素。

evictAfter():用于在窗口函数执行之后剔除元素。

flink有三个预设的Evictors:

CountEvictor:当窗口内元素到达自定义的个数后,则不再向窗口中添加元素。

DeltaEvictor:需要传入一个DeltaFunction和一个threshold,使用DeltaFunction计算窗口缓冲区中最后一个元素与缓冲区中其余所有元素的delta值,并移除delta值大于或等于threshold的元素。

TimeEvictor:使用窗口的时间间隔作为参数,在窗口元素中查找最大时间戳max-ts,并删除时间戳小于max-ts减去interval的所有元素。即剔除过期数据。

默认情况下,预设的剔除器都在窗口函数之前执行。

注意:如果指定了剔除器,则预聚合不生效,因为在进行计算之前,每一条元素都会经过剔除器才会进入窗口。

注意:Flink不能保证窗口中元素的顺序。这意味着尽管剔除器即使从窗口的开头移除元素,但这些元素不一定是最先到达或最后到达的元素。

如何处理迟到的事件元素? 

使用事件时间(event-time)窗口时,可能会发生元素到达晚的情况,即本应该在上一个窗口处理的元素,由于延迟到达flink,而watermark已经超过窗口的endtim而启动计算,导致这个元素没有在上一个窗口中处理。

关于watermark,在我的另一篇文章中有介绍:https://blog.csdn.net/x950913/article/details/106246807

默认情况下,当watermark超过窗口的endtime时,将删除延迟到达的元素。但是,Flink可以为窗口指定一个最大延迟时间。Allowed lateness指定watermark超过endtime多少时间后,再收到事件时间在该窗口内的元素时,会再次触发窗口的计算,其默认值为0。watermark超过endtime后,会触发一次计算,在允许延迟的时间范围内到达的元素,仍然会添加到窗口中,且再次触发计算。所以如果使用的是EventTimeTrigger触发器,延迟但未丢弃的元素可能会导致窗口再次触发。

默认情况下,允许的延迟设置为0。也就是说,在endtime后到达的元素将被删除。

指定允许延迟时间,如下所示:

val input: DataStream[T] = ...

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .allowedLateness(<time>)
    .<windowed transformation>(<window function>)

另外,当使用GlobalWindows窗口时,任何数据都不会被认为是延迟的,因为全局窗口的结束时间戳是Long的最大值.

将迟到元素从侧输出流输出

可以将延迟到达的元素从侧输出流中输出。

首先需要创建一个OutputTag用于接收延迟数据。然后,指定将窗口中的延迟数据发送到OutputTag中:

val lateOutputTag = OutputTag[T]("late-data")

val input: DataStream[T] = ...

val result = input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .allowedLateness(<time>)
    .sideOutputLateData(lateOutputTag)
    .<windowed transformation>(<window function>)

val lateStream = result.getSideOutput(lateOutputTag)

迟到元素处理注意事项

当指定允许的延迟大于0时,在watermark超过窗口的endtime后,仍会保留窗口及其内容。在这些情况下,当一个延迟但未被丢弃的元素到达时,它可能会再次触发这个窗口的触发器。此时这些触发器的Fire被称为late firings,因为它们是由迟到元素触发的,而主Fire是窗口的第一次Fire。在会话窗口(session windows)的情况下,延迟触发可能进一步导致窗口合并,因为它们可能“桥接”两个预先存在的未合并窗口。

注意:延迟到达的元素所触发的计算应被视为对先前计算的结果的更新,即watermark到达endtime后窗口会进行一次计算,之后延迟到达的元素会触发新的计算来更新计算结果,所以数据流将包含同一计算的多个结果。根据应用场景不同,需要考虑是否需要消除重复数据。

窗口计算后还可以做什么?

使用窗口计算后得出的结果仍是一个DataStream,该DataStream中的元素不会保留有关窗口的信息。所以,如果需要保存窗口的元数据信息,就必须编写代码,在ProcessWindowFunction中将窗口的元数据信息与元素进行整合。输出元素中的时间戳是唯一与窗口相关的信息,可以将其设置为窗口的最大允许延迟时间,即endtime-1,因为endtime之前的元素才属于这个窗口,大于等于endtime的则属于下一个窗口(event-time windows 和processing-time windows都是这样的)。

在经过窗口函数处理后的元素总会包含一个时间戳,可以是event-time时间戳也可以是 processing-time时间戳。

对于 processing-time 窗口可能没什么作用,但对于event-time窗口,配合watermark机制,可以将event-time属于同一窗口的元素放在另一个窗口中处理,即连续的窗口操作,在下面有介绍。

watermarks对窗口的作用 

在此稍微提及一点关于watermark和窗口的作用。

触发器基于watermark有两种处理方式:

  1. 当watermark大于等于窗口的endtime时,触发窗口计算。
  2. 当watermark小于于窗口的endtime时,则将watermark转发给下游操作(维护watermark为窗口中所有元素中的最大时间戳)

连续的窗口操作

如上所述,经过窗口函数计算后的结果仍然带有时间戳,且与watermark搭配使用,就可以连续使用多个窗口操作。如在上游窗口计算之后,仍可以对得出的结果以不同的key、不同的function进行计算。如:

DataStream<Integer> input = ...;

DataStream<Integer> resultsPerKey = input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .reduce(new Summer());

DataStream<Integer> globalResults = resultsPerKey
    .windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
    .process(new TopKWindowFunction());

上述的例子中,事件时间戳在0~5秒的元素(包含0秒,不含5秒),经过第一个窗口计算后,产生的结果传入第二个窗口时也属于0~5秒的窗口,即同窗口的元素在下游窗口中仍属于同一窗口。如第一个窗口计算0~5秒内每个key的和,再在第二个窗口中取出0~5秒内key的和的TopK。

如何估计窗口存储大小? 

定义窗口的时间范围时,可以定义一个很长的时间,甚至是几天、几周或几月。因此窗口可以累积相当多的数据。在估计窗口计算的存储量时,需要记住以下几个规则:

  • Flink会为每个窗口的所有元素都创建一个副本,因此,滚动窗口对每个元素仅保留一个副本,因为一个元素恰好仅属于一个窗口,除非它被搁置(dropped late)。相反,滑动窗口会为元素保存多个副本,因为一个元素可能属于多个窗口,每个窗口都会保存一次。所以,如果使用滑动窗口,那么应该尽量避免窗口大小大,而滑动步长小的情况,如窗口大小为1天,滑动步长为1秒。
  • ReduceFunction,AggregateFunction和FoldFunction可以大大减少存储需求,因为他们会尽量早地对元素进行聚合,且每个窗口仅存储一个值,而不是所有元素。相反,ProcessWindowFunction需要存储每个元素。
  • 使用剔除器Evictor会阻止所有预聚合操作,因为在算之前,必须将窗口的所有元素传递到剔除器。

 

猜你喜欢

转载自blog.csdn.net/x950913/article/details/106203894