九 flink Process Function

本文深入探讨了Flink中的Process Function,包括ProcessFunction、KeyedProcessFunction及其在无窗流和有窗流中的应用,详细讲解了TimerService、侧输出流、触发器和清理器的使用,以及如何处理迟到数据。
摘要由CSDN通过智能技术生成

1 Process Function

• ProcessFunction
用在没有keyby 和 没有开窗的流上

• KeyedProcessFunction
用在keyby之后,非常常用

• CoProcessFunction
• ProcessJoinFunction
• BroadcastProcessFunction
• KeyedBroadcastProcessFunction
上面四个我也没有用过

• ProcessWindowFunction
用在开窗之后

• ProcessAllWindowFunction
主要讲KeyedProcessFunction

1.1 window调用图

stream
.keyBy(...)
.window(...)
[.trigger(...)]
[.evictor(...)]
.reduce/aggregate/process(...)

evictor 算子可以在 window function 求值之前或者之后移除窗口中的元素.

2 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 为定时器所设定的触发的时间戳。

2.1 TimerService and Timers

Context 和 OnTimerContext 所持有的 TimerService 对象拥有以下方法:
• currentProcessingTime(): Long 返回当前处理时间
• currentWatermark(): Long 返回当前水位线的时间戳
• registerProcessingTimeTimer(timestamp: Long): Unit 会注册当前 key 的 processing time 的 timer。当 processing time 到达定时时间时,触发 timer。
• registerEventTimeTimer(timestamp: Long): Unit 会注册当前 key 的 event time timer。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
• deleteProcessingTimeTimer(timestamp: Long): Unit 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
• deleteEventTimeTimer(timestamp: Long): Unit 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行

当定时器 timer 触发时,执行回调函数 onTimer()。 processElement() 方法和 onTimer() 方法是同步(不是异步)方法,这样可以避免并发访问和操作状态。
定时器 timer 只能在 KeyedStream 上面使用。
针对每一个 key 和 timestamp,只能注册一个定期器。也就是说,每一个 key 可以注册多个定时器,但在每一个时间戳只能注册一个定时器。 KeyedProcessFunction 默认将所有定时器的时间戳放在一个优先队列中。在 Flink 做检查点操作时,定时器也会被保存到状态后端中。

2.1.1 timer案例

package org.example.watermark

import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

// nc -lk 9999
//a 1  设定定时器为 11秒,这时候的水位线为 long的最小值
//a 12 设定定时器为 22秒(这是有两个定时器),水位线为 12 - 11.999秒,定时器1会被触发
//a 23 设定定时器为 33秒(这是有两个定时器,有一个定时器已经被触发),水位线为 22.999秒,定时器2会被触发
object EventTimeOnTimer {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    val stream = env.socketTextStream("xiaoai01", 9999, '\n')
        .map(line => {
          val arr = line.split(' ')
          (arr(0),arr(1).toLong * 1000L)
        })
        .assignAscendingTimestamps(_._2)//指定水位线字段
        .keyBy(_._1) //分区
        .process(new MyKeyedProcess)
        .print()


    env.execute()
  }

  /**
   * [String,(String,Long),String]
   * 1.key值类型
   * 2.in 值类型
   * 3.out值类型
   *
   *
   */
  class MyKeyedProcess extends KeyedProcessFunction[String,(String,Long),String] {

    /**
     * 每来一条数据调用一次
     * @param value 输入的值
     * @param ctx
     * @param out
     */
    override def processElement(value: (String, Long), ctx: KeyedProcessFunction[String, (String, Long), String]#Context, out: Collector[String]): Unit = {
      /**
       * 在当前元素时间戳的10s钟以后,注册一个定时器,定时器的业务逻辑由`onTimer`函数实现
       * 时间到了之后会回调
       */
      ctx.timerService().registerEventTimeTimer(value._2 + 10 * 1000L)
      out.collect("当前的水位线是: " + ctx.timerService().currentWatermark())
    }


    /**
     * 该函数是在定时器被触发的时候调用
     * 因为现在程序是使用eventtime,所以这里的时间都是用eventtime来表示
     * 比如设定定时器的时间为为10秒钟触发,那么当水位先到达10秒才会触发
     * 如果水位线线不到达10秒,那么无论机器时间多大都不会触发定时器
     * @param timestamp 定时器的时间(如果是event time,那么timestamp也是eventtime)
     * @param ctx
     * @param out
     */
    override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, (String, Long), String]#OnTimerContext, out: Collector[String]): Unit = {

      out.collect("位于时间戳:" + timestamp + "的定时器触发了!")
    }
  }
}

3 侧输出流(Side Outputs)

