Windows是处理无限流的核心。Windows将流分成有限大小的“存储桶”,我们可以在其上应用计算。本文档重点介绍如何在Flink中执行窗口化,以及程序员如何从其提供的功能中获得最大收益。

窗口式Flink程序的一般结构如下所示。第一个片段指的是键控流,而第二个片段指的是非键控流。正如人们所看到的,唯一的区别是keyBy(...)呼吁密钥流和window(...)成为windowAll(...)非键控流。这还将用作本页面其余部分的路线图。

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"

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

窗口生命周期 #

简而言之,一旦应属于该窗口的第一个元素到达,就会创建一个窗口,并且当时间(事件或处理时间)超过其结束时间戳加上用户指定的时间(请参阅“允许延迟”)后,该窗口将被完全删除。 )。Flink保证只删除基于时间的窗口,而不能删除其他类型的窗口,例如全局窗口(请参阅窗口分配器)。例如,采用基于事件时间的开窗策略,该策略每5分钟创建一次不重叠(或翻滚)的窗口,并允许延迟1分钟,因此Flink将为和之间的间隔创建一个新窗口。allowed lateness12:0012:05当带有时间戳的第一个元素落入此间隔时,当水印通过12:06 时间戳时,它将删除它。

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

除上述内容外,您还可以指定一个Evictor(请参阅Evictors),它将在触发触发器后以及应用该功能之前和/或之后从窗口中删除元素。

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

键控与非键控Windows #

要指定的第一件事是您的流是否应该设置密钥。这必须在定义窗口之前完成。使用keyBy(...)会将您的无限流分割成逻辑键流。如果keyBy(...)未调用,则不会为您的流设置密钥。

在密钥流的情况下,你的传入事件的任何属性可以作为一个按键(详情点击这里)。拥有键控流将使您的窗口化计算可以由多个任务并行执行,因为每个逻辑键控流都可以独立于其余逻辑流进行处理。引用同一键的所有元素将被发送到同一并行任务。

对于非键控流,您的原始流将不会拆分为多个逻辑流,并且所有窗口逻辑将由单个任务(即并行度为1)执行。

The first thing to specify is whether your stream should be keyed or not. This has to be done before defining the window. Using the keyBy(...) will split your infinite stream into logical keyed streams. If keyBy(...) is not called, your stream is not keyed.

In the case of keyed streams, any attribute of your incoming events can be used as a key (more details here). Having a keyed stream will allow your windowed computation to be performed in parallel by multiple tasks, as each logical keyed stream can be processed independently from the rest. All elements referring to the same key will be sent to the same parallel task.

In case of non-keyed streams, your original stream will not be split into multiple logical streams and all the windowing logic will be performed by a single task, i.e. with parallelism of 1.

 

Window Assigners #

窗口Assigners #

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

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

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

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

翻滚的Windows #

甲翻滚窗口分配器受让人的每个元素到指定的窗口的窗口大小。滚动窗口具有固定的大小,并且不重叠。例如,如果您指定大小为5分钟的翻滚窗口,则将评估当前窗口,并且每五分钟将启动一个新窗口,如下图所示。

flink的keyby原理 flink keyby_flink的keyby原理

以下代码段显示了如何使用滚动窗口。

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

// 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 参数,该参数可用于更改窗口的对齐方式。例如,如果没有偏移,则每小时滚动窗口与epoch对齐,即您将获得诸如的窗口 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)。

滑动Windows #

该滑动窗口分配器受让人元件以固定长度的窗口。类似于滚动窗口分配器,窗口的大小由窗口大小参数配置。附加的窗口滑动参数控制滑动窗口启动的频率。因此,如果幻灯片小于窗口大小,则滑动窗口可能会重叠。在这种情况下,元素被分配给多个窗口。

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

flink的keyby原理 flink keyby_Time_02

以下代码段显示了如何使用滑动窗口。

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

