Flink 窗口计算
一、概述
窗⼝计算是流计算的核⼼,窗⼝将流数据切分成有限⼤⼩的“buckets”,我们可以对这个“buckets”中的有限数据做运算。
在Flink中整体将窗⼝计算按分为两⼤类:keyedstream
窗⼝、datastream
窗⼝,以下是代码结构:
keyedStream
stream
.keyBy(…) <--------------- keyed versus non-keyed windows
.window(…) <---------------必须指定: “window assigner”
[.trigger(…)] <--------------- 可选: “trigger” (else default trigger) 决定了窗⼝何时触发计算
[.evictor(…)] <--------------- 可选: “evictor” (else no evictor) 剔除器,剔除窗⼝内的元素
[.allowedLateness(…)] <---------------可选: “lateness” (else zero) 是否允许有迟到
[.sideOutputLateData(…)] <--------------- 可选: “output tag” (else no side output for latedata)
.reduce/aggregate/fold/apply() <--------------- 必须: “Window Function” 对窗⼝的数据做运算
[.getSideOutput(…)] <---------------可选: “output tag” 获取迟到的数据
Non-Keyed Windows
stream
.windowAll(…) <--------------- 必须指定: “window assigner”
☆.Window Lifecycle
当有第⼀个元素落⼊到窗⼝中的时候窗⼝就被 创建
,当时间(⽔位线)越过窗⼝的EndTime
的时候,该窗⼝认定为是就绪状态
,可以应⽤WindowFunction对窗⼝中的元素进⾏运算。当前的时间(⽔位线)越过了窗⼝的EndTime+allowed lateness
时间,该窗⼝会被删除
。
只有time-based windows 才有⽣命周期的概念
,因为Flink还有⼀种类型的窗⼝global window不是基于时间的,因此没有⽣命周期的概念。- 每⼀种窗⼝都有⼀个Trigger和function与之绑定,function的作⽤是⽤于对窗⼝中的内容实现运算。⽽Trigger决定了窗⼝什么时候是就绪的,因为只有就绪的窗⼝才会运⽤function做运算。除了指定以上的策略以外,我们还可以指定 Evictor ,该 Evictor 可以在窗⼝就绪以后且在function运⾏之前或者之后删除窗⼝中的元素。
Keyed vs Non-Keyed Windows
:
Keyed Windows:
在某⼀个时刻,会触发多个window任务,取决于Key的种类。Non-Keyed Windows:
因为没有key概念,所以任意时刻只有⼀个window任务执⾏。
二、Window Assigners
窗口分配器
Window Assigner定义了如何将元素分配给窗⼝,这是通过在 window(...) / windowAll() 指定⼀个Window Assigner实现。
Window Assigner负责将接收的数据分配给1~N窗⼝,Flink中预定义了⼀些Window Assigner分如下:
tumbling windows , sliding windows , session windows 和 global windows
.- ⽤户可以同过实现WindowAssigner类⾃定义窗⼝。除了global windows 以外其它窗⼝都是基于时间TimeWindow.Timebased窗⼝都有 start timestamp (包含)和end timestamp (排除)属性描述⼀个窗⼝的⼤⼩。
①.Tumbling Windows
滚动窗口
package com.baizhi.window
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object Tumbling {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(0)
//设置一个滚动窗口 周期长度为 5秒
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.reduce((y,z)=>(y._1,y._2+z._2))
.print()
env.execute("Tumbling Processing Time Window Word Count")
}
}
②.Sliding Windows
滑动窗口
package com.baizhi.window
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.{
SlidingProcessingTimeWindows, TumblingProcessingTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
object Sliding {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(0)
//设置一个滑动窗口
.window(SlidingProcessingTimeWindows.of(Time.seconds(4),Time.seconds(2)))
//自定义一个聚合规则
.aggregate(new MydefinedSlidingAggregateFunction)
.print()
env.execute("Sliding Processing Time Window Word Count")
}
}
//自定义一个聚合函数
class MydefinedSlidingAggregateFunction extends AggregateFunction[(String,Int),(String,Int),(String,Int)]{
override def createAccumulator(): (String, Int) = {
//创建一个累加器
("",0)
}
override def add(in: (String, Int), acc: (String, Int)): (String, Int) = {
//求和
(in._1,in._2+acc._2)
}
override def getResult(acc: (String, Int)): (String, Int) = {
acc
}
override def merge(acc: (String, Int), acc1: (String, Int)): (String, Int) = {
(acc._1,acc._2+acc1._2)
}
}
③.Session Windows
会话窗⼝分配器按活动会话对元素进⾏分组。与滚动窗⼝和滑动窗⼝相⽐,会话窗⼝不重叠且没有固定的开始和结束时间。相反,当会话窗⼝在⼀定时间段内未接收到元素时(即,发⽣不活动间隙时),它将关闭。
package com.baizhi.window
import java.text.SimpleDateFormat
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.assigners.{
ProcessingTimeSessionWindows, SlidingProcessingTimeWindows, TumblingProcessingTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
object Session {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(_._1)
//设置一个session,间隔时间为5S
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(5)))
//自定义一个聚合规则 ,此算子不支持 位置的聚合
.apply(new MydefinedWindowsFunction)
.print()
env.execute("Session Processing Time Window Word Count")
}
}
//自定义一个聚合函数
class MydefinedWindowsFunction extends WindowFunction[(String,Int),(String,Int),String,TimeWindow]{
override def apply(key: String, window: TimeWindow, input: Iterable[(String, Int)], out: Collector[(String, Int)]): Unit = {
//定义一个时间格式
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
//解析时间格式
val stTime = sdf.format(window.getStart)
val edTime = sdf.format(window.getEnd)
//获取总值
val count = input.map(_._2).sum
//输出
out.collect(s"$key \t $stTime \t $edTime \t",count)
}
}
④.Global Windows
global window不是基于时间的
全局窗⼝分配器将具有相同键的所有元素
分配给同⼀单个全局窗⼝。仅当您指定了触发器时,此窗⼝⽅案才有⽤。否则,将不会执⾏任何计算,因为全局窗⼝没有可以处理聚合元素的⾃然终点。
package com.baizhi.window
import java.text.SimpleDateFormat
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.assigners.{
GlobalWindows, SlidingProcessingTimeWindows, TumblingProcessingTimeWindows, WindowAssigner}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.triggers.CountTrigger
import org.apache.flink.streaming.api.windowing.windows.GlobalWindow
import org.apache.flink.util.Collector
object Global {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(_._1)
//设置一个Global窗口
.window(GlobalWindows.create())
//定义一个触发器 触发数量为3
.trigger(CountTrigger.of(3))
//自定义一个聚合规则
.apply(new MydefinedGlobalFunction)
.print()
env.execute("Global Processing Time Window Word Count")
}
}
//自定义一个聚合函数
class MydefinedGlobalFunction extends WindowFunction[(String,Int),(String,Int),String,GlobalWindow]{
override def apply(key: String, window: GlobalWindow, input: Iterable[(String, Int)], out: Collector[(String, Int)]): Unit = {
out.collect(key,input.map(_._2).sum)
}
}
三、Window Functions
定义窗⼝分配器后,我们需要指定要在每个窗⼝上执⾏的计算。这是Window Function的职责,⼀旦系统确定窗⼝已准备好进⾏处理,就可以处理每个窗⼝的元素。
窗⼝函数可以是ReduceFunction,AggregateFunction,FoldFunction、ProcessWindowFunction或WindowFunction
(古董)之⼀。
- 其中ReduceFunction和AggregateFunction在运⾏效率上⽐ProcessWindowFunction要⾼,因为前俩个⽅法执⾏的是增量计算,只要有数据抵达窗⼝,系统就会调⽤ReduceFunction,AggregateFunction实现增量计算;
- ProcessWindowFunction在窗⼝触发之前会⼀直缓存接收数据,只有当窗⼝就绪的时候才会对窗⼝中的元素做批量计算,但是该⽅法可以获取窗⼝的元数据信息。但是可以通过将ProcessWindowFunction与ReduceFunction,AggregateFunction或FoldFunction结合使⽤来获得窗⼝元素的增量聚合以及ProcessWindowFunction接收的其他窗⼝元数据,从⽽减轻这种情况。
①.ReduceFunction
package com.baizhi.windowfunction
import org.apache.flink.api.common.functions.{
AggregateFunction, ReduceFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.{
SlidingProcessingTimeWindows, TumblingProcessingTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
object ReduceFunction {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//测试使用
println("====STREAM START====")
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(0)
//设置一个滚动窗口
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
//自定义一个聚合规则
.reduce(new UserDefinedReduceFunction)
.print()
env.execute("Sliding Processing Time Window Word Count")
}
}
//自定义一个聚合函数
class UserDefinedReduceFunction extends ReduceFunction[(String,Int)]{
override def reduce(t: (String, Int), t1: (String, Int)): (String, Int) = {
println("----执行一次reduce算子----")
(t._1,t._2+t1._2)
}
}
②.AggregateFunction
package com.baizhi.windowfunction
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object AggregateFunction {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//测试使用
println("====STREAM START====")
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(0)
//设置一个滚动窗口
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.aggregate(new UserDefinedAggregateFunction)
.print()
env.execute("Tumbling Processing Time Window Word Count")
}
}
//自定义一个类
class UserDefinedAggregateFunction extends AggregateFunction[(String,Int),(String,Int),(String,Int)]{
override def createAccumulator(): (String, Int) = {
("",0)
}
override def add(in: (String, Int), acc: (String, Int)): (String, Int) = {
println("合并一次")
(in._1,in._2+acc._2)
}
override def getResult(acc: (String, Int)): (String, Int) = {
//返回
acc
}
override def merge(acc: (String, Int), acc1: (String, Int)): (String, Int) = {
(acc._1,acc._2+acc1._2)
}
}
③.FoldFunction
FoldFunction不可以⽤在Session Window中
package com.baizhi.windowfunction
import org.apache.flink.api.common.functions.{
AggregateFunction, FoldFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object FoldFunction {
def main(args: Array[String]): Unit = {
//获取执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取输入源
val stream: DataStream[String] = env.socketTextStream("hbase",9999)
//测试使用
println("====STREAM START====")
//处理
stream.flatMap(_.split("\\s+"))
.map(x=>(x,1))
.keyBy(0)
//设置一个滚动窗口
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.fold(