Flink-DataStream 窗口

窗口 window

窗口是另一类的算子,是DataStream的逻辑边界,在第一个元素到达后创建,在生命周期结束后被销毁。

窗口分为两大类:

  • Keyed Window:通过在keyedStream上定义window()获得。可并行计算。
  • Non-Keyed Window:通过在DataStream上定义windowAll()获得。
Keyed Windows

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

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

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

窗口的生命周期

当属于该窗口的第一个元素到达时,就会创建一个窗口,当时间(事件或处理时间)通过它的结束时间戳加上用户指定的允许延迟时,该窗口将被完全删除。Flink指只保证基于时间的窗口删除。
此外,每个窗口都具有一个触发器和一个函数(ProcessWindowFunction、ReduceFunction、AggregateFunction或FoldFunction)。函数包含窗口内容的计算,触发器为已做好准备运行函数的条件。

Keyed vs Non-Keyed Windows

在定义窗口前,应先确定流是否使用KeyBy(…)进行逻辑分组。
在KeyByStream的情况下,将允许多个任务窗口并行执行运算,因为每个逻辑KeyByStream都可以独立于其他逻辑KeyByStream进行处理。所有引用相同键的元素将被发送到相同的并行任务。
在non-keyedStreams的情况下,原始流不会被分割成多个逻辑流,所有的窗口逻辑将由一个任务执行,即并行度为1。

窗口分配器(Window Assigners)

窗口分配器定义将一个元素分配给一个或多个窗口。通过调用window(…)(keyByStream)或windowAll()(Non-KeyedStream)来完成。

  • 内置窗口分配器
    • 滚动窗口(基于时间(处理时间或事件时间)分配元素),
    • 滑动窗口(基于时间(处理时间或事件时间)分配元素)
    • 会话窗口(基于时间(处理时间或事件时间)分配元素)
      TIPS:基于时间的窗口具有开始时间戳(包括)和结束时间戳(不包括),它们一起描述窗口的大小。
    • 全局窗口
  • 自定义窗口分配器(扩展WindowAssigner Class)

滚动窗口(Tumbling Windows)

将时间拆分成具有固定长度,不重叠的时间片段。例如,如果滚动窗口将windows Size设置为5分钟,则每五分钟将启动一个新窗口,如下图所示。
Tumbling Windows
时间间隔设置

  • Time.milliseconds(x)
  • Time.seconds(x)
  • Time.minutes(x)
DataStream<T> input = ...;

// 滚动窗口-事件时间
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>);

// 滚动窗口-处理时间
input
    .keyBy(<key selector>)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>);

// 滚动窗口-事件时间并设置8小时的偏移量(用于抵消时差)
input
    .keyBy(<key selector>)
    // 偏移量15分钟:1:00:00.000 - 1:59:59.999 -> 1:15:00.000 - 2:14:59.999
    // 在中国,必须指定偏移量Time.hours(-8) (中国是UTC +8 ,flink默认使用UTC-0)
    .window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
    .<windowed transformation>(<window function>);

滑动窗口(Sliding Windows)

由windows Size设置一个固定大小的窗口。并附加一个window slide来设置窗口移动的距离。因此如果window slide小于window size将会有同一元素被分配到多个窗口的情况发生。
在这里插入图片描述

DataStream<T> input = ...;

// 滑动窗口-事件时间
input
    .keyBy(<key selector>)
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>);

// 滑动窗口-处理时间
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>);

// 滑动窗口-事件时间并设置8小时的偏移量(用于抵消时差)
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
    .<windowed transformation>(<window function>);

会话窗口(Session Windows)

按活动会话分配元素。会话窗口不重叠,没有固定的开始和结束时间,与滚动窗口和滑动窗口相反。当会话窗口在一段时间内没有接收到元素时,即当发生不活动的间隙时会关闭会话窗口。会话窗口可以设置静态会话间隙或动态会话间隙函数(通过实现SessionWindowTimeGapExtractor接口),该功能定义不活动时间段的长度。当到达此期限使,将关闭当前会话,后续元素将分配给新的会话窗口。
在这里插入图片描述