大部分的 DataStream API 的算子的输出是单一输出,也就是某种数据类型的流。除了 split
算子,可以将一条流分成多条流,这些流的数据类型也都相同。 process function 的 side outputs 功能可以产生多条流,并且这些流的数据类型可以不一样。一个 side output 可以定义为OutputTag[X] 对象, X 是输出流的数据类型。 process function 可以通过 Context 对象发射一个事件到一个或者多个 side outputs。

3. 1 定义侧输出流

/**
[String] 流中数据的类型
"freezing-alarms" 侧输出流的id
*/
val freezingAlarmOutput = new OutputTag[String]("freezing-alarms")

3.2 侧输出的案例

package org.example.watermark

import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
import org.example.source.self.{SensorReading, SensorSource}

/**
 * 把大于32度的温度输出到侧输出流
 */
object SideOutputExample {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    val stream = env
      .addSource(new SensorSource)
      .process(new FreezingMonitor)
    
    //获取侧输出流
    //在这里不会创建新的侧输出流,因为会根据id找有没有freezing-alarms
    //如果有不会创建
    stream.getSideOutput(new OutputTag[String]("freezing-alarms"))
        .print()
    
    env.execute()
  }

  class FreezingMonitor extends ProcessFunction[SensorReading,SensorReading] {

    /**
     * 定义一条侧输出流
     * lazy 赖加载,需要用到才会被创建
     */
    lazy val freezingAlarmOutput = new OutputTag[String]("freezing-alarms")

    /**
     * 数据每来一条会执行一次
     * @param value 来的数据的值
     * @param ctx
     * @param out
     */
    override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
      if (value.temperature > 32.0){
        ctx.output(freezingAlarmOutput,s"传感器ID为 ${value.id} 的传感器发出低于32华氏度的低温报警!")
      }

      out.collect(value) //输出到主流上
    }
  }

}

3.3 把迟到的数据输出到侧输出流

可以把迟到的数据输出到指定的侧输出流中.
该函数只有在window stream中才有.

.sideOutputLateData(
        new OutputTag[(String, Long)]("late")
      )

案例:

package org.example.watermark

import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

object LateElementToSideOutput {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)

    val readings = env
      .socketTextStream("localhost", 9999, '\n')
      .map(line => {
        val arr = line.split(" ")
        (arr(0), arr(1).toLong * 1000)
      })
      .assignAscendingTimestamps(_._2)
        .keyBy(_._1)
        .timeWindow(Time.seconds(10))
        .sideOutputLateData(
          new OutputTag[(String, Long)]("late")
        )
        .process(new CountFunction)
    
    readings.print() //输出的是主流的数据(就是没有迟到的数据
    //获取侧输出的数据
    readings.getSideOutput(new OutputTag[(String, Long)]("late")).print()
    
    
    env.execute()
  }

  class CountFunction extends ProcessWindowFunction[(String, Long), String, String, TimeWindow] {
    override def process(key: String, context: Context, elements: Iterable[(String, Long)], out: Collector[String]): Unit = {
      out.collect(context.window.getStart + "到" + context.window.getEnd + "的窗口闭合了!")
    }
  }

}

3.4 没开窗的stream 把迟到数据输出到侧输出流

因为没有开窗的流是没有sideOutputLateData算子的,所以我们要拿到迟到数据只能在processElement中通过判断,把迟到的数据输出到侧输出流中,看下面的案例:

package org.example.watermark

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.util.Collector

/**
 * 把没开窗的流的迟到数据输出到侧输出流中
 */
object LateElementToSideOutputNonWindow {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    val readings = env
      .socketTextStream("localhost", 9999, '\n')
      .map(line => {
        val arr = line.split(" ")
        (arr(0), arr(1).toLong * 1000L)
      })
      .assignAscendingTimestamps(_._2)
      .process(new LateToSideOutput)

    readings.print()
    readings.getSideOutput(new OutputTag[String]("late")).print()

    env.execute()
  }

  class LateToSideOutput extends ProcessFunction[(String, Long), String] {
    val lateReadingOutput = new OutputTag[String]("late")

    override def processElement(value: (String, Long), ctx: ProcessFunction[(String, Long), String]#Context, out: Collector[String]): Unit = {
      if (value._2 < ctx.timerService().currentWatermark()) {
        ctx.output(lateReadingOutput, "迟到事件来了!")
      } else {
        out.collect("没有迟到的事件来了!")
      }
    }
  }
}

4 触发器(Trigger)

触发器要实现继承类:Trigger
使用:

.trigger(new OneSecondIntervalTrigger)  //设置触发器
class OneSecondIntervalTrigger extends Trigger //定义触发器类

触发器的作用是控制下一个算子计算什么时候触发.

4.1 TriggerResult 的值

  • CONTINUE : 表示对窗口不执行任何操作
  • FIRE : 表示对窗口中的数据按照窗口函数中的逻辑进行计算,并将结果输出。注意计算完成后,窗口中的数据并不会被清除,将会被保留
  • PURGE : 表示将窗口中的数据和窗口清除(不做计算)
  • FIRE_AND_PURGE : 表示先将数据进行计算,输出结果,然后将窗口中的数据和窗口进行清除

