Flink Streaming (DataStream API) Operators - Windows



Application Development / Streaming (DataStream API) / Operators / Windows

Windows

Windows是处理无限流(infinite streams)的核心。Windows将流拆分为有限大小的“桶”,我们可以在其上应用计算。本文档重点介绍如何在Flink中执行窗口,以及程序员如何从其提供的函数中获益最大。

窗口形式的Flink程序的一般结构如下所示。第一个小片指的是Keys化的流,而第二个小片指的是非Keys化的流。正如你所见,唯一的区别是keyBy(...) 调用Keys化的流,window(…)变为windowAll(…)调用的为非Keys化的数据流。这也将作为本页其余部分的路线图。

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"

在上面,方括号([…])中的命令是可选的。这表明Flink允许您以多种不同方式自定义窗口逻辑,以便最适合您的需求。

目录


Window 生命周期

简而言之,只要应该属于此窗口的第一个数据元到达就会创建一个 window,当时间(事件时间或处理时间)超过其结束时间戳加上用户指定的允许延迟时窗口将被完全删除(请参阅允许的延迟)。Flink保证只删除基于时间的窗口,而不能删除其他类型的窗口,例如全局窗口(请参阅窗口分配器)。例如,使用基于事件时间的窗口策略,每隔5分钟创建一个非重叠(或翻滚)的窗口并允许延迟1分钟,Flink将为12:00到12之间的间隔创建一个新窗口,当具有落入此间隔的时间戳的第一个数据元到达时,并且当水位线通过12:06时间戳时它将删除它。

此外,每个窗口都有触发器 Trigger(参见 Triggers)和一个函数(ProcessWindowFunction,ReduceFunction,AggregateFunction 或 FoldFunction)(见Window Functions)连接到它。该函数将包含要应用于窗口内容的计算,而 Trigger 指定了在什么条件下可以将窗口视为要应用该函数的条件。触发策略可能类似于当窗口中的数据元数量大于4时或者当水位线通过窗口结束时。触发器还可以决定在创建和删除之间的任何时间清除窗口的内容。在这种情况下,清除仅指窗口中的元素,而不是窗口元数据,这意味着仍然可以将新数据添加到该窗口。

除了上述内容之外,您还可以指定一个 Evictor(参见 Evictors),它可以在 trigger 触发后以及应用函数之前 and/or 之后从窗口中删除元素。

在下文中,我们将对上述每个组件进行更详细的介绍。我们先从上面的代码片段中的必需部分开始(请参阅Keyed vs Non-Keyed WindowsWindow AssignerWindow Function)),然后再转到可选部分。

Keys 化与非被Keys 化 Window 对比

首先要说明的是你的流是否应该设置key,这必须在定义窗口之前完成。使用 keyBy(...) 会将无限流拆分为逻辑键流。如果未调用keyBy(...),则不会为您的流设置key。

对于Keys化的数据流,可以将传入事件的任何属性用作为键(更多详细信息查看here)。拥有keys化的数据流将使您的 window 计算可以由多个任务并行执行,因为每个逻辑keys 化流都可以独立于其余逻辑流进行处理。。引用相同Key的所有元素将被发送到同一并行任务(the same parallel task)

对于非 Keys化的数据流情况下,您的原始流将不会被拆分为多个逻辑流,并且所有窗口逻辑将由单个任务执行,即并行度为1。

窗口分配器(Window Assigners)

在指定是否对流进行 kyes 化后,下一步是定义一个窗口分配器(window assigner)。窗口分配器定义如何将元素分配给窗口,这是通过在 window(...)(对于Keys 化的流)或 windowAll() (对于非 Keys 化流)调用中指定您选择的 WindowAssigner 来完成的。

WindowAssigner 负责将每个传入的元素分配给一个或多个窗口,Flink 带有针对最常见用例的预定义窗口分配器,即翻滚窗口(tumbling windows)、滑动窗口(sliding windows)、会话窗口(session windows)和全局窗口( global windows)。您还可以通过扩展 WindowAssigner 类来实现自定义窗口分配器。所有内置窗口分配器(全局窗口除外)均基于时间将元素分配给窗口,时间可以是处理时间,也可以是事件时间。请查看我们关于 event time的部分,了解处理时间和事件时间之间的差异以及时间戳和水位线的生成方式。

