flink文档之window操作


  初学flink和第一次进行博客编写,决定从文档看起,以及附上从各位博主总结来的内容。主要作用是督促自己养成读文档,记录博客的习惯以及记录下自己在学习、实际生产过程中的心得,记录下来留给需要之时参考,如有错误还请批评指正。
原文: flink 1.7文档
本文多部分引用自 简书上一位大大:写Bug的张小天

Windows

  Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的桶(buckets),我们可以在这些桶上做计算操作。本文主要聚焦于在Flink中如何进行窗口操作,以及程序员如何从window提供的功能中获得最大的收益。
  窗口化的Flink程序的通用结构如下,第一个代码段中是分组的流( keyed streams),而第二段是非分组的流(non-keyed)。正如我们所见,唯一的区别是分组的stream调用keyBy(…)和window(…),而非分组的stream中window()换成了windowAll(…),这些也将贯穿都这一页的其他部分中。

Keyed Windows

stream
       .keyBy(...)            <-  根据某字段指定keyBy操作,是是否是一个分组窗口的划分标志
       .window(...)           <-  window assingers 窗口分配
      [.trigger(...)]         <-  窗口触发器
      [.evictor(...)]         <-  窗口驱逐器
      [.allowedLateness(...)]    <-  窗口允许数据最大延迟时间
      [.sideOutputLateData(...)] <-  窗口指定延迟的数据操作
       .reduce/aggregate/fold/apply()  <-  窗口聚合操作(分为两种:增量聚合、全量聚合)
      [.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 的生命周期

  简单地说,当一个属于window的元素到达之后这个window就创建了,而当当前时间(事件或者处理时间)为window的创建时间跟用户指定的延迟时间(allowed lateness)相加时,窗口将被彻底清除。Flink 确保了只清除基于时间的window,其他类型的window不清除,(例如:全局window)。例如:对于一个每5分钟创建无覆盖的(即 翻滚窗口)窗口,允许一个1分钟的时延的窗口策略,Flink将会在12:00到12:05这段时间内第一个元素到达时创建窗口,当水印通过12:06时,移除这个窗口。
  此外,每个 Window 都有一个Trigger(触发器) 和一个附属于 Window 的函数(例如:ProcessWindowFunction, ReduceFunction, AggregateFunction or FoldFunction)。函数里包含了应用于窗口(Window)内容的计算,而Trigger(触发器)则指定了函数在什么条件下可被应用(函数何时被触发),一个触发策略可以是 “当窗口中的元素个数超过4个时” 或者 “当水印达到窗口的边界时”。触发器还可以决定在窗口创建和删除之间的任意时刻清除窗口的内容,这种情况下的清除仅指清除窗口的内容而不是窗口的元数据,也就是说新的数据还是可以被添加到当前的window中。
  除了上面的提到之外,你还可以指定一个驱逐器(Evictor), Evictor将在触发器触发之后或者在函数被应用之前或者之后,清除窗口中的元素。
  接下来我们将更深入的去了解上述的部件,我们从上述片段的主要部分开始(如:Keyed vs Non-Keyed Windows, Window Assigner, 及 Window Function),然后是可选部分。

分组和非分组Windows (Keyed vs Non-Keyed Windows)

  首先,第一件事是指定你的数据流是分组的还是未分组的,这个必须在定义 window 之前指定好。使用 keyBy(…) 会将你的无限数据流拆分成逻辑分组的数据流,如果 keyBy(…) 函数不被调用的话,你的数据流将不是分组的。
  在分组数据流中,任何正在传入的事件的属性都可以被当做key(更多详情),分组数据流将使你的window计算通过多任务并发执行,每一个逻辑分组流在执行中与其他的逻辑分组流是独立进行的,所有具有相同key的元素都将被送到同一个并行度中计算。
  在非分组数据流中,你的原始数据流并不会拆分成多个逻辑流并且所有的window逻辑将在一个任务中执行,并发度为1。

窗口分配器(Window Assingers)

  指定完你的数据流是分组的还是非分组的之后,接下来你需要定义一个窗口分配器(window assigner),窗口分配器定义了元素如何分配到窗口中,这是通过在分组数据流中调用window(…)或者非分组数据流中调用windowAll(…)时你选择的窗口分配器(WindowAssigner)来指定的。
  一个WindowAssigner负责将每一个到来的元素分配给一个或者多个窗口(window),Flink 提供了一些常用的预定义窗口分配器,即:滚动窗口( tumbling windows)、滑动窗口(sliding windows)、会话窗口(session windows)和全局窗口(global windows)。你也可以通过继承WindowAssigner类来自定义自己的窗口。所有的内置窗口分配器(除了全局窗口 global window)都是通过时间来分配元素到窗口中的,这个时间要么是处理的时间,要么是事件发生的时间。请看一下我们的event time部分来了解更多处理时间和事件时间的区别及时间戳(timestamp)和水印(watermark)是如何产生的。
  接下来我们将展示Flink的预定义窗口分配器是如何工作的,以及它们在DataStream程序中是如何使用的。接下来我们将展示Flink的预定义窗口分配器是如何工作的,以及它们在DataStream程序中是如何使用的。下图中展示了每个分配器是如何工作的,紫色圆圈代表着数据流中的一个元素,这些元素是通过一些key进行分区(在本例中是 user1,user2,user3), X轴显示的是时间进度。

滚动窗口

  滚动窗口分配器将每个元素分配的一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,当前窗口将被评估并将按下图说明每5分钟创建一个新的窗口。
创建一个滚动窗口
下面的代码片段展示了如何使用滚动窗口。

DataStream<T> input = ...;

// 滚动事件时间窗口( 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>);

// 每日偏移8小时的滚动事件时间窗口(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>);
    
滑动窗口

  滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于滚动参数的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。
  例如,你有10分钟的窗口和5分钟的滑动,那么每个窗口中5分钟的窗口里包含着上个10分钟产生的数据,如下图所示:

滑动窗口
下面的代码片段中展示了如何使用滑动窗口:

DataStream<T> input = ...;

// 滑动事件时间窗口 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>);

// 偏移8小时的滑动处理时间窗口(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)等来指定。
  正如上述例子所示,滑动窗口分配器也有一个可选的偏移参数来改变窗口的对齐。例如,没有偏移参数,按小时的窗口,有30分钟的滑动,将根据时间纪元来对齐,也就是说你将得到如下的窗口1:00:00.001:59:59.999,1:30:00.0002:29:59.999等。而如果你想改变窗口的对齐,你可以给定一个偏移,如果给定一个15分钟的偏移,你将得到如下的窗口:1:15:00.000~2:14.59.999, 1:45:00.000~2:44:59.999等。时间偏移一个很大的用处是用来调准非0时区的窗口,例如:在中国你需要指定一个8小时的时间偏移。

会话窗口(Session Windows)

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

会话窗口
下面的代码片段中展示了如何使用session窗口
Java代码:

DataStream<T> input = ...;

// 事件时间会话窗口(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((element) -> {
        // 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(ProcessingTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .<windowed transformation>(<window function>);
    

  时间间隔可以通过Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等来指定。注意: 因为session看窗口没有一个固定的开始和结束,他们的评估与滑动窗口和滚动窗口不同。在内部,session操作为每一个到达的元素创建一个新的窗口,并合并间隔时间小于指定非活动间隔( the defined gap )的窗口。为了进行合并,session窗口的操作需要指定一个合并触发器(Trigger)和一个合并窗口函数(Window Function),如:ReduceFunction或者WindowFunction(FoldFunction不能合并)。

全局窗口(Global Windows)

  全局窗口分配器将所有具有相同key的元素分配到同一个全局窗口中,这个窗口模式仅适用于用户还需自定义触发器的情况。否则,由于全局窗口没有一个自然的结尾,无法执行元素的聚合,将不会有计算被执行。下面的代码片段展示了如何使用全局窗口:
Java 代码:

DataStream<T> input = ...;

input
    .keyBy(<key selector>)
    .window(GlobalWindows.create())
    .<windowed transformation>(<window function>);
    
窗口函数(Window Functions)

  定义完窗口分配器后,我们还需要为每一个窗口指定我们需要执行的计算,这是窗口函数的责任,当系统决定一个窗口已经准备好执行之后,这个窗口函数将被用来处理窗口中的每一个元素(可能是分组的)。(了解当一个窗口准备好之后,Flink是如何决定的
  window函数可以是ReduceFunction, AggregateFunction, FoldFunction 或者 ProcessWindowFunction中的一个。前面两个更高效一些(State Size 部分),因为在每个窗口中增量地对每一个到达的元素执行聚合操作。一个 ProcessWindowFunction 可以获取一个窗口中的所有元素的一个迭代以及哪个元素属于哪个窗口的额外元信息。
  有ProcessWindowFunction的窗口化操作会比其他的操作效率要差一些,因为Flink内部在调用函数之前会将窗口中的所有元素都缓存起来。这个可以通过ProcessWindowFunction和ReduceFunction或者AggregateFunction、FoldFunction结合使用来获取窗口中所有元素的增量聚合和ProcessWindowFunction接收的额外的窗口元数据,接下来我们将看一看每一种变体的示例。

1、ReduceFunction

  ReduceFunction指定了如何通过两个输入的参数进行合并输出一个同类型的参数的过程,Flink使用ReduceFunction来对窗口中的元素进行增量聚合。
  一个ReduceFunction 可以通过如下的方式来定义和使用:
Java 代码:

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

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .reduce(new ReduceFunction<Tuple2<String, Long>> {
      public Tuple2<String, Long> reduce(Tuple2<String, Long> v1, Tuple2<String, Long> v2) {
        return new Tuple2<>(v1.f0, v1.f1 + v2.f1);
      }
    });
2、AggregateFunction

  AggregateFunction是ReduceFunction的通用版本,有三种类型:输入类型 input type (IN),累加器类型 accumulator type (ACC)和输出类型 output type (OUT)。输入类型是输入流中元素的类型,AggregateFunction有将一个输入元素添加到累加器(an accumulator)的方法。 该接口还具有用于创建初始累加器的方法,用于将两个累加器合并到一个累加器中以及用于从累加器提取输出(类型OUT)。我们将在下面的示例中看到它的工作原理。
  与ReduceFunction相同,当一个窗口的输入元素到达的时,Flink将增量地聚合它们。一个AggregateFunction将以如下方式定义和使用:

/**
 * The accumulator is used to keep a running sum and a count. The {@code getResult} method
 * computes the average.
 */
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);
  }
}

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

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

