文章目录
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 上,用来清除部分元素,而不是将窗口中的元素全部清空