我们之前学习的转换算子是无法访问事件的时间戳信息和水位线信息的。而这在一些应用场景下,极为重要。例如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。
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 参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
TimerService
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)连续上升,则报警。
object ProcessFunctionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 读取数据
val inputStream = env.socketTextStream("localhost", 7777)
// 定义样例类,温度传感器
case class SensorReading( id: String, timestamp: Long, temperature: Double )
// 先转换成样例类类型(简单转换操作)
val dataStream = inputStream
.map( data => {
val arr = data.split(",")
SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
} )
val warningStream = dataStream
.keyBy(_.id)
.process( new TempIncreWarning(10000L) )
warningStream.print()
env.execute("process function test")
}
}
/* 实现自定义的KeyedProcessFunction
KeyedProcessFunction[String, SensorReading, String]
String:keyBy时key的数据类型
SensorReading:输入的数据类型
String:输出的数据类型
*/
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[String, SensorReading, String]{
// 定义状态:保存上一个温度值进行比较,保存注册定时器的时间戳用于删除
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last-temp", classOf[Double]))
lazy val timerTsState: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("timer-ts", classOf[Long]))
/*
value:输入的数据
ctx:上下文,用于访问时间戳、watermark 以及注册定时事件等
out:由于返回值为Unit,所以需要out(Collector)来输出
*/
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 先取出状态
val lastTemp = lastTempState.value()
val timerTs = timerTsState.value()
// 更新温度值
lastTempState.update(value.temperature)
// 当前温度值和上次温度进行比较
// timerTs == 0 用于判断是否有定时器,第一条数据肯定没有定时器,当温度下降的时候定时器清空也没有定时器
if( value.temperature > lastTemp && timerTs == 0 ){
// 如果温度上升,且没有定时器,那么注册当前时间10s之后的定时器
val ts = ctx.timerService().currentProcessingTime() + interval
ctx.timerService().registerProcessingTimeTimer(ts)
timerTsState.update(ts)
} else if( value.temperature < lastTemp ){
// 如果温度下降,那么删除定时器
ctx.timerService().deleteProcessingTimeTimer(timerTs)
timerTsState.clear()
}
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
out.collect("传感器" + ctx.getCurrentKey + "的温度连续" + interval/1000 + "秒上升")
timerTsState.clear()
}
}
2、 侧输出流(SideOutput)
大部分的DataStream API 的算子的输出是单一输出,也就是某种数据类型的流。除了split 算子,可以将一条流分成多条流,这些流的数据类型也都相同。processfunction 的side outputs 功能可以产生多条流,并且这些流的数据类型可以不一样。一个side output 可以定义为OutputTag[X]对象,X 是输出流的数据类型。processfunction 可以通过Context 对象发射一个事件到一个或者多个side outputs。
举例说明:
需求:如果当前温度值大于30,那么输出到主流(high);如果不超过30度,那么输出到侧输出流(low)。
object SideOutputTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 读取数据
val inputStream = env.socketTextStream("localhost", 7777)
// 定义样例类,温度传感器
case class SensorReading( id: String, timestamp: Long, temperature: Double )
// 先转换成样例类类型(简单转换操作)
val dataStream = inputStream
.map( data => {
val arr = data.split(",")
SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
} )
val highTempStream = dataStream
.process( new SplitTempProcessor(30.0) )
highTempStream.print("high")
highTempStream.getSideOutput(new OutputTag[(String, Long, Double)]("low")).print("low")
env.execute("side output test")
}
}
// 实现自定义ProcessFunction,进行分流
class SplitTempProcessor(threshold: Double) extends ProcessFunction[SensorReading, SensorReading]{
override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
if( value.temperature > threshold ){
// 如果当前温度值大于30,那么输出到主流,主流输出value,类型为SensorReading
out.collect(value)
} else {
// 如果不超过30度,那么输出到侧输出流,侧流输出元组(String, Long, Double)
ctx.output(new OutputTag[(String, Long, Double)]("low"), (value.id, value.timestamp, value.temperature))
}
}
}
3、 CoProcessFunction
对于两条输入流,DataStream API 提供了CoProcessFunction 这样的low-level操作CoProcessFunction 提供了操作每一个输入流的方法: processElement1()和processElement2()。
类似于ProcessFunction,这两种方法都通过Context 对象来调用。这个Context对象可以访问事件数据,定时器时间戳,TimerService,以及side outputs。CoProcessFunction 也提供了onTimer()回调函数。