Flink 时间和窗口

文章目录

  • Flink 时间和窗口
    • 一、时间语义
      • 1、数据处理系统中的时间语义
      • 2、两种时间语义的对比
    • 二、水位线
      • 1、什么是水位线
        • (1)、有序流中的水位线
        • (2)、乱序流中的水位线
        • (3)、水位线的特性
      • 2、如何生成水位线
        • (1)、生成水位线的总体原则
        • (2)、水位线生成策略(Watermark Strategies)
        • (3)、flink内置的水位线生成策略
        • (4)、自定义水位线策略
      • 3、在自定义源中生成水位线
      • 4、水位线的传递
    • 三、窗口
      • 1、窗口的概念
      • 2、窗口的分类
        • (1)、按照驱动类型分类
          • 1) 时间窗口(Time Window)
          • 2) 计数窗口(Count Window)
        • (2) 按照窗口分配数据的规则分类(重点!!!)
          • 1) 滚动窗口(Tumbling Windows)
          • 2) 滑动窗口(Sliding Windows)
          • 3) 会话窗口(Session Window)
          • 4) 全局窗口(Global Windows)
      • 3、窗口 API(预览)
        • (1)、按键分区(Keyed)和非按键分区(Non-Keyed)
          • 1) 按键分区窗口(Keyd Windows)
          • 2) 非按键分区(Non-Keyed Windows)(不推荐)
        • (2)、代码中窗口 API 的调用
      • 4、窗口分配器(Window Assigners)

Flink 时间和窗口

一、时间语义

在流数据处理应用中,一个很重要、也很常见的操作就是窗口计算。所谓的“窗口”,一般就是划定的一段时间范围,也就是“时间窗”;对在这范围内的数据进行处理,就是所谓的窗口计算。所以窗口和时间往往是分不开的。
Flink 时间和窗口_第1张图片
在事件发生之后,生成的数据被收集起来,首先进入分布式消息队列,然后被 Flink 系统中的 Source 算子读取消费,进而向下游的转换算子(窗口算子)传递,最终由窗口算子进行计算处理。
很明显,这里有两个非常重要的时间点:一个是数据产生的时间,我们把它叫作“事件时间”(Event Time);另一个是数据真正被处理的时刻,叫作“处理时间”(Processing Time)。我们所定义的窗口操作,到底是以那种时间作为衡量标准,就是所谓的“时间语义”(Notions of Time)。由于分布式系统中网络传输的延迟和时钟漂移,处理时间相对事件发生的时间会有所滞后。
1. 处理时间(Processing Time)
处理时间的概念非常简单,就是指执行处理操作的机器的系统时间。
在这种时间语义下处理窗口非常简单粗暴,不需要各个节点之间进行协调同步,也不需要考虑数据在流中的位置,简单来说就是“我的地盘听我的”。所以处理时间是最简单的时间语义。
2. 事件时间(Event Time)
事件时间,是指每个事件在对应的设备上发生的时间,也就是数据生成的时间。
数据一旦产生,这个时间自然就确定了,所以它可以作为一个属性嵌入到数据中。这其实就是这条数据记录的“时间戳”(Timestamp)。
在事件时间语义下,我们对于时间的衡量,就不看任何机器的系统时间了,而是依赖于数据本身。但是由于分布式系统中网络传输延迟的不确定性,实际应用中我们要面对的数据流往往是乱序的。在这种情况下,就不能简单地把数据自带的时间戳当作时钟了,而需要用另外的标志来表示事件时间进展,在 Flink 中把它叫作事件时间的“水位线”(Watermarks)。

1、数据处理系统中的时间语义

在计算机系统的实际应用中,事件时间语义会更为常见。一般情况下,业务日志数据中都会记录数据生成的时间戳(timestamp),它就可以作为事件时间的判断基础。

2、两种时间语义的对比

通常来说,处理时间是我们计算效率的衡量标准,而事件时间会更符合我们的业务计算逻辑。所以更多时候我们使用事件时间;不过处理时间也不是一无是处。对于处理时间而言,由于没有任何附加考虑,数据一来就直接处理,因此这种方式可以让我们的流处理延迟降到最低,效率达到最高。

二、水位线

1、什么是水位线

