Flink实时计算划分窗口时,如果使用时间作为划分窗口的依据,时间有不同的类型,分别是Event Time、Ingestion Time、Processing Time。如下图所示:
在大数据领域,日志服务器生成的一条数据也可以称为一个事件。Event Time是指在数据产生时该设备上对应的时间,这个时间在进入Flink之前已经存在于数据记录中了。以后数据被Flink处理数据,如果使用Event Time作为时间标准,那么数据并不是按照Event Time的先后顺序被处理的,由于数据可能产生在多个不同的日志服务器,然后通常是再将数据写入到分布性消息中间件,然后被被Flink拉取进行处理时,处理的实际时间相对于数据产生的实际肯定有一定的延迟,并且Event Time可能也是乱序的。那么为什么还要使用Event Time呢?是因为使用Event Time时,Flink程序可以处理乱序事件和延迟数据。并且最重要的功能就是可以统计在数据产生时,对应时间的数据指标。
Ingestion Time指的是事件数据进入到Flink的时间。每条数据的Ingestion Time就是进入到Source Operator时所在机器的系统时间。比如Flink从Kafka消息中间件消费数据,每一条数据的Ingestion Time就是FlinkKafkaConsumer拉取数据进入到TaskManager对应的时间。Ingestion Time介于Event Time和Processing Time之间,与 Event Time 相比,Ingestion Time程序无法处理任何无序事件或延迟数据,并且程序不必指定如何生成水,Flink会自动分配时间戳和自动生成水位线。
Processing Time是指事件数据被Operator处理时所在机器的系统时间,是Flink默认使用的时间标准,它提供了最好的性能和最低的延迟。但是,Flink是一个在分布式的计算框架,数据从产生到被处理会有一定的延迟(例如从消息队列拉取数据到Source,Source再到处理的Operator会有一定的延迟),所以Processing Time无法精准的体现出数据在产生的那个时刻的变化情况。
在不设置任何的时间标准的情况下,默认使用的是ProcessingTime,如果要使用某一种时间类型作为作为时间标准,那么就要使用StreamExecutionEnvironment的setStreamTimeCharacteristic,传入TimeCharacteristic其中的一个的枚举类型参数。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();//设置EventTime作为时间标准
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//设置IngestionTime作为时间标准
//env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
//设置ProcessingTime作为时间标准
//env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
流式计算是一种被设计用于处理无限数据集的数据计算引擎,所谓无限数据集是指一种源源不断的数据流抽象成的集合。而Window就是一种将无限数据集切分成多个有限数据集并对每一个有限数据集分别进行处理的手段。Window本质上是将数据流按照一定的规则,逻辑地切分成很多个有限大小的“bucket”桶,这样就可以对每一个在“桶里面”的有限的数据依次地进行计算了。
流式计算引擎的特点是每输入一条数据就立即处理,延迟低。然而在一些场景下偏偏希望将数据先攒成一个个小批次,然后对每一个小批次再进行运算。例如用FlinkDataStream API将数据进行实时的聚合后,再将结果实时的写入到数据库,每输入一条数据都会输出一个聚合后的结果,如果数据量非常大,那么对外部的数据库的写入压力就比较大。而划分成Window,即将每一个Window中的有限的数据先聚合成一条或几条,再写入到数据库中,虽然延迟变高了,但是对数据库的写入压力变小了。Window操作可以认为是微批次准实时的计算,这样Flink DataStream API 既可以实现高效的实时运算,又可以实现微批次的准实时运算,让Flink在实时计算领域更加强大和灵活。
Flink的Window常用的两类:
GlobalWindow: 按照指定的数据条数生成一个Window,与时间无关。
TimeWindow: 按照时间生成Window,可以根据窗口实现原理的不同分成三类:滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)和会话窗口(Session Window)。
滚动窗口(Tumbling Windows)
将数据依据固定的窗口⻓度对数据进行切片。特点:时间对⻬,窗口⻓度固定,没有重叠。
滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会 出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,窗口的创建如下图所示:
滑动窗口(Sliding Windows)
滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口⻓度和滑动间隔组成。 特点:时间对⻬,窗口⻓度固定,有重叠。
滑动窗口分配器将元素分配到固定⻓度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。
会话窗口(Session Windows)
由一系列事件组合一个指定时间⻓度的timeout间隙组成,类似于web应用的session,也就是一段时间没有接收到新数据就会生成新的窗口。
特点:时间无对⻬。
session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即 非活动间隔产生,那个这个窗口就会关闭。一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的⻓度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。
在划分Window之前,首先要确定该DataStream是否调用了key算子将数据按照key进行分组了。如果没有调用keyBy算子,可以调用windowAll方法的返回一个AllWindowedStream,这种window叫做Non-Keyed Windows(未分组的Widnows);如果事先已经调用了keyBy算子,即对KeyedStream可以调用window方法返回一个WindowedStream,这种window叫做Keyed Windows(分组的Widnows)。由于调用windowAll/window算子后会生成会生成新WindowedStream/WindowedStream,所以窗口算也是属于Transformation。
未分组的Widonws,即DataSteam直接调用windowAll算子得到的Windows,Non-Keyed Windows的特点是,在某一个具体的窗口,所有的数据都会被窗口算子路由到一个subtask中进行运算,如果并行度大于1,下一次生成的window的数据会被路由到其他的subtask中进行运算。
下面是Non-Keyed Windows的方法调用顺序和方法说明:
stream.windowAll(…) //<- 必选方法: 指定相应的窗口分配器
[.trigger(…)] //<- 可选方法: 指定触发器,如果不指定有默认的触发器
[.evictor(…)] //<- 可选方法: 指定剔除器,如果不指定有默认的剔除器
[.allowedLateness(…)] //<- 可选方法: 指定延迟触发时间,如果不指定,默认为0
[.sideOutputLateData(…)] //<- 可选方法: 指定延迟数据的侧流输出的Tag
.sum/reduce/aggregate/fold/apply() //<- 必选方法: 指定窗口函数
[.getSideOutput(…)] //<- 可选方法: 指定侧流数据的Tag
windowAll():划分Non-Keyed Windows,参数为指定的Window Assinger。
trigger():指定触发器,如果不指定有默认的触发器。
evictor():指定剔除器,如果不指定有默认的剔除器。
allowedLateness():指定延迟触发时间,如果不指定,默认为0。
sideOutputLateData():指定延迟数据的侧流输出的tag,用来筛选出打上指定tag的迟到数据
sum/reduce/aggregate/fold/apply():指定窗口函数,窗口触发时会应该改函数对窗口中的数据进行计算。
getSideOutput():指定侧流数据的tag,筛选出指定tag类型的数据。
分组的Widonws,即KeyedStream直接调用window算子得到的Windows。Keyed Windows
的特点是:窗口中的数据会根据key进行分组,key相同的数据一定会被分到同一个组内,并被路由到同一个subtask中,一个key对应一个组,一个subtask中可以有零到多个组。窗口触发会对每个组进行计算,每个组都会得到一个结果。
下面是Keyed Windows的方法调用顺序和方法说明:
stream.keyKey(…) //<- 先对DataStream调用keyBy得到keyedStream
.window(…) //<- 必选方法: 指定相应的窗口分配器
[.trigger(…)] //<- 可选方法: 指定触发器,如果不指定有默认的触发器
[.evictor(…)] //<- 可选方法: 指定剔除器,如果不指定有默认的剔除器
[.allowedLateness(…)] //<- 可选方法: 指定延迟触发时间,如果不指定,默认为0
[.sideOutputLateData(…)] //<- 可选方法: 指定延迟数据的测流输出的Tag
.sum/reduce/aggregate/fold/apply() //<- 必选方法: 指定窗口函数
[.getSideOutput(…)] //<- 可选方法: 指定测流数据的Tag
keyBy():按照key进行分组,参数为一或多个分组字段
windw():划分keyed Windows,参数为指定的Window Assinger
增量聚合:指窗口每进入一条数据就计算一次
常见的聚合方法:
全量聚合:指在窗口触发的时候才会对窗口内的所有数据进行一次计算(等窗口的数据到齐,才开始进行聚合计算,可实现对窗口内的数据进行排序等需求)
常见的聚合方法:
processWindowFunction比windowFunction提供了更多的上下文信息