window api基于keyedStream。
window是桶。
1. window操作两个主要步骤:
窗口分配器(.window),窗口函数(reduce,aggregate,apply,process)
2. window类型
通过窗口分配器来决定,时间窗口和计数窗口
按照窗口起止时间(个数)的定义,可以有滚动窗口、滑动窗口、会话窗口
滑动窗口中,每条数据可以属于多个窗口,属于size/slide个窗口
会话窗口,窗口长度不固定,需要指定间隔时间
3. 窗口函数
窗口函数是基于当前窗口内的数据的,是有界数据集的计算,通常只在窗口关闭时输出一次
增量聚合函数:ReduceFunction, AggregateFunction,流式处理过程,来一条处理一次
全窗口函数;WindowFunction,ProcessWindowFunction,类似于批处理过程,类似于批处理,所有数据存起来,触发计算再同意计算。
4. 程序默认的时间语义,是 Processing Time
---01---
滚动窗口假如我设置一小时的滚动窗口。
小时的起始点如何定义的呢?默认起始点是整小时的。偏移量是起始点的偏移量。
窗口的默认就是整点的时间点开始的,一般用来处理时区的offset。要是不设置你默认的话就是早上8点到第二天8点的。
太阳从东方升起来所以我们用的是时区是
---02-03---
事件时间。
需要提取时间戳,这个是提取的是升序的时间戳,就是5秒的来了我就关闭了。
实际情况是分布式的,网络时间是有延迟的,如何提取乱序的时间戳呢?
、
---
乱序时间是怎么确定的呢?
延时时间2S则5秒来了,水位线时间是3秒,就是当前的事件时间戳减去延时时间就是当前时间。上面的例子可以设置延迟1秒。
在事件时间的语义下,Watermark就是事件时间。
---04---
我们现在是事件时间了。
延迟发车是怎么回事,就是假如5这个数据来了,但是我并不是马上关闭窗口的,而是延时1秒,此时实践时间是4,就是6这个数据来了,我才会关闭窗口。延时3秒,就是假如是7就是7-3为现在到4秒了。
延时1秒,6秒的数据来了,我5秒的发车了,一切都是按照时间戳的。
直观看的是最大的乱序时间。
1. Watermark就是事件时间,代表当前时间的进展
2. Watermark主要用来处理乱序数据,一般就是直接定义一个延迟时间,延迟触发窗口操作
这里的延迟,指的是当前收到的数据内的时间戳
3. Watermark延迟时间的设置,一般要根据数据的乱序情况来定,通常设置成最大乱序程度
如果按照最大乱序程度定,那么就能保证所有窗口的数据都是正确的
要权衡正确性和实时性的话,可以不按最大乱序程度,而是给一个相对较小的watermark延迟
watermark延迟时间,完全是程序自己定义的,可以拍脑袋给一个数
最好的处理方式,是先了解数据的分布情况(抽样、或者根据经验、机器学习算法),可以指定一个合理的延迟,比较小,还能处理绝大多数乱序的情况
4. 关窗操作,必须是时间进展到窗口关闭时间,事件时间语义下就是watermark达到窗口关闭时间
当前Ts最大时间戳-延迟时间 = watermark,如果现在的watermark大于等于窗口结束时间,就关闭窗口
5. watermark代表的含义是,之后就不会再来时间戳比watermark里面的数值小的数据了
如果有不同的上游分区,当前任务会对它们创建各自的分区watermark,当前任务的事件时间就是最小的那个
6. 处理乱序数据,Flink有三重保证
watermark可以设置延迟时间
window的allowedLateness方法,可以设置窗口允许处理迟到数据的时间
window的sideOutputLateData方法,可以将迟到的数据写入侧输出流
窗口有两个重要操作:触发计算,清空状态(关闭窗口)
waterMark可以认为是插入在数据流里面的一条特殊的是数据,一条一条来。
watermark必须是单调递增的,总是和时间戳是相关的。
waterMark就是一条特殊的数据记录,代表之前的数据都到齐了。
下游的任务waterMark应该广播出去。
应该以最小的watermark来判断,代表之前的数据都到齐了。
---05---
其中三角形是代表的是插入的warterMark。因为延迟3秒所以在8秒的时间戳之后。
延时3秒就是-3s,8s相当于5s,8-3=5就相当于5s,关闭5秒的窗口。
waterMark按照当前得最大的时间戳-延迟时间。
9-4=5才关闭
waterMark就是最大的时间戳-延迟时间去生成的。
1.升序的数据,升序数据是不用定义waterMark的。
2.乱序的数据,这里定义了延时时间和提取什么时间戳。
完整的代码:
---
waterMark是广播出去的。
---06---
生成的waterMark就是当前的最大的时间戳减去延时的时间。
再来一次:
小时间戳来了还是按照大时间戳算的。
关于watermark的设置的问题:
代码:
正常的窗口是触发计算输出结果然后关闭。
设置了allowedLateness之后,窗口再保持一分钟,先不要关闭,只是触发计算,等到waterMark涨到一分钟后再关闭。先输出一个统计结果但是不关闭。以后来一条更新一次来一条更新一次输出结果,不是往桶里面扔了。
窗口是左闭右开的。
即使这样了,还有乱序数据呢?我还是不想丢呢?侧输出流,注意侧输出流是不能叠加更新了,因为窗口是真的关闭了。
看下这个名字就是sideOutPutlateData再取出来,然后sink出去:
---07---
侧输出流相当于下一班车的时间。
---08---
无
---09---
如果是升序的不设置延迟时间也是可以的:
1是配置时间戳字段,2是给一个延迟时间。完全是升序的话可以不设置延迟也是可以的。
点进去assign这个方法,可以传入的参数。
可以传两种参数的:
这个是我们自己之前一直用的,是周期的:
第一个是周期的第二个是间断的。
// 自定义一个周期性生成watermark的Assigner
class MyWMAssigner(lateness: Long) extends AssignerWithPeriodicWatermarks[SensorReading]{
// 需要两个关键参数,延迟时间,和当前所有数据中最大的时间戳
// val lateness: Long = 1000L
var maxTs: Long = Long.MinValue + lateness
// 这个我隔一段时间要调用getWaterMark方法的 插入到DataStream中去
override def getCurrentWatermark: Watermark =
new Watermark(maxTs - lateness)
// 来一个数据我就调用这个方法
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
maxTs = maxTs.max(element.timestamp * 1000L)
element.timestamp * 1000L
}
}
看下这个就是隔一段时间自动生成watermark插入进去,看下那个减法满足最大时间戳-当前时间延迟
---
根据数据触发的,来一次触发一次,只是和当前的数据有关的。
// 自定义一个断点式生成watermark的Assigner
class MyWMAssigner2 extends AssignerWithPunctuatedWatermarks[SensorReading]{
val lateness: Long = 1000L
override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
if( lastElement.id == "sensor_1" ){
new Watermark(extractedTimestamp - lateness)
} else
null
}
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long =
element.timestamp * 1000L
}
大部分是周期的。数据稀疏适合2 数据密集适合1
默认的时间周期是200ms,自己配置:
一般是往小调。
---10---
DataStream keyedStream window之后 都可以调用
可以访问时间戳信息和水位线信息,其他是不能访问的。
processWindow是一个全窗口函数。
我们看下reduceFunction,两个参数,之前的结果和现在的数据。
1. 普通的transform算子,只能获取到当前的数据,或者加上聚合状态
如果是RichFunction,可以有生命周期方法,还可以获取运行时上下文,进行状态编程
但是它们都不能获取时间戳和watermark相关的信息
2. Process Function是唯一可以获取到时间相关信息的API
RichFunction能做的ProcessFunction都能做,三实际上是特殊的richFunction。
另外,可以获取到timestamp和watermark
可以注册定时器,指定某个时间点发生的操作
还可以输出侧输出流
看下继承的关系
---11---
可以获取当前运行时上下文。需要获得当前的时间戳的信息的话就要用到和水位线的信息。
可以注册定时器。
可以分流的操作。
这个每个窗口都是不是连续上升的,但是虚线是连续上升的。
滚动和滑动都不靠谱的。
滚动窗口可以完成吗?---不可以
滑动可以吗?---不可以
全窗口函数:https://blog.csdn.net/weixin_46266718/article/details/109521248
这里可以定义闹钟的。
------------------------------
keyBy之后可以传processFunction也可以传KeyedProcessFunction,因为KeyedStream继承了DataStream
------------------------------
注意这个注册的是注册的10s后的定时器。
不是来一个就注册一个定时器,如果比之前的温度高并且没有定时器才会注册定时器的。
package day4
import com.atguigu.apitest.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.{Tuple, Tuple1}
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
/**
* Copyright (c) 2018-2028 尚硅谷 All Rights Reserved
*
* Project: FlinkTutorial
* Package: day4
* Version: 1.0
*
* Created by wushengran on 2020/4/20 15:40
*/
object ProcessFunctionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("192.168.244.133", 7777)
val dataStream: DataStream[SensorReading] = inputStream
.map( data => {
val dataArray = data.split(",")
SensorReading( dataArray(0), dataArray(1).toLong, dataArray(2).toDouble )
} )
// 检测每一个传感器温度是否连续上升,在10秒之内
val warningStream: DataStream[String] = dataStream
.keyBy("id")
.process( new TempIncreWarning(10000L) )// 注意这里是process方法
warningStream.print()
env.execute("process function job")
}
}
// 自定义 KeyedProcessFunction k i o keyBy("id")的时候必须要元组的
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String]{
// 由于需要跟之前的温度值做对比,所以将上一个温度保存成状态 在类开始就创建可能还没有上下文呢
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
// 为了方便删除定时器,还需要保存定时器的时间戳
lazy val curTimerTsState: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("cur-timer-ts", classOf[Long]) )
// 每来一条数据就会调用这个方法
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 首先取出状态
val lastTemp = lastTempState.value()
val curTimerTs = curTimerTsState.value()
// 将上次温度值的状态更新为当前数据的温度值
lastTempState.update(value.temperature)
// 判断当前温度值,如果比之前温度高,并且没有定时器的话,注册10秒后的定时器,默认值是0,就是没有注册定时器
if( value.temperature > lastTemp && curTimerTs == 0 ){
val ts = ctx.timerService().currentProcessingTime() + interval
ctx.timerService().registerProcessingTimeTimer(ts)
curTimerTsState.update(ts)
}
// 如果温度下降,删除定时器
else if( value.temperature < lastTemp ){
ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
// 清空状态
curTimerTsState.clear()
}
}
// 定时器触发,说明10秒内没有来下降的温度值,报警
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
val key = ctx.getCurrentKey.asInstanceOf[Tuple1[String]].f0
out.collect( "温度值连续" + interval/1000 + "秒上升" )
curTimerTsState.clear()
}
}
注册定义一个闹钟是10s之后去触发的。
如果有10s下降的就删除定时器。
只有当前的数据肯定是搞不定的,还要知道上一个数据。
所以还涉及到状态编程的。
为了方便删除定时器,还要保留定时器设置时候的时间戳。
这个默认值是0。
最后注意一个细节:为什么是lazy呢?
注意下这个:开始只是定义,等到调用才去创建,你那么着急创建的话可能还没有运行时上下文呢。
来一个数据,注册一个10s之后的定时器,如果温度是一直上升的,那么我就什么也不干,等着定时器触发。
假如中间来了下降的数据。
---12---
开始做10s温度上升的测试:
传感器10s温度上升用窗口做是不合适的。
package day4
import com.atguigu.apitest.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.{Tuple, Tuple1}
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
/**
* Copyright (c) 2018-2028 尚硅谷 All Rights Reserved
*
* Project: FlinkTutorial
* Package: day4
* Version: 1.0
*
* Created by wushengran on 2020/4/20 15:40
*/
object ProcessFunctionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("hadoop102", 7777)
val dataStream: DataStream[SensorReading] = inputStream
.map( data => {
val dataArray = data.split(",")
SensorReading( dataArray(0), dataArray(1).toLong, dataArray(2).toDouble )
} )
// 检测每一个传感器温度是否连续上升,在10秒之内
val warningStream: DataStream[String] = dataStream
.keyBy("id")
.process( new TempIncreWarning(10000L) )
warningStream.print()
env.execute("process function job")
}
}
// 自定义 KeyedProcessFunction k i o keyBy("id")的时候必须要元组的
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String]{
// 由于需要跟之前的温度值做对比,所以将上一个温度保存成状态
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
// 为了方便删除定时器,还需要保存定时器的时间戳
lazy val curTimerTsState: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("cur-timer-ts", classOf[Long]) )
// 每来一条数据就会调用这个方法
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 首先取出状态
val lastTemp = lastTempState.value()
val curTimerTs = curTimerTsState.value()
// 将上次温度值的状态更新为当前数据的温度值
lastTempState.update(value.temperature)
// 判断当前温度值,如果比之前温度高,并且没有定时器的话,注册10秒后的定时器
if( value.temperature > lastTemp && curTimerTs == 0 ){
val ts = ctx.timerService().currentProcessingTime() + interval
ctx.timerService().registerProcessingTimeTimer(ts)
curTimerTsState.update(ts)
}
// 如果温度下降,删除定时器
else if( value.temperature < lastTemp ){
ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
// 清空状态
curTimerTsState.clear()
}
}
// 定时器触发,说明10秒内没有来下降的温度值,报警
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
val key = ctx.getCurrentKey.asInstanceOf[Tuple1[String]].f0
out.collect( "温度值连续" + interval/1000 + "秒上升" )
curTimerTsState.clear()
}
}
思路就是定义一个闹钟,10s触发,来一个定义一个。
有下降就删掉。
---12---
复习:
略。
---13---