Flink05——一文了解Flink的Window和Time

前言

窗口计算是流式计算中非常常用的数据计算方式之一,通过按照固定时间或长度将 数据流切分成不同的窗口,然后对数据进行相应的聚合运算,从而得到一定时间范围内的统计结果。窗口(Windows)在SparkStreaming Flink中都是非常重要的概念。
例如统计最近5分钟内某基站的呼叫数,此时基站的数据在不断地产生,但是通过5分钟的窗口将数据限定在固定时间范围内,就可以对该范围内的有界数据执行聚合处理,得出最近5分钟的基站的呼叫数量。

window分类

1.Global Window 和 Keyed Window

在运用窗口计算时,Flink根据上游数据集是否为KeyedStream类型,对应的Windows 也会有所不同

  • Keyed Window:上游数据集如果是 KeyedStream 类型,则调用 DataStream API 的 window() 方法,数据会根据 Key 在不同的Task 实例中并行分别计算,最后得出针对每个 Key 统 计的结果。
  • Global Window:如果是 Non-Keyed 类型,则调用 WindowsAll()方法,所有的数据都会在窗口算子中由到一个Task中计算,并得到全局统计结果。

举个例子:

//读取文件数据 
val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)   	
  .map(line=>{ 
  	var arr =line.split(",") 
  	new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long) 
  	}) 
  	//Global Window 
  	data.windowAll (自定义的WindowAssigner) 
  	//Keyed Window 
  	data.keyBy(_.sid) .window(自定义的WindowAssigner)

2.Time Window和Count Window

基于业务数据的方面考虑,Flink 又支持两种类型的窗口,一种是基于时间的窗口叫Time Window。还有一种基于输入数据数量的窗口叫Count Window。

根据不同的业务场景,Time Window 也可以分为三种类型,分别是滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)和会话窗口(Session Window)

1.滚动窗口(Tumbling Window)
滚动窗口是根据固定时间进行切分,且窗口和窗口之间的元素互不重叠。这种类型的窗 口的最大特点是比较简单。只需要指定一个窗口长度(window size)。
在这里插入图片描述

//每隔5秒统计每个基站的日志数量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) .timeWindow(Time.seconds(5)) 	
	//.window(TumblingEventTimeWindows.of(Time.seconds(5))) 
	.sum(1) //聚合

2. 滑动窗口(Sliding Window)
滑动窗口也是一种比较常见的窗口类型,其特点是在滚动窗口基础之上增加了窗口滑动时间(Slide Time),且允许窗口数据发生重叠。当Windows size 固定之后,窗口并不像滚动窗口按照 Windows Size向前移动,而是根据设定的 Slide Time向前滑动。窗口之间的数据重叠大小根据 Windows size 和 Slide time决定,当Slide time小于Windows size便会发生窗口重叠,Slide size 大于 Windows size 就会出现窗口不连续,数据可能不能在 任何一个窗口内计算,Slide size 和 Windows size相等时,Sliding Windows其实就是 Tumbling Windows。
在这里插入图片描述

//每隔3秒计算最近5秒内,每个基站的日志数量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1)  
	.timeWindow(Time.seconds(5),Time.seconds(3)) 
	//.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(3))) 
	.sum(1)

3. 会话窗口(Session Window)
会话窗口(Session Windows)主要是将某段时间内活跃度较高的数据聚合成一个窗口进行计算,窗口的触发的条件是 Session Gap,是指在规定的时间内如果没有数据活跃接入,则认为窗口结束,然后触发窗口计算结果。需要注意的是如果数据一直不间断地进入窗口,也会导致窗口始终不触发的情况。与滑动窗口、滚动窗口不同的是,Session Windows 不需要有固定 windows size 和 slide time,只需要定义 session gap,来规定不活跃数据的时间上限即可。
在这里插入图片描述

//3秒内如果没有数据进入,则计算每个基站的日志数量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.window(EventTimeSessionWindows.withGap(Time.seconds(3))) 
	.sum(1)

4) Count Window(数量窗口)
Count Window 也有滚动窗口、滑动窗口等。在实际应用中比较少。

window API