在事件时间语义下,我们不依赖系统时间,而是基于数据自带的时间戳去定义了一个时钟,用来表示当前时间的进展。于是每个并行子任务都会有一个自己的逻辑时钟,它的前进是靠数据的时间戳来驱动的。
我们可以把时钟也以数据的形式传递出去,告诉下游任务当前时间的进展;而且这个时钟的传递不会因为窗口聚合之类的运算而停滞。一种简单的想法是,在数据流中加入一个时钟标记,记录当前的事件时间;这个标记可以直接广播到下游,当下游任务收到这个标记,就可以更新自己的时钟了。由于类似于水流中用来做标志的记号,在 Flink 中,这种用来衡量事件时间(Event Time)进展的标记,就被称作“水位线”(Watermark)。
具体实现上,水位线可以看作一条特殊的数据记录,它是插入到数据流中的一个标记点,主要内容就是一个时间戳,用来指示当前的事件时间。而它插入流中的位置,就应该是在某个数据到来之后;这样就可以从这个数据中提取时间戳,作为当前水位线的时间戳了。
Flink 时间和窗口_第2张图片

(1)、有序流中的水位线

在理想状态下,数据应该按照它们生成的先后顺序、排好队进入流中;而在实际应用中,如果当前数据量非常大,可能会有很多数据的时间戳是相同的,这时每来一条数据就提取时间戳、插入水位线就做了大量的无用功。所以为了提高效率,一般会每隔一段时间生成一个水位线,这个水位线的时间戳,就是当前最新数据的时间戳,所以这时的水位线,其实就是有序流中的一个周期性出现的时间标记。
在这里插入图片描述

(2)、乱序流中的水位线

在分布式系统中,数据在节点间传输,会因为网络传输延迟的不确定性,导致顺序发生改变,这就是所谓的“乱序数据”。
在这里插入图片描述
对于连续数据流,我们插入新的水位线时,要先判断一下时间戳是否比之前的大,否则就不再生成新的水位线。也就是说,只有数据的时间戳比当前时钟大,才能推动时钟前进,这时才插入水位线。
Flink 时间和窗口_第3张图片
如果考虑到大量数据同时到来的处理效率,我们同样可以周期性地生成水位线。这时只需要保存一下之前所有数据中的最大时间戳,需要插入水位线时,就直接以它作为时间戳生成新的水位线。
Flink 时间和窗口_第4张图片
为了让窗口能够正确收集到迟到的数据,我们可以等上 2 秒;也就是用当前已有数据的最大时间戳减去 2 秒,就是要插入的水位线的时间戳。
Flink 时间和窗口_第5张图片
如果仔细观察就会看到,这种“等 2 秒”的策略其实并不能处理所有的乱序数据。所以我们可以试着多等几秒,也就是把时钟调得更慢一些。最终的目的,就是要让窗口能够把所有迟到数据都收进来,得到正确的计算结果。对应到水位线上,其实就是要保证,当前时间已经进展到了这个时间戳,在这之后不可能再有迟到数据来了。
Flink 时间和窗口_第6张图片

(3)、水位线的特性

现在我们可以知道,水位线就代表了当前的事件时间时钟,而且可以在数据的时间戳基础上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。

  • 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
  • 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展
  • 水位线是基于数据的时间戳生成的
  • 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
  • 水位线可以通过设置延迟,来保证正确处理乱序数据
  • 一个水位线 Watermark(t),表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据

水位线是 Flink 流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对乱序数据的正确处理。

2、如何生成水位线

(1)、生成水位线的总体原则

如果我们希望计算结果能更加准确,那可以将水位线的延迟设置得更高一些,等待的时间
越长,自然也就越不容易漏掉数据。不过这样做的代价是处理的实时性降低了,我们可能为极
少数的迟到数据增加了很多不必要的延迟。
如果我们希望处理得更快、实时性更强,那么可以将水位线延迟设得低一些。这种情况下,
可能很多迟到数据会在水位线之后才到达,就会导致窗口遗漏数据,计算结果不准确。对于这
些 “漏网之鱼”,Flink 另外提供了窗口处理迟到数据的方法,我们会在后面介绍。当然,如
果我们对准确性完全不考虑、一味地追求处理速度,可以直接使用处理时间语义,这在理论上
可以得到最低的延迟。
所以 Flink 中的水位线,其实是流处理中对低延迟和结果正确性的一个权衡机制,而且把
控制的权力交给了程序员,我们可以在代码中定义水位线的生成策略。

(2)、水位线生成策略(Watermark Strategies)