基于时间的窗口具有开始时间戳(包括端点)和结束时间戳(不包括端点),它们一起描述窗口的大小。在代码中,Flink在使用基于时间的窗口时使用 TimeWindow,该窗口具有查询开始和结束时间戳的方法 ,以及返回给定窗口的最大允许时间戳的附加方法maxTimestamp()

下面我们将展示 Flink 的预定义窗口分配器如何工作以及如何在 DataStream 程序中使用它们。下图显示了每个分配器的工作情况。紫色圆圈表示数据流的元素,这些元素由某个键(本例中为user 1,user 2 和 user 3)划分。x轴显示时间的进度。

翻滚的Windows(Tumbling Windows)

翻滚窗口分配器将每个元素分配给指定窗口大小的窗口。翻滚窗口具有固定的大小,不重叠。例如,如果指定大小为5分钟的翻滚窗口,则将评估当前窗口,并每隔5分钟启动一个新窗口,如下图所示。
tumbling-windows.svg
以下代码段显示了如何使用翻滚窗口。

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-0,例如在中国,您必须指定Time.hours(-8)的偏移量。

Sliding Windows

滑动窗口分配器将元素分配给固定长度的窗口。 与翻滚窗口分配器类似,窗口大小由窗口 size参数设置,附加的窗口滑动参数控制滑动窗口的启动频率。 因此如果滑动小于窗口大小,则滑动窗口会重叠,在这种情况下,元素会被分配给多个窗口。

例如,您可以将大小为10分钟的窗口滑动5分钟,这样你每隔5分钟就会得到一个窗口,其中包含过去10分钟内到达的事件,如下图所示。
sliding-windows.svg
以下代码段显示了如何使用滑动窗口。

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>)

可以使用Time.milliseconds(x)Time.seconds(x)Time.minutes(x)等指定时间间隔。

如上示例所示,滑动窗口分配器还采用可选的offset参数,该参数可用于更改窗口的对齐方式。 例如在没有偏移的情况下,每小时滑动30分钟的窗口与时期对齐,即您将获得诸如 1:00:00.000 - 1:59:59.999, 1:30:00.000 - 2:29:59.999 等窗口。 如果你想改变它,你可以给出一个偏移量。 如果偏移15分钟,你会得到 1:15:00.000 - 2:14:59.999, 1:45:00.000 - 2:44:59.999 等。偏移量的重要用例是将窗口调整为时区 除了UTC-0,例如在中国您必须指定 Time.hours(-8) 的偏移量。

Session Windows

会话窗口分配器按活动会话对元素进行分组。 与滚动窗口和滑动窗口相比,会话窗口不重叠且没有固定的开始和结束时间。 相反当会话窗口在一段时间内没有接收到元素时,即当发生不活动的间隙时,会话窗口关闭。 会话窗口分配器可以配置有静态会话间隔,也可以配置有会话间隔提取器功能,该功能定义不活动的时间长度。 当该期限到期时,当前会话将关闭,后续元素将分配给新的会话窗口。
session-windows.svg
以下代码段显示了如何使用会话窗口。

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接口指定动态间隙。

注意 由于会话窗口没有固定的开始和结束时间,因此对它们的评估不同于滚动窗口和滑动窗口。 在内部会话窗口运算符会为每个到达的记录创建一个新窗口,如果窗口彼此之间的距离比已定义的间隔小,则将它们合并在一起。 为了可合并,会话窗口运算符需要合并触发器和合并窗口函数,例如 ReduceFunctionAggregateFunctionProcessWindowFunction(FoldFunction无法合并。)

Global Windows

全局窗口分配器将具有相同 key 的所有元素分配给同一个全局窗口。 此窗口方案仅在您还指定自定义触发器时才有用,否则将不执行任何计算,因为全局窗口没有我们可以处理聚合元素的自然端。
non-windowed.svg
以下代码段显示了如何使用全局窗口。

val input: DataStream[T] = ...

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

Window Functions

定义窗口分配器后,我们需要指定要在每个窗口上执行的计算,这是窗口函数的职责,一旦系统确定窗口已准备好进行处理(请参阅Flink如何确定窗口准备就绪的触发器),该窗口功能便会处理每个(可能是keys化的)窗口的元素。

