Flink-5
ProcessFunction
是Flink中用户可以操作的最底层的API,
ProcessFunction
的运行机制可以近似理解为
FlatMapFunction
,同样是对流中的每个元素进行处理,每个输入的元素可以对应零到多个的输出;
在ProcessFunction
中,可以获取流的以下基本构件:
- events:流中的元素
- state:状态(仅用于
keyed stream
) - timers:定时器,支持事件时间和处理时间,仅用于
keyed stream
Flink提供了8个Process Function:
- ProcessFunction:dataStream
- KeyedProcessFunction:用于KeyedStream,keyBy之后的流处理
- CoProcessFunction:用于connect连接的流
- ProcessJoinFunction:用于join流操作
- BroadcastProcessFunction:用于广播
- KeyedBroadcastProcessFunction:keyBy之后的广播
- ProcessWindowFunction:窗口增量聚合
- ProcessAllWindowFunction:全窗口聚合
在数据流中,通过调用process()
函数,传入对应的ProcessFunction
即可
下面通过案例演示用法,案例需求:对传感器传回的温度进行监控,如果温度10S内持续上升,则触发报警。每当接收到一条记录后,输出当前数据记录相对于上一条记录的温度变化值
案例代码:
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.{KeyedProcessFunction, ProcessFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
import transform.SensorReading
object ProcessFunctionDemo1 {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val source: DataStream[String] = env.socketTextStream("192.168.226.10", 7777)
source
.process(ParseSensorReading) // 前几篇中的flatMap和map函数,使用process实现
.keyBy("id")
.process(TemperatureMonitor) // keyBy之后,需要使用KeyedProcessFunction
.print("test")
env.execute("blogTest")
}
private object ParseSensorReading extends ProcessFunction[String, SensorReading] {
override def processElement(value: String, ctx: ProcessFunction[String, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
if (value.matches("^sensor_\\d,\\d{13},\\d{2}(\\.\\d+)?$")) {
val fields: Array[String] = value.split(",")
// out.collect的内容会被作为一条记录返回
out.collect(SensorReading(fields(0), fields(1).toLong, fields(2).toDouble))
}
}
}
private object TemperatureMonitor extends KeyedProcessFunction[Tuple, SensorReading, Double] {
// 记录回调函数设定的时间
private var tsTimer: ValueState[Long] = _
// 记录上一条记录的温度
private var lastTemperature: ValueState[Double] = _
// 元素处理前,对上述两个状态初始化
override def open(parameters: Configuration): Unit = {
tsTimer = getRuntimeContext.getState(new ValueStateDescriptor[Long]("registeredTimer", classOf[Long]))
lastTemperature = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTemperature", classOf[Double]))
}
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, Double]#Context, out: Collector[Double]): Unit = {
if (lastTemperature.value() != null) { // 如果不是第一条传入的数据
// 当前未注册回调函数,且当前温度大于上一条数据的温度
if (tsTimer.value() == null && value.temperature > lastTemperature.value()) {
// 记录当前处理时间,如果不记录该时间,后面两行分别获取当前处理时间可能会导致记录时间和实际注册时间不符
val registerTimer: Long = ctx.timerService().currentProcessingTime() + (10 * 1000)
// 注册回调函数
ctx.timerService().registerProcessingTimeTimer(registerTimer)
// 更新触发时间
tsTimer.update(registerTimer)
// 当前已经注册了回调函数,且当前温度不大于上一条数据的温度,则取消回调函数
} else if (tsTimer.value() != null && value.temperature <= lastTemperature.value())
ctx.timerService().deleteProcessingTimeTimer(tsTimer.value())
}
// 如果不是第一条传入的数据,则输出当前温度与上一条数据温度的差值
if (lastTemperature.value() != null) out.collect(value.temperature - lastTemperature.value())
// 记录当前温度
lastTemperature.update(value.temperature)
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, Double]#OnTimerContext, out: Collector[Double]): Unit = {
println("警告:" + ctx.getCurrentKey.toString + "温度持续10S上升!")
// 触发后清空计数,后续的触发才能再记录温度
tsTimer.clear()
}
}
}
可以看到定时器的定义和工作机制和前面的Evictor很相似,只是没有在两个函数中区分ProcessTime和EventTime,而是统一在onTimer
函数中添加回调函数的逻辑
使用ProcessFunction
对数据流分流:
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
import transform.SensorReading
object ProcessFunctionDemo2 {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val source: DataStream[String] = env.socketTextStream("192.168.226.10", 7777)
val res: DataStream[SensorReading] = source.filter(_.matches("^sensor_\\d,\\d{13},\\d{2}(\\.\\d+)?$"))
.map(line => {
val fields: Array[String] = line.split(",")
SensorReading(fields(0), fields(1).toLong, fields(2).toDouble)
})
.keyBy("id")
.process(SplitProcess)
res.print("normal")
// 获取侧输出流,java创建OutputTag时后面的大括号不能省略
res.getSideOutput(new OutputTag[SensorReading]("high")).print("high")
res.getSideOutput(new OutputTag[SensorReading]("low")).print("low")
env.execute("blogTest")
}
private object SplitProcess extends KeyedProcessFunction[Tuple, SensorReading, SensorReading] {
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
// 温度大于37度的放入侧输出流
if (value.temperature > 37) ctx.output(new OutputTag[SensorReading]("high"), value)
// 温度小于35度的放入另一个侧输出流
else if (value.temperature < 35) ctx.output(new OutputTag[SensorReading]("low"), value)
// 其余正常输出
else out.collect(value)
}
}
}
使用process输出到侧输出流可以代替split-select用来分流。