在 Flink 的 DataStream API 中 , 有 一 个 单 独 用 于 生 成 水 位 线 的 方 法 :
assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指
示事件时间。
具体使用时,直接用 DataStream 调用该方法即可。

val stream = env.addSource(new ClickSource)
val withTimestampsAndWatermarks = 
stream.assignTimestampsAndWatermarks(<watermark strategy>)

assignTimestampsAndWatermarks()方法需要传入一个 WatermarkStrategy 作为参数,这就是
所谓的“水位线生成策略”。WatermarkStrategy 中包含了一个“时间戳分配器”TimestampAssigner
和一个“水位线生成器”WatermarkGenerator。

  • TimestampAssigner:主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础。时间戳的分配器。
  • WatermarkGenerator:主要负责按照既定的方式,基于时间戳生成水位线。在WatermarkGenerator
    接口中,主要又有两个方法:onEvent()onPeriodicEmit()
  • onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,以及允许发出水位线的一个WatermarkOutput,可以基于事件做各种操作.这种生成策略就是每个数据来都给他标记一下时间戳。
  • onPeriodicEmit:周期性调用的方法,可以由 WatermarkOutput 发出水位线。周期时间为处理时间,可以调用环境配置env.getConfigsetAutoWatermarkInterval()方法来设置,系统默认为200ms。
  env.getConfig.setAutoWatermarkInterval(500L)  //自动生成水位线的周期时间间隔,他是长整型的,这里设置为500毫秒
(3)、flink内置的水位线生成策略

建议使用,flink内置的乱序流水位线策略,就可以了。
1、有序流的水位线生成策略
思路:
我们直接用当前的 DataStream流数据对象 调用 assignTimestampsAndWatermarks 方法,分配时间戳,然后里面使用 WatermarkStrategy 这个是水位线生成策略,他下面两个方法,一个是有序流的,一个是乱序流的,我们这里使用有序流的,forMonotonousTimestamps[Events]() 他有个泛型,是当前的数据类型,然后继续调用 withTimestampAssigner() 方法来指定哪个字段为时间戳,这里面 new SerializableTimestampAssigner[Events]这个是可以序列化的提取时间戳,这种比较简单,然后里面重写一个方法 extractTimestamp(t: Events, l: Long): Long l里面两个字段第一个是当前的每一条数据,第二个参数是指定的时间戳,最后返回的也是这个,我们直接 t.shijian 就把我们泛型 Eventsshijian 字段指定为时间戳了。

//1、有序流的水位线生成策略
//WatermarkStrategy 这是flink考虑到我们实现太麻烦,所以给我们写好了这种策略,下面有两种方法,
// 一种是有序流的forMonotonousTimestamps(),还有一种forBoundedOutOfOrderness() 乱序流的

stream.assignTimestampsAndWatermarks( WatermarkStrategy.forMonotonousTimestamps[Events]() 泛型需要指定输入的数据类型
.withTimestampAssigner(   //withTimestampAssigner() 需要用这个方法来制定哪个时间为时间戳
  new  SerializableTimestampAssigner[Events]{ //这种是可以序列化的提取时间戳,这种会比较简单
    override def extractTimestamp(t: Events, l: Long): Long = { // 重写 extractTimestamp 提取时间戳,两个参数,第一个是当前每条数据
      t.shijian   //还有一个是指定好的当前的时间戳,我们直接 t.时间戳,就把指定好的时间戳字段提取出来了
    }
  }
))

2、乱序流水位线生成策略
思路: 与有序流很相似,只需要改一点点
我们直接用当前的 DataStream流数据对象 调用 assignTimestampsAndWatermarks 方法,分配时间戳,然后里面使用 WatermarkStrategy 这个是水位线生成策略,他下面两个方法,一个是有序流的,一个是乱序流的,我们这里使用乱序流的,forBoundedOutOfOrderness[Events](Duration.ofSeconds(2))Duration.ofSeconds(2) 指定最大的延迟时间,因为是乱序的,有些数据还没来,所以我们设置个延迟时间,他有个泛型,是当前的数据类型,然后继续调用 withTimestampAssigner() 方法来指定哪个字段为时间戳,这里面 new SerializableTimestampAssigner[Events]这个是可以序列化的提取时间戳,这种比较简单,然后里面重写一个方法 extractTimestamp(t: Events, l: Long): Long l里面两个字段第一个是当前的每一条数据,第二个参数是指定的时间戳,最后返回的也是这个,我们直接 t.shijian 就把我们泛型 Eventsshijian 字段指定为时间戳了。

