Flink的Window操作
Flink认为Batch是Streaming的一个特例,所以Flink底层引擎是一个流式处理引擎,而Window就是Streaming和Batch链接的桥梁。
Window的概念:在流式处理应用中,数据是连续不断的,我们不可能等到所有数据都到了之后再去处理,但是有时根据业务的需求我们需要做一些聚合类的处理。例如:在过去的一分钟内有多少用户点击了我们的网页,在这种情况下,我们必须定义一个窗口,用来收集最近一分钟的数据,并对这个窗口内的数据进行计算。
Window的类型:
时间窗口(time-window):根据时间进行截取,比如1分钟统计一次或者10分钟统计一次
1.tumbling-time-window(滚动时间窗口-无重叠数据):按照时间进行窗口划分,每次窗口的滑动距离等于窗口的长度,数据就不会重复计算。
原理:滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中
使用场景:适合做BI统计等
/**
* Flink默认的时间窗口根据ProcessTime进行窗口的划分,
* 将Flink获取到的数据根据进入的Flink的时间划分不同的窗口中,
* 时间间隔可以通过Time.millseconds(),Time.seconds(),Time.minutes()等其中的一个指定。
*
**/
object StreamingTumblingTimeWindow{
def main(args:Array[String]):Unit = {
//1.创建运行环境
val env:StreamExecutionEnvironment = StreamExceptionEnvironment.getExecutionEnvironment
//2.定义数据源
val textStream = env.socketTextStream("node01",9999)
//3.转换数据格式
val data = textStream.map(line => {
val array = line.split(",")
WordCountCart(array(0).toInt,array(1).toInt)
})
//4.执行统计操作,每个sensorId一个tumling窗口,窗口大小为5S
val keyByData:KeyedStream[WordCountCart,Int] = data.keyBy(line => line.sen)
//无重叠数据,所以只需给一个参数即可
val result = KeyByData.timeWindow(Time.seconds(5)).sum(1)
//5.显示统计结果
result.print()
//6.触发流计算
env.excute()
}
}
case class WordCountCart(sen:Int,carNum:Int)
2.sliding-time-window(滑动时间窗口-有重叠数据):窗口大小固定,可以有重叠。
原理:
(1)滑动窗口分配器将数据分配到固定的长度的窗口中,与滚动窗口类似。
(2)窗口的大小由窗口大小参数来配置
(3)窗口滑动的频率由窗口滑动的参数来配置
(4)如果滑动窗口的大小小于窗口的大小,数据会发生重叠,这种情况下元素会分配到多个窗口中。
适用场景:对最近一个时间段内的统计(求某接口最近5分钟的失败率决定是否报警)
/**
* 滑动窗口和滚动窗口的函数名是完全一致的
* 在传参时需要传入两个参数,一个是window_size,一个是sliding_size
* 时间间隔可以通过Time.millseconds(),Time.seconds(),Time.minutes()等其中一个来指定
**/
Object StreamingTimeSlidingWindow{
def main(args:Array[String]):Unit = {
//1.创建运行环境
val env:StreamExecutionEnviroment = StreamExecutionEnviroment.getExecutionEnvironment
//2.定义数据源
val textStream = env.socketTextStream("node01",9999)
//3.转换数据格式
val data = textStream.map(line => {
val array = line.split(",")
WordCountCart(arr(0).toInt,arr(1).toInt)
})
//4.执行计算操作,每个sensorId一个Sliding窗口,窗口大小为5秒
//也就是说没2S统计一次,在这过去的10S内各个路口通过红绿灯汽车的数量
val keyByData:KeyedStream[WordcountCart,Int] = data.keyBy(line => line.sen)
val result = keyByData.timeWindow(Time.seconds(10),Time.seconds(2)).sum(1)
//5.显示统计结果
result.print()
//6.触发流计算
env.execute
}
}
case class WordCountCart(sen:Int,carNum:Int)
计数窗口(count window):按照指定的数据条数生成一个Window,与时间无关。
1.tumbling-count-window(无重叠数据):按照事件的个数进行统计
/**
* CountWindow根据窗口中相同key元素的数量触发执行,执行时只计算事件个数达到窗口大小的key对应的结果
**/
object StreamingCountTumblingWindow {
def main(args: Array[String]): Unit = {
//1.创建运行环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//2.定义数据流来源
val textStream = env.socketTextStream("node01", 9999)
//3.转换数据格式,text->CountCart
val data = textStream.map(line => {
val array = line.split(",")
CountCart(array(0).toInt, array(1).toInt)
})
//4.执行统计操作
//按照key进行收集,对应的key出现的次数达到5次作为一个结果
val keyByData: KeyedStream[CountCart, Int] = data.keyBy(line => line.sen)
//相同的key出现三次才做一次sum聚合
val result = keyByData.countWindow(3).sum(1)
//5、显示统计结果
result.print()
//6、触发流计算
env.execute()
}
}
case class CountCart(sen:Int, cardNum:Int)
2.sliding-count-window(有重叠数据)
object StreamingCountSlidingWindow {
def main(args: Array[String]): Unit = {
//1.创建运行环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//2.定义数据流来源
val textStream = env.socketTextStream("node01", 9000)
//3.转换数据格式,text->CarWc
val data = textStream.map(line => {
val array = line.split(",")
CountCart(array(0).toInt, array(1).toInt)
})
//4.执行统计操作,每个sensorId一个sliding窗口,窗口大小5条数据,窗口滑动为3条数据
//也就是说,每个路口分别统计,收到关于它的3条消息时统计在最近5条消息中,各自路口通过的汽车数量
val keyByData: KeyedStream[CountCart, Int] = data.keyBy(line => line.sen)
val result = keyByData.countWindow(5, 3).sum(1)
//5、显示统计结果
result.print()
//6、触发流计算
env.execute()
}
}
会话窗口(session window):由一系列事件组合一个指定时间长度的timeout间隙组成,类似于web的session,也就是一段时间没有接收到新数据就会生成新的窗口。
特点:时间不对齐
原理:
(1)session窗口分配器通过session活动来元素进行分组。
(2)session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间情况。
(3)当它在一个固定的时间内没有接收到数据,即非活动间隔产生,那么这个窗口就会关闭。
(4)一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度。
(5)当这个非活跃周期产生,那么当前的session将关闭并且后续的数据会发送到新的session窗口中。
计数窗口(Count-window):根据消息的数量进行截取,按照指定的数据条数生成一个window,与时间无关。
Window的API
窗口的分配器-window()方法
KeyedStream实例.window()来定义一个窗口,然后基于这个窗口做一些聚合或者其他处理。
注意:window()方法必须在KeyBy方法之后使用
Flink提供了更加简单的KeyedStream实例.timeWindow()和KeyedStream。实例.countWindow()方法,用于定义时间窗口和计数窗口。
窗口分配器(window assigner)
1.KeyedStream实例.window()接收输入的参数是一个WindowAssigner。
2.WindowAssigner负责指定窗口的类型,将数据流分发到正确的window中。
3.窗口所表现出的类型特性取决于WindowAssigner的定义。
4.Flink提供了通用的WindowAssigner
滚动窗口:tumbling window
滑动窗口:sliding window
会话窗口:session window
全局窗口:global window
自定义窗口:custom window
window fuction
window function定义了要对窗口中收集的数据做的计算操作,主要分为两类:
1.增量聚合函数(incerment alaggregation functions)
窗口不维护原始数据,只维护中间结果,每次基于中间结果和增量的数据进行聚合。
每条数据到来就进行计算,保持一个简单的状态。
典型的增量聚合函数有ReduceFunction、AggregateFunction。
注意:FoldFunction也是增量聚合函数,但在Flink1.9.0中标记过时,可用AggreateFunction函数代替。
案例:获取一段时间内(Window size)每个红外测温仪(KeyBy)监测到的旅客的平均体温(aggregateFunction)。
import org.apache.flink.api.scala._
env.socketTextStream("node01",8888)
.filter(_.trim.nonEmpty)
.map(line => {
val arr = line.split(",")
val id = arr(0).trim
val temperature = arr(1).trim.Double
val name = arr(2).trim
val timeStamp = arr(3).trim.toLong
val location = arr(4).trim
Raytek(id,temperature,name,timestamp,location)
}).keyBy(0)
.timeWindow(Time.seconds(5))
.aggregate(new AggregateFunction[Raytek,(String,Int,Double),(String,Double)]{
override def createAccumulator():(String,Int,Double)=("",0.0.0)
override def add(value:Raytek,accumulator:(String,Int,Double)):(String,Int,Double) = {
val cnt = accumulator._2 + 1
val temp = accumulator._3 + value.temperature
(value.id,cnt,temp)
}
override def merge(a:(String,Int,Double),b:(String,Int,Double)):(String,Int,Double) = {
val mergeCnt = a._2 + b._2
val mergeTemp = a._3 + b._3
(a._1,mergeCnt,mergeTemp)
}
override def getResult(accumulator:(String,Int,Double)):(String,Double)=(accumulator._1,accumulator._3/accumulator._2)
})
.print()
2.全窗口函数(fulll window functions)
窗口需要维护全部原始数据,窗口触发进行全量聚合。
先把窗口所有数据全部收集起来,等到遍历的时候会遍历所有数据。
可以实现对窗口内的数据进行排序等需求。
ProcessWindowFunction就是一个全窗口函数。
注意:WindowFunction也是全量聚合函数,已被更高级的ProcessWindowFunction逐渐代替。
案例:获取一段时间内(Window Size)每个红外测温仪(KeyBy)监测到的旅客平均体温(ProcessWindowFunction)
env.socketTextStream("node01",8888)
.filter(_.trim.nonEmpty)
.map(line => {
val arr = linr.split(",")
val id = arr(0).trim
val temperature = arr(1).trim.toDouble
val name = arr(2).trim
val timeStamp = arr(3).trim.toLong
val location = arr(4).trim
Raytek(id,temperature,name,timestamp,location)
})
.KeyBy("id")
.timeWindow(Time.seconds(5))
.process[(String,Double)](new ProcessWindowFunction[Raytek,(String,Double),Tuple,TimeWindow]{
override def process(key:Tuple,context:Context,elements:Iterable[Raytek],out:Collector[(String,Double)]):Unit = {
val cnt = 0
val totalTemp = 0.0
elements.foreach(line => {
cnt = cnt + 1
totalTemp = totalTemp + line.temperature
})
out.collect((key.getField(0),TotalTemp/cnt))
}
}).print()
案例:增量聚合(AggrateFunction)+全窗口函数(ProcessWindowFunction),获取一段时间内(Window Size)每个红外测温仪(KeyBy)监测到的旅客的平均体温,并求出窗口信息。
env.socketTextStream("node01",8888)
.filter(_.trim.nonEmpty)
.map(line => {
val arr = linr.split(",")
val id = arr(0).trim
val temperature = arr(1).trim.toDouble
val name = arr(2).trim
val timeStamp = arr(3).trim.toLong
val location = arr(4).trim
Raytek(id,temperature,name,timestamp,location)
})
.KeyBy(0)
.timeWindow(Time.seconds(5))
.aggrate(new AggregateFunction[Raytek,(String,Int,Double),(String,Double)]{
override def createAccumulator() = ("",0,0.0)
override def add(value:Raytek,accumulator:(String,Int,Double)):(String,Int,Double) = {
val cnt = accumulator._2 + 1
val temp = accumulator._3 + value.temperature
(value,id,cnt,temp)
}
override def getResult(accumulator:(String,Int,Double)):(String,Double) = (accumulator._1,accumulator._3/accumulator._2)
},
new ProcessWindowFunction[(String,Double),String,Tuple,TimeWindow]
override def process(key:Tuple,context:Context,elements:Iterator[(String,Double)],out:Colletors[String]):Unit = {
val(id,avgTemp) = elements.itearator.next()
val fmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒")
val date = new Data()
date.setTime(context,window.getStart)
val windowStart = fmt.format(date)
date.setTime(context.window.getEnd)
val windowEnd = fmt.format(date)
val record = f"红外测温仪id->${key.getField(0)},窗口开始时间->$windowStart,窗口结束时间->$windowEnd,旅客的平均体温->$avgTemp%.2f"
out.collect(record);
}
}).print()
Window的apply()方法
apply方法可以进行一系列的自定义处理,通过匿名内部类的方法实现,有复杂的计算时使用
用法:
1.实现WindowFunction类
2.指定该类的泛型【输入数据类型,输出数据类型,KeyBy中使用分组字段的类型,窗口类型】
示例:
使用apply方法来实现单词统计,每一行数据按照空格切分单词。
步骤:
1.获取流处理运行环境。
2.构建socket流数据源,并指定IP地址和端口号
3.对接收到的数据转换成单词元组。
4.使用keyBy进行分流(分组)
5.使用timeWindow指定窗口的长度(每3S计算一次)
6.实现一个WindowFunction匿名内部类
在apply方法中实现聚合计算
使用Collector.collect收集数据
7.打印输出
8.启动执行
9.在Linux中,使用nc -lk 端口号监听端口,并发送单词
object WindowApply {
def main(args:Array[String]):Unit = {
//1.获取流处理运行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//2.构建socket流数据源,并指定IP地址和端口号
val socketDataStream = env.socketTextStream("node01",9999)
//3.对接收到的数据转换成单词元组
val wordcountDataStream:DataStream[(String,Int)] = socketDataStream.flatMap{
text => text.split(",").map(_ -> 1)
}
//4.使用keyBy进行分流(分组)
val groupedDataStream = wordCountDataStream.KeyBy(_._1)
//5.使用timewindow指定窗口的长度(每3S计算一次)
val windowDataStream = groupDataStream.timeWindow(Time.seconds(3))
//6.实现一个WindowFunction匿名内部类
val resultDataStream:DataStream[(String,Int)] = windowDataStream.apply(new WindowFunction[(String,Int),(String,Int),(String,Int),String,TimeWindow]{
// 在apply方法中实现聚合计算
override def apply(key:String,window:TimeWindow,Input:Iterable[(String,Int)],out:Collector[(String,Int)]):Unit = {
val resultWordCount:(String,Int) = input.reduce{
(wc1,wc2) => (wc1._1,wc1._2+wc2._2)
}
//使用Collector.collect收集数据
out.collect(resultWordCount)
}
})
//7.打印输出
resultDataStream.print()
//8.启动执行
env.execute("App")
}
}
触发器(.trigger())
1.定义window什么时候关闭,出发计算并输出结果
2.每一个window分会有一个默认的Trigger(触发器),如果默认的触发器不能满足需要,可以自定义触发器
3.常见的trigger
EventTimeTrigger:基于EventTime Window的默认触发器
ProcessTimeTrigger:基于ProcessingTime Window的默认触发器
CountTrigger:基于Count Window的默认触发器
PurgingTrigger:内部使用,用于清楚窗口内容
NeverTrigger:永不触发的触发器