Flink 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 val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment 2 env.setParallelism(1) 3 4 val dataDS: DataStream[String] = env.readTextFile("input/data1.txt") 5 6 val mapDS: DataStream[(String, Long, Int)] = dataDS.map(data => { 7 val datas = data.split(",") 8 (datas(0), datas(1).toLong, datas(2).toInt) 9 }) 10 11 mapDS.keyBy(0) 12 .process( 13 new KeyedProcessFunction[Tuple,(String, Long, Int), String]{ 14 override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, (String, Long, Int), String]#OnTimerContext, out: Collector[String]): Unit = super.onTimer(timestamp, ctx, out) 15 16 override def processElement(value: (String, Long, Int), ctx: KeyedProcessFunction[Tuple, (String, Long, Int), String]#Context, out: Collector[String]): Unit = { 17 println(ctx.getCurrentKey) 18 out.collect(value.toString()) 19 } 20 } 21 ).print("keyprocess:")
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上面使用。
需求:监控水位传感器的水位值,如果水位值在五秒之内(processing time)连续上升,则报警。
1 def main(args: Array[String]): Unit = { 2 val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment; 3 env.setParallelism(1) 4 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 5 6 val dataDS: DataStream[String] = env.socketTextStream("linux1", 9999) 7 val waterDS = dataDS.map( 8 data=>{ 9 val datas = data.split(",") 10 WaterSensor(datas(0), datas(1).toLong, datas(2).toInt) 11 } 12 ) 13 14 // 设定数据的事件时间已经定义Watermark 15 val markDS: DataStream[WaterSensor] = waterDS.assignAscendingTimestamps(_.ts * 1000) 16 17 //对分区后的数据进行处理 18 markDS.keyBy(_.id) 19 .process( new KeyedProcessFunction[String, WaterSensor, String] { 20 21 private var lastWaterVal : Int = 0 22 private var alarmTS : Long = 0L 23 24 // TODO 当连续五秒水位上升时,需要发出警报 25 override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, WaterSensor, String]#OnTimerContext, out: Collector[String]): Unit = { 26 out.collect("当前传感器连续五秒水位上升") 27 alarmTS = 0L 28 } 29 30 // 处理每一条传感器数据 31 override def processElement(value: WaterSensor, ctx: KeyedProcessFunction[String, WaterSensor, String]#Context, out: Collector[String]): Unit = { 32 33 // 当前传感器的值是否大于上一次的值 34 if ( value.vc > lastWaterVal && alarmTS == 0 ) { 35 // 如果中间过程不被打断,那么需要5s后报警 36 alarmTS = value.ts * 1000 + 5000 37 ctx.timerService().registerEventTimeTimer(alarmTS) 38 } else if ( value.vc < lastWaterVal || lastWaterVal == 0 ) { 39 // TODO 如果水位下降 40 ctx.timerService().deleteEventTimeTimer(alarmTS) 41 alarmTS = 0L 42 } 43 44 // 设定lastWaterVal的值等于当前传感器的值 45 lastWaterVal = value.vc 46 } 47 } ).print("alarm>>>>") 48 markDS.print("mark>>>>>>>") 49 env.execute() 50 51 }
def main(args: Array[String]): Unit = { val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment; env.setParallelism(1) env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) val dataDS: DataStream[String] = env.socketTextStream("linux1", 9999) val waterDS = dataDS.map( data=>{ val datas = data.split(",") WaterSensor(datas(0), datas(1).toLong, datas(2).toInt) } ) // 设定数据的事件时间已经定义Watermark val markDS: DataStream[WaterSensor] = waterDS.assignAscendingTimestamps(_.ts * 1000) // TODO 对分区后的数据进行处理 markDS.keyBy(_.id) .process( new KeyedProcessFunction[String, WaterSensor, String] { //private var lastWaterVal : Int = 0 //private var alarmTS : Long = 0L private var lastWaterVal:ValueState[Int] = getRuntimeContext.getState( new ValueStateDescriptor[Int]("lastWaterVal", classOf[Int]) ) private var alarmTS:ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("alarmTS", classOf[Long]) ) // private var lastWaterVal:ValueState[Int] = _ // private var alarmTS:ValueState[Long] = _ // 初始化,一般完成状态对象的初始化 override def open(parameters: Configuration): Unit = { // lastWaterVal = // getRuntimeContext.getState( // new ValueStateDescriptor[Int]("lastWaterVal", classOf[Int]) // ) // alarmTS = // getRuntimeContext.getState( // new ValueStateDescriptor[Long]("alarmTS", classOf[Long]) // ) } // TODO 当连续五秒水位上升时,需要发出警报 override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, WaterSensor, String]#OnTimerContext, out: Collector[String]): Unit = { out.collect("当前传感器连续五秒水位上升") // 清空预警状态 alarmTS.clear() } // 处理每一条传感器数据 override def processElement(value: WaterSensor, ctx: KeyedProcessFunction[String, WaterSensor, String]#Context, out: Collector[String]): Unit = { val lastVal = lastWaterVal.value() val alarm = alarmTS.value() // TODO 当前传感器的值是否大于上一次的值 if ( value.vc > lastVal && alarm == 0 ) { // 如果中间过程不被打断,那么需要5s后报警 val newTS = value.ts * 1000 + 5000 // 更新预警状态 alarmTS.update(newTS) ctx.timerService().registerEventTimeTimer(newTS) } else if ( value.vc < lastVal || lastVal == 0 ) { // TODO 如果水位下降 ctx.timerService().deleteEventTimeTimer(alarm) alarmTS.clear() } // 设定lastWaterVal的值等于当前传感器的值 lastWaterVal.update(value.vc) } } ).print("alarm>>>>") markDS.print("mark>>>>>>>") env.execute() }
3、侧输出流(SideOutput)
- 大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流。
- 除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同。
- process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。
- 一个side output可以定义为OutputTag[X]对象,X是输出流的数据类型。
- process function可以通过Context对象发送一个事件到一个或者多个side outputs。
When using side outputs, you first need to define an OutputTag
that will be used to identify a side output stream:
val outputTag = OutputTag[String]("side-output")
Emitting data to a side output is possible from the following functions:
- ProcessFunction
- KeyedProcessFunction
- CoProcessFunction
- KeyedCoProcessFunction
- ProcessWindowFunction
- ProcessAllWindowFunction
You can use the Context
parameter, which is exposed to users in the above functions, to emit data to a side output identified by an OutputTag
. Here is an example of emitting side output data from a ProcessFunction
:
val input: DataStream[Int] = ... val outputTag = OutputTag[String]("side-output") val mainDataStream = input .process(new ProcessFunction[Int, Int] { override def processElement( value: Int, ctx: ProcessFunction[Int, Int]#Context, out: Collector[Int]): Unit = { // emit data to regular output out.collect(value) // emit data to side output ctx.output(outputTag, "sideout-" + String.valueOf(value)) } })
For retrieving the side output stream you use getSideOutput(OutputTag)
on the result of the DataStream
operation. This will give you a DataStream
that is typed to the result of the side output stream:
val outputTag = OutputTag[String]("side-output") val mainDataStream = ... val sideOutputStream: DataStream[String] = mainDataStream.getSideOutput(outputTag)