//2、乱序流的水位线生成策略
//orBoundedOutOfOrderness() 乱序流水位线生成策略
stream.assignTimestampsAndWatermarks( WatermarkStrategy.forBoundedOutOfOrderness[Events](Duration.ofSeconds(2)) //最大延迟时间,因为是乱序的有些还没来,我们这里等延迟数据两秒
.withTimestampAssigner(
  new SerializableTimestampAssigner[Events] {
    override def extractTimestamp(t: Events, l: Long): Long = {
      t.shijian
    }
  }
))
(4)、自定义水位线策略

建议使用,flink 内置的乱序流水位线生成策略

package chat02

import org.apache.flink.api.common.eventtime._
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}

// 自定义水位线

class flink02_Watermark2 {

}
object flink02_Watermark2{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(4)
    env.getConfig.setAutoWatermarkInterval(500L) //自动生成水位线的周期时间间隔,他是长整型的,这里设置为500毫秒
    val stream: DataStream[Events] = env.fromElements(
      Events("Mary", "./home", 1000L),
      Events("Bob", "./cart", 2000L),
      Events("Alice", "./cart", 3000L),
      Events("Mary", "./prod?id=1", 4000L),
      Events("Mary", "./prod?id=2", 6000L),
      Events("Mary", "./prod?id=3", 5000L)
    )

    stream.assignTimestampsAndWatermarks(new WatermarkStrategy[Events] { // WatermarkStrategy[Events] 水位线生成策略

      override def createTimestampAssigner(context: TimestampAssignerSupplier.Context): TimestampAssigner[Events] = { //重写
        new SerializableTimestampAssigner[Events] {
          override def extractTimestamp(t: Events, l: Long): Long = t.shijian //提取时间戳
        }
      }

      override def createWatermarkGenerator(context: WatermarkGeneratorSupplier.Context): WatermarkGenerator[Events] = {
        new WatermarkGenerator[Events] { //这里是重写水位线生成器
          //定义一个延迟时间
          val delay = 5000L
          //定义属性保存最大时间戳
          var maxTs = Long.MinValue + delay + 1

          override def onEvent(t: Events, l: Long, watermarkOutput: WatermarkOutput): Unit = { //这种是每个数据来都标记依次
            maxTs = Math.max(maxTs, t.shijian)  //更新当前的最大时间戳
          }

          override def onPeriodicEmit(watermarkOutput: WatermarkOutput): Unit = { //这种是周期性的标记
            val watermark = new Watermark(maxTs - delay - 1L)
            watermarkOutput.emitWatermark(watermark)
          }
        }
      }
    })


}
}

3、在自定义源中生成水位线

这两步经过之后,后面都不用在去定义水位线策略了。就不能再调用assignTimestampsAndWatermarks这个方法去指定水位线生成策略了,因为之前都定义好了,这是一件二选一的事,一般在生产过程中,还是在使用中去定义的。

  //为要发送的数据分配时间戳
  sourceContext.collectWithTimestamp(event,event.shijian) //两个参数,第一个是当前每条数据,第二个是提取时间戳

  //向下游发送水位线
  sourceContext.emitWatermark(new Watermark(event.shijian - 1L)) //经过这两步,后面都不用经过水位线生成策略了
package chat01

import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark

import java.util.Calendar
import scala.util.Random

class Source {

}
object Source{
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val data = env.addSource(new ClickSource) //使用 addSource 方法
      .setParallelism(1)

    data.print()

    env.execute()
  }
}

class ClickSource extends SourceFunction[Events] {
  var running = true

  override def run(sourceContext: SourceFunction.SourceContext[Events]): Unit = {
    // 实例化一个随机数发生器
    val random = new Random()
    // 供随机选择的用户名的数组
    val users = Array("Mary", "Bob", "Alice", "Cary")
    // 供随机选择的 城市 的数组
    val urls = Array("四川", "上海", "北京", "山西", "苏杭")
    //通过 while 循环发送数据,running 默认为 true,所以会一直发送数据
    while (running) {
      val event = Events(users(random.nextInt(users.length)), // 随机选择一个用户名
        urls(random.nextInt(urls.length)), // 随机选择一个 url
        Calendar.getInstance.getTimeInMillis // 当前时间戳
      )

      //为要发送的数据分配时间戳
      sourceContext.collectWithTimestamp(event,event.shijian)

      //向下游发送水位线
      sourceContext.emitWatermark(new Watermark(event.shijian - 1L)) //经过这两步,后面都不用经过水位线生成策略了

      // 调用 collect 方法向下游发送数据
      // //sourceContext是SourceFunction.SourceContext[Events]类型的参数,它是一个上下文对象,用于向下游发送数据。
      // 在run方法中,通过调用ctx.collect方法向下游发送数据。
      sourceContext.collect( event )// 使用上下文对象 .collent 方法来采集数据

      54
      // 隔 1 秒生成一个点击事件,方便观测
      Thread.sleep(1000)
    }
  }