上述示例计算了窗口中元素的第二个字段的平均值。

3、FoldFunction

  FoldFunction 指定了一个输入元素如何与一个输出类型的元素合并的过程,这个FoldFunction 会被每一个加入到窗口中的元素和当前的输出值增量地调用,第一个元素是与一个预定义的类型为输出类型的初始值合并。
  一个FoldFunction可以通过如下的方式定义和调用:

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

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .fold("", new FoldFunction<Tuple2<String, Long>, String>> {
       public String fold(String acc, Tuple2<String, Long> value) {
         return acc + value.f1;
       }
    });

上面例子追加所有输入的长整型到一个空的字符串中。
注意: fold()不能应用于回话窗口或者其他可合并的窗口中。

4、ProcessWindowFunction

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

public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> implements 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.
     */
    public abstract void process(
            KEY key,
            Context context,
            Iterable<IN> elements,
            Collector<OUT> out) throws Exception;

   	/**
   	 * The context holding window metadata.
   	 */
   	public abstract class Context implements java.io.Serializable {
   	    /**
   	     * Returns the window that is being evaluated.
   	     */
   	    public abstract W window();

   	    /** Returns the current processing time. */
   	    public abstract long currentProcessingTime();

   	    /** Returns the current event-time watermark. */
   	    public abstract long currentWatermark();

   	    /**
   	     * State accessor for per-key and per-window state.
   	     *
   	     * <p><b>NOTE:</b>If you use per-window state you have to ensure that you clean it up
   	     * by implementing {@link ProcessWindowFunction#clear(Context)}.
   	     */
   	    public abstract KeyedStateStore windowState();

   	    /**
   	     * State accessor for per-key global state.
   	     */
   	    public abstract KeyedStateStore globalState();
   	}

}

  注: 关键参数是通过为keyBy() 调用指定的KeySelector提取的密钥。 在元组索引键或字符串字段引用的情况下,此键类型始终为元组,你必须手动将其转换为正确大小的元组以提取键字段。
  一个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)效率非常低。 下一节将介绍如何将ReduceFunction或AggregateFunction与ProcessWindowFunction结合使用,进行增量聚合和获取ProcessWindowFunction中的额外信息。

