0 ProcessFunction API
我们之前学习的转换算子是无法访问事件的时间戳信息和水位线信息的。而这在一些应用场景下,极为重要。例如MapFunction这样的map转换算子就无法访问时间戳或者当前事件的事件时间。
基于此,DataStream API提供了一系列的Low-Level转换算子。可以访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。Process Function用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window函数和转换算子无法实现)。例如,Flink SQL就是使用Process Function实现的。
Flink提供了8个Process Function:
• ProcessFunction
• KeyedProcessFunction
• CoProcessFunction
• ProcessJoinFunction
• BroadcastProcessFunction
• KeyedBroadcastProcessFunction
• ProcessWindowFunction
• ProcessAllWindowFunction
1 KeyedProcessFunction
KeyedProcessFunction用来操作KeyedStream。KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素。所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:
• processElement(v: IN, ctx: Context, out: Collector[OUT]), 流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出。Context可以访问元素的时间戳,元素的key,以及TimerService时间服务。Context还可以将结果输出到别的流(side outputs)。
• onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一个回调函数。当之前注册的定时器触发时调用。参数timestamp为定时器所设定的触发的时间戳。Collector为输出结果的集合。OnTimerContext和processElement的Context参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
1.1 TimerService 和 定时器(Timers)
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
• currentProcessingTime(): Long 返回当前处理时间
• currentWatermark(): Long 返回当前watermark的时间戳
• registerProcessingTimeTimer(timestamp: Long): Unit 会注册当前key的processing time的定时器。当processing time到达定时时间时,触发timer。
• registerEventTimeTimer(timestamp: Long): Unit 会注册当前key的event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
• deleteProcessingTimeTimer(timestamp: Long): Unit 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
• deleteEventTimeTimer(timestamp: Long): Unit 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。
当定时器timer触发时,会执行回调函数onTimer()。注意定时器timer只能在keyed streams上面使用。
1.2 案例1
检测数据10秒钟内温度是否持续升高,如果则报警。当前使用场景不适合用窗口的方式进行,不管是是滚动窗口还是滑动窗口都可能存在如下情况无法检测到。如图所示:在窗口1和2中由于都存在下降的数据,因此不会发生报警,而在图中绿色方框内都是上升的数据,应该触发报警。而如果采用窗口的方式,则不会发生报警。有的人说可以通过调小滑动窗口滑动大小的方式检测出这种情况,但实际是不管将滑动窗口的步长调到多小,都无法完全避免上述情况。
因此针对以上情况,可以采用ProcessFunction的方式。Demo如下:
import com.chen.flink.part01.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object ProcessFunctionTest {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//设置并行度为1
env.setParallelism(1)
val dataStream: DataStream[String] = env.socketTextStream("192.168.199.101", 7777)
val mapStream: DataStream[SensorReading] = dataStream.map(
data => {
val strings = data.split(",")
SensorReading(strings(0), strings(1).toLong, strings(2).toDouble)
}
)
val warningStream = mapStream.keyBy("id").process(new TempIncreaseAlertFunction2(10000L))
warningStream.print("warning test")
env.execute()
}
class TempIncreaseAlertFunction2(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String] {
//获取之前状态的温度值和时间戳
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lasttemp", classOf[Double]))
lazy val timestampState: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("timestamp", classOf[Long]))
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
//获取状态值
val lastTemp: Double = lastTempState.value()
val timestamp: Long = timestampState.value()
//更新最新的温度值
lastTempState.update(value.temperature)
//如果温度升高且未注册定时器,则完成定时器的注册
if (value.temperature > lastTemp && timestamp == 0) {
val ts = ctx.timerService().currentProcessingTime() + interval
//注册定时器
ctx.timerService().registerProcessingTimeTimer(ts)
//更新定时器的值
timestampState.update(ts)
} else if (value.temperature < lastTemp) {
//取消定时器注册,并将状态清空
ctx.timerService().deleteProcessingTimeTimer(timestamp)
timestampState.clear()
}
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
//当定时时间达到时,触发定时器响应
out.collect("温度连续" + interval / 1000 + "秒上升")
out.collect("currentProcessingTime: "+ctx.timerService().currentProcessingTime())
//清除状态,使得二次触发定时器
timestampState.clear()
}
}
}
结果如下:
可以看到两次检测时间为10秒,若在则10秒时间内,温度持续升高则会产生报警信息。
1.3 案例2
案例2与案例1大致相同,区别在于案例2采用的是事件时间,而案例1采用的ProgressTime
package com.chen.flink.part02
import com.chen.flink.part01.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
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
import org.apache.flink.util.Collector
object ProcessFunctionDemo {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//设置读取时间为event time
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val dataStream: DataStream[String] = env.socketTextStream("192.168.199.101", 7777)
val mapStream: DataStream[SensorReading] = dataStream.map(
data => {
val strings = data.split(",")
SensorReading(strings(0), strings(1).toLong, strings(2).toDouble)
}
)
//设置watermark延迟时间
val waterStream: DataStream[SensorReading] = mapStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(2)) {
override def extractTimestamp(element: SensorReading): Long = {
element.timestamp
}
})
//如果10秒钟内温度持续上升,则输出告警信息
val warningStream = waterStream.keyBy(0).process(new TempIncreaseAlertFunction(10000L))
warningStream.print("warning")
env.execute()
}
// 自定义 KeyedProcessFunction
class TempIncreaseAlertFunction(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String] {
// 需要跟之前的温度值做对比,所以将上一个温度保存成状态
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
// 为了方便删除定时器,还需要保存定时器的时间戳
lazy val curTimestampState: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("cur-time", classOf[Long]))
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
//获取上一个温度和时间戳的状态值
val lastTemp: Double = lastTempState.value()
val curTimestamp: Long = curTimestampState.value()
//将上一次的温度值更新为最新的温度值
lastTempState.update(value.temperature)
//温度上升且未注册定时器,则注册定时器
if (lastTemp < value.temperature && curTimestamp == 0) {
val curtime: Long = ctx.timerService().currentWatermark() + interval
ctx.timerService().registerEventTimeTimer(curtime)
//更新时间戳
curTimestampState.update(curtime)
} else if (lastTemp > value.temperature) {
//如果温度下降,则删除定时器
ctx.timerService().deleteEventTimeTimer(curTimestamp)
//清空状态
curTimestampState.clear()
}
}
//定时器触发
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
out.collect("温度连续" + interval / 1000 + "秒上升")
out.collect("当前水位线是" + ctx.timerService().currentWatermark())
curTimestampState.clear()
}
}
}
当前的告警信息是检测数据中的事件时间,检测事件时间是否在连续的10秒内温度持续上涨,如果是则生成报警信息。
按上述代码逻辑,在输入第一条数据时就会产生如下所示的告警信息提示:
从上图可以发现不管第一条输入的数据是什么都会有结果产生。这显然是不符合我们要求的检测连续10秒内的温度上升数据并发出警报的要求。原因是,在第一次获取水位线时,默认获取到的是Long.MIN_VALUE,该数值是一个负数,因此使用该数值注册定时器时,会导致定时器即刻被触发,即立马产生输出。验证方法如下:
在代码注册定时器前逻辑中增加如下逻辑:
启动程序,并输出结果:
可以发现触发定时器前的水位线为Long.MIN_VALUE+interval < 0,因此会直接触发定时器执行,导致输入第一条数据时就产生输出。
解决方法:
新增判断,当前水位线大于0时才将事件时间更新为currentWatermark+interval,否则事件时间为interval
运行程序:
此时输入第一条数据时不会再产生告警输出了,且此时的告警时间是根据输入数据的事件时间来判断的。也就是说当水位线watermark >=事件时间-延迟时间2秒=10秒时,才会触发计算。
如下图:当事件时间-延迟时间2秒>=10秒时,即会触发计算
当前水位线是10300毫秒,下次触发计算的水位线时间为>=22000毫秒。如果在这期间发生温度的下降,则不会触发告警,如下图:
而当前输入是在数据15000毫秒时开始上升,此时注册定时器的时间为watermark(13000)+interval(10000)=23000毫秒。若事件时间一直到13000+10000(设定检测时间)+2000(延迟时间)=25000毫秒都保持上升的话,此时会继续输出告警数据。
2 ProcessFunction侧输出流(SideOutput)
大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流。除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同。process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。一个side output可以定义为OutputTag[X]对象,X是输出流的数据类型。process function可以通过Context对象发射一个事件到一个或者多个sideoutputs。
2.1 示例
将温度值低于32F的温度输出到side output
package com.chen.flink.part02
import com.chen.flink.part01.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
/**
* 当前示例用于演示ProcessFunction的侧输出流
*/
object ProcessFunctionDemo03 {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val dataStream: DataStream[String] = env.socketTextStream("192.168.199.101", 7777)
val mapedStream: DataStream[SensorReading] = dataStream.map(
data => {
val strings = data.split(",")
(SensorReading(strings(0), strings(1).toLong, strings(2).toDouble))
}
)
val processStream: DataStream[SensorReading] = mapedStream.process(new LowTempMonitorFunction)
//获取侧输出流,注意和之前定义的侧输出流标签一致
val sideStream = processStream.getSideOutput(new OutputTag[String]("low-temp"))
//分别打印侧输出流和整个流
sideStream.print("sideStream")
processStream.print("allStream")
env.execute()
}
}
class LowTempMonitorFunction extends ProcessFunction[SensorReading,SensorReading]{
//定义侧输出流的标签
lazy val lowTempAlarmOutput: OutputTag[String] = new OutputTag[String]("low-temp")
override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
//如果温度小于32度,则放到侧输出流中
if(value.temperature < 32){
ctx.output(lowTempAlarmOutput,value.id+" 当前温度是:"+value.temperature)
}
//所有数据都放到输出的主流中
out.collect(value)
}
}
如下图所示,当前温度小于32度时则会进入侧输出流,并在测输出流中输出