DataStream<T> input = ...;

// 会话窗口 - 事件时间 - 静态会话间隙
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>);
    
// 会话窗口 - 事件时间 - 动态会话间隙函数
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .<windowed transformation>(<window function>);

// 会话窗口 -处理时间 - 静态会话间隙
input
    .keyBy(<key selector>)
    .window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>);
    
// 会话窗口 - 处理时间 - 动态会话间隙函数
input
    .keyBy(<key selector>)
    .window(ProcessingTimeSessionWindows.withDynamicGap((element) -> {
        // determine and return session gap
    }))
    .<windowed transformation>(<window function>);

全局窗口(Global Windows)

将具有相同键的所有元素分配给同一个全局窗口。只有在指定自定义触发器时,此窗口模式才有用。否则,将不执行任何计算,因为全局窗口没有一个自然的结束,我们不能在结束处处理聚合的元素。
在这里插入图片描述

DataStream<T> input = ...;

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

窗口函数(Window Functions)

定义窗口分配器后,需要指定需要在每个窗口上执行的计算。这是窗口函数的职责,窗口函数在系统确定窗口准备好进行处理后用于处理每个窗口的元素。
窗口函数可以是ReduceFunction,AggregateFunction,FoldFunction或ProcessWindowFunction。前两个可以更有效率地执行,因为Flink可以在每个窗口进行增量聚合。ProcessWindowFunction获取Iterable窗口中包含的所有元素以及有关元素所属窗口的其他元信息。

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

ReduceFunction

ReduceFunction将输入的两个元素进行组合以生成相同类型的新元素。Flink使用ReduceFunction对窗口元素进行增量聚合。

ReduceFunction的定义和使用:

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

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

ProcessWindowFunction

ProcessWindowFunction获取一个包含窗口所有元素(全量计算)的迭代器,以及一个具有时间和状态信息访问权的上下文对象,这使它能够比其他窗口函数提供更大的灵活性。这是以性能和资源消耗为代价的,因为元素不能增量地聚合,而是需要在内部进行缓冲,直到认为窗口已经做好进行处理的准备。
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做简单的聚合(例如count)效率非常低。

ProcessWindowFunction with Incremental Aggregation

ProcessWindowFunction可以与ReduceFunction、AggregateFunction或FoldFunction组合使用,以便在元素到达窗口时增量地聚合它们。当窗口关闭时,ProcessWindowFunction将提供聚合的结果。这允许它在访问ProcessWindowFunction的附加窗口元信息的同时递增地计算窗口。
使用ProcessWindowFunction + ReduceFunction基于增量聚合

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

使用ProcessWindowFunction + AggregateFunction基于增量聚合

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

使用ProcessWindowFunction + FoldFunction基于增量聚合

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

在ProcessWindowFunction中使用每窗口状态

除了访问键控状态(如任何丰富的函数可以),a ProcessWindowFunction还可以使用键控状态,该键控状态的作用域是函数当前正在处理的窗口。在这种情况下,了解每个窗口状态所指的窗口是很重要的。涉及不同的“窗口”:

指定窗口操作时定义的窗口:这可能是1小时的翻滚窗口或滑动1小时的2小时滑动窗口。
给定键的已定义窗口的实际实例:对于user-id xyz,这可能是从12:00到13:00的时间窗口。这基于窗口定义,并且将基于作业当前正在处理的键的数量以及基于事件落入的时隙而存在许多窗口。
每窗口状态与后两者相关联。这意味着如果我们处理1000个不同键的事件,并且所有这些事件的事件当前都落入[12:00,13:00]时间窗口,那么将有1000个窗口实例,每个窗口实例都有自己的键控每窗口状态。

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

globalState(),允许访问没有作用于窗口的键控状态
windowState(),允许访问也限定在窗口范围内的键控状态
如果您预计同一窗口会发生多次触发,则此功能非常有用,如果您迟到的数据或者您有自定义触发器进行投机性早期触发时可能会发生这种情况。在这种情况下,您将存储有关先前点火的信息或每个窗口状态的点火次数。

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

