目录
普通函数类、富函数类、底层函数类三者的区别
普通转换函数:仅能获取当前元素和聚合结果。
富函数:在普通函数之上还有生命周期方法以及运行时上下文对象,能进行状态编程,但不能获取时间戳和watermark等
底层函数:在富函数之上,还能获得时间相关的信息,如时间戳和watermark。能将元素从侧输出流输出。
普通函数、富函数、底层函数,傻傻分不清楚,但是只要搞清楚三者之间的继承关系,就明白三者之间的区别了。
三者的继承关系为,底层函数继承自富函数,富函数继承自普通函数。如下图:
1.从他们的继承关系来看,普通函数能做的,富函数都能做;富函数能做的,底层函数都能做。
2.富函数在普通函数的功能之上,增加了生命周期方法,可以进行初始化和清理操作,还能获得运行时上下文,如函数并行度及state状态等。
3.底层函数在富函数的功能之上,增加了更多功能,如底层函数的上下文对象还可以获取时间戳、watermark以及定时时间;可以对延迟到达的元素进行指定操作,如侧输出流等;可以设置定时任务;
底层函数
ProcessFunction
是一种底层处理操作,可访问所有(非循环)流的基本模块:
- 事件(流元素)
- 状态(容错性,一致性,仅在键控流上)
- 计时器(事件时间和处理时间,仅在键控流上)
ProcessFunction
可被认为是一个可以访问状态和计时器的特殊算子。它通过processElement方法处理输入流中的每个元素。
对于容错状态,可以通过 ProcessFunction
的RuntimeContext方法无障碍地访问键控状态。
计时器可以对处理时间和事件时间的变化做出反应。每次对该函数的调用processElement(...)
都会得到一个Context
对象,该对象可以访问元素的时间戳和TimerService。TimerService
可用于注册未来的某一个时间触发的回调函数。对于事件时间计时器,当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(即,在此分钟内未发生进一步的更新)
计时器
计时器有以下几个注意事项:
- 每个键和时间戳最多有一个计时器。如果为同一时间戳注册了多个计时器,则该
onTimer()
方法将仅被调用一次。 - Flink同步调用
onTimer()
和processElement()
。因此,不必担心状态的并发修改。 - 计时器具有容错能力,并与应用程序状态一起使用checkpoint保存状态。如果发生故障恢复或从checkpoint启动应用程序,将还原计时器。在恢复时,如果使用的是处理时间计时器,则应该触发的计时器会被立即触发。
- 计时器是异步被Checkpointed的,除了RocksDB backend、增量快照、基于堆的计时器的组合(但是在FLINK-10026也会被异步执行,所以以后所有计时器都是异步被检查的了)。
- 大量计时器会增加保存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)
注意:如果没有注册具有给定时间戳的计时器,则停止计时器无效。