Flink窗口理解

Windows(窗口分类)
Keyed Stream和Non-Keyed Stream
代码定义上唯一的区别是Keyed Stream以keyBy()开始,后接window(),而Non-Keyed Stream以windowAll()开始,且windowAll是单slot运行的。
Keyed Window

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/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/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

Window Lifecycle(窗口生命周期)
窗口在属于这个窗口的第一条数据到达时创建,在“事件时间或者处理时间 + 用户自定义的允许的延迟时间”到达时移除。
Flink只会移除基于时间的窗口,而不会移除诸如global的窗口。
例如,一个基于事件时间的5分钟滚动窗口,允许时间延迟是1分钟。当12:00的第一条数据到达时,flink会创建一个时间在12:00~12:05的新窗口,这个窗口会在12:06分被移除掉。
另外,每个窗口都会被绑定一个Trigger和一个function (ProcessWindowFunction, ReduceFunction, or AggregateFunction)。这个function包含这个窗口中数据的计算逻辑,而这个Trigger指定了这个窗口中函数被执行的条件。一个trigger策略可能像是“当这个窗口中的元素到达多于4个”,或者“当水位线超过了窗口的截止时间”。一个Trigger也可以决定在窗口存活期任意时间内清除里面的数据,这个例子中清除仅仅是指清除这个窗口内的数据,而不是窗口的元数据。这就意味着新数据仍然可以加入到这个窗口中。
除了上面说的这些,你还可以指定一个Evictor用来在触发器触发后,或者在函数执行前后,从这个window中删除数据。
在Keyed Stream中,你的输入事件中的任何属性(字段)都可以被用来当做key。Keyed Stream能让你的窗口计算被多个task并行执行,因为每个逻辑上的keyed stream都可以被单独处理,同一个key的所有数据会被发送到同一个task去处理。
在Non-Keyed Stream中,你的输入数据不会被拆分成多个流,并且所有的窗口逻辑都会被一个任务处理,也就是说并行度为1.

Window Assigner
指定了你的数据流是Keyed或者Non-Keyed后,下一步是定义WindowAssigner。WindowAssigner定义了数据是如何指定到窗口的。WindowAssigner通过window()(Keyed)或者windowAll()(Non-Keyed)来定义。
一个WindowAssigner负责指定每个输入流数据到一个或者多个窗口。Flink有4中最通用的WindowAssigner类型,滚动窗口(tumbling windows),滑动窗口(sliding windows),会话窗口(session windows)和全局窗口(global windows)。你也可以通过扩展WindowAssigner类来自定义窗口类型。所有的内置WindowAssigner(全局窗口除外)都是基于事件时间或者处理时间指定数据位于哪个窗口的。
基于时间的窗口都有一个开始时间和一个结束时间来描述窗口的大小。在写代码时,当使用基于时间的窗口时,flink用可以查询开始时间戳,结束时间戳或者最大时间戳的方法(返回窗口内最大允许的时间戳)的TimeWindow。
接下来我们展示一下flink的预定义窗口是如何工作的,并且他们在数据流程序中是如何使用的。接下来的图表将可视化的展现每个WindowAssigner的工作机制。紫色的圆圈代表数据流中的数据,这些数据被某些key(user1, user2, user3)分区了。X轴代表处理时间。

Tumbling Windows(滚动窗口)
滚动窗口指定每个数据到一个特定大小的窗口内。滚动窗口有固定的大小并且不会重合。例如,如果你指定了一个5分钟的滚动窗口,每5分钟前一个窗口会被计算并且开启另一个新的窗口接收新数据。如下图所示
Flink窗口理解_第1张图片
下面的代码展示如何使用滚动窗口

DataStream input = ...;

// tumbling event-time windows
input
    .keyBy()
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .();

// tumbling processing-time windows
input
    .keyBy()
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .();

// daily tumbling event-time windows offset by -8 hours.
input
    .keyBy()
    .window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
    .();

时间间隔可以用 Time.milliseconds(x), Time.seconds(x), Time.minutes(x) 等等中的一个去指定。
在上面的例子中,滚动窗口也可以使用可选offset参数,用来改变窗口的对齐时间。例如,没有offset,小时滚动窗口按整点对齐,你将会拿到诸如1:00:00.000 - 1:59:59.999, 2:00:00.000 - 2:59:59.999这样的窗口。如果你想修改,你可以给定一个offset。例如你想要一个由15分钟offset的窗口,那你将会拿到诸如1:15:00.000 - 2:14:59.999, 2:15:00.000 - 3:14:59.999这样的时间窗口。一个offset重要的使用案例是调整时区,比如在中国,你要设置offset Time.hours(-8)。