5、ProcessWindowFunction with Incremental Aggregation

  ProcessWindowFunction可以与ReduceFunction,AggregateFunction或FoldFunction结合使用,以便在元素到达窗口时增量聚合元素。 当一个窗口关闭时,将为ProcessWindowFunction提供聚合结果。 这允许它在访问ProcessWindowFunction的附加窗口元信息的同时递增地计算窗口。
   注意: 您还可以使用旧版WindowFunction而不是ProcessWindowFunction进行增量窗口聚合。

6、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>(window.getStart(), min));
  }
}
7、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

/**
 * The accumulator is used to keep a running sum and a count. The {@code getResult} method
 * computes the average.
 */
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));
  }
}
8、Incremental Window Aggregation with FoldFunction

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

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 state(和任何富函数一样),ProcessWindowFunction还可以使用键控状态,该键控状态的作用域是函数当前正在处理的窗口。 在这种情况下,了解每个窗口的状态(understand what the window that per-window state is referring to)是很重要的。 涉及不同的“窗口”:

  • 指定窗口操作时定义的窗口:这可能是1小时的翻滚窗口或滑动1小时的2小时滑动窗口。
  • 给定键的已定义窗口的实际实例:对于user-id xyz,这可能是从12:00到13:00的时间窗口。 这基于窗口定义,并且将基于作业当前正在处理的键的数量以及基于事件落入的时隙而存在许多窗口。

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

  在Context对象上有两个方法,一个process()调用接收到允许访问两种类型的状态:

  • globalState(), 它允许访问没有作用于窗口的键控状态。
  • windowState(), 它允许访问同样限定在窗口范围内的键控状态。

  如果你预计同一窗口会发生多次触发,则此功能非常有用,如果迟到的数据延迟触发或者你有自定义触发器进行投机性早期触发时可能会发生这种情况。 在这种情况下,你将存储有关先前触发的信息或每个窗口状态的触发次数。
  使用窗口状态时,清除窗口时清除该状态也很重要。 这应该在clear()方法中发生。