  override def cancel(): Unit = running = false
}

4、水位线的传递

在“重分区”(redistributing)的传输模式下,一个任务有可能会收到来自不同分区上游子
任务的数据。而不同分区的子任务时钟并不同步,所以同一时刻发给下游任务的水位线可能并
不相同。这说明上游各个分区处理得有快有慢,进度各不相同,这时我们应该以最慢的那个时
钟,也就是最小的那个时间戳水位线为准。
Flink 时间和窗口_第7张图片
水位线在上下游任务之间的传递,非常巧妙地避免了分布式系统中没有统一时钟的问题,每个任务都以“处理完之前所有数据”为标准来确定自己的时钟,就可以保证窗口处理的结果总是正确的。对于有多条流合并之后进行处理的场景,水位线传递的规则是类似的。

三、窗口

在 Flink 中,提供了非常丰富的窗口操作。

1、窗口的概念

Flink 是一种流式计算引擎,主要是来处理无界数据流的,数据源源不断、无穷无尽。想
要更加方便高效地处理无界流,一种方式就是将无限数据切割成有限的“数据块”进行处理,这
就是所谓的“窗口”(Window)。在 Flink 中, 窗口就是用来处理无界流的核心。
Flink 时间和窗口_第8张图片
这里注意为了明确数据划分到哪一个窗口,定义窗口都是包含起始时间、不包含结束时间的,用数学符号表示就是一个左闭右开的区间。对于处理时间下的窗口而言,这样理解似乎没什么问题。然而如果我们采用事件时间语义,就会有些令人费解了。由于有乱序数据,我们需要设置一个延迟时间来等所有数据到齐。比如上面的例子中,我们可以设置延迟时间为 2 秒,如图 6-12 所示,这样 0~10 秒的窗口会在时间戳为 12 的数据到来之后,才真正关闭计算输出结果,这样就可以正常包含迟到的 9 秒数据了。
Flink 时间和窗口_第9张图片
但是这样一来,0~10 秒的窗口不光包含了迟到的 9 秒数据,连 11 秒和 12 秒的数据也包
含进去了。我们为了正确处理迟到数据,结果把早到的数据划分到了错误的窗口——最终结果
都是错误的。
所以在 Flink 中,窗口其实并不是一个“框”,流进来的数据被框住了就只能进这一个窗
口。相比之下,我们应该把窗口理解成一个“桶”,如图0所示。在 Flink 中,窗口可以把
流切割成有限大小的多个“存储桶”(bucket);每个数据都会分发到对应的桶中,当到达窗口
结束时间时,就对每个桶中收集的数据进行计算处理。
Flink 时间和窗口_第10张图片
这里需要注意的是,Flink 中窗口并不是静态准备好的,而是动态创建——当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。另外,这里我们认为到达窗口结束时间时,窗口就触发计算并关闭,事实上“触发计算”和“窗口关闭”两个行为也可以分开,这部分内容我们会在后面详述。

2、窗口的分类

上面的例子其实是最为简单的一种时间窗口。在 Flink 中,窗口的应用非常灵活,我们可以使用各种不同类型的窗口来实现需求。接下来我们就从不同的角度,对 Flink中内置的窗口做一个分类说明。

(1)、按照驱动类型分类

窗口本身是截取有界数据的一种方式,所以窗口一个非常重要的信息其实就是“怎样截取
数据”。换句话说,就是以什么标准来开始和结束数据的截取,我们把它叫作窗口的“驱动类
型”。
我们最容易想到的就是按照时间段去截取数据,这种窗口就叫作“时间窗口”(Time Window)。这在实际应用中最常见,之前所举的例子也都是时间窗口。除了由时间驱动之外,窗口其实也可以由数据驱动,也就是说按照固定的个数,来截取一段数据集,这种窗口叫作“计数窗口”(Count Window) 如图所示,下面是时间窗口和计数窗口。
Flink 时间和窗口_第11张图片