在以后的实际案例中 Keyed Window 使用最多,所以我们需要掌握 Keyed Window 的算子, 在每个窗口算子中包含了 Windows Assigner(窗口指定器)、Windows Trigger(窗口触发器)、Evictor(数据剔除器)、Lateness(时延设定)、Output Tag(输出标签)以及Windows Funciton 等组成部分,其中 Windows Assigner 和 Windows Funciton是所有窗口算子必须指定的属性,其余的属性都是根据实际情况选择指定。

stream.keyBy(...) // 是Keyed类型数据集 
.window(...) //指定窗口分配器类型 
[.trigger(...)] //指定触发器类型(可选) 
[.evictor(...)] //指定evictor或者不指定(可选) 
[.allowedLateness(...)] //指定是否延迟处理数据(可选) 
[.sideOutputLateData(...)] //指定Output Lag(可选) 
.reduce/aggregate/fold/apply() //指定窗口计算函数
[.getSideOutput(...)] //根据Tag输出数据(可选)

Windows Assigner:指定窗口的类型,定义如何将数据流分配到一个或多个窗口;
Windows Trigger:指定窗口触发的时机,定义窗口满足什么样的条件触发计算;
Evictor:用于数据剔除;
allowedLateness:标记是否处理迟到数据,当迟到数据到达窗口中是否触发计算;
Output Tag:标记输出标签,然后在通过 getSideOutput 将窗口中的数据根据标签输出;
Windows Funciton:定义窗口上数据处理的逻辑,例如对数据进行 sum 操作。

window聚合函数

如果定义了 Window Assigner 之后,下一步就可以定义窗口内数据的计算逻辑,这也就 是 Window Function 的定义。Flink 中提供了四种类型的 Window Function,分别为 ReduceFunction、AggregateFunction 以及 ProcessWindowFunction,(sum 和 max)等。

前三种类型的 Window Fucntion 按照计算原理的不同可以分为两大类:

  • 一类是增量聚合函数:对应有 ReduceFunction、AggregateFunction
  • 另一类是全量窗口函数,对应有 ProcessWindowFunction(还有 WindowFunction)

增量聚合函数计算性能较高,占用存储空间少,主要因为基于中间状态的计算结果,窗口中只维护中间结果状态值,不需要缓存原始数据。而全量窗口函数使用的代价相对较高, 性能比较弱,主要因为此时算子需要对所有属于该窗口的接入数据进行缓存,然后等到窗口 触发的时候,对所有的原始数据进行汇总计算。

1) ReduceFunction
ReduceFunction定义了对输入的两个相同类型的数据元素按照指定的计算方法进行聚合的逻辑,然后输出类型相同的一个结果元素。

//每隔5秒统计每个基站的日志数量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.window(TumblingEventTimeWindows.of(Time.seconds(5))) 
	.reduce((v1,v2)=>(v1._1,v1._2+v2._2))

2) AggregateFunction
和 ReduceFunction 相似,AggregateFunction也是基于中间状态计算结果的增量计算函数,但AggregateFunction在窗口计算上更加通用。AggregateFunction接口相对ReduceFunction更加灵活,实现复杂度也相对较高。AggregateFunction接口中定义了三个需要复写的方法,其中add()定义数据的添加逻辑,getResult 定义了根据 accumulator计算结果的逻辑,merge方法定义合并accumulator的逻辑

//每隔3秒计算最近5秒内,每个基站的日志数量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.timeWindow(Time.seconds(5),Time.seconds(3)) 
	.aggregate(new AggregateFunction[(String,Int),(String,Long),(String,Long)] {
		override def createAccumulator() = ("",0) 
		override def add(in: (String, Int), acc: (String, Long)) = { 
			(in._1,acc._2+in._2) 
			}
		override def getResult(acc: (String, Long)) = acc 
		override def merge(acc: (String, Long), acc1: (String, Long)) = {
			 (acc._1,acc1._2+acc._2) 
			 } 
		})

3) ProcessWindowFunction
前面提到的ReduceFunction 和 AggregateFunction 都是基于中间状态实现增量计算的窗口函数,虽然已经满足绝大多数场景,但在某些情况下,统计更复杂的指标可能需要依赖于窗口中所有的数据元素,或需要操作窗口中的状态数据和窗口元数据,这时就需要使用到 ProcessWindowsFunction,ProcessWindowsFunction能够更加灵活地支持基于窗口全部数据元素的结果计算,例如对整个窗口数据排序取TopN,这样的需要就必须使用 ProcessWindowFunction。