In addition to accessing keyed state (as any rich function can) a ProcessWindowFunction can also use keyed state that is scoped to the window that the function is currently processing. In this context it is important to understand what the window that per-window state is referring to is. There are different “windows” involved:

The window that was defined when specifying the windowed operation: This might be tumbling windows of 1 hour or sliding windows of 2 hours that slide by 1 hour.
An actual instance of a defined window for a given key: This might be time window from 12:00 to 13:00 for user-id xyz. This is based on the window definition and there will be many windows based on the number of keys that the job is currently processing and based on what time slots the events fall into.
Per-window state is tied to the latter of those two. Meaning that if we process events for 1000 different keys and events for all of them currently fall into the [12:00, 13:00) time window then there will be 1000 window instances that each have their own keyed per-window state.

There are two methods on the Context object that a process() invocation receives that allow access two the two types of state:

globalState(), which allows access to keyed state that is not scoped to a window
windowState(), which allows access to keyed state that is also scoped to the window
This feature is helpful if you anticipate multiple firing for the same window, as can happen when you have late firings for data that arrives late or when you have a custom trigger that does speculative early firings. In such a case you would store information about previous firings or the number of firings in per-window state.

When using windowed state it is important to also clean up that state when a window is cleared. This should happen in the clear() method.
Triggers触发器

  触发器决定了一个窗口何时可以被窗口函数处理,每一个窗口分配器都有一个默认的触发器,如果默认的触发器不能满足你的需要,你可以使用触发器trigger(…)指定自定义触发器。
  触发器接口有五种方法允许触发器对不同的事件做出反应:

  • onElement() 方法作用于每个元素并将之加入到一个窗口中。
  • onEventTime() 方法在已注册的事件时间计时器触发时调用。
  • onProcessingTime() 方法在一个注册的处理时间器计时器(a registered processing-time timer)触发时调用。
  • onMerge() 方法与状态触发器相关,并且当它们的相应窗口合并时合并两个触发器的状态,例如, 使用会话窗口时。
  • clear() 方法执行删除相应窗口时所需的任何操作。

