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

二 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参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
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:")

三 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)连续上升,则报警。

// 自定义数据处理函数
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
    }
}

四 侧输出流(SideOutput)

大部分的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")

五 CoProcessFunction

对于两条输入流,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()

你可能感兴趣的:(#,Flink,大数据,flink)