Flink中的转换算子是无法访问事件的时间戳信息和水位线信息,但是在一些应用场景下,这些信息却十分重要。基于此,DataStream API提供了一系列的Low-Level转换算子。可以访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。Process Function用来构建时间驱动的应用以及实现用户自定义的业务逻辑。
Flink 提供了8个Process Function
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参数一样,提供了上下文的一些信息,例如定时器 触发的时间信息(事件时间或者处理时间)。
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
Tips1: 当定时器 timer 触发时,会执行回调函数 onTimer()。注意定时器 timer 只能在 keyed streams 上面使用。
Tips2: registerEventTimeTimer要和deleteEventTimeTimer一一对应,不能和processingTimeTimer的操作混用。
示例:监控温度传感器的温度值,如果温度值在一秒钟之内(processing time)连 续上升,则报警,代码如下:
package com.hjt.yxh.hw.processfunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.util.Collector
case class SensorReading(id:String, timestamp: Long, temperature: Double)
object KeyedProcessFunctionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream = env.socketTextStream("192.168.0.52",7777)
//需求,如果同一个传感器的温度连续10秒上升,需要发出预警信息
val sensorStream = dataStream
.filter(_.nonEmpty)
.map(data=>{
val array = data.split(",")
SensorReading(array(0),array(1).toLong,array(2).toDouble)
})
.keyBy(data=>{
data.id
})
.process(new WarnProcessFunction(10000L))
.print()
env.execute("KeyedProcessFunction job test")
}
}
class WarnProcessFunction(interval:Long) extends KeyedProcessFunction[String,SensorReading,String]{
//保存上一个温度的状态
lazy val lastTempState:ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTempState",classOf[Double]))
//保存定时器的时间戳状态
lazy val timerTsState:ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("timerTsState",classOf[Long]))
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
out.collect("传感器:"+ctx.getCurrentKey + "的温度连续" + interval/1000 + "秒连续上升")
timerTsState.clear()
}
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
println("ID" + value.id + "溫度" + value.temperature + "時間"+value.temperature)
//先取出状态
val lastTemp = lastTempState.value()
val timerTs = timerTsState.value()
//如果温度上升,且没有定时器
if(value.temperature>lastTemp && timerTs == 0){
val ts = ctx.timerService().currentProcessingTime() + interval
ctx.timerService().registerProcessingTimeTimer(ts)
timerTsState.update(ts)
}
//如果温度下降,删除定时器
else if (value.temperature<lastTemp && timerTs >0){
ctx.timerService().deleteProcessingTimeTimer(timerTs)
timerTsState.clear()
}
//更新温度值
lastTempState.update(value.temperature)
}
}
我们之前在使用DataStream API的算子输出时,使用split将一条流分成多条流,这些流的数据类型也都相同。但是在使用这一API时,提示该API将会被移除,所以我们可以使用ProcessFunction来实现这一目标。
package com.hjt.yxh.hw.processfunction
import com.hjt.yxh.hw.apitest.SensorReading
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object SideOutputTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val datastream = env.socketTextStream("192.168.0.52",7777)
.filter(_.nonEmpty)
.map(data=>{
val array = data.split(",")
SensorReading(array(0),array(1).toLong,array(2).toDouble)
})
val highTempStream = datastream
.process( new SplitTempProcessor(30.0,"Low"))
highTempStream.print("high")
val lowTempStream = highTempStream.getSideOutput(new OutputTag[SensorReading]("Low"))
lowTempStream.print("low")
env.execute("side output test")
}
}
class SplitTempProcessor(threshold: Double,sideOutputTag: String) extends ProcessFunction[SensorReading,SensorReading]{
override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
if (value.temperature > threshold) {
//如果当前温度值大于30,那么输出到主流
out.collect(value)
}
else{
//温度低于30度,输出到侧输出流
ctx.output[SensorReading](new OutputTag[SensorReading](sideOutputTag),value)
}
}
}