使用以上方法有两点需要注意:

1)前三个决定如何通过返回TriggerResult来对其调用事件进行操作。 该操作可以是以下之一

  • CONTINUE: 什么也不做
  • FIRE: 触发计算
  • PURGE: 清除窗口中的数据
  • FIRE_AND_PURGE: 触发计算并清除窗口中的数据

2)所有的这些方法可用于注册处理或事件时间计时器以用于将来的操作。

Fire and Purge 触发和清除

  一旦触发器确定窗口已准备好进行处理,它就会触发,即返回FIRE或FIRE_AND_PURGE。 这是窗口操作发出当前窗口结果的信号。 给定一个带有ProcessWindowFunction的窗口,所有元素都传递给ProcessWindowFunction(可能在将它们传递给驱逐器后)。具有ReduceFunction,AggregateFunction或FoldFunction的窗口只会发出当前聚合的结果。
  当触发器触发时,它可以是FIRE或FIRE_AND_PURGE。 当FIRE保留窗口内容时,FIRE_AND_PURGE会删除其内容。 默认情况下,预先实现的触发器只需FIRE而不会清除窗口状态。
注意: 清除将简单地删除窗口的内容,并将保留有关窗口和任何触发状态的任何潜在元信息。

Default Triggers of WindowAssigners 窗口分配中的默认触发器

  WindowAssigner的默认触发器适用于许多用例。 例如,所有事件时窗口分配器都将EventTimeTrigger作为默认触发器。 一旦水印通过窗口的末端,该触发器就会触发。
注意: GlobalWindow的默认触发器是NeverTrigger,它永远不会触发。 因此,在使用GlobalWindow时,你始终必须定义自定义触发器。
注意: 通过使用trigger() 指定触发器,您将覆盖WindowAssigner的默认触发器。 例如,如果为TumblingEventTimeWindows指定CountTrigger,则不会再根据时间进度获取窗口,而只能按count计数。 现在,如果你想根据时间和数量做出反应,你必须编写自己的自定义触发器。

内置的和自定义的触发器(Build-in and Custom Triggers)

  Flink有一些内置的触发器:

  • EventTimeTrigger(前面提到过)触发是根据由水印衡量的事件时间的进度来的
  • ProcessingTimeTrigger 根据处理时间来触发
  • CountTrigger 一旦窗口中的元素个数超出了给定的限制就会触发
  • PurgingTrigger 作为另一个触发器的参数并将它转换成一个清除类型

如果你想实现一个自定义的触发器,你需要查看一下这个抽象类Trigger

Evictors驱逐器

  Flink的窗口模型允许指定一个除了WindowAssigner和Trigger之外的可选参数Evitor,这个可以通过调用evitor(…)方法(在这篇文档的开头展示过)来实现。这个驱逐器(evitor)可以在触发器触发之前或者之后,或者窗口函数被应用之前清理窗口中的元素。为了达到这个目的,Evitor接口有两个方法:

