之前学习的转换算子是无法访问事件的时间戳信息和水位线信息的,基于此,DataStream API 提供了一系列的 Low-Level 转换算子。可以访问时间 戳、watermark 以及注册定时事件。还可以输出特定的一些事件,
Flink SQL 就是使用 Process Function 实 现的。 Flink 提供了 8 个 Process Function:
ProcessFunction
KeyedProcessFunction // keyby之后 的 可以操作keyStream
CoProcessFunction
ProcessJoinFunction
BroadcastProcessFunction
KeyedBroadcastProcessFunction
ProcessWindowFunction // 全窗口函数
ProcessAllWindowFunction
KeyedProcessFunction 用来操作 KeyedStream。KeyedProcessFunction 会处理流 的每一个元素,而 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 对象拥有以下方法:
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)连 续上升,则报警。
object ProcessFunctionTestTwo {
case class SensorReading(id: String, timestamp: Long, temperature: Double)
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.enableCheckpointing(60000)
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE)
env.getCheckpointConfig.setCheckpointTimeout(100000)
env.getCheckpointConfig.setFailOnCheckpointingErrors(false)
// env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(100)
env.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION)
env.setRestartStrategy(RestartStrategies.failureRateRestart(3, org.apache.flink.api.common.time.Time.seconds(300), org.apache.flink.api.common.time.Time.seconds(10)))
// env.setStateBackend( new RocksDBStateBackend("") )
val stream = env.socketTextStream("localhost", 7777)
val dataStream = stream.map(data => {
val dataArray = data.split(",")
SensorReading(dataArray(0).trim, dataArray(1).trim.toLong, dataArray(2).trim.toDouble)
})
// 处理乱序问题
.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading]( Time.seconds(1) ) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000
} )
// 经过keyby后调用process处理时间报警问题
// keyby后则继承KeyedProcessFunction, 没有keyby则继承ProcessFunction以此类推
val processedStream = dataStream.keyBy(_.id)
.process( new TempIncreAlert() )
dataStream.print("input data")
processedStream.print()
env.execute("process function test")
}
}
// 输入是字符串,输出的报警信息也用字符串
class TempIncreAlert() extends KeyedProcessFunction[String, SensorReading, String]{
/**2、因为是连续上升,所以定义一个状态,用来保存上一个数据的温度值 (如果一直大就是连续上升)
* ValueState这是flink定义状态的类型,通过getRuntimeContext运行上下文获取状态句柄
* 里面传的是一个描述器(名字,类型)
*/
lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]) )
// 6、定义一个状态,用来保存定时器的时间戳
lazy val currentTimer: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("currentTimer", classOf[Long]) )
// 1、要实现这个方法 因为所有来的元素都要经过这个方法
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 3、先取出上一个温度值
val preTemp = lastTemp.value()
// 4、更新温度值(更新的值从传入的数据中取)
lastTemp.update( value.temperature )
// 7、获取当前时间戳
val curTimerTs = currentTimer.value()
if( value.temperature < preTemp || preTemp == 0.0 ){
// 8、如果温度下降,或是第一条数据,删除定时器并清空状态
ctx.timerService().deleteProcessingTimeTimer( curTimerTs )
currentTimer.clear() // 情况状态避免撑爆内存
} else if ( value.temperature > preTemp && curTimerTs == 0 ){ // 当前温度大于上一次的
// 5、温度上升且没有设过定时器,则注册定时器
val timerTs = ctx.timerService().currentProcessingTime() + 5000L
ctx.timerService().registerProcessingTimeTimer( timerTs ) // 这里给的timerTs是时间戳
currentTimer.update( timerTs )
}
}
/** 9、上面定义了定时器,那么定时器触发之后需要做什么操作呢,定时器是在回调函数里面调出来
*/
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
// 只要触发就输出报警信息
out.collect( ctx.getCurrentKey + " 温度连续上升" ) // 通过上下文获取key
currentTimer.clear()
}
}
侧输出流
大部分的 DataStream API 的算子的输出是单一输出,也就是某种数据类型的流。 除了 split 算子,可以将一条流分成多条流,这些流的数据类型也都相同。processfunction 的 side outputs 功能可以产生多条流,并且这些流的数据类型可以不一样。 一个 side output 可以定义为 OutputTag[X]对象,X 是输出流的数据类型。process function 可以通过 Context 对象发射一个事件到一个或者多个 side outputs。
object SideOutputTest {
case class SensorReading(id: String, timestamp: Long, temperature: Double)
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val stream = env.socketTextStream("localhost", 7777)
val dataStream = stream.map(data => {
val dataArray = data.split(",")
SensorReading(dataArray(0).trim, dataArray(1).trim.toLong, dataArray(2).trim.toDouble)
})
.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading]( Time.seconds(1) ) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000
} )
// 这里不用keyby了,则后面直接继承ProcessFunction
val processedStream = dataStream
.process( new FreezingAlert() )
// 这里打印出来的是处理之后的主流
processedStream.print("processed data")
// 获取侧输出流,然后打印
processedStream.getSideOutput( new OutputTag[String]("freezing alert") ).print("alert data")
env.execute("side output test")
}
}
// 冰点报警,如果小于32F,输出报警信息到侧输出流(这里还是主输出流)
class FreezingAlert() extends ProcessFunction[SensorReading, SensorReading]{
lazy val alertOutput: OutputTag[String] = new OutputTag[String]( "freezing alert" )
override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
if( value.temperature < 32.0 ){
// 输出到侧输出流
// ctx.output( new OutputTag[String]( "freezing alert" ), "freezing alert for " + value.id )
ctx.output( alertOutput, "freezing alert for " + value.id )
}
out.collect( value ) // 直接输出到主流
}
}