Flink提供的转换算子是无法访问事件的时间戳信息和水位线信息的。而这在一些应用场景下,极为重要。例如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参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
2. 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上面使用。
下面举个例子说明KeyedProcessFunction如何操作KeyedStream。
需求:监控温度传感器的温度值,如果温度值在一秒钟之内(processing time)连续上升,则报警。
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.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("hadoop", 7777)
val dataStream = inputStream
.map(data => {
val dataArray = data.split(",")
// println(dataArray(2).toDouble.getClass)
SensorReading4(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
})
// 检测每个传感器温度是否连续上升,在10秒之内
val warningStream = dataStream
.keyBy("id")
.process(new TempIncreWarning(10000L))
warningStream.print()
env.execute("XXXXXX")
}
}
// 自定义KeyedProcessFunction 泛型 key的类型, 输入类型 输出类型
class TempIncreWarning(interal: Long) extends KeyedProcessFunction[Tuple, SensorReading4, 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: SensorReading4, context: KeyedProcessFunction[Tuple, SensorReading4, String]#Context, collector: Collector[String]): Unit = {
// 首先取出状态
val lastTemp = lastTempState.value()
val curTimerTs = curTimerTsState.value()
// 将上次温度值的状态更新为当前数据温度值
lastTempState.update(value.temperature)
// 判断当前温度值,如果比之前温度高,并且没有定时器的话,注册10秒后的定时器
if (value.temperature > lastTemp && curTimerTs == 0) {
val ts = context.timerService().currentProcessingTime() + interal
context.timerService().registerProcessingTimeTimer(ts)
curTimerTsState.update(ts)
}
// 如果温度下降就删除定时器
else if (value.temperature < lastTemp) {
context.timerService().deleteProcessingTimeTimer(curTimerTs)
// 清空状态
curTimerTsState.clear()
}
}
// 10秒内温度没有下降,定时器到达触发的时间
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading4, String]#OnTimerContext, out: Collector[String]): Unit = {
//定时器触发
out.collect("温度值连续" + interal / 1000 + "秒上升")
// 清空状态
curTimerTsState.clear()
}
}
case class SensorReading4(id: String, timestamp: Long, temperature: Double)
3. 侧输出流(SideOutput)
大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流。除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同。process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。一个side output可以定义为OutputTag[X]对象,X是输出流的数据类型。process function可以通过Context对象发射一个事件到一个或者多个side outputs。
下面是一个示例程序:
import org.apache.flink.streaming.api.functions.ProcessFunction
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 inputStream = env.socketTextStream("hadoop", 7777)
val dataStream = inputStream.map {
data =>
val dataArray = data.split(",")
SensorReading5(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
}
// 用ProcessFunction的侧输出流实现分流
val hightempStream = dataStream.process(new SplitTempProcessor(30.0))
val lowTempStream = hightempStream.getSideOutput(new OutputTag[(String, Double, Long)]("low-temp"))
// 打印输出
hightempStream.print("high")
lowTempStream.print("low")
env.execute("XXXX")
}
}
// 自定义ProcessFunction,用于区分高低温的数据
class SplitTempProcessor(threshold: Double) extends ProcessFunction[SensorReading5, SensorReading5] {
override def processElement(value: SensorReading5, context: ProcessFunction[SensorReading5, SensorReading5]#Context, out: Collector[SensorReading5]): Unit = {
// 判断当前数据的温度值,如果大于阈值,输出到主流,否则输出到低温流
if (value.temperature > threshold) {
out.collect(value)
} else {
context.output(new OutputTag[(String, Double, Long)]("low-temp"), (value.id, value.temperature, value.timestamp))
}
}
}
case class SensorReading5(id: String, timestamp: Long, temperature: Double)
4. CoProcessFunction
对于两条输入流,DataStream API提供了CoProcessFunction这样的low-level操作。CoProcessFunction提供了操作每一个输入流的方法: processElement1()和processElement2()。
类似于ProcessFunction,这两种方法都通过Context对象来调用。这个Context对象可以访问事件数据,定时器时间戳,TimerService,以及side outputs。CoProcessFunction也提供了onTimer()回调函数。