窗口函数可以是ReduceFunctionAggregateFunctionFoldFunctionProcessWindowFunction之一。 前两个可以更有效地执行(参见State Size部分),因为Flink可以在每个窗口到达时增量地聚合它们的元素。 ProcessWindowFunction获取窗口中包含的所有元素的 Iterable 以及有关元素所属窗口的其他元信息。

使用ProcessWindowFunction的窗口转换不能像其他情况一样有效地执行,因为Flink必须在调用函数之前在内部缓冲窗口的所有元素。 这可以通过将ProcessWindowFunctionReduceFunctionAggregateFunctionFoldFunction结合使用来获得窗口元素的增量聚合和ProcessWindowFunction接收的其他窗口元数据,从而缓解这种情况。下面我们将查看每个变体的示例。

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) }

上面的示例汇总了窗口中所有元素的元组的第二个字段值。

AggregateFunction

AggregateFunction 是 ReduceFunction 的通用版本,参数有三种类型:输入类型(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,第一个元素与输出类型的预定义初始值组合。

可以像这样定义和使用FoldFunction:

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

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

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

注意 fold()不能与session window或其他可合并 windows一起使用。

ProcessWindowFunction

ProcessWindowFunction 获取包含窗口所有元素的 Iterable,以及可访问时间和状态信息的Context对象,这使其能够提供比其他窗口函数更多的灵活性。 这是以性能和资源消耗为代价的,因为元素不能以增量方式聚合而是需要在内部进行缓冲,直到认为窗口已准备好进行处理为止。

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参数 是通过为keyBy()调用指定的 KeySelector 提取的 key。 在 tuple-index 键或字符串字段引用的情况下此key类型始终为Tuple,您必须手动将其转换为正确大小的元组以提取key字段。

可以这样定义和使用ProcessWindowFunction:

DataStream<Tuple2<String, Long>> input = ...;

input
  .keyBy(t -> t.f0)
  .timeWindow(Time.minutes(5))
  .process(new MyProcessWindowFunction());

/* ... */

public class MyProcessWindowFunction 
    extends ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow> {

  @Override
  public void process(String key, Context context, Iterable<Tuple2<String, Long>> input, Collector<String> out) {
    long count = 0;
    for (Tuple2<String, Long> in: input) {
      count++;
    }
    out.collect("Window: " + context.window() + "count: " + count);
  }
}

该示例显示了一个ProcessWindowFunction,它对窗口中的元素进行计数。 另外窗口功能将有关窗口的信息添加到输出。

注意 请注意使用 ProcessWindowFunction 进行简单的聚合(例如count)效率非常低。 下一节将介绍如何将 ReduceFunctionAggregateFunctionProcessWindowFunction 结合使用,以获取增量聚合和 ProcessWindowFunction 的添加信息。

ProcessWindowFunction with Incremental Aggregation

ProcessWindowFunction 可以与 ReduceFunctionAggregateFunctionFoldFunction结合使用,以便在元素到达窗口时递增聚合元素。 关闭窗口时,将为 ProcessWindowFunction 提供聚合结果。 这允许它在访问 ProcessWindowFunction 的附加窗口元信息的同时增量地计算窗口。

注意 您还可以使用旧版 WindowFunction 而不是 ProcessWindowFunction 进行增量窗口聚合。

Incremental Window Aggregation with ReduceFunction

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

DataStream<SensorReading> input = ...;

input
  .keyBy(<key selector>)
  .timeWindow(<duration>)
  .reduce(new MyReduceFunction(), new MyProcessWindowFunction());

// Function definitions

private static class MyReduceFunction implements ReduceFunction<SensorReading> {

  public SensorReading reduce(SensorReading r1, SensorReading r2) {
      return r1.value() > r2.value() ? r2 : r1;
  }
}

private static class MyProcessWindowFunction
    extends ProcessWindowFunction<SensorReading, Tuple2<Long, SensorReading>, String, TimeWindow> {

  public void process(String key,
                    Context context,
                    Iterable<SensorReading> minReadings,
                    Collector<Tuple2<Long, SensorReading>> out) {
      SensorReading min = minReadings.iterator().next();
      out.collect(new Tuple2<Long, SensorReading>(context.window().getStart(), min));
  }
}

Incremental Window Aggregation with AggregateFunction

以下示例显示如何将增量 AggregateFunction 与 ProcessWindowFunction 结合以计算平均值,并同时发出键和窗口以及平均值。

DataStream<Tuple2<String, Long>> input = ...;

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

// Function definitions

/**
 * 累加器用于保持运行中 sum 和 count.  {@code getResult} 方法计算平均值
 */
private static class AverageAggregate
    implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> {
  @Override
  public Tuple2<Long, Long> createAccumulator() {
    return new Tuple2<>(0L, 0L);
  }

  @Override
  public Tuple2<Long, Long> add(Tuple2<String, Long> value, Tuple2<Long, Long> accumulator) {
    return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
  }

  @Override
  public Double getResult(Tuple2<Long, Long> accumulator) {
    return ((double) accumulator.f0) / accumulator.f1;
  }

  @Override
  public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
    return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
  }
}

