原文链接:https://www.toutiao.com/i6860721527952769539/
本文主要从以下几个方面介绍Flink流处理API——ProcessFunction API (底层API)
一、产生背景
二、KeyeProcesFunction
三、TimerService和定时器(Timers)
四、侧输出流(SideOutPut)
五、CoProcessFunction
版本:
scala:2.11.12
Kafka:0.8.2.2
Flink:1.7.2
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.11</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.11</artifactId>
<version>1.7.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_2.11</artifactId>
<version>1.7.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-connector-kafka-0.10 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.8_2.11</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
一、产生背景
我们之前学习的转换算子Flink流处理API——Transform(转换算子) 是无法访问事件的时间戳信息和水位线信息Flink流处理API——window(窗口) 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
二、KeyeProcesFunction
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和定时器(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上面使用。
Demo(实现当传感器温度连续5秒上升进行报警)
package xxx
import java.util.Properties
import com.njupt.ymh.APITest.SensorReading
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, KeyedProcessFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer08
import org.apache.flink.util.Collector
// 样例类,传感器ID,时间戳,温度
case class SensorReading(id: String, timestamo: Long, temperature: Double){
override def toString: String = {
id+":"+ timestamo.toString + "," + temperature
}
}
object ProcessFunctionTest {
def main(args: Array[String]): Unit = {
val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
environment.setParallelism(1) // 这里要设置并行度为1,才好把所有sensor_1的数据放在同一个window
// 需要通过setStreamTimeCharacteristic显示的指出以EventTime作为时间戳时
environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 后面采用周期性的抽取时间戳,这里通过ExecutionConfig.setAutoWatermarkInterval进行这是抽取周期,默认200毫秒
// environment.getConfig.setAutoWatermarkInterval(2000L) // 按照系统时间的每隔2秒抽取一次
val inputStream = environment.socketTextStream("master", 7777)
val watermarked: DataStream[SensorReading] = inputStream.map(line => {
val fildes: Array[String] = line.split(",") // 这里的split是scala的split方法
val sensorReading = new SensorReading(fildes(0).trim,
fildes(1).trim.toLong, fildes(2).trim.toDouble)
println("input " + sensorReading)
sensorReading
})
// 以周期性的生成的方式产生时间戳和watermark
.assignTimestampsAndWatermarks(new MyAssigner())
.assignAscendingTimestamps(_.timestamo * 1000)
.keyBy(_.id)
// 自定义KeyedProcessFunction
val processed: DataStream[String] = watermarked.keyBy(_.id).process(new Myprocess())
processed.print("process").setParallelism(1)
environment.execute()
}
}
/**
* 自定义抽取时间戳,以及生成watermark方法,
*继承AssignerWithPeriodicWatermarks,表示周期性的抽取时间戳
* 也可以使用AssignerWithPunctuatedWatermarks,打断的方式生成时间戳
*/
class MyAssigner() extends AssignerWithPeriodicWatermarks[SensorReading]{
val bound: Long = 1000 // 延时1秒
var maxTimestamp: Long = Long.MinValue // 记录观察到的最大时间
// 计算当前watermark的值,以Flink接收到的数据中最大的时间戳减1秒作为watermark
override def getCurrentWatermark: Watermark = {
val watermark = new Watermark(maxTimestamp - bound)
// println(" " + watermark)
watermark
}
// 抽取时间戳方法
override def extractTimestamp(t: SensorReading, l: Long): Long = {
maxTimestamp = maxTimestamp.max(t.timestamo*1000)
t.timestamo*1000
}
}
/**
* 自定义KeyedProcessFunction
* 实现,当同一传感器连续5秒温度都在上升进行报警
*
* KeyedProcessFunction[String, SensorReading, String] 三个泛型分别表示 key的类型,
*输入数据类型以及输出数据类型
*/
class Myprocess() extends KeyedProcessFunction[String, SensorReading, String]{
// 定义一个状态,用来保存上一个数据的温度值,初始值为0.0
// getRuntimeContext 是 AbstractRichFunction中的方法,
// KeyedProcessFunction继承AbstractRichFunction
lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState(
new ValueStateDescriptor[Double]("lastTemp", classOf[Double]) )
// 定义一个状态,用来保存定时器的时间戳,初始值为0
lazy val currentTimer: ValueState[Long] = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("currentTimer", classOf[Long]))
lazy val tag: ValueState[Boolean] = getRuntimeContext.getState(new
ValueStateDescriptor[Boolean]("tag", classOf[Boolean]))
override def processElement(i: SensorReading, context: KeyedProcessFunction[String,
SensorReading, String]#Context, collector: Collector[String]): Unit = {
// 获取上一个温度值
val lastTempValue: Double = lastTemp.value()
// 获取上一个定时器的时间戳
val currentTimerValue: Long = currentTimer.value()
// 获取上一次tag的值
val flag = tag.value()
// 更新温度值
lastTemp.update(i.temperature)
if (!flag){ // 第一条数据
tag.update(true)
// 设置定时器启动时间为当前watermark时间加10秒
val timerTs: Long = i.timestamo + 5000L
// 注册定时器(以EventTime为时间)
context.timerService().registerEventTimeTimer(timerTs)
}else if (i.temperature > lastTempValue && currentTimerValue == 0){ // 温度上升,并且当前没有定时器
// 设置定时器启动时间为当前watermark时间加5秒
val timerTs: Long = context.timerService().currentWatermark() + 5000L
// 注册定时器(以EventTime为时间)
context.timerService().registerEventTimeTimer(timerTs)
// 更新定时器时间戳
currentTimer.update(timerTs)
} else if (lastTempValue > i.temperature){ // 温度下降,删除定时器
// 删除定时器,并清空
context.timerService().deleteEventTimeTimer(currentTimerValue)
currentTimer.clear()
}
}
// 回调函数,当定时器触发时调用该方法
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
// 输出报警信息
out.collect(ctx.getCurrentKey + "已经持续5秒上升温度")
currentTimer.clear()
}
}
四、侧输出流(SideOutPut)
大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流。除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同。process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。
一个side output可以定义为OutputTag[X]对 象,X是输出流的数据类型。process function可以通过Context对象发射一个事件到一个或者多个side outputs。
Demo(对传感器温度进行判断,当温度大于20度时,将数据输出到侧输出流,并进行报警)
package xxx
import java.util.Properties
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer08
import org.apache.flink.util.Collector
// 样例类,传感器ID,时间戳,温度
case class SensorReading(id: String, timestamo: Long, temperature: Double){
override def toString: String = {
id+":"+ timestamo.toString + "," + temperature
}
}
object SideOutPutTest {
def main(args: Array[String]): Unit = {
val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
environment.setParallelism(1) // 这里要设置并行度为1,才好把所有sensor_1的数据放在同一个window
// 需要通过setStreamTimeCharacteristic显示的指出以EventTime作为时间戳时
environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 后面采用周期性的抽取时间戳,这里通过ExecutionConfig.setAutoWatermarkInterval进行这是抽取周期,默认200毫秒
// environment.getConfig.setAutoWatermarkInterval(2000L) // 按照系统时间的每隔2秒抽取一次
val inputStream = environment.socketTextStream("master", 7777)
val watermarked: DataStream[SensorReading] = inputStream.map(line => {
val fildes: Array[String] = line.split(",") // 这里的split是scala的split方法
val sensorReading = new SensorReading(fildes(0).trim, fildes(1).trim.toLong, fildes(2).trim.toDouble)
println("input " + sensorReading)
sensorReading
})
// 自定义ProcessFunction,实现超过20度报警
val processed: DataStream[SensorReading] = watermarked.process(new TempAlert() )
processed.print("process").setParallelism(1) // 这是主输出流
processed.getSideOutput(new OutputTag[String]("altert")).print("altert").setParallelism(1) // 侧输出流
environment.execute()
}
}
/**
* 实现温度超过20度报警
* [SensorReading, SensorReading] 输入数据流的类型,主输出流的类型
*/
class TempAlert() extends ProcessFunction[SensorReading, SensorReading]{
lazy val alterOutPut = new OutputTag[String]("altert")
override def processElement(i: SensorReading, context: ProcessFunction[SensorReading, SensorReading]#Context, collector: Collector[SensorReading]): Unit = {
if(i.temperature > 20){ // 测流
context.output[String](alterOutPut, i.id+"温度过高")
}else{ // 主流
collector.collect(i)
}
}
}
五、CoProcessFunction
对于两条输入流,DataStream API提供了CoProcessFunction这样的low-level操作。CoProcessFunction提供了操作每一个输入流的方法: processElement1()和processElement2()。
类似于ProcessFunction,这两种方法都通过Context对象来调用。这个Context对象可以访问事件数据,定时器时间戳,TimerService,以及side outputs。CoProcessFunction也提供了onTimer()回调函数。