flink底层函数与计时器

目录

普通函数类、富函数类、底层函数类三者的区别

底层函数

底层函数类别

以KeyedProcessFunction为例

在底层函数中将元素输出至侧输出流

在底层函数中使用定时器

计时器

计时器合并


普通函数类、富函数类、底层函数类三者的区别

普通转换函数:仅能获取当前元素和聚合结果。

富函数:在普通函数之上还有生命周期方法以及运行时上下文对象,能进行状态编程,但不能获取时间戳和watermark等

底层函数:在富函数之上,还能获得时间相关的信息,如时间戳和watermark。能将元素从侧输出流输出。

普通函数、富函数、底层函数,傻傻分不清楚,但是只要搞清楚三者之间的继承关系,就明白三者之间的区别了。

三者的继承关系为,底层函数继承自富函数,富函数继承自普通函数。如下图:

1.从他们的继承关系来看,普通函数能做的,富函数都能做;富函数能做的,底层函数都能做。

2.富函数在普通函数的功能之上,增加了生命周期方法,可以进行初始化和清理操作,还能获得运行时上下文,如函数并行度及state状态等。

3.底层函数在富函数的功能之上,增加了更多功能,如底层函数的上下文对象还可以获取时间戳、watermark以及定时时间;可以对延迟到达的元素进行指定操作,如侧输出流等;可以设置定时任务;

底层函数

ProcessFunction是一种底层处理操作,可访问所有(非循环)流的基本模块:

  • 事件(流元素)
  • 状态(容错性,一致性,仅在键控流上)
  • 计时器(事件时间和处理时间,仅在键控流上)

ProcessFunction可被认为是一个可以访问状态和计时器的特殊算子。它通过processElement方法处理输入流中的每个元素。

对于容错状态,可以通过 ProcessFunction的RuntimeContext方法无障碍地访问键控状态。

计时器可以对处理时间和事件时间的变化做出反应。每次对该函数的调用processElement(...)都会得到一个Context对象,该对象可以访问元素的时间戳和TimerServiceTimerService可用于注册未来的某一个时间触发的回调函数。对于事件时间计时器,当watermark到达或者超过定义的时间时,就会触发onTimer(...);而对于处理时间计时器,onTimer(...)直接在处理时间达到指定时间时调用。在调用onTimer时,所有状态都将再次限定在用于创建计时器的键上,从而允许计时器操作键控状态。

底层函数类别

底层函数分为八种

  • ProcessFunction
  • KeyedProcessFunction
  • CoProcessFunction
  • ProcessJoinFunction
  • BroadcastProcessFunction
  • KeyedBroadcastProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction

每一种函数对应的场景不同,不同的流在使用ProcessFunction时,应实现不同的接口。

以KeyedProcessFunction为例

KeyedProcessFunction的代码结构如下


 *
 * @param <K> key的类型.
 * @param <I> 输入元素的类型.
 * @param <O> 输出元素的类型.
 */
@PublicEvolving
public abstract class KeyedProcessFunction<K, I, O> extends AbstractRichFunction {

	private static final long serialVersionUID = 1L;

	/**
	 * processElement方法用于处理输入流中的每一个元素。
     *
	 * I value :即输入的单个元素
	 * Collector : 收集器,可以输出元素,用于返回结果。
	 * Context :上下文对象,可以更新内部状态或设置计时器,也可以查询并获取元素的时间戳。
	 * TimerService : 用于注册计时器和查询时间。
	 */
	public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;

	/**
	 * 当使用TimerService设置的计时器触发时调用此方法。
	 *
	 * @param timestamp :触发器的时间戳
	 * @param OnTimerContext :一个上下文对象,可以查询时间戳和触发计时器的key。可以获取一个TimerService来注册计时器和查询时间。
	 * @param out :用于返回结果的收集器
	 */
	public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {}

	/**
	 * 上下文对象,在上述两个方法中均有用到
	 */
	public abstract class Context {

		/**
		 * 当前正在处理的元素的时间戳或触发计时器的时间戳。
		 *
		 * 当使用处理时间作为时间特征时,时间戳可能为空。
		 */
		public abstract Long timestamp();

		/**
		 *  TimerService :用于查询时间和注册计时器。
		 */
		public abstract TimerService timerService();

		/**
		 * Output方法 :用于将数据发送到侧输出流。
		 *
		 * @param outputTag :侧输出流标签,用于标记将哪些元素发到侧输出流。
		 * @param value :要发出的值。
		 */
		public abstract <X> void output(OutputTag<X> outputTag, X value);

		/**
		 * 用于获取正在处理的元素的key
		 */
		public abstract K getCurrentKey();
	}

	/**
	 * IOnTimerContext 上下文对象,在调用onTimer方法时,用于查询时间戳和触发计时器的key,以及获取TimerService来注册计时器和查询时间。
	 */
	public abstract class OnTimerContext extends Context {
		/**
		 * TimeDomain : 触发计时器的时间域.
		 */
		public abstract TimeDomain timeDomain();

		/**
		 * 获取当前触发计时器的key
		 */
		@Override
		public abstract K getCurrentKey();
	}

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

在底层函数中将元素输出至侧输出流

自定义底层函数,从中输出侧输出流

import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala.OutputTag
import org.apache.flink.util.Collector

//继承ProcessFunction,指定输入输出元素的泛型
class SplitProcessFunction extends ProcessFunction[Int,Int]{
  //重写processElement方法
  override def processElement(value: Int, ctx: ProcessFunction[Int, Int]#Context, out: Collector[Int]): Unit = {
    //判断value的值
    if(value > 0){
      //正常输出
      out.collect(value)
    }else{
      //导入隐式转换,否则会报错
      import org.apache.flink.api.scala._
      //从测输出流输出
      //OutputTag为测输出流的标签,在外部使用与之相同的OutputTag用于接收
      //第二个参数为输出值,可以不必时value,可以是任意其他形式的数据,也就是说,主流和测输出流的数据类型可以不一致
      ctx.output( new OutputTag[Int]("less zero") ,value )
    }
  }
}

接收侧输出流