private static class MyProcessWindowFunction
    extends ProcessWindowFunction<Double, Tuple2<String, Double>, String, TimeWindow> {

  public void process(String key,
                    Context context,
                    Iterable<Double> averages,
                    Collector<Tuple2<String, Double>> out) {
      Double average = averages.iterator().next();
      out.collect(new Tuple2<>(key, average));
  }
}

Incremental Window Aggregation with FoldFunction

以下示例显示如何将增量 FoldFunction 与 ProcessWindowFunction 结合以提取窗口中的事件数并返回窗口的 key 和结束时间。

DataStream<SensorReading> input = ...;

input
  .keyBy(<key selector>)
  .timeWindow(<duration>)
  .fold(new Tuple3<String, Long, Integer>("",0L, 0), new MyFoldFunction(), new MyProcessWindowFunction())

// Function definitions

private static class MyFoldFunction
    implements FoldFunction<SensorReading, Tuple3<String, Long, Integer> > {

  public Tuple3<String, Long, Integer> fold(Tuple3<String, Long, Integer> acc, SensorReading s) {
      Integer cur = acc.getField(2);
      acc.setField(cur + 1, 2);
      return acc;
  }
}

private static class MyProcessWindowFunction
    extends ProcessWindowFunction<Tuple3<String, Long, Integer>, Tuple3<String, Long, Integer>, String, TimeWindow> {

  public void process(String key,
                    Context context,
                    Iterable<Tuple3<String, Long, Integer>> counts,
                    Collector<Tuple3<String, Long, Integer>> out) {
    Integer count = counts.iterator().next().getField(2);
    out.collect(new Tuple3<String, Long, Integer>(key, context.window().getEnd(),count));
  }
}

Using per-window state in ProcessWindowFunction

除了访问keyed状态(任何富函数都可以)之外,ProcessWindowFunction 还可以使用key状态,该key状态的范围仅限于该函数当前正在处理的窗口。 在这种情况下,重要的是要了解每个窗口状态所指的窗口是什么,涉及不同的“window”:

  • 指定窗口操作时定义的window:这可能是1小时的滚动窗口或长度为 2 小时的1小时滑动的滑动窗口。
  • 给定key的已定义window的实际实例:对于 xyz 的 user-id,这可能是从12:00到13:00的一个时间窗口。这是基于窗口定义的,并且有许多窗口基于当前 job 正在处理的 key 的数量以及事件属于哪个时隙。

每窗口状态与两者窗口中的最后一个相关。 这意味着如果我们处理1000个不同key的事件,并且当前所有事件的事件都落入[12:00, 13:00]时间窗口中,那么将有1000个窗口实例,每个窗口实例都有自己 key 的窗口状态。

在Context对象上调用 process() 有两种方法可以访问两种类型的状态:

  • globalState():允许访问不在窗口内的 key 状态
  • windowState():它允许访问也限定在窗口范围内的 key 状态

如果您预计同一窗口会发生多次触发,则此功能非常有用。例如,对于迟到的数据较晚的触发,或者您有一个推测性较早触发的自定义触发器时,可能会发生这种情况。 在这种情况下,您将存储有关先前触发或每个窗口状态的触发次数的信息。

使用窗口状态时,清除窗口时也要清除该状态,这一点很重要, 这应该在clear() 方法中发生。