/**
 * 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);

  evitorBefore()方法包含了在window function之前被应用的驱逐逻辑,而evitorAfter()方法包含了在window function之后被应用的驱逐逻辑。在window function应用之前被驱逐的元素将不会再被window function处理。
Flink有三个预实现的驱逐器,他们是:

  • CountEvictor: 在窗口中保持一个用户指定数量的元素,并在窗口的开始处丢弃剩余的其他元素。
  • DeltaEvictor: 通过一个DeltaFunction和一个阈值,计算窗口缓存中最近的一个元素和剩余的所有元素的delta值,并清除delta值大于或者等于阈值的元素。
  • TimeEvictor: 使用一个interval的毫秒数作为参数,对于一个给定的窗口,它会找出元素中的最大时间戳max_ts,并清除时间戳小于max_tx - interval的元素。

默认情况下: 所有预实现的evitor都是在window function前应用它们的逻辑。
注意: 指定一个Evitor要防止预聚合,因为窗口中的所有元素必须得在计算之前传递到驱逐器中。
注意: Flink 并不保证窗口中的元素是有序的,所以驱逐器可能从窗口的开始处清除,元素到达的先后不是那么必要。

允许延迟(Allowed Lateness)

  当处理事件时间的window时,可能会出现元素到达晚了,Flink用来与事件时间联系的水印已经过了元素所属的窗口的最后时间。可以查看事件时间( event time)尤其是晚到元素(late elements)来了解Flink如何处理事件时间的讨论。
  默认情况下,当水印已经过了窗口的最后时间时晚到的元素会被丢弃。然而,Flink允许为窗口操作指定一个最大允许时延,允许时延指定了元素可以晚到多长时间,默认情况下是0。水印已经过了窗口最后时间后才来的元素,如果还未到窗口最后时间加时延时间,那么元素任然添加到窗口中。如果依赖触发器的使用的话,晚到但是未丢弃的元素可能会导致窗口再次被触发。
  为了达到这个目的,Flink将保持窗口的状态直到允许时延的发生,一旦发生,Flink将清除Window,删除window的状态,如Window 生命周期章节中所描述的那样。
默认情况下,允许时延为0,也就是说水印之后到达的元素将被丢弃。你可以按如下方式来指定一个允许时延:

Java 代码:

DataStream<T> input = ...;

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

注意: 当使用GlobalWindows分配器时,没有数据会被认为是延迟的,因为Global Window的最后时间是Long.MAX_VALUE。

以侧输出来获取延迟数据(Getting Late Data as a Site Output)

  使用Flink的侧输出特性,你可以获得一个已经被丢弃的延迟数据流。
  首先你需要在窗口化的数据流中调用sideOutputLateData(OutputTag)指定你需要获取延迟数据,然后,你就可以在window 操作的结果中获取到侧输出流了(side-output stream )。
代码如下:

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时,window以及window中的内容将会继续保持即使水印已经达到了window的最后时间。在这种情况下,当一个延迟事件到来而未丢弃时,它可能会触发window中的其他触发器。这些触发叫做late firings,因为它们是由延迟事件触发的,并相对于window中第一个触发即主触发而言。对于session window而言,late firing还会进一步导致window的合并,因为它们桥接了两个之前存在差距,而未合并的window。

使用窗口结果Working with window results

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

水印和窗户的互动 Interaction of watermarks and windows

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

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

直观地,水印“超出”任何窗口,一旦接收到该水印时间,将在下游操作中被认为是迟到的。

连续窗口操作 Consecutive windowed operations

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

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

使用状态大小的考虑(Useful state size considerations)

  window 可以定义一个很长的周期(例如:一天、一周或者一月),因此积累了相当大的状态。这里有些规则,当估计你的窗口计算的存储要求时,需要记住。
  1、Flink会在每个窗口中为每个属于它的元素创建一份备份,鉴于此,滚动窗口保存了每个元素的一个备份,与此相反,滑动窗口会为每个元素创建几个备份,如Window Assigner章节所述。因此,一个窗口大小为1天,滑动大小为1秒的滑动窗口可能就不是个好的策略了。
  2、FoldFunction和ReduceFunction可以制定reduce的存储需求,因为它们预聚合元素并且每个窗口只保存一个值。相反,只有WindowFunction需要累积所有的元素。
  3、使用Evitor需要避免任何预聚合操作,因为窗口中的所有元素都需要在应用于计算之前传递到evitor中(evitor

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值