1) 时间窗口(Time Window)

时间窗口以时间点来定义窗口的开始(start)和结束(end),所以截取出的就是某一时间段的数据。到达结束时间时,窗口不再收集数据,触发计算输出结果,并将窗口关闭销毁。用结束时间减去开始时间,得到这段时间的长度,就是窗口的大小(window size)。这里的时间可以是不同的语义,所以我们可以定义处理时间窗口和事件时间窗口。
Flink 中有一个专门的类来表示时间窗口,名称就叫作 TimeWindow。这个类只有两个私有属性:start 和 end,表示窗口的开始和结束的时间戳,单位为毫秒。
我们可以调用公有的 getStart()和 getEnd()方法直接获取这两个时间戳。另外,TimeWindow 还提供了一个 maxTimestamp()方法,用来获取窗口中能够包含数据的最大时间戳。
很明显,窗口中的数据,最大允许的时间戳就是 end - 1,这也就代表了我们定义的窗口时间范围都是左闭右开的区间[start,end)

2) 计数窗口(Count Window)

计数窗口基于元素的个数来截取数据,到达固定的个数时就触发计算并关闭窗口。每个窗
口截取数据的个数,就是窗口的大小。
计数窗口相比时间窗口就更加简单,我们只需指定窗口大小,就可以把数据分配到对应的窗口中了。在 Flink 内部也并没有对应的类来表示计数窗口,底层是通过“全局窗口”(Global Window)来实现的。

(2) 按照窗口分配数据的规则分类(重点!!!)

时间窗口和计数窗口,只是对窗口的一个大致划分;在具体应用时,还需要定义更加精细的规则,来控制数据应该划分到哪个窗口中去。不同的分配数据的方式,就可以有不同的功能应用。
根据分配数据的规则,窗口的具体实现可以分为 4 类:
滚动窗口(Tumbling Window)滑动窗口(Sliding Window)会话窗口(Session Window),以及全局窗口(Global Window)

1) 滚动窗口(Tumbling Windows)

滚动窗口有固定的大小,是一种对数据进行“均匀切片”的划分方式。窗口之间没有重叠,也不会有间隔,是“首尾相接”的状态。如果我们把多个窗口的创建,看作一个窗口的运动,那就好像它在不停地向前“翻滚”一样。这是最简单的窗口形式,我们之前所举的例子都是滚动窗口。
滚动窗口可以基于时间定义,也可以基于数据个数定义;需要的参数只有一个,就是窗口的大小(window size)。比如我们可以定义一个长度为 1 小时的滚动时间窗口,那么每个小时就会进行一次统计;或者定义一个长度为 10 的滚动计数窗口,就会每 10 个数进行一次统计。
Flink 时间和窗口_第12张图片
滚动窗口应用非常广泛,它可以对每个时间段做聚合统计,很多 BI 分析指标都可以用它来实现。

2) 滑动窗口(Sliding Windows)

与滚动窗口类似,滑动窗口的大小也是固定的。区别在于,窗口之间并不是首尾相接的,而是可以“错开”一定的位置。如果看作一个窗口的运动,那么就像是向前小步“滑动”一样。
既然是向前滑动,那么每一步滑多远,就也是可以控制的。所以定义滑动窗口的参数有两个:除去窗口大小(window size)之外,还有一个“滑动步长”(window slide),它其实就代表了窗口计算的频率。同样,滑动窗口可以基于时间定义,也可以基于数据个数定义。
Flink 时间和窗口_第13张图片
我们可以看到,当滑动步长小于窗口大小时,滑动窗口就会出现重叠,这时数据也可能会被同时分配到多个窗口中。而具体的个数,就由窗口大小和滑动步长的比值(size/slide)来决定。所以,滑动窗口其实是固定大小窗口的更广义的一种形式。
在一些场景中,可能需要统计最近一段时间内的指标,而结果的输出频率要求又很高,甚至要求实时更新,比如股票价格的 24 小时涨跌幅统计,或者基于一段时间内行为检测的异常报警。这时滑动窗口无疑就是很好的实现方式。

3) 会话窗口(Session Window)

