转换算子是无法访问事件的时间戳信息和水位线信息的。而这在一些应用场景下,极为重要。例如MapFunction这样的map转换算子就无法访问时间戳或者当前事件的事件时间。
基于此,DataStream API提供了一系列的Low-Level转换算子。可以访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。Process Function用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window函数和转换算子无法实现)。例如,Flink SQL就是使用Process Function实现的。
Flink提供了8个Process Function:
KeyedProcessFunction用来操作KeyedStream。KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素。所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val dataDS: DataStream[String] = env.readTextFile("input/data1.txt")
val mapDS: DataStream[(String, Long, Int)] = dataDS.map(data => {
val datas = data.split(",")
(datas(0), datas(1).toLong, datas(2).toInt)
})
mapDS.keyBy(0)
.process(
new KeyedProcessFunction[Tuple,(String, Long, Int), String]{
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, (String, Long, Int), String]#OnTimerContext, out: Collector[String]): Unit = super.onTimer(timestamp, ctx, out)
override def processElement(value: (String, Long, Int), ctx: KeyedProcessFunction[Tuple, (String, Long, Int), String]#Context, out: Collector[String]): Unit = {
println(ctx.getCurrentKey)
out.collect(value.toString())
}
}
).print("keyprocess:")
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
当定时器timer触发时,会执行回调函数onTimer()。注意定时器timer只能在keyed streams上面使用。
需求:监控水位传感器的水位值,如果水位值在五分钟之内(processing time)连续上升,则报警。
// 自定义数据处理函数
class MyKeyedProcessFunction extends KeyedProcessFunction[String, WaterSensor, String] {
private var currentHeight = 0L
private var alarmTimer = 0L
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, WaterSensor, String]#OnTimerContext, out: Collector[String]): Unit = {
out.collect("水位传感器" + ctx.getCurrentKey + "在 " +new Timestamp(timestamp)+"已经连续5s水位上涨。。。")
}
// 分区中每来一条数据就会调用processElement方法
override def processElement(value: WaterSensor, ctx: KeyedProcessFunction[String, WaterSensor, String]#Context, out: Collector[String]): Unit = {
// 判断当前水位值和之前记录的水位值的变化
if (value.vc > currentHeight) {
// 当水位值上升的时候,开始计算时间,如果到达5s,
// 中间水位没有下降,那么定时器应该执行
if ( alarmTimer == 0 ) {
alarmTimer = value.ts * 1000 + 5000
ctx.timerService().registerEventTimeTimer(alarmTimer)
}
} else {
// 水位下降的场合
// 删除定时器处理
ctx.timerService().deleteEventTimeTimer(alarmTimer)
alarmTimer = 0L
}
// 保存当前水位值
currentHeight = value.vc
}
}
大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流。除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同。process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。
一个side output可以定义为OutputTag[X]对象,X是输出流的数据类型。process function可以通过Context对象发送一个事件到一个或者多个side outputs。
小练习:采集监控传感器水位值,将水位值高于5cm的值输出到side output。
class MyHighLevelAlarm extends ProcessFunction[(String, Long, Int),(String, Long, Int)]{
private lazy val highLevelVal = new OutputTag[Long]("highLevel");
//private lazy val highLevelVal: OutputTag[Int] = new OutputTag[Int]("highLevel")
override def processElement(value: (String, Long, Int), ctx: ProcessFunction[(String, Long, Int), (String, Long, Int)]#Context, out: Collector[(String, Long, Int)]): Unit = {
if ( value._3 > 5 ) {
ctx.output(highLevelVal, value._2)
}
out.collect(value)
}
}
val value: DataStream[(String, Long, Int)] = eventDS.keyBy(0).process(new MyHighLevelAlarm)
value.print("keyprocess:")
value.getSideOutput(new OutputTag[Long]("highLevel")).print("high")
对于两条输入流,DataStream API提供了CoProcessFunction这样的low-level操作。CoProcessFunction提供了操作每一个输入流的方法: processElement1()和processElement2()。
类似于ProcessFunction,这两种方法都通过Context对象来调用。这个Context对象可以访问事件数据,定时器时间戳,TimerService,以及side outputs。CoProcessFunction也提供了onTimer()回调函数。
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val dataDS: DataStream[String] = env.readTextFile("input/data.txt")
val splitDS: SplitStream[WaterSensor] = dataDS.map(
s => {
val datas = s.split(",")
WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
}
).split(
sensor => {
if (sensor.vc >= 40) {
Seq("alarm")
} else if (sensor.vc >= 30) {
Seq("warn")
} else {
Seq("normal")
}
}
)
val alarmDS: DataStream[WaterSensor] = splitDS.select("alarm")
val warnDS: DataStream[WaterSensor] = splitDS.select("warn")
val normalDS: DataStream[WaterSensor] = splitDS.select("normal")
val connectDS: ConnectedStreams[WaterSensor, WaterSensor] = alarmDS.connect(warnDS)
connectDS.process(new CoProcessFunction[WaterSensor, WaterSensor, WaterSensor] {
override def processElement1(value: WaterSensor, ctx: CoProcessFunction[WaterSensor, WaterSensor, WaterSensor]#Context, out: Collector[WaterSensor]): Unit = {
out.collect(value)
}
override def processElement2(value: WaterSensor, ctx: CoProcessFunction[WaterSensor, WaterSensor, WaterSensor]#Context, out: Collector[WaterSensor]): Unit = {
out.collect(value)
}
})
env.execute()