Flink详解系列之六--窗口机制

窗口是flink处理无限流的核心,窗口将流拆分为有限大小的“桶”,我们可以在这些桶上进行计算。

1、Keyed vs Non-Keyed Windows

根据上游数据是否为Keyed Stream类型(是否将数据按照某个指定的Key进行分区),将窗口划分为Keyed Window和Non-Keyed Windows。两者的区别在于KeyStream调用相应的window()方法来指定window类型,数据会根据Key在不同的Task中并行计算,而Non-Keyed Stream需要调用WindowsAll()方法来指定window类型,所有的数据都会在一个Task进行计算,相当于没有并行。

1.1 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"
1.2 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"

2、窗口分配器

窗口分配器负责将一个事件分配给一个或多个窗口,内置窗口包括: 滚动窗口(Tumbling Windows)、滑动窗口(Sliding Windows)、会话窗口(Session Windows)、全局窗口(Global Windows),也可以通过继承WindowAssigner类来自定义窗口。

2.1 基于时间的窗口

Flink中所有的内置窗口(全局窗口除外)都有基于时间的实现,这个时间可以是事件时间(event time),也可以是处理时间(processing time)。其中,处理滚动窗口和滑动窗口的算子,在1.12版本之前使用timeWindow(),在1.12版本被标记为废弃,转而使用window()来作为窗口处理算子,这里只介绍最新版本的使用算子。

  • 滚动时间窗口(Tumbling Time Windows)
    滚动窗口将每一个事件分配给一个有特定大小的窗口,滚动窗口有固定大小,不会重叠。比如一个滚动窗口大小(window size)为5分钟。

    使用示例如下:
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)))
    .();

由于Flink默认使用的时间基准是UTC±00:00时间,在中国需要使用UTC+08:00时间,所以最后一个示例中窗口大小为1天,时间偏移量就是8小时。

  • 滑动窗口(Sliding Time Windows)
    跟滚动窗口类似,滑动窗口也是将每一个事件分配给特定大小的窗口,且窗口有固定的大小,但它有一个窗口滑动的参数,标识一个窗口滑动的频率,或者说是每隔多久窗口滑动一次。比如一个窗口的大小为10秒钟,滑动频率为5秒。

    使用示例如下:
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.hours(-8)含义与滚动窗口一致。从滑动窗口的使用来看,滚动窗口其实是滑动窗口的一个特例,但窗口大小和滑动间隔相等的时候,滑动窗口就是一个滚动窗口。

  • 会话窗口(Session Windows)
    会话窗口按活动的会话对事件进行分组。与滑动窗口和滚动窗口相比,会话窗口没有固定的大小,也没有固定的起止时间,它是以一段时间没有接收到事件为窗口结束条件的。会话窗口分配器可以配置成一个固定的session gap,或者定义成一个session gap提取函数,在函数中定义一个不活跃的时长,一旦这个时长结束,当前会话就结束。

    使用示例如下:
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
    }))
    .();

动态的会话gap需要实现SessionWindowTimeGapExtractor接口。

2.2 基于计数的窗口

基于计数的窗口是根据事件的个数来对窗口进行划分的,概念跟基于时间的滚动窗口差不多,只不过窗口大小的划分,有时间变成了事件的个数。

  • 滚动计数窗口(Tumbling Count Windows)
stream
      .keyBy(1)
      .countWindow(100) \\100为事件的个数,即窗口的大小
      .sum(1);
  • 滑动计数窗口(Sliding Count Windows)
stream
      .keyBy(1)
      .countWindow(100, 10) \\100为事件的个数,即窗口的大小,10为滑动的间隔
      .sum(1);
2.3 全局窗口(Global Windows)

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



使用示例如下:

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

3、触发器(Triggers)

触发器决定了一个窗口何时可以被窗口函数处理,每一个窗口分配器都有一个默认的触发器,如果默认的触发器不能满足你的需要,你可以通过调用trigger(...)来指定一个自定义的触发器。触发器的接口有5个方法来允许触发器处理不同的事件:

  • onElement()方法,每个元素被添加到窗口时调用
  • onEventTime()方法,当一个已注册的事件时间计时器启动时调用
  • onProcessingTime()方法,当一个已注册的处理时间计时器启动时调用
  • onMerge()方法,与状态性触发器相关,当使用会话窗口时,两个触发器对应的窗口合并时,合并两个触发器的状态。
  • 最后一个clear()方法执行任何需要清除的相应窗口

Flink有一些内置的触发器:

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

GlobalWindow默认的触发器是NeverTrigger,是永远不会触发的,因此,如果你使用的是GlobalWindow的话,需要定义一个自定义触发器。

4、驱逐器(Evictors)

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

void evictBefore(Iterable> elements, int size, W window, EvictorContext evictorContext);
void evictAfter(Iterable> elements, int size, W window, EvictorContext evictorContext);

注:指定一个Evitor要防止预聚合,因为窗口中的所有元素必须得在计算之前传递到驱逐器中

你可能感兴趣的:(Flink详解系列之六--窗口机制)