//每隔5秒统计每个基站的日志数量 
data.map(stationLog=>((stationLog.sid,1))) 
	.keyBy(_._1) 
	.timeWindow(Time.seconds(5)) 
	.process(new ProcessWindowFunction[(String,Int),(String,Int),String,TimeWindow] {
		 override def process(key: String, context: Context, elements: Iterable[(String, Int)], out: Collector[(String, Int)]): Unit = { 
		 	println("-------") out.collect((key,elements.size)) 
		 	} 
		})
		.print()

Time和watermark

Time

Flink根据时间产生的位置不同,将时间区分为三种时间语义,分别为事件生成时间(Event Time)、事件接入时 间(Ingestion Time)和事件处理时间(Processing Time)。

  • Event Time:事件产生的时间,它通常由事件中的时间戳描述。
  • Ingestion Time:事件进入 Flink 的时间。
  • Processing Time:事件被处理时当前系统的时间。

关于三者之间的区别,我们来看下面这张图:
在这里插入图片描述
在 Flink 中默认情况下使用是 Process Time 时间语义,如果用户选择使用 Event Time 或 者 Ingestion Time 语 义 , 则 需 要 在 创 建 的 StreamExecutionEnvironment 中 调 用 setStreamTimeCharacteristic() 方 法 设 定 系 统 的 时 间 概 念 , 如 下 代 码 使 用 TimeCharacteristic.EventTime 作为系统的时间语义:

 //设置使用EventTime 
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 
 //设置使用IngestionTime 
 streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime) 

在日常应用中,Event Time是应用的最多的。
注意:但是上面的代码还没有指定具体的时间到底是什么值,所以后面还有代码需要设置!

(2020.6.17更新)

Processing Time和Event Time的区别

在这里插入图片描述

WaterMark

在使用 EventTime 处理 Stream 数据的时候会遇到数据乱序的问题,流处理从 Event(事件)产生,流经 Source,再到 Operator,这中间需要一定的时间。虽然大部分情况下,传 输到 Operator 的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因而导致乱序的产生,特别是使用 Kafka 的时候,多个分区之间的数据无法保证有序。因此, 在进行 Window 计算的时候,不能无限期地等下去,必须要有个机制来保证在特定的时间后, 必须触发Window 进行计算,这个特别的机制就是 Watermark(水位线)。Watermark 是用于 处理乱序事件的。

一句话概括:WaterMark就是一种延迟触发窗口机制
Watermark 的使用存在三种情况:

  1. 本来有序的 Stream 中的 Watermark 如果数据元素的事件时间是有序的,Watermark 时间戳会随着数据元素的事件时间按顺 序生成,此时水位线的变化和事件时间保持一直(因为既然是有序的时间,就不需要设置延迟时间,watermark=window_end_time),也就是理想状态下的水位线。当 Watermark 时间大于 Windows 结束时间就会触发对 Windows 的数据计算,以此类推, 下一个 Window 也是一样。
    在这里插入图片描述
  2. 乱序事件中的 Watermark 现实情况下数据元素往往并不是按照其产生顺序接入到 Flink 系统中进行处理,而频繁 出现乱序或迟到的情况,这种情况就需要使用 Watermarks 来应对。比如下图,设置延迟时间 t 为 2
    在这里插入图片描述
    3.并行数据流中的 Watermark 在多并行度的情况下,Watermark 会有一个对齐机制,这个对齐机制会取所有 Channel 中最小的 Watermark。

关于WaterMark的详细分析,在并行度paralism=1的情况下可查看大神李麦迪早前的文章,这里说明一下结论:

  • Flink如何处理乱序?
    watermark+window机制。window中可以对input进行按照Event Time排序,使得完全按照Event Time发生的顺序去处理数据,以达到处理乱序数据的目的。

  • Flink何时触发window?
    对于late element太多的数据而言:1.Event Time < watermark时间
    对于out-of-order以及正常的数据而言:1. watermark时间 >= window_end_time; 2.在 [window_start_time,window_end_time)中有数据存在

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章