4.2 使用案例:

package org.example.watermark

import java.sql.Timestamp

import org.apache.flink.api.common.state.ValueStateDescriptor
import org.apache.flink.api.scala.typeutils.Types
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.triggers.{Trigger, TriggerResult}
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
import org.example.source.self.{SensorReading, SensorSource}

/**
 * 实现整数秒计算输出
 */
object TriggerExample {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    val stream = env
      .addSource(new SensorSource)
      .filter(r => r.id.equals("sensor_2"))
      .keyBy(_.id)
      .timeWindow(Time.seconds(10))
      .trigger(new OneSecondIntervalTrigger) //设置触发器,触发器的作用是控制触发计算下一个算子process的逻辑
      .process(new WindowResult)
      .print()

    env.execute()
  }

  /**
   * 实现ProcessWindowFunction
   */
  class WindowResult extends ProcessWindowFunction[SensorReading, String, String, TimeWindow] {
    override def process(key: String, context: Context, elements: Iterable[SensorReading], out: Collector[String]): Unit = {
      out.collect("传感器ID为 " + key + " 的传感器窗口中元素的数量是 " + elements.size)
    }
  }

  /**
   * 定义触发器类
   * SensorReading : 输入类型
   * TimeWindow : 窗口类型
   */
  class OneSecondIntervalTrigger extends Trigger[SensorReading,TimeWindow] {

    /**
     * 每来一条数据执行一次
     * @param element
     * @param timestamp
     * @param window
     * @param ctx
     * @return
     */
    override def onElement(element: SensorReading, timestamp: Long, window: TimeWindow, ctx: Trigger.TriggerContext): TriggerResult = {

      //定义一个状态变量
      // 是单例模型,只会创建一次,出事值为false
      val firstSenn = ctx.getPartitionedState(
        new ValueStateDescriptor[Boolean]("first-seen", Types.of[Boolean])
      )

      //判断是否为第一条元素来的时候
      if(!firstSenn.value()){
        //计算整数秒
         假设第一条事件来的时候,机器时间是1234ms,t是多少?t是2000ms
        val t = ctx.getCurrentProcessingTime + (1000 - (ctx.getCurrentProcessingTime % 1000))

        //注册定时器
        ctx.registerProcessingTimeTimer(t)

        // 在窗口结束时间注册一个定时器
        ctx.registerEventTimeTimer(window.getEnd)

        //更新状态
        firstSenn.update(true)
      }

      TriggerResult.CONTINUE
    }

    override def onProcessingTime(time: Long, window: TimeWindow, ctx: Trigger.TriggerContext): TriggerResult = {
      println("回调函数触发时间:" + new Timestamp(time))
      if (time == window.getEnd) {
        TriggerResult.FIRE_AND_PURGE
      } else {
        //注册一下一个整数秒的触发器
        val t = ctx.getCurrentProcessingTime + (1000 - (ctx.getCurrentProcessingTime % 1000))
        if (t < window.getEnd) {
          ctx.registerProcessingTimeTimer(t)
        }
        TriggerResult.FIRE
      }
    }

    override def onEventTime(time: Long, window: TimeWindow, ctx: Trigger.TriggerContext): TriggerResult = {
      TriggerResult.CONTINUE
    }

    //方法会在窗口清除的时候调用
    override def clear(window: TimeWindow, ctx: Trigger.TriggerContext): Unit = {
      // SingleTon, 单例模式,只会被初始化一次
      val firstSeen = ctx.getPartitionedState(
        new ValueStateDescriptor[Boolean]("first-seen", Types.of[Boolean])
      )
      firstSeen.clear()
    }
  }
}

5 清理器(EVICTORS)

evictor 可以在 window function 求值之前或者之后移除窗口中的元素。
Evictor 的接口定义:

public interface Evictor<T, W extends Window>
extends Serializable {
	void evictBefore(
		Iterable<TimestampedValue<T>> elements,
		int size,
		W window,
		EvictorContext evictorContext);
	void evictAfter(
		Iterable<TimestampedValue<T>> elements,
		int size,
		W window,
		EvictorContext evictorContext);
	interface EvictorContext {
		long getCurrentProcessingTime();
		long getCurrentWatermark();
	}
}

evictBefore() 和 evictAfter() 分别在 window function 计算之前或者之后调用。 Iterable 迭代器包含了窗口所有的元素, size 为窗口中元素的数量, window object 和 EvictorContext 可以访问当前处理时间和水位线。可以对 Iterator 调用 remove() 方法来移除窗口中的元素。
evictor 也经常被用在 GlobalWindow 上,用来清除部分元素,而不是将窗口中的元素全部清空

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值