WindowFunction (旧版)

在某些可以使用 ProcessWindowFunction 的地方您也可以使用 WindowFunction。 这是 ProcessWindowFunction 的旧版本,它提供较少的上下文信息,并且没有一些高级功能,例如每个window 的 key 状态。 此接口将在某个时候被弃用

WindowFunction的代码如下所示:

public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, Serializable {

  /**
   * Evaluates the window and outputs none or several elements.
   *
   * @param key The key for which this window is evaluated.
   * @param window 正在计算的 window.
   * @param input 窗口中正在计算的元素.
   * @param out 用于emitting元素的Collector.
   *
   * @throws Exception 该函数可能会引发异常而是程序失败并触发恢复.
   */
  void apply(KEY key, W window, Iterable<IN> input, Collector<OUT> out) throws Exception;
}

可以像如下使用:

DataStream<Tuple2<String, Long>> input = ...;

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .apply(new MyWindowFunction());

Triggers

触发器确定了一个窗(由窗口分配器生成)何时准备好并可以被window函数处理,每个WindowAssigner都带有一个默认触发器,如果默认触发器不符合您的需要,您可以使用trigger(...)指定自定义触发器。

触发器接口具有五种方法,这些方法允许触发器对不同事件做出反应:

  • onElement()方法:添加到窗口的每个元素时调用。
  • onEventTime()方法:当注册事件时间计时器触发时调用。
  • onProcessingTime()方法:当注册的处理时间计时器触发时调用。
  • onMerge()方法:它与有状态触发器相关,两个触发器的相应窗口合并时合并两个触发器的状态,例如使用会话窗口时。
  • clear()方法:执行删除相应窗口时所需的任何操作。

关于上述方法需要注意两点:
1)前三个解决如何通过 TriggerResult 来对其调用事件进行操作,该操作可以是以下之一:

  • CONTINUE:继续,什么都不做;
  • FIRE:触发计算,会处理窗口数据;
  • PURGE:触发清理,移除窗口和其中的数据
  • FIRE_AND_PURGE(FIRE+PURGE):触发计算并在之后清除窗口及其中的数据。

2)这些方法中的任何一种都可以被用于为将来的操作注册处理时间或事件时间计时器。

Fire and Purge

触发器确定窗口已准备好进行处理后就会触发,即返回FIREFIRE_AND_PURGE。这是窗口 operator 发出当前窗口结果的信号,给定一个带有ProcessWindowFunction的窗口,所有元素都传递给ProcessWindowFunction(可能之后将他们交给一个 evictor)。 带有ReduceFunctionAggregateFunctionFoldFunction的Windows会马上发出聚合的结果。

当触发器触发时,它可以是 FIREFIRE_AND_PURGEFIRE 会保留窗口内容,FIRE_AND_PURGE会删除其内容。 默认情况下预先实现的触发器仅触发 FIRE 而不会清除窗口状态。

注意 清除将仅简单地删除窗口的内容,并将保留有关该窗口的任何潜在元信息和任何触发状态。

WindowAssigners 的默认 Triggers

WindowAssigner 的默认触发器适用于许多用例,例如所有事件时间窗口分配器都有一个EventTimeTrigger作为默认触发器。 一旦 watermark 通过窗口的末端,该触发器就会触发。

注意 GlobalWindow 的默认触发器是NeverTrigger,它永远不会触发。 因此在使用GlobalWindow时,您始终必须定义自定义触发器

注意 通过使用trigger() 指定触发器,您将覆盖WindowAssigner的默认触发器。 例如如果为TumblingEventTimeWindows指定CountTrigger,则将不再基于时间进度而是仅通过计数来获取窗口触发。 现在如果你想根据时间和数量做出反应,你必须编写自己的自定义触发器。

内置和自定义 Triggers

Flink带有一些内置触发器。

  • EventTimeTrigger(已经提到过)根据watermark 测量的事件时间进度触发。
  • ProcessingTimeTrigger 根据处理时间触发。
  • CountTrigger 一旦窗口中的元素数量超过给定限制就会触发。
  • PurgingTrigger将另一个触发器作为参数,并将其转换为清除触发器。

如果需要实现自定义触发器,则应该 check out 抽象的 Trigger类。请注意,API仍在不断发展并可能会在Flink的未来版本中发生变化。