会话窗口顾名思义,是基于“会话”(session)来来对数据进行分组的。这里的会话类似Web 应用中 session 的概念,不过并不表示两端的通讯过程,而是借用会话超时失效的机制来描述窗口。
与滑动窗口和滚动窗口不同,会话窗口只能基于时间来定义。对于会话窗口而言,最重要的参数就是会话超时时间的长度(size),也就是两个会话窗口之间的最小距离。如果相邻两个数据到来的时间间隔(Gap)小于指定的大小(size),那说明还在保持会话,它们就属于同一个窗口;如果 gap 大于 size,那么新来的数据就应该属于新的会话窗口,而前一个窗口就应该关闭了。在具体实现上,我们可以设置静态固定的大(size),也可以通过一个自定义的提取器(gap extractor)动态提取最小间隔 gap 的值。
Flink 时间和窗口_第14张图片
在一些类似保持会话的场景下,往往可以使用会话窗口来进行数据的处理统计。

4) 全局窗口(Global Windows)

还有一类比较通用的窗口,就是“全局窗口”。这种窗口全局有效,会把相同 key 的所有数据都分配到同一个窗口中。无界流的数据永无止尽,所以这种窗口也没有结束的时候,默认是不会做触发计算的。如果希望它能对数据进行计算处理,还需要自定义“触发器”(Trigger)。
Flink 时间和窗口_第15张图片
Flink 中的计数窗口(Count Window),底层就是用全局窗口实现的。

3、窗口 API(预览)

已经了解了Flink窗口中的概念和分类,对Window API 有了一个基本的整体认识,接下来了解一下是怎样调用的。

(1)、按键分区(Keyed)和非按键分区(Non-Keyed)

在定义窗口操作之前,首先需要确定,到底是基于按键分区(Keyed)的数据流 KeyedStream来开窗,还是直接在没有按键分区的DataStream上开窗。也就是说,在调用窗口算子之前,是否有 keyBy()操作。这两种方式的调用方式是不一样的。

1) 按键分区窗口(Keyd Windows)

经过按键分区 keyBy()操作后,数据流会按照 key 被分为多条逻辑流(logical streams),这就KeyedStream。基于 KeyedStream 进行窗口操作时, 窗口计算会在多个并行子任务上同时执行。相同 key 的数据会被发送到同一个并行子任务,而窗口操作会基于每个 key 进行单独的处理。所以可以认为,每个 key 上都定义了一组窗口,各自独立地进行统计计算。
在代码实现上,我们需要先对DataStream调用keyBy()进行按键分区,然后再调用window()定义窗口。

stream.keyBy(_.user)
 .window(...) //keyBy 按键分区之后是这个样子进行调用。
2) 非按键分区(Non-Keyed Windows)(不推荐)

如果没有进行 keyBy(),那么原始的 DataStream 就不会分成多条逻辑流。这时窗口逻辑只能在一个任务(task)上执行,就相当于并行度变成了 1。所以在实际应用中一般不推荐使用这种方式。
在代码中,直接基于 DataStream 调用 windowAll()定义窗口。
stream.windowAll(...) 这是没按键分区的调用方式
这里需要注意的是,对于非按键分区的窗口操作,手动调大窗口算子的并行度也是无效的,windowAll 本身就是一个非并行的操作。

(2)、代码中窗口 API 的调用

有了前置的基础,接下来我们就可以真正在代码中实现一个窗口操作了。简单来说,窗口操作主要有两个部分:窗口分配器(Window Assigners)和窗口函数(Window Functions)

stream.keyBy(<key selector>)
 .window(<window assigner>)
 .aggregate(<window function>)

4、窗口分配器(Window Assigners)

定义窗口分配器(Window Assigners)是构建窗口算子的第一步,它的作用就是定义数据应该被“分配”到哪个窗口。而窗口分配数据的规则,其实就对应着不同的窗口类型。所以可以说,窗口分配器其实就是在指定窗口的类型。
窗口分配器最通用的定义方式,就是调用 window()方法。这个方法需要传入一个WindowAssigner 作为参数,返回 WindowedStream。如果是非按键分区窗口,那么直接调用windowAll()方法,同样传WindowAssigner,返回的是 AllWindowedStream。窗口按照驱动类型可以分成时间窗口和计数窗口,而按照具体的分配规则,又有滚动窗口、滑动窗口、会话窗口、全局窗口四种。除去需要自定义的全局窗口外,其他常用的类型 Flink中都给出了内置的分配器实现,我们可以方便地调用实现各种需求。

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