Sliding Windows(滑动窗口)
滑动窗口将数据指定到大小固定的窗口。和滚动窗口相似,窗口的大小通过窗口参数配置。另外一个滑动窗口参数控制窗口触发的频率。因此,如果滑动的尺寸小于整个窗口尺寸,滑动窗口之间可能会重合。在这种情况下,数据会被分配到多个不同的窗口。
例如,你可以设置一个10分钟大小的窗口,每5分钟滑动一次。这样你可以每5分钟拿到一个包含前面10分钟数据的窗口。如下图所示
Flink窗口理解_第2张图片
下面的java代码展示如何使用滑动窗口

DataStream input = ...;

// sliding event-time windows
input
    .keyBy()
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .();

// sliding processing-time windows
input
    .keyBy()
    .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .();

// sliding processing-time windows offset by -8 hours
input
    .keyBy()
    .window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
    .();

时间间隔可以用Time.milliseconds(x), Time.seconds(x), Time.minutes(x)等等指定。
在上面的例子中,滚动窗口也可以使用可选offset参数,用来改变窗口的对齐时间。例如,没有offset,小时滚动窗口按整点对齐,你将会拿到诸如1:00:00.000 - 1:59:59.999, 2:00:00.000 - 2:59:59.999这样的窗口。如果你想修改,你可以给定一个offset。例如你想要一个由15分钟offset的窗口,那你将会拿到诸如1:15:00.000 - 2:14:59.999, 2:15:00.000 - 3:14:59.999这样的时间窗口。一个offset重要的使用案例是调整时区,比如在中国,你要设置offset Time.hours(-8)。

Session Windows(会话窗口)
会话窗口通过会话活动将数据分组。相比于滚动窗口和滑动窗口,会话窗口不会重合,并且没有固定的开始和结束时间。然而会话窗口会在窗口一段时间内不接受新数据时关闭,这段时间是指非活跃发生的间隔时间。会话窗口可以被配置成一个固定的会话间隔,或者通过一个会话间隔提取器函数去定义非活动的间隔时长。当这段时间过期,当前会话关闭,并且接下来的新数据会被指定到一个新的会话窗口。
Flink窗口理解_第3张图片
下面的java代码展示如何使用会话窗口

DataStream input = ...;

// event-time session windows with static gap
input
    .keyBy()
    .window(EventTimeSessionWindows.withGap(Time.minutes(10)))
    .();
    