    val less_zreo = kafkaSourceStream.getSideOutput(new OutputTag[Int]("less zero"))

在底层函数中使用定时器

在下面的示例中,KeyedProcessFunction维护每个键的计数,并在每分钟(基于事件时间)传递一个(key, count)的元组,且不更新该键:

import org.apache.flink.api.common.state.ValueState
import org.apache.flink.api.common.state.ValueStateDescriptor
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.util.Collector

// the source data stream
val stream: DataStream[Tuple2[String, String]] = ...

// apply the process function onto a keyed stream
val result: DataStream[Tuple2[String, Long]] = stream
  .keyBy(0)
  .process(new CountWithTimeoutFunction())

/**
  * 定义存储状态的数据类型
  */
case class CountWithTimestamp(key: String, count: Long, lastModified: Long)

/**
  * 自定义ProcessFunction方法
  */
class CountWithTimeoutFunction extends KeyedProcessFunction[Tuple, (String, String), (String, Long)] {

  /** 创建一个状态,由处理函数维护。状态的数据类型为自定义的 CountWithTimestamp样例类*/
  lazy val state: ValueState[CountWithTimestamp] = getRuntimeContext
    .getState(new ValueStateDescriptor[CountWithTimestamp]("myState", classOf[CountWithTimestamp]))


  override def processElement(
      value: (String, String), 
      ctx: KeyedProcessFunction[Tuple, (String, String), (String, Long)]#Context, 
      out: Collector[(String, Long)]): Unit = {

    // 初始化或更新状态
    val current: CountWithTimestamp = state.value match {
      case null =>
        CountWithTimestamp(value._1, 1, ctx.timestamp)
      case CountWithTimestamp(key, count, lastModified) =>
        CountWithTimestamp(key, count + 1, ctx.timestamp)
    }

    // 写入更新状态
    state.update(current)

    // 定义下一个触发时间为当前时间加60秒
    ctx.timerService.registerEventTimeTimer(current.lastModified + 60000)
  }

    //重写onTimer方法,对状态进行模式匹配,如果状态的当前时间到达上一次触发后的60s,则输出(key, count)
  override def onTimer(
      timestamp: Long, 
      ctx: KeyedProcessFunction[Tuple, (String, String), (String, Long)]#OnTimerContext, 
      out: Collector[(String, Long)]): Unit = {

    state.value match {
      case CountWithTimestamp(key, count, lastModified) if (timestamp == lastModified + 60000) =>
        out.collect((key, count))
      case _ =>
    }
  }
}
  • count,key和上次修改时间戳记存储在中ValueState,该ValueState由key隐式限定范围,每个key都有对应的ValueState。
  • 对于每个记录,KeyedProcessFunction递增计数器并设置最后修改时间戳
  • 该函数还计划在将来的一分钟内(发生时间)进行回调。
  • 在每个回调上,它将对照存储的计数的最后修改时间检查回调的事件时间戳,并在它们匹配时发出key/count(即,在此分钟内未发生进一步的更新)

计时器

计时器有以下几个注意事项:

  1. 每个键和时间戳最多有一个计时器。如果为同一时间戳注册了多个计时器,则该onTimer()方法将仅被调用一次。
  2. Flink同步调用onTimer()processElement()。因此,不必担心状态的并发修改。
  3. 计时器具有容错能力,并与应用程序状态一起使用checkpoint保存状态。如果发生故障恢复或从checkpoint启动应用程序,将还原计时器。在恢复时,如果使用的是处理时间计时器,则应该触发的计时器会被立即触发。
  4. 计时器是异步被Checkpointed的,除了RocksDB backend、增量快照、基于堆的计时器的组合(但是在FLINK-10026也会被异步执行,所以以后所有计时器都是异步被检查的了)。
  5. 大量计时器会增加保存checkpoint的时间,因为计时器是checkpoint状态的一部分。有关如何减少计时器数量的建议,请参阅后面的“计时器合并”部分。

计时器合并

由于Flink每个键和时间戳仅维护一个计时器,因此可以通过降低计时器精度以合并它们来减少计时器的数量。

对于1秒(事件或处理时间)的计时器精度,可以将目标时间舍入为整秒。计时器最多可提前1秒触发一次,但不晚于毫秒精度要求的时间。每个键和秒最多有一个计时器。

val coalescedTime = ((ctx.timestamp + timeout) / 1000) * 1000
ctx.timerService.registerProcessingTimeTimer(coalescedTime)

由于事件时间计时器仅在watermark到达时触发,因此还可以使用当前的watermark调度这些计时器并将其与下一个watermark合并:

val coalescedTime = ctx.timerService.currentWatermark + 1
ctx.timerService.registerEventTimeTimer(coalescedTime)

计时器也可以按如下方式停止和移除:

停止处理时间计时器:

val timestampOfTimerToStop = ...
ctx.timerService.deleteProcessingTimeTimer(timestampOfTimerToStop)

停止事件时间计时器:

val timestampOfTimerToStop = ...
ctx.timerService.deleteEventTimeTimer(timestampOfTimerToStop)

注意:如果没有注册具有给定时间戳的计时器,则停止计时器无效。

猜你喜欢

转载自blog.csdn.net/x950913/article/details/106461227