Evictors(逐出器)

Flink的窗口模型允许除了WindowAssignerTrigger之外还指定一个可选的 Evictor可以理解为窗口函数数据的一个过滤器)。 可以使用evictor(...)方法来完成此操作(如本文档开头所示)。 Evictors可以在触发器触发后,应用窗口函数之前 and/or 之后从窗口中移除元素。 为此,Evictor接口有两种方法:

/**
 * 可选地逐出元素. windowing 函数前调用.
 *
 * @param elements 窗口中当前的元素.
 * @param size 窗口中当前的元素数.
 * @param window The {@link Window}
 * @param evictorContext Evictor的上下文
 */
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);

/**
 * 可选地 evicts 元素. windowing 函数之后调用.
 *
 * @param elements 窗口中当前的元素.
 * @param size 窗口中当前的元素数.
 * @param window The {@link Window}
 * @param evictorContext Evictor的上下文
 */
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);

evictBefore()包含要在窗口函数之前应用的逐出逻辑,而evictAfter()包含要在窗口函数之后应用的逐出逻辑。 在应用窗口函数之前被逐出的元素将不会被其处理。

Flink附带三个预先实施的evictors。 它们是:

  • CountEvictor:窗口保持用户指定数量的元素,并从窗口缓冲区的头部开始丢弃其余的元素。
  • DeltaEvictor:采用DeltaFunction和阈值,计算窗口缓冲区中最后一个元素与其余每个元素之间的差值,并删除delta大于或等于阈值的数据。
  • TimeEvictor:将以毫秒为单位的间隔作为参数,对于给定的窗口它将查找其元素中的最大时间戳max_ts,并删除所有时间戳小于max_ts - interval的数据。
    Evictor
    默认:在默认情况下所有预先实现的evictors在均在窗口函数之前应用其的逻辑。

注意 指定Evictors会阻止任何预聚合,因为在应用计算之前,必须将窗口的所有数据传递给Evictors

注意 Flink不保证窗口内元素的顺序。 这意味着尽管Evictors可以从窗口的开头移除元素,但这些元素不一定是首先到达或最后到达的元素。

Allowed Lateness

当正处理 event-time 窗口时,可能会发生元素迟到的情况,即 Flink 用于跟踪事件时间进度的 watermark 已经超过元素所属的窗口的结束时间戳。 可查看event time,特别是late elements,以便更全面地讨论Flink如何处理事件时间。

默认情况下,当 watermark 超过窗口末尾时,会删除延迟数据。 但是,Flink允许为窗口运算指定最大允许延迟。允许指定延迟元素在被删除之前可以延迟多少时间,其默认值为0。在水位线通过窗口结束时间点之后但在通过窗口结束时间点前到达的元素加上允许的延迟时间, 仍会添加到窗口中。 根据所使用的触发器,延迟但未丢弃的元素可能会导致窗口再次触发,EventTimeTrigger就是这种情况。

为了使此工作正常进行Flink保持窗口的状态,直到他们允许的延迟到期。 一旦发生这种情况,Flink将删除窗口并删除其状态,如Window Lifecycle部分中所述。

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

您可以指定允许的延迟,如下所示:

DataStream<T> input = ...;

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

注意当使用 GlobalWindows 窗口分配器时,没有数据被认为是迟到的,因为全局窗口的结束时间戳是Long.MAX_VALUE

Getting late data as a side output

使用Flink的side output(旁路输出)功能,您可以获得最近丢弃的数据流。

首先需要在窗口化流上使用sideOutputLateData(OutputTag)指定要获取延迟数据。 然后您可以根据窗口操作的结果获取旁路输出流:

final OutputTag<T> lateOutputTag = new OutputTag<T>("late-data"){};

DataStream<T> input = ...;

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

DataStream<T> lateStream = result.getSideOutput(lateOutputTag);

Late elements considerations

当指定允许的延迟大于0时,在水位线通过窗口末尾之后将保留窗口及其内容。 在这些情况下,当延迟但未丢弃的元素到达时,可能会触发该窗口的另一次触发。 这些触发称为延迟触发,因为它们是由迟到事件触发的,与主触发相反,后者(主触发)是窗口的第一次发射。 在会话窗口的情况下,延迟触发会进一步导致窗口的合并,因为它们可能“bridge”两个预先存在的未合并窗口之间的间隙。。