// event-time session windows with dynamic gap
input
    .keyBy()
    .window(EventTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .();

// processing-time session windows with static gap
input
    .keyBy()
    .window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
    .();
    
// processing-time session windows with dynamic gap
input
    .keyBy()
    .window(ProcessingTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .();

固定的时间间隔可以用 Time.milliseconds(x), Time.seconds(x), Time.minutes(x)等指定。
动态的时间间隔通过实现SessionWindowTimeGapExtractor接口指定。
由于会话窗口没有固定的开始和结束时间,所以他们里面的数据和滚动窗口,滑动窗口的计算方式也不同。会话窗口操作符为每个新数据创建一个新窗口,然后如果他们之间的时间间隔相比于定义好的时间间隔更近的话,就合并这些窗口。为了使这些窗口是可合并的,会话窗口操作符需要一个合并触发器和一个合并窗口函数,比如ReduceFunction, AggregateFunction, or ProcessWindowFunction。

Global Windows(全局窗口)
全局窗口指定所有的有相同key的数据到同一个全局窗口。这个窗口只有在你指定一个自定义的触发器时才有用。否则,什么计算都没有,因为全局窗口没有结束时间,所以我们不能做任何的聚合运算。
下面的java代码展示如何使用全局窗口

DataStream input = ...;

input
    .keyBy()
    .window(GlobalWindows.create())
    .();

Window Functions(窗口函数)
定义完了window assigner后,我们需要在每个窗口指定我们需要的计算。这就是窗口函数的作用,一旦系统知道一个窗口已经准备就绪,它就会开始处理每个窗口里面(可能是keyed)的数据(Flink如何决定一个窗口已经准备就绪可参考triggers)。
window函数可以是ReduceFunction, AggregateFunction, or ProcessWindowFunction中的一个。前面2个执行的效率更高,因为Flink可以增量聚合到达这个窗口的数据。ProcessWindowFunction可以获得一个包含窗口内所有数据的迭代器(Iterable)和额外的关于这个窗口数据的元数据信息。
因为Flink不得不在调用函数前缓存窗口内的所有数据,所以基于ProcessWindowFunction的窗口转化不能像其他的函数一样被高效地执行。通过结合ReduceFunction或者AggregateFunction一起使用,既可以获得窗口内每个数据的增量聚合结果,也可以同时拿到ProcessWindowFunction接收到的数据的元数据信息,这样就可以有效缓解上述的这种情况。接下来,我们可以看到这些变种的例子。

ReduceFunction(Reduce函数)
ReduceFunction定义了两个输入数据如何结合到一起去生成一个同样类型的输出结果。Flink用ReduceFunction去增量聚合窗口内的数据。
ReduceFunction可以像下面这样定义和使用

DataStream> input = ...;

input
    .keyBy()
    .window()
    .reduce(new ReduceFunction>() {
      public Tuple2 reduce(Tuple2 v1, Tuple2 v2) {
        return new Tuple2<>(v1.f0, v1.f1 + v2.f1);
      }
    });

上面的例子是为窗口内所有二元组数据的第二个字段求和。

AggregateFunction(Aggregate函数)
AggregateFunction是ReduceFunction的更通用版本,它有三个类型:输入类型,累加器类型,还有输出类型。输入类型是指输入流中的数据类型,AggregateFunction有一个方法去把输入数据加到累加器上面。除此之外,接口中还有其他的方法,比如创建一个初始的累加器的方法,用来将累加器合二为一的方法,将输出数据从累加器中抽取出来的方法等。我们将在下面的例子中了解它们如何工作。
和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, Double> {
  @Override
  public Tuple2 createAccumulator() {
    return new Tuple2<>(0L, 0L);
  }

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

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

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

DataStream> input = ...;

input
    .keyBy()
    .window()
    .aggregate(new AverageAggregate());

上面的例子计算的是窗口内二元组数据的第二个字段的平均值。

ProcessWindowFunction(ProcessWindow函数)
ProcessWindowFunction可以获得一个包含所有窗口内元素的迭代器和可以访问到时间和状态信息的上下文对象,这使得它能够提供比其他window function更多的灵活性。不过这会导致一些性能和资源上的损耗,因为数据不会被增量聚合,而是需要被缓存起来,一直到这个窗口内的数据被处理的时候。
下面是ProcessWindowFunction的签名(原始定义)

public abstract class ProcessWindowFunction 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 elements,
            Collector 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.
   	     *
   	     * 

NOTE: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(); } }

参数key是指在调用keyBy()时使用KeySelector指定的那个。以防是tuple中的索引或者String类型的引用,这个key的类型都设计成了Tuple,并且你必须手动转换成Tuple的正确长度的类型去取这个key。
ProcessWindowFunction可以像下面这样定义和使用

DataStream> input = ...;

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

/* ... */

public class MyProcessWindowFunction 
    extends ProcessWindowFunction, String, String, TimeWindow> {

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

这个例子展示了ProcessWindowFunction如何将窗口内的数据计数。另外,这个窗口函数还添加了一些关于窗口的输出信息。
注意用ProcessWindowFunction来做简单的聚合如计数是很低效的。下面将介绍如何将ReduceFunction、AggregateFunction与ProcessWindowFunction结合起来使用去既获得增量聚合又获得ProcessWindowFunction的额外的信息。

ProcessWindowFunction with Incremental Aggregation(使用ProcessWindowFunction实现增量聚合)
ProcessWindowFunction可以和ReduceFunction或者AggregateFunction一起使用去增量聚合到达窗口内的数据。当窗口关闭,ProcessWindowFunction会立即给出聚合结果。这允许它在增量计算的同时,还可以获取一些关于ProcessWindowFunction的额外的窗口元数据信息。
你也可以使用已过期的WindowFunction替代ProcessWindowFunction来做增量窗口聚合。

Incremental Window Aggregation with ReduceFunction(使用ReduceFunction实现增量窗口聚合)
下面的例子展示了如何将增量的ReduceFunction和ProcessWindowFunction结合起来使用,计算窗口内数据的最小值,并获得窗口的开始时间。

DataStream input = ...;

input
  .keyBy()
  .window()
  .reduce(new MyReduceFunction(), new MyProcessWindowFunction());

// Function definitions

private static class MyReduceFunction implements ReduceFunction {

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

private static class MyProcessWindowFunction
    extends ProcessWindowFunction, String, TimeWindow> {

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

Incremental Window Aggregation with AggregateFunction(使用AggregateFunction实现增量窗口聚合)
下面的例子展示了如何将增量的AggregateFunction和ProcessWindowFunction结合起来使用,计算平均值,并且输出key和带上平均值的窗口。

Using per-window state in ProcessWindowFunction(在ProcessWindowFunction中使用每个窗口状态)
除了访问keyed state(所有rich function都可以),ProcessWindowFunction也可以在这个函数正在处理的窗口范围内使用。在这种情况下,了解每个窗口状态所指的窗口是什么非常重要。这里涉及到不同的“窗口”:

  • 在指定窗口操作是被定义的窗口:可能是1小时滚动窗口,或者每1个小时滑动一次的2小时窗口。
  • 给定key的已定义窗口的实际实例:对于用户xyz来说,可能是12:00~13:00的时间窗口。这是基于窗口定义,并且根据正在运行的任务key的数量和任务里面的这些事件落在哪个时间段,将会有许多的窗口。
    Per-window状态与后面两种情况相关。意味着如果我们处理1000个不同key的数据,并且这些数据都落在了[12:00, 13:00)的时间窗口内,那么就会有1000个窗口实例,其中每个窗口都有他们自己的窗口状态。
    process()调用能接收到上下文对象中的可以访问这两类状态的2个方法:
  • globleState(),允许访问不在这个窗口范围内的keyed state
  • windowState(),允许访问在这个窗口范围内的keyed state
    如果你想要在同一个窗口触发多个操作,这个特性将非常有用,比如当你要为迟到的数据延迟触发或者当你有一个自定义的trigger需要提早触发。在这种情况下,你会保存关于前一次触发的信息或者每个窗口状态的触发次数。
    当使用窗口的状态时,在窗口清除时,清理state也很重要。这个应该在clear()函数里面定义。

WindowFunction(窗口函数已过期)
在可以使用ProcessWindowFunction的地方,你也可以使用WindowFunction。这个是ProcessWindowFunction的老的版本,它提供了更少的上下文信息,并且没有一些高级的特性,比如每个窗口的keyed state。这个接口会在某个时间点被过期掉。
WindowFunction接口的定义如下

public interface WindowFunction extends Function, Serializable {

  /**
   * Evaluates the window and outputs none or several elements.
   *
   * @param key The key for which this window is evaluated.
   * @param window The window that is being evaluated.
   * @param input 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.
   */
  void apply(KEY key, W window, Iterable input, Collector out) throws Exception;
}

它可以像下面这样使用

DataStream> input = ...;

input
    .keyBy()
    .window()
    .apply(new MyWindowFunction());

Tiggers(触发器)
触发器定义一个被窗口函数指定的窗口(在window assigner中形成)什么时候准备好开始处理数据。每一个WindowAssigner都有一个默认的触发器。如果默认的触发器不能满足你的需求,你可以用trigger(…)指定一个自定义的触发器。
触发器接口有5个允许它对不同事件做出反应的方法。

  • onElement方法为进入到窗口内的每个数据所调用
  • onEventTime方法在注册的事件时间定时器触发时调用
  • onProcessTime方法在注册的处理时间定时器触发时调用
  • onMerge方法和有状态的触发器相关,并且当响应的两个窗口合并时,合并两个触发器的状态,比如会话窗口
  • clear方法在移除窗口时处理任何需要的动作
    关于上面的方法,有两点需要注意:
  • 前三个通过返回一个TriggerResult决定对于他们的调用事件做出何种反应。反应可以是如下中的一种:
    CONTINUE:什么都不做
    FIRE:触发计算
    PURGE:清除窗口中的数据
    FIRE_AND_PURGE:触发计算,然后清除窗口中的数据
  • 这些方法中的任何一个都可以用来为未来的反应注册处理时间或者事件时间定时器。

Fire And Purge(触发和清除)
一旦触发器确定了窗口已经准备好了处理,也就是说触发了,它就会返回Fire或者FIRE_AND_PURGE。这个是窗口操作符发送当前窗口结果的信号。给定一个ProcessWindowFunction的窗口,所有的数据都会被传送到ProcessWindowFunction里面(可能是在把他们传送给了evictor之后)。而ReduceFunction和AggregateFunction的窗口只是简单地把聚合结果发出来。
当一个触发器触发了,它可能是FIRE或者FIRE_AND_PURGE。FIRE会保留这个窗口中的状态内容,而FIRE_AND_PURGE会清除里面的内容。默认情况下,预实现的触发器只是FIRE而不会清除window里面的状态。
Purge会仅仅移除窗口里面的内容,并且会保留有关窗口的任何潜在元信息和保持任何触发器状态完好无损。

Default Triggers of WindowAssigners(默认触发器)
WindowAssigner的默认Trigger在很多情况下是恰当的。例如,所有的事件时间window assigner都有一个EventTimeTrigger作为默认的trigger。这个trigger简单地在水位线到达窗口的时候触发一次。
GlobalWindow的默认Trigger是NeverTrigger,也就是说一直都不会触发。因此,当使用GlobalWindow时,你总是要自定义一个Trigger。
通过用函数trigger()指定一个触发器,你可以覆盖掉WindowAssigner的默认触发器。例如,如果你为TumblingEventTimeWindows指定一个CountTrigger,你将不再会基于处理时间触发窗口,而是基于计数值。现在,如果你想基于时间和计数值触发窗口,你必须写你自己的自定义触发器。

Built-in and Custom Triggers(内置的和自定义的触发器)
Flink有一些内置的触发器

  • EventTimeTrigger基于通过水位线衡量的事件时间触发
  • ProcessingTimeTrigger基于处理时间触发
  • CountTrigger在窗口内的数据超过给定的限定值时触发
  • PurgingTrigger作为另一个触发器的参数,并将这个触发器转换为一个清除触发器
    如果你需要实现一个自定义触发器,你应该查看抽象的Trigger类。请注意这个API还在进化中并且有可能在Flink的未来版本改变。

Evictors(驱除器)
除了WindowAssigner和Trigger,Flink的窗口模型允许再指定一个可选的Evictor。可以使用evictor(…)函数指定。在触发器触发后,window函数执行之前或者之后,Evictor可以将窗口中的数据移除。Evictor接口通过两个方法来实现这个效果

/**
 * 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> 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> elements, int size, W window, EvictorContext evictorContext);

evictorBefore()函数包含在窗口函数之前使用的清除逻辑,evictorAfter()函数包含在窗口函数之后使用的逻辑。在使用窗口函数之前被清除的数据不会被处理。
Flink有如下三个预实现的Evictor:

  • CountEvictor保留窗口中用户指定数量的数据,丢弃从窗口头部开始的其他的数据
  • DeltaEvictor使用DeltaFunction和一个阈值(threshold),计算窗口中最后一个数据和其他的每一个数据之间的差值,移除差值大于或者等于这个阈值的数据
  • TimeEvictor使用毫秒值interval作为参数,对于一个给定的窗口,在这个窗口中所有的数据中,找到最大的时间戳max_ts,移除掉所有时间戳小于max_ts - interval的数据

默认情况下,所有预实现evictor的逻辑都是在窗口函数之前执行的。
通过指定evictor可以防止预聚合,因为窗口里面所有的数据都必须在执行计算之前传给evictor。这就意味着带evictor的窗口会创建非常多的State。
Flink不保证窗口内的数据的顺序。这意味着,虽然evictor可以从窗口的开头删除元素,但这些元素不一定是先到达或最后到达的元素。

Allowed Lateness(允许延迟)
当使用事件时间窗口时,可能会发生数据延迟到达,也就是说Flink用来跟踪事件时间的进度的水位线已经超过了数据所在窗口的结束时间。在event time和late elements章节,可以找到关于Flink如何处理事件时间的更详细的说明。
默认情况下,当水位线超过了窗口的结束时间,迟到的数据会被丢弃。可是,对于窗口操作,Flink允许指定最大的数据延迟。允许延迟是指在数据被丢弃前最多可以迟到多长时间,默认值是0。在水位线超过窗口结束时间之后,但是在窗口结束时间加上允许的延迟之前到达,这些导到的数据仍然能够被加入到这个窗口。根据使用的触发器,一个迟到但是不被丢弃的数据可能会导致窗口的再次触发。事件时间触发器的情况就是这样。
为了使这个工作,Flink保留窗口的状态知道允许的延迟过期。一旦这种情况发生,Flink移除这个窗口并且删除里面的状态,就像Window Lifecycle部分所说的。
默认情况下,允许延迟参数设置为0。也就是说,在水位线之后到达的数据会被删除。
你可以像下面这样指定允许延迟:

DataStream input = ...;

input
    .keyBy()
    .window()
    .allowedLateness(

当使用GlobalWindow时,没有数据会迟到,因为global window的结束时间是Long.MAX_VALUE。

Getting late data as a side output(将迟到数据作为侧输出流)
根据Flink侧输出的特性,可以将迟到的数据输出到侧数据流。
你首先需要说明你想要在windowed stream中通过sideOutputLateData(OutputTag) 获得迟到的数据。然后,你可以在窗口操作的结果上获取侧输出流。

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

DataStream input = ...;

SingleOutputStreamOperator result = input
    .keyBy()
    .window()
    .allowedLateness(

Late elements considerations(考虑迟到数据)
当将允许延迟参数指定为大于0,水位线超过了窗口结束时间之后,窗口和里面的内容会保留下来。在这种情况下,当一个迟到但是没有丢弃的数据到达时,这个窗口会再一次触发。这些触发都叫做迟到触发,因为它们都是被迟到的事件触发,并且是和这个窗口的第一次主要触发相比的。在session window的情况下,迟到触发可进一步导致窗户的合并,因为它们可能会"弥合"两个预先存在的未合并窗户之间的间隙。
迟到触发的结果可以被视为前一次计算结果的更新值,也就是说,你的数据流会包含同一个计算的多个结果。根据你的应用程序不同,你需要考虑这些重复的结果并且给他们去重。

Working with window results(处理窗口结果)
窗口函数的结果又是另外一个数据流,在结果数据中不会保留有关窗口操作的任何信息,因此如果你想要保留关于这个窗口的元数据信息,你必须在你的ProcessWindowFunction的结果数据中手动编码这些信息。在结果数据中设置的仅有的相关信息是数据时间戳。这个时间戳被设置为被处理窗口的最大允许的时间戳,也就是结束时间戳 减去 1,因为窗口结束时间不包含在内。注意事件时间窗口和处理时间窗口都是这样的,也就是说,窗口操作之后,数据都会有一个时间戳,但这个时间戳既可能是事件时间戳或者处理时间戳。对于处理时间戳这个没有什么特殊的含义,但是对于事件时间戳,这关系到水位线如何与窗口交互能使连续窗口操作具有相同的窗口大小。我们会在水位线如何与窗口交互后讲到这个。

Interaction of watermarks and windows(水位线和窗口的交互)
在继续这部分之前,你可能想要先看一下event time and watermarks部分。
当窗口操作符的水位线到达时,会触发两件事:

  • 水位线会在最大时间戳(结束时间 - 1)小于新的水位线时,触发所有窗口的计算
  • 水位线会被转发到下游操作
    直观上,水位线"冲洗"所有窗口,一旦它们收到该水位线,就会在下游作业中考虑迟到。

Consecutive windowed operations(连续窗口操作)
就如前面提到的,窗口结果的时间戳的计算方式,和水位线如何与窗口交互方式,都允许串联连续窗口操作。当你想要执行连续两个窗口操作时,如果要使用不同的key,但仍希望来自同一上游窗口的元素最终位于同一下游窗口,这会很有用。如下所示:

DataStream input = ...;

DataStream resultsPerKey = input
    .keyBy()
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .reduce(new Summer());

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

在这种情况下,第一次操作中的时间窗口[0, 5)的结果也将在随后的窗口操作中以时间窗口 [0, 5)结束。这允许在同一个窗口内先用第一个操作计算每个key的和再用第二个操作计算top-k的数据。

Useful state size considerations(有用的状态大小考虑)
窗口可以定义成很长时间(比如1天,1周,或者1个月),因此而需要累积非常大的状态。当评估你的窗口计算的存储需求时,有以下一些原则需要记住:

  • Flink为每个窗口创建每个数据所属窗口的一份拷贝。有了这个,滚动窗口保留每个数据的一份拷贝(除非迟到被丢弃,一个数据精确属于一个窗口)。相反,滑动窗口创建几份拷贝,就像WindowAssigner章节所说的。因此,每1秒执行一次的1天大小的滑动窗口可能不是一个好的方式
  • ReduceFunction和AggregeateFunction可能会很大程度上减少存储消耗,因为他们迅速聚合数据,并且仅仅为每个窗口保留一个值。相反,只使用ProcessWindowFunction需要累积所有数据
  • 用evictor可以防止预聚合,因为窗口内的所有数据都必须在计算之前经过evictor

你可能感兴趣的:(flink,flink,大数据)