window基本知识点
Window窗口
streaming 流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集, 而 window 是一种切割无限数据为有限块进行处理的手段。
无限切割为有限流的方式,将流数据分发到有限大小的桶中进行分析。
窗口的类型
CountWindow
按照指定的数据条数生成一个 Window, 与时间无关。
TimeWindow
按照时间生成 Window。
对于 TimeWindow,可以根据(窗口实现原理的不同)数据分发到桶的规则不同分成三类:滚动窗口( Tumbling Window)、滑动窗口( Sliding Window) 和会话窗口( Session Window)
滚动窗口(Tumbling Windows)
数据只属于一个窗口。
滑动窗口(Sliding Windows)
数据不只是属于一个窗口,属于多个窗口。
会话窗口(Session Windows)
Window API
窗口分配器 – window() 方法,需要注意的是,KeyedStream里面才有window()方法,因此window()方法的使用,必须在keyby之后,才能使用。
需要注意的是Time这个包,要引用对,streaming api 下面的。注意里面有windowing。
import org.apache.flink.streaming.api.windowing.time.Time
TimeWindow
滚动窗口(Tumbling Windows)
val resultStream = dataStream
.map(data =>(data.id,data.tempreture)) //转换成二元组,求id分组的最小温度,显然时间戳不需要了。
.keyBy(_._1) // 以id进行分组
.timeWindow(Time.seconds(15)) //15s的窗口大小,不滑动,也就是滚动。
//后面可以继续跟着聚合的方法。
滑动窗口(Sliding Windows)
val resultStream = dataStream
.map(data =>(data.id,data.tempreture)) //转换成二元组,求id分组的最小温度,显然时间戳不需要了。
.keyBy(_._1) // 以id进行分组
.timeWindow(Time.seconds(15), Time.seconds(5)) //15s的窗口大小,滚动5s。
//后面可以继续跟着聚合的方法。
会话窗口(Session Windows)
会话窗口的话,只能这么写。没有timewindow(),这种简写的方式。
CountWindow
滚动窗口(Tumbling Windows)
val resultStream = dataStream
.map(data =>(data.id,data.tempreture)) //转换成二元组,求id分组的最小温度,显然时间戳不需要了。
.keyBy(_._1) // 以id进行分组
.countWindow(10)
//后面可以继续跟着聚合的方法。
滑动窗口(Sliding Windows)
val resultStream = dataStream
.map(data =>(data.id,data.tempreture)) //转换成二元组,求id分组的最小温度,显然时间戳不需要了。
.keyBy(_._1) // 以id进行分组
.countWindow(10,2)
//后面可以继续跟着聚合的方法。
window function 窗口函数
window function 定义了要对窗口中收集的数据做的计算操作,(计算过程中,用哪种方式进行计算,来进行分类)主要可以分为两类
增量聚合函数
每条数据到来就进行计算, 保持一个简单的状态。典型的增量聚合函数有 ReduceFunction, AggregateFunction。
在当前的有界流中,还是流处理,也就是来一个聚合一次。等到窗口关闭的时候,直接拿出累加好的结果。
也就是在等数据收集的时候,已经在做叠加,已经计算完了。只是等到时间到的时候,把结果输出一下而已。
全窗口函数
先把窗口所有数据收集起来, 等到计算的时候会遍历所有数据。 ProcessWindowFunction 就是一个全窗口函数。
这种就是攒成一批,有界流当做一批,攒齐了。然后做一个批处理。
等到时间到了,最后要输出结果的时候,去遍历所有的数据,依次叠加,然后算完,输出结果。显然这种效率是比增量聚合的效率是低的。
使用场景
排序、当前所有数据的中位数、百分之多少的数据。 这些场景的话,如果来一个数,重新整合一遍,显然是效率太低了,所以直接全量聚合。
全量窗口函数比增量窗口函数获取的信息要多,能够在上下文拿到当前的窗口信息。拿到当前运行时上下文的状态的信息。所以能做的操作更多一些。比较更底层,更加的灵活。
其它可选API
trigger() —— 触发器
定义 window 什么时候关闭, 触发计算并输出结果。
evitor() —— 移除器
定义移除某些数据的逻辑
allowedLateness() ——分布式架构,有可能出现数据的乱序,窗口要关闭的时候,数据还没有到,那么窗口等一会再关闭,解决数据的迟到问题。允许处理迟到的数据。
在开启allowedLateness()之后,可以允许处理迟到数据,当前分布式架构有可能出现数据的乱序,本来应该来的数据,之后才来。窗口关闭的时候,数据还没到,可以等一会,allowedLateness()里面可以传一个时间。
sideOutputLateData() —— 上面allowedLateness()之后,发现还有没到的,放在侧输出流。将迟到的数据放入侧输出流。
sideOutputLateData() 和 allowedLateness()是配合起来用的。允许处理迟到数据的话,那么这个迟到数据要等多久?到底等多久才能保证结果的正确呢?这个就没准了。如果一直等待的话,当前窗口里面的数据,状态,上下文等。一直保持在这里不能释放。这都会占据内存。allowedLateness()给的时间太长的话,那么内存的压力比较大,不能无限等下去。所以,allowedLateness() 一般给一个大概差不多的时间,还有少量的这个时间段没来的,那么把它扔到侧输出流中。
getSideOutput() —— 基于最后结果的datastream,获取侧输出流
官网不推荐直接用WindowAll,因为直接用的话,flink底层会把所有的数据都发送到同一个分区里面,然后再做窗口的分桶。相当于并行度变成了1,整个的任务没有并行了,性能是不好的。所以,非必要情况,还是要先进行keyby的。
解释
1、进行keyby操作,形成多个分区。
2、进行开窗操作,具体开什么样的窗。
3、在窗口里面,对数据的加工计算。
上面3项呢,是必选的。
还有一些可以选的呢。就是定义 分布式架构下数据的迟到如何处理,以及如何触发窗口关闭等,都是关于在窗口中计算时的数据问题。
时间语义与 Wartermark
Flink 中的时间语义
Event Time: 是事件创建的时间。它通常由事件中的时间戳描述, 例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。
Ingestion Time: 是数据进入 Flink 的时间。
Processing Time: 是每一个执行基于时间操作的算子的本地系统时间, 与机器相关, 默认的时间属性就是 Processing Time。
一个例子—— 电影《星球大战》:
上面的例子
事件时间,显然就是,了解事件情节的先后顺序,而不是拍摄的早晚,也就是1234567,这样的。
处理时间,就是按拍摄的先后顺序。也就是按照的拍摄时间的先后。
EventTime 的引入
在 Flink 的流式处理中, 绝大部分的业务都会使用 eventTime, 一般只在 eventTime 无法使用时, 才会被迫使用 ProcessingTime 或者 IngestionTime。比较靠前的数据没有到,那么我们就等待数据的到来,但是因为无法判断数据等待多久,窗口何时关闭无法确定,时效性很差了就。那么就不要想着用processing time,用事件时间,至于现在系统本身是多少时间,并不重要。只考虑数据本身的时间戳进展到什么程度了。
如果要使用 EventTime,那么需要引入 EventTime 的时间属性。引入方式也是特别的简单,就是紧跟环境的后面,进行设置。
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env 创建的每一个stream 追加时间特征,时间特性。
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
需要注意的是,使用事件时间语义的时候,因为我们是要从数据的字段中提取一个时间类型的值,来作为时间特性。所以在获取输入流的时候,就要进行设置。
时间戳的提取和后面的watermark是结合在一起的,所以在后面的时候进行写相关的代码。
Watermark
数据中的时间戳进展到什么程度了。以事件时间,从数据里面提取的发生的时间戳,作为当前时间推进的考量。那么现在的时间就是用时间戳来控制,就和系统时间没有关系了。
理想的情况就是,数据来的顺序和它产生的数据一个一个的来。现实的情况,并不是这个样子,因为分布式系统的存在,在不停的传输的过程中,先发生的数据到后面处理的时候,未必排在前面。
由于乱序的影响,本该小的数据的时间戳没有来,那么相对较大的数据时间戳来了,那么时间也就推进了,如果数据的时间戳到了窗口关闭的时间,那么该关闭?显然是不行的,关了的话,数据就是丢了。因为后面本应该在窗口的数据还没有进入窗口中。
如果说让着整个窗口多等一段时间的话,一方面窗口的状态不能释放,另一方面,实际的场景,就是数据相差几毫秒,几十毫秒,也就是很短的乱序程度,如果说这时候窗口多等一分钟话,这种情况就是没有必要的。正确的做法就是将整体的时间进行推移,就可以把乱序数据处理了。之前是来了多大的时间戳的数据,我们就以为时间就进行到哪里了。实施整体的时间延迟机制,让时间滞后一点。比如说5秒的时间戳来了,我们延迟2秒的话,我们就认为-- 窗口的时间 --现在只进行到3秒钟了,那么现在认为3秒钟之前的数据该到的都到齐了。
那么引入的这个延迟机制,就是watermark。也就是不用数据中的时间戳来触发窗口关闭,而是用watermark来触发窗口操作。
1、Watermark 是一种衡量 Event Time 进展的机制。
2、Watermark 是用于处理乱序事件的, 而正确的处理乱序事件, 通常用watermark 机制结合 window 来实现。
3、数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了, 因此, window 的执行也是由 Watermark 触发的。
4、Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime小于 maxEventTime - t 的所有数据都已经到达, 如果有窗口的停止时间等于maxEventTime – t, 那么这个窗口被触发执行。
watermark的特点
watermark传递原理
选最小的表明,最小的数之前的数都到齐了。
watermark的引入
assignTimestampsAndWatermarks – 水印
关于 assignTimestampsAndWatermarks 这个方法。
点进去,第一眼看,啥也没有。
仔细看上面的英文注释。
按住Ctrl,点进去。
这个对象实现的类,刚好是 assignTimestampsAndWatermarks 中所需要的类。
而且构造器是 public的,也就是公共的,所以是可以直接new 这个类的。
输入一个乱序程度,也就是我们的watermark的值。
sideOutputLateData – 侧输出流
需要注意的是,里面的T,是数据的类型,不要忘了外边的[ ],括号。
点进去这个类。
注意类型
看上面的解释。
可以看出,是伴生类对象,所以不用new,直接这个类,就相当于是创建了对象。
代码体现
// watermark触发窗口之前,小于窗口时间的数据,都是增量聚合,不输出。
// 到了watermark的时候,就输出了一次。
//延迟时间范围内的数据,每来一次就输出一次。窗口真正关闭的时候,是watermark的基础上加上延迟时间。
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.api.scala.createTypeInformation
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
case class SensorReading2 (id :String , timestamp: Long ,tempreture:Double)
object WindowTest {
def main(args: Array[String]): Unit = {
// 定义流式处理环境。
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val parameter = ParameterTool.fromArgs(args)
val host = parameter.get("host")
val port = parameter.getInt("port")
//创建一个socket文本流
val inputDataSet = env.socketTextStream(host,port)
val dataStream = inputDataSet.map(data => {
val strings = data.split(",")
SensorReading2(strings(0), strings(1).toLong, strings(2).toDouble) //方法的返回值
})
// 在数据源有了具体的类型之后, 然后设置时间戳和水印。
.assignTimestampsAndWatermarks( //在数据源后设置水印,这个方法翻译过来: 分配时间戳和水印
//当水印需要落后于流元素中迄今为止看到的最大时间戳固定的时间量时,
//并且该量是预先知道的,请使用 BoundedOutOfOrdernessTimestampExtractor。
new BoundedOutOfOrdernessTimestampExtractor[SensorReading2](Time.seconds(3)) {
override def extractTimestamp(element: SensorReading2): Long = element.timestamp * 1000L
}
) //快速得到几乎一个正确的结果,能抓住大多数的数据,近似正确。
// new OutputTag[T] (STRING) ,其中 T是数据的类型, STRING 是一个标记,也就是标记这个流,对这个流进行一个标记。
val latetag = OutputTag[(String,Double)] ("late")
val resultStream = dataStream
.map(data =>(data.id,data.tempreture)) //转换成二元组,求id分组的最小温度,显然时间戳不需要了。
.keyBy(_._1) // 以id进行分组
.timeWindow(Time.seconds(15))
.allowedLateness(Time.seconds(15)) // 要求更精确,那么允许处理迟到数据,时间不能太久,否则的话,资源不释放。
// new OutputTag[T] (STRING) ,其中 T是数据的类型, STRING 是一个标记,也就是标记这个流,对这个流进行一个标记。
.sideOutputLateData(latetag) //侧输出流,允许迟到数据之后,还没到那么就到侧输出流中。
.reduce((cur,newstate)=>(cur._1,cur._2.min(newstate._2)))
resultStream.print("result")
resultStream.getSideOutput(latetag).print("late")
env.execute()
// watermark触发窗口之前,小于窗口时间的数据,都是增量聚合,不输出。
// 到了watermark的时候,就输出了一次。
//延迟时间范围内的数据,每来一次就输出一次。窗口真正关闭的时候,是watermark的基础上加上延迟时间。
}
}
窗口起点的确定
来到了一个方法,就是得到窗口欧的起始偏移量。offset默认是0,timestamp是当前数据的时间戳,windowsize是窗口的大小。
最后得到的结果就是窗口的整数倍。