注意 :您应该注意到,延迟触发的元素被视为先前计算的更新结果,即您的数据流将包含同一计算的多个结果。 根据您的应用程序,您需要考虑这些重复的结果或对它们进行重复数据删除。

Working with window results

窗口化操作的结果还是一个DataStream,结果操作元素中没有保留任何有关窗口操作的信息,因此如果要保留有关窗口的元信息,则必须在ProcessWindowFunction的结果元素中手动编码该信息。 在结果元素上设置的唯一相关信息是元素时间戳,由于窗口结束时间戳是唯一的,因此将其设置为已处理窗口的最大允许时间戳,即结束时间戳 -1。 请注意,事件时间窗口和处理时间窗口都是如此,即在窗口操作元素之后总是具有时间戳,但是这可以是 event-time时间戳 或 processing-time时间戳。 对于processing-time窗口,这没有特别的含义,但对于事件时间窗口,这与watermark与窗口交互的方式一起使得能够以相同的窗口大小进行连续的窗口操作。 在查看 watermark 如何与窗口交互后,我们将介绍这一点。

Interaction of watermarks and windows

在继续本节之前,您可能需要查看有关event time and watermarks的部分。

当watermark到达窗口操作时,会触发两件事:

  • watermark触发计算所有窗口,其中最大时间戳(即结束时间戳-1)小于新水位线的所有窗口的计算
  • watermark被转发(按原样)到下游操作

直观地,一旦下游接收到该watermark,watermark “刷出”所有后期操作中被认为是迟到的窗口。

Consecutive windowed operations

如前所述,计算窗口结果的时间戳的方式以及watermark与窗口交互的方式允许将连续的窗口操作串联在一起,当您想要执行两个连续的窗口操作时,如果要使用不同的 key 但仍希望来自同一上游窗口的元素最终位于同一下游窗口中,此功能将非常有用。 考虑以下示例:

ataStream<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) 中结束,这允许计算每个key的和,然后在第二个operation中计算同一窗口内的前k个元素。

Useful state size considerations

Windows可以定义很长时间(例如几天,几周或几个月),因此会累积非常大的状态。 在估算窗口计算的存储要求时,需要牢记一些规则:

  1. Flink为每个窗口所属的每个元素创建一个副本。 鉴于此滚动窗口保留每个元素的一个副本(一个元素恰好属于一个窗口,除非它被延迟)。 相反如Window Assigners 部分中所述,滑动窗口会为每个元素创建多个元素。 因此,大于 1 天的滑动窗口和滑动 1 秒的滑动窗口可能不是一个好主意。
  2. ReduceFunctionAggregateFunctionFoldFunction可以显著降低存储的要求,因为它们立即聚合元素并且每个窗口只存储一个值。 相反仅使用ProcessWindowFunction就需要累积所有元素。
  3. 使用Evictor可以防止任何预聚合,因为在应用计算之前,窗口的所有元素都必须通过逐出器传递(参见Evictors)。







Watermark 的理解

Watermark是用来处理 eventtime 中数据的乱序问题,是Eventtime处理进度的一个标志。通常又是需要结合 window 来实现。如下图,事件到达Flink是一个乱序的,图中的数字表示时间戳。

stream_watermark_out_of_order

从图中可以看到时间戳为9的事件延迟到达了,当处理时按照事件发生的时间处理,这里显然就有问题了。为了保证基于事件时间在处理实时数据还是重新处理历史的数据时都能保证结果一致,需要一些额外的处理,这时就需要用到 Watermark 了。

例如在处理上面数据时,第一个为7,地二个是11,这时就输出结果吗,这个不好判断了,当数据时乱序到达,可能有些事件会晚到达,谁都不确定事件7和事件11中间的事件是否存在,是否已经全部到达,或者什么时候到达。那么我们只能等待,等待就会有缓存,然后必然就会产生延迟。那么等待多久,如果没有限制,那么有可能存在一直等下去,这样程序一直不敢输出结果了,Watermark 正是限定这个等待时间的,其表示早于这个时间的所有事件已全部到达,可以将计算结果输出了。



更详细部分可访问我的Github
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值