// 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分钟的窗口将与epoch对齐,即您将获得诸如的窗口 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)。

会话Windows #

在会话窗口出让方按活动的会话组中的元素。与滚动窗口和滑动窗口相比,会话窗口不重叠且没有固定的开始和结束时间。相反,当会话窗口在一定时间段内未收到元素时(即,发生不活动间隙时),它将关闭。会话窗口分配器可与静态配置会话间隙或与 会话间隙提取功能,其限定不活动周期有多长。当此时间段到期时,当前会话将关闭,随后的元素将分配给新的会话窗口。

flink的keyby原理 flink keyby_Time_03

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

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

由于会话窗口没有固定的开始和结束,因此对它们的评估不同于滚动窗口和滑动窗口。在内部,会话窗口运算符会为每个到达的记录创建一个新窗口,如果窗口彼此之间的距离比已定义的间隔更近,则将它们合并在一起。为了可合并的,会话窗口操作者需要一个合并 触发器以及合并  的窗函数,如 ReduceFunction, AggregateFunction或 ProcessWindowFunction

 

全局Windows #

一个全球性的窗口分配器分配使用相同的密钥相同的单个的所有元素全局窗口。仅当您还指定了自定义触发器时,此窗口方案才有用。否则,将不会执行任何计算,因为全局窗口没有可以处理聚合元素的自然端。

flink的keyby原理 flink keyby_flink的keyby原理_04

DataStream<T> input = ...;

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

 

窗口功能 #

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

的窗函数可以是一个ReduceFunction,AggregateFunction或ProcessWindowFunction。前两个可以更有效地执行(请参阅“状态大小”部分),因为Flink可以在到达每个窗口时逐步地汇总每个窗口的元素。AProcessWindowFunction获取Iterable窗口中包含的所有元素的,以及有关元素所属窗口的其他元信息。

带a的窗口转换ProcessWindowFunction不能像其他情况一样有效地执行,因为Flink必须在调用函数之前在内部缓冲窗口的所有元素。可以通过ProcessWindowFunction与组合使用来缓解这种情况ReduceFunction,或者AggregateFunction同时获取窗口元素的增量聚合和ProcessWindowFunction接收到的其他窗口元数据 。我们将看每个变体的示例。

ReduceFunction #

AReduceFunction指定如何将输入中的两个元素组合在一起以产生相同类型的输出元素。Flink使用aReduceFunction来逐步聚合窗口的元素。

阿ReduceFunction可以定义像这样使用:

A ReduceFunction specifies how two elements from the input are combined to produce an output element of the same type.

Flink uses a ReduceFunction to incrementally aggregate the elements of a window.

A ReduceFunction can be defined and used like this:

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

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

AggregateFunction #

一个AggregateFunction是一个一般化版本ReduceFunction,其具有三种类型:输入类型(IN),蓄压式(ACC),和一个输出类型(OUT)。输入类型是输入流中元素的类型,并且AggregateFunction具有将一个输入元素添加到累加器的方法。该接口还具有创建初始累加器,将两个累加器合并为一个累加器以及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());

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

ProcessWindowFunction #

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

ProcessWindowFunctionlook的签名如下:

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

}

该key参数是通过所提取的关键KeySelector这是为指定keyBy()调用。如果是元组索引键或字符串字段引用,则始终使用此键类型,Tuple并且必须手动将其强制转换为正确大小的元组以提取键字段。

阿ProcessWindowFunction可以定义像这样使用:

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