触发器(Triggers)

触发器负责决定在窗口的什么时间点启动应用程序定义的数据处理任务。水印迟到会拉长窗口生存周期,水印早到会导致数据处理结果不准确,触发器就是为解决这两个问题而引入的。每个都有一个默认值。如果默认触发器不符合需要,可以使用指定自定义触发器(WindowAssignerTriggertrigger(…))
触发器接口有五种方法可以Trigger对不同的事件做出反应:

  • onElement()为添加到窗口的每个元素调用该方法。
  • onEventTime()在注册的事件时间计时器触发时调用该方法。
  • onProcessingTime()在注册的处理时间计时器触发时调用该方法。
  • 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作为默认触发器。一旦水印经过窗口的末端,这个触发器就会触发。
注意:

  • 全局窗口的默认触发器是永不触发的NeverTrigger。因此,在使用全局窗口时,您总是必须定义一个自定义触发器。
  • 通过使用trigger()指定触发器,覆盖WindowAssigner的默认触发器。例如,如果您为ingeventtime窗口指定了CountTrigger,那么您将不再根据时间进度获得窗口触发,而是只根据count。现在,如果想根据时间和计数做出反应,就必须编写自己的自定义触发器。

Built-in and Custom Triggers

Flink附带了一些内置触发器。

  • (已经提到的)EventTimeTrigger基于水印测量的事件时间的进展而发生火灾。
  • 在ProcessingTimeTrigger基于处理时间的火灾。
  • CountTrigger一旦窗口中的元素数量超过给定限制,就会触发。
  • 将PurgingTrigger另一个触发器作为参数作为参数并将其转换为清除触发器。

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

清除器(Evictors)

清除器在触发器触发后,窗口函数执行前或窗口函数执行后清除窗口内元素。

有以下两个方法:

/**
 * 触发器触发后,窗口函数执行前
 *
 * @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);

/**
 * 触发器触发后,窗口函数执行后
 *
 * @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);

Flink附带三个预实现的evictors。这些都是:

  • CountEvictor:保持窗口内元素数量为预设值。
  • DeltaEvictor:获取一个阈值,计算窗口缓冲区中的最后一个元素与其余每个元素之间的增量,并删除增量大于或等于阈值的元素。
  • TimeEvictor:将一个以毫秒为单位的间隔作为参数,对于给定的窗口,它在其元素中找到最大时间戳max_ts,并删除所有时间戳小于max_ts - interval的元素。

默认情况下,所有预先实现的evictors在窗口函数之前应用它们的逻辑。
注意:指定逐出器会阻止任何预聚合,因为在应用计算之前,必须将窗口的所有元素传递给逐出器。同时Flink不保证窗口内元素的顺序。这意味着尽管逐出器可以从窗口的开头移除元素,但这些元素不一定是首先到达或最后到达的元素。

迟到生存期(Allowed Lateness)

当使用事件时间窗口时,可能会出现元素延迟到达的情况,即Flink用来跟踪事件时间进程的水印已经超过了元素所属窗口的结束时间戳。
默认情况下,当水印经过窗口末尾时,将删除较晚的元素。然而,Flink允许为窗口操作符指定允许的最大延迟。允许延迟指定元素在被删除之前可以延迟多少时间,其默认值为0。在水印经过窗口末尾之后到达的元素,但是在水印经过窗口末尾之前到达的元素,加上允许的延迟,仍然被添加到窗口中。根据使用的触发器的不同,延迟但未删除的元素可能会导致窗口再次触发。EventTimeTrigger就是这种情况。
为了使这个工作,Flink保持窗口的状态,直到它们允许的延迟过期为止。一旦发生这种情况,Flink就会删除窗口并删除它的状态,正如窗口生命周期部分中描述的那样。
情况下,允许的延迟设置为 0。也就是说,到达水印后面的元素将被丢弃。

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

DataStream<T> input = ...;

input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .allowedLateness(<time>)
    .<windowed transformation>(<window function>);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值