Flink中的Window和Time详解
Window(窗口)
Flink 认为 批处理 是 流处理 的一个特例,所以 Flink 底层引擎是一个流式引擎,在上面实现了流处理和批处理。而Window就是从 流处理 到 批处理 的一个桥梁。
通常来讲,Window是一种可以把无界数据切割为有界数据块的手段
例如,对流中的所有元素进行计数是不可能的,因为通常流是无限的(无界的)。所以,流上的聚合需要由 Window 来划定范围,比如 “计算过去5分钟” 或者 “最后100个元素的和”。
window可以是时间驱动的 (Time Window)(比如:每30秒)或者数据驱动的(Count Window)(如每100个元素)。
DataStream API提供了基于Time和Count的Window。同时,由于某些特殊的需要,DataStream API也提供了定制化的Window操作,供用户自定义Window。
Window的类型
Window根据类型可以分为两种。
- Tumbling Windows:滚动窗口,表示窗口内的数据没有重叠
- Sliding Windows:滑动窗口,表示窗口内的数据有重叠
Window类型汇总
TimeWindow
TimeWindow 是根据时间对数据流切分窗口,TimeWindow可以支持滚动窗口和滑动窗口。
- 其中timeWindow( Time.seconds(10))方法表示滚动窗口的窗口大小为10秒,对每10秒内的数据进行聚合计算。
- timeWindow(Time.seconds(10),Time.seconds(5))方法表示滑动窗口的窗口大小为10秒,滑动间隔为5秒。就是每隔5秒计算前10秒内的数据,
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.windowing.time.Time
/**
* TimeWindow的使用
* 1:滚动窗口
* 2:滑动窗口
*
*/
object TimeWindowOpScala {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val text = env.socketTextStream("bigdata01", 9001)
import org.apache.flink.api.scala._
//TimeWindow之滚动窗口:每隔10秒计算一次前10秒时间窗口内的数据
/*text.flatMap(_.split(" "))
.map((_,1))
.keyBy(0)
//窗口大小
.timeWindow(Time.seconds(10))
.sum(1).print()*/
//TimeWindow之滑动窗口:每隔5秒计算一次前10秒时间窗口内的数据
text.flatMap(_.split(" "))
.map((_, 1))
.keyBy(tup => tup._1)
//第一个参数:窗口大小,第二个参数:滑动间隔
.timeWindow(Time.seconds(10), Time.seconds(5))
.sum(1).print()
env.execute("TimeWindowOpScala")
}
}
在bigdata01上开启socket
nc -l 9001
CountWindow
CountWindow 是根据元素个数对数据流切分窗口,CountWindow也可以支持滚动窗口和滑动窗口。
- 其中countWindow(5)方法表示滚动窗口的窗口大小是5个元素,也就是当窗口中填满5个元素的时候,就会对窗口进行计算了。
- countWindow(5,1)方法表示滑动窗口的窗口大小是5个元素,滑动的间隔为1个元素,也就是说每新增1个元素就会对前面5个元素计算一次。
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
/**
* CountWindow的使用
* 1:滚动窗口
* 2:滑动窗口
*
*/
object CountWindowOpScala {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val text = env.socketTextStream("bigdata01", 9001)
import org.apache.flink.api.scala._
/**
* 注意:由于我们在这里使用了keyBy,会先对数据分组
* 如果某个分组对应的数据窗口内达到了5个元素,这个窗口才会被触发执行
*/
//CountWindow之滚动窗口:每隔5个元素计算一次前5个元素
/*text.flatMap(_.split(" "))
.map((_,1))
.keyBy(0)
//指定窗口大小
.countWindow(5)
.sum(1).print()*/
//CountWindow之滑动窗口:每隔1个元素计算一次前5个元素
text.flatMap(_.split(" "))
.map((_, 1))
.keyBy(0)
//第一个参数:窗口大小,第二个参数:滑动间隔
.countWindow(5, 1)
.sum(1).print()
env.execute("CountWindowOpScala")
}
}
自定义Window
下面我们来看一下自定义window
其实window还可以再细分一下
- 一种是基于Key的Window
- 一种是不基于Key的Window
我们前面演示的都是基于key的window,就是在使用window之前,先执行了keyBy分组操作,如果需求中不需要根据key进行分组的话,可以不使用keyBy,这样在使用window的时候需要使用timeWindowAll()和countWindowAll()。
针对基于key的window需要使用window函数。
针对不基于key的window需要使用windowAll函数。
其实我们前面所说的TimeWindow和TimeWindowAll底层用的就是window和windowAll函数,可以这样理解,TimeWindow是官方封装好的window。
那下面我们来试一下自己使用window函数封装一个MyTimeWindow
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
/**
* 需求:自定义MyTimeWindow
* 、
*/
object MyTimeWindowScala {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val text = env.socketTextStream("bigdata01", 9001)
import org.apache.flink.api.scala._
//自定义MyTimeWindow滚动窗口:每隔10秒计算一次前10秒时间窗口内的数据
text.flatMap(_.split(" "))
.map((_,1))
.keyBy(0)
//窗口大小
.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
.sum(1).print()
env.execute("MyTimeWindowScala")
}
}
Window聚合
在进行Window聚合操作的时候可以分为两种
- 一种是增量聚合
- 一种是全量聚合
Window聚合之增量聚合
增量聚合:窗口中每进入一条数据,就进行一次计算
常见的一些增量聚合函数如下:
reduce()、aggregate()、sum()、min()、max()
下面我们来看一个增量聚合的例子,累加求和,对 8 、12、7、10这四条数据进行累加求和:
- 第一次进来一条数据8,则立刻进行累加求和,结果为8。
- 第二次进来一条数据12,则立刻进行累加求和,结果为20。
- 第三次进来一条数据7,则立刻进行累加求和,结果为27。
- 第四次进来一条数据10,则立刻进行累加求和,结果为37。
Window聚合之全量聚合
全量聚合:等属于窗口的数据到齐,才开始进行聚合计算【可以实现对窗口内的数据进行排序等需求】
常见的一些全量聚合函数为apply(windowFunction)和process(processWindowFunction)
注意:processWindowFunction比windowFunction提供了更多的Context(上下文)信息。
下面我们来看一个全量聚合的例子,求最大值,对8 、12、7、10这四条数据求最大值
- 第一次进来一条数据8。
- 第二次进来一条数据12。
- 第三次进来一条数据7。
- 第四次进来一条数据10,此时窗口触发,才会对窗口内的数据进行排序,获取最大值。
Time
针对流数据中的Time(时间),可以分为以下3种。
- Event Time:事件产生的时间,它通常由事件中的时间戳描述。
- Ingestion time:事件进入Flink的时间。
- Processing Time:事件被处理时当前系统的时间
Time案例分析
举个例子,原始日志是这样的:
2026-01-01 10:00:01 INFO executor.Executor: Finished task in state 0.0
2026-01-01 10:00:01是日志数据产生的时间
日志数据进入Flink的时间是:2026-01-01 20:00:01
日志数据到达Window处理的时间是:2026-01-01 20:00:02
如果我们想要统计每分钟内接口调用失败的错误日志个数,使用哪个时间才有意义?
因为数据有可能会出现延迟,如果使用 数据进入Flink的时间 或者 Window处理的时间,其实是没有意义的,这个时候需要使用原始日志中的时间才是有意义的,这个才是数据产生的时间。
Time类型设置
那我们在Flink的流处理中,默认使用的是哪个时间呢?
默认情况下Flink在流处理中使用的时间是ProcessingTime
查看源码:在类StreamExecutionEnvironment中
private static final TimeCharacteristic DEFAULT_TIME_CHARACTERISTIC = TimeCharacteristic.ProcessingTime;
如何修改呢?想要修改的话可以使用setStreamTimeCharacteristic(…)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
或者
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)