Flink流处理API——ProcessFunction API (底层API)

9 篇文章 0 订阅
8 篇文章 0 订阅

原文链接: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()回调函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值