input
  .keyBy(t -> t.f0)
  .window(TumblingEventTimeWindows.of(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)用于效率很低。下一部分说明如何将a ReduceFunction或or AggregateFunction与a组合以 ProcessWindowFunction同时获得增量聚合和a的附加信息 ProcessWindowFunction。

触发器 #

ATrigger确定窗口(由窗口分配器形成)何时准备好由窗口函数处理。每个WindowAssigner都有一个默认值Trigger。如果默认触发器不符合您的需求,则可以使用指定自定义触发器trigger(...)。

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

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

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

  1. 前三个决定了如何通过返回返回值来对调用事件采取行动TriggerResult。该动作可以是以下之一:
  • CONTINUE: 没做什么,
  • FIRE:触发计算,
  • PURGE:清除窗口中的元素,然后
  • FIRE_AND_PURGE:触发计算并随后清除窗口中的元素。
  1. 这些方法中的任何一种均可用于注册处理或事件时间计时器以用于将来的操作。

 

消防和清除 #

一旦触发器确定窗口已准备好进行处理,它将触发,即返回FIRE或FIRE_AND_PURGE。这是窗口操作员发出当前窗口结果的信号。给定一个包含ProcessWindowFunction 所有元素的窗口ProcessWindowFunction(可能在将它们传递到逐出者之后)传递给。带有的Windows ReduceFunction,或AggregateFunction仅发出其热切的汇总结果。

当触发器触发时,它可以是FIRE或FIRE_AND_PURGE。在FIRE保留窗口内容的同时,FIRE_AND_PURGE删除其内容。默认情况下,仅在FIRE不清除窗口状态的情况下预先执行触发器即可。

清除将仅删除窗口的内容,并且将保留有关该窗口的任何潜在元信息以及任何触发状态。

的WindowAssigners默认触发器 #

默认Trigger的WindowAssigner是适用于许多使用情况。例如,所有事件时间窗口分配器都有EventTimeTrigger默认触发器。一旦水印通过窗口的末端,此触发器便会触发。

的默认触发器GlobalWindow是NeverTrigger永不触发的。因此,在使用时,您始终必须定义一个自定义触发器GlobalWindow。

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

内置和自定义触发器 #

Flink带有一些内置触发器。

  • (已经提到)EventTimeTrigger根据事件时间(由水印测量)的进度触发。
  • 在ProcessingTimeTrigger基于处理时间的火灾。
  • CountTrigger一旦窗口中的元素数量超过给定的限制,就会触发。
  • 在PurgingTrigger采用作为参数另一触发并将其转换为一个吹扫之一。

如果需要实现自定义触发器,则应签出抽象的 Trigger 类。请注意,API仍在不断发展,并可能在Flink的未来版本中更改。

驱逐者 #

Flink的窗口模型允许Evictor除了WindowAssigner和之外还指定一个可选内容Trigger。可以使用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<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附带了三个预先实施的驱逐程序。这些都是:

  • CountEvictor:从窗口中保留用户指定数量的元素,并从窗口缓冲区的开头丢弃其余的元素。
  • DeltaEvictor:使用aDeltaFunction和a threshold,计算窗口缓冲区中最后一个元素与其余每个元素之间的差值,并删除差值大于或等于阈值的那些值。
  • TimeEvictor:以interval毫秒为单位作为参数,对于给定的窗口,它将max_ts在其元素中找到最大时间戳,并删除所有时间戳小于的元素max_ts - interval。

默认情况下,所有预先实现的驱逐程序均在窗口函数之前应用其逻辑。

指定逐出者可防止任何预聚集,因为在应用计算之前必须将窗口的所有元素传递给逐出者。这意味着带有驱逐者的窗口将创建更多的状态。

Flink不保证窗口中元素的顺序。这意味着,尽管退出者可以从窗口的开头删除元素,但是这些元素不一定是最先到达或最后到达的元素。

允许迟到 #

在使用事件时间窗口时,可能会发生元素到达较晚的情况,即Flink用于跟踪事件时间进度的水印已经超过了元素所属窗口的结束时间戳。请参阅 事件时间,尤其是后期元素,以更全面地讨论Flink如何处理事件时间。

默认情况下,当水印超过窗口末端时,将删除晚期元素。但是,Flink允许为窗口运算符指定最大允许延迟。允许延迟指定元素删除之前可以延迟的时间,其默认值为0。在水印通过窗口末端之后但在通过窗口末端之前到达的元素加上允许的延迟,仍添加到窗口中。根据使用的触发器,延迟但未掉落的元素可能会导致窗口再次触发。的情况就是这样EventTimeTrigger。

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

默认情况下,允许的延迟设置为0。也就是说,到达水印后的元素将被丢弃。

您可以这样指定允许的延迟:

爪哇

<span style="color:#000000"><code class="language-java">DataStream<span style="color:#000000"><strong><</strong></span>T<span style="color:#000000"><strong>></strong></span> input <span style="color:#000000"><strong>=</strong></span> <span style="color:#000000"><strong>...;</strong></span>

input
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">keyBy</span><span style="color:#000000"><strong>(<</strong></span>key selector<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">window</span><span style="color:#000000"><strong>(<</strong></span>window assigner<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">allowedLateness</span><span style="color:#000000"><strong>(<</strong></span>time<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.<</strong></span>windowed transformation<span style="color:#000000"><strong>>(<</strong></span>window function<span style="color:#000000"><strong>>);</strong></span>
</code></span>

使用 GlobalWindows窗口分配器时,永远不会考虑任何数据,因为全局窗口的结束时间戳为 Long.MAX_VALUE。

天色已晚数据作为侧的输出 #

使用Flink的侧面输出功能,您可以获取最近被丢弃的数据流。

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

爪哇

<span style="color:#000000"><code class="language-java"><span style="color:#000000"><strong>final</strong></span> OutputTag<span style="color:#000000"><strong><</strong></span>T<span style="color:#000000"><strong>></strong></span> lateOutputTag <span style="color:#000000"><strong>=</strong></span> <span style="color:#000000"><strong>new</strong></span> OutputTag<span style="color:#000000"><strong><</strong></span>T<span style="color:#000000"><strong>>(</strong></span><span style="color:#dd1144">"late-data"</span><span style="color:#000000"><strong>){};</strong></span>

DataStream<span style="color:#000000"><strong><</strong></span>T<span style="color:#000000"><strong>></strong></span> input <span style="color:#000000"><strong>=</strong></span> <span style="color:#000000"><strong>...;</strong></span>

SingleOutputStreamOperator<span style="color:#000000"><strong><</strong></span>T<span style="color:#000000"><strong>></strong></span> result <span style="color:#000000"><strong>=</strong></span> input
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">keyBy</span><span style="color:#000000"><strong>(<</strong></span>key selector<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">window</span><span style="color:#000000"><strong>(<</strong></span>window assigner<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">allowedLateness</span><span style="color:#000000"><strong>(<</strong></span>time<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">sideOutputLateData</span><span style="color:#000000"><strong>(</strong></span>lateOutputTag<span style="color:#000000"><strong>)</strong></span>
    <span style="color:#000000"><strong>.<</strong></span>windowed transformation<span style="color:#000000"><strong>>(<</strong></span>window function<span style="color:#000000"><strong>>);</strong></span>

DataStream<span style="color:#000000"><strong><</strong></span>T<span style="color:#000000"><strong>></strong></span> lateStream <span style="color:#000000"><strong>=</strong></span> result<span style="color:#000000"><strong>.</strong></span><span style="color:teal">getSideOutput</span><span style="color:#000000"><strong>(</strong></span>lateOutputTag<span style="color:#000000"><strong>);</strong></span>
</code></span>

晚元素的考虑 #

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

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

与窗口工作的结果 #

窗口操作的结果再次是a DataStream,结果元素中没有保留任何有关窗口操作的信息,因此,如果要保留有关窗口的元信息,则必须在的结果元素中手动编码该信息 ProcessWindowFunction。在结果元素上设置的唯一相关信息是元素timestamp。设置为已处理窗口的最大允许时间戳,即结束时间戳-1,因为窗口结束时间戳是唯一的。请注意,这对于事件时间窗口和处理时间窗口都是正确的。也就是说,在窗口操作元素之后始终具有时间戳,但这可以是事件时间时间戳或处理时间时间戳。对于处理时间窗口,这没有特殊的含义,但是对于事件时间窗口,这连同水印与窗口的交互方式一起,可以以相同的窗口大小进行 连续的窗口操作。在查看水印如何与窗口交互之后,我们将进行介绍。

的水印和窗口交互 #

在继续本节之前,您可能需要看一下有关 事件时间和水印的部分。

当水印到达窗口运算符时,将触发两件事:

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

直观地,一旦下游操作收到水印后,水印就会“溢出”到所有在下游操作中被认为是后期的窗口。

连续加窗操作 #

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

爪哇

<span style="color:#000000"><code class="language-java">DataStream<span style="color:#000000"><strong><</strong></span>Integer<span style="color:#000000"><strong>></strong></span> input <span style="color:#000000"><strong>=</strong></span> <span style="color:#000000"><strong>...;</strong></span>

DataStream<span style="color:#000000"><strong><</strong></span>Integer<span style="color:#000000"><strong>></strong></span> resultsPerKey <span style="color:#000000"><strong>=</strong></span> input
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">keyBy</span><span style="color:#000000"><strong>(<</strong></span>key selector<span style="color:#000000"><strong>>)</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">window</span><span style="color:#000000"><strong>(</strong></span>TumblingEventTimeWindows<span style="color:#000000"><strong>.</strong></span><span style="color:teal">of</span><span style="color:#000000"><strong>(</strong></span>Time<span style="color:#000000"><strong>.</strong></span><span style="color:teal">seconds</span><span style="color:#000000"><strong>(</strong></span>5<span style="color:#000000"><strong>)))</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">reduce</span><span style="color:#000000"><strong>(</strong></span><span style="color:#000000"><strong>new</strong></span> Summer<span style="color:#000000"><strong>());</strong></span>

DataStream<span style="color:#000000"><strong><</strong></span>Integer<span style="color:#000000"><strong>></strong></span> globalResults <span style="color:#000000"><strong>=</strong></span> resultsPerKey
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">windowAll</span><span style="color:#000000"><strong>(</strong></span>TumblingEventTimeWindows<span style="color:#000000"><strong>.</strong></span><span style="color:teal">of</span><span style="color:#000000"><strong>(</strong></span>Time<span style="color:#000000"><strong>.</strong></span><span style="color:teal">seconds</span><span style="color:#000000"><strong>(</strong></span>5<span style="color:#000000"><strong>)))</strong></span>
    <span style="color:#000000"><strong>.</strong></span><span style="color:teal">process</span><span style="color:#000000"><strong>(</strong></span><span style="color:#000000"><strong>new</strong></span> TopKWindowFunction<span style="color:#000000"><strong>());</strong></span>

</code></span>

在此示例中,[0, 5)来自第一个操作的时间窗口结果也将最终出现在[0, 5)随后的窗口化操作的时间窗口中。这允许计算每个键的总和,然后在第二个操作中计算同一窗口内的前k个元素。

可用的状态大小的考虑 #

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

  1. Flink为每个窗口所属的每个元素创建一个副本。鉴于此,滚动窗口保留每个元素的一个副本(一个元素恰好属于一个窗口,除非它被延迟放置)。相反,如“窗口分配器”部分中所述,滑动窗口会为每个元素创建多个。因此,大小为1天的滑动窗口和滑动1秒的滑动窗口可能不是一个好主意。
  2. ReduceFunction并AggregateFunction极大地减少了存储需求,因为它们热切地聚合元素并且每个窗口仅存储一个值。相反,仅使用aProcessWindowFunction需要累积所有元素。
  3. 使用Evictor防止了任何预聚合,作为窗口的所有元件必须通过逐出器施加的计算(参见前通过逐出器)。