——wirte by 橙心橙意橙续缘,
前言
白话系列
————————————————————————————
也就是我在写作时完全不考虑写作方面的约束,完全把自己学到的东西、以及理由和所思考的东西等等都用大白话诉说出来,这样能够让信息最大化的从自己脑子里输出并且输入到有需要的同学的脑中。PS:较为专业的地方还是会用专业口语诉说,大家放心!
白话Flink系列
————————————————————————————
主要是记录本人(国内某985研究生)在Flink基础理论阶段学习的一些所学,更重要的是一些所思所想,所参考的视频资料或者博客以及文献资料均在文末放出.由于研究生期间的课题组和研究方向与Flink接轨较多,而且Flink的学习对于想进入大厂的同学们来说也是非常的赞,所以该系列文章会随着本人学习的深入来不断修改和完善,希望大家也可以多批评指正或者提出宝贵建议。
说在前面
————————————————
我们之前学习的转换算子是无法访问事件的时间戳信息和水位线信息的。而这 在一些应用场景下,极为重要。例如 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
Process Function是底层API,所以实现的功能也很多,所以如果要进行一些复杂的Flink所提供的算子无法实现的,那么就需要使用Process Function。
注意:只有流里面才支持processFunction
KeyedProcessFunction
程序结构
KeyedProcessFunction是使用最多的函数,就通过它来简单了解一下ProcessFunction。
- 通过
.process()
算子来调用,参数为一个定义好的KeyedProcessFunction类对象。
public <R> SingleOutputStreamOperator<R> process(KeyedProcessFunction<KEY, T, R> keyedProcessFunction) {}
public abstract class KeyedProcessFunction<K, I, O> extends AbstractRichFunction {
public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;
public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {};
}
所有的Process Function底层都继承了
AbstractRichFunction
,所以一些算子生命周期的操作也能做,例如.open()
等,所以Process Function理论上可以实现任何的操作。具体看一下富函数中有什么操作。
富函数AbstractRichFunction
——————————————————————————————
public abstract class AbstractRichFunction implements RichFunction, Serializable {
private static final long serialVersionUID = 1L;
private transient RuntimeContext runtimeContext;
@Override
public void setRuntimeContext(RuntimeContext t) { this.runtimeContext = t; }
@Override
public RuntimeContext getRuntimeContext() {} // 获取上下文信息,这个是最重要的
@Override
public IterationRuntimeContext getIterationRuntimeContext() {}
@Override // 初始化工作,一般是定义状态,或者建立数据库连接
public void open(Configuration parameters) throws Exception {}
@Override// 一般是关闭连接和清空状态的收尾操作
public void close() throws Exception {}
}
可以看到,在上下文信息中有这么多
getxx()
方法,很显然是用于获取具体的上下文信息的。
言归正传
————————————
除了可以使用富函数中的操作,在KeyedProcessFunction类内部提供了2个方法:
processElement
和onTimer
.processElement
用户定义处理输入数据的逻辑;而onTimer
是定时函数,也就是在processElement
中定义好的定时器
对应的执行逻辑。
这里我们需要重点看一下Context ctx
这个参数,因为Process Function之所以功能强大,就是因为它可以获得Context上下文信息,上下文中包含着很多普通算子获取不到的内容,我们接下来仔细看一下里面有什么?
public abstract class Context {
public abstract Long timestamp(); //当前时间戳,由时间语义决定是哪种时间
public abstract TimerService timerService(); //时间服务,可用来定义定时器
public abstract <X> void output(OutputTag<X> outputTag, X value); //侧输出流
public abstract K getCurrentKey(); //获取当前的Key
}
再来看一下TimerService 里可以做什么?
可以发现,里面提供了获取
当前处理时间和水印的操作
,以及注册和删除定时器的操作,而且可以发现定时器相关的函数只有Long1个参数,所以说明不同的定时器是以时间戳(Long类型)来进行区分的
。
- 自定义KeyedProcessFunction类的逻辑。
// 实现自定义的处理函数
public static class MyProcess extends KeyedProcessFunction<Tuple, SensorReading, Integer>{
@Override
public void open(Configuration parameters) throws Exception {
}
@Override
public void processElement(SensorReading value, Context ctx, Collector<Integer> out) throws Exception{
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<Integer> out) throws Exception {
}
@Override
public void close() throws Exception {
}
}
自定义KeyedProcessFunction类的逻辑需要继承KeyedProcessFunction类,并重写类中的
processElement
方法来实现具体的逻辑,onTimer
方法是如果有定时器的话才需要,open和close
是底层继承的AbstractRichFunction类中包含的,也不是非要实现的。
- 将自定义的KeyedProcessFunction类的对象传入process算子中。
dataStream.keyBy("id").process( new MyProcess())
对于KeyedProcessFunction来说,在此之前一定要进行
keyBy
。
具体操作示例
dataStream.keyBy("id")
.process( new MyProcess() )
.print();
// 实现自定义的处理函数
public static class MyProcess extends KeyedProcessFunction<Tuple, SensorReading, Integer>{
ValueState<Long> tsTimerState;
@Override
public void open(Configuration parameters) throws Exception {
tsTimerState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("ts-timer", Long.class));
}
@Override
public void processElement(SensorReading value, Context ctx, Collector<Integer> out) throws Exception {
out.collect(value.getId().length());
// context
ctx.timestamp();
ctx.getCurrentKey();
// ctx.output();
ctx.timerService().currentProcessingTime();
ctx.timerService().currentWatermark();
ctx.timerService().registerProcessingTimeTimer( ctx.timerService().currentProcessingTime() + 5000L);
tsTimerState.update(ctx.timerService().currentProcessingTime() + 1000L); //通过本地valueState传入定时时间
// ctx.timerService().registerEventTimeTimer((value.getTimestamp() + 10) * 1000L);
// ctx.timerService().deleteProcessingTimeTimer(tsTimerState.value());
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<Integer> out) throws Exception {
System.out.println(timestamp + " 定时器触发");
ctx.getCurrentKey();
// ctx.output();
ctx.timeDomain();
}
@Override
public void close() throws Exception {
tsTimerState.clear();
}
}
定时器时间的起始是1970年,所以需要获取当前时间戳,在此基础上加上一个整数进行定时触发。
应用案例1—持续升温(定时器)
目标
————————————————————
传感器温度在10s内连续上升就报警。
思路
————————————————————
在温度开始出现上升现象时开启一个10s的定时器
,如果在这个过程中出现下降,那么就把定时器取消;否则就会在10s后触发定时器函数进行报警。
代码
dataStream.keyBy("id")
.process( new TempConsIncreWarning(10) )
.print();
// 实现自定义处理函数,检测一段时间内的温度连续上升,输出报警
public static class TempConsIncreWarning extends KeyedProcessFunction<Tuple, SensorReading, String>{
// 定义私有属性,当前统计的时间间隔
private Integer interval;
public TempConsIncreWarning(Integer interval) {
this.interval = interval;
}
// 定义状态,保存上一次的温度值,定时器时间戳
private ValueState<Double> lastTempState;
private ValueState<Long> timerTsState; //定时器有时间戳,说明现在有定时器
@Override
public void open(Configuration parameters) throws Exception {
lastTempState = getRuntimeContext().getState(new ValueStateDescriptor<Double>("last-temp", Double.class, Double.MIN_VALUE));
timerTsState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("timer-ts", Long.class));
}
@Override
public void processElement(SensorReading value, Context ctx, Collector<String> out) throws Exception {
// 取出状态
Double lastTemp = lastTempState.value();
Long timerTs = timerTsState.value();
// 如果温度上升并且没有定时器,注册10秒后的定时器,开始等待
if( value.getTemperature() > lastTemp && timerTs == null ){
// 计算出定时器时间戳
Long ts = ctx.timerService().currentProcessingTime() + interval * 1000L;
ctx.timerService().registerProcessingTimeTimer(ts);
timerTsState.update(ts);
}
// 如果温度下降,那么删除定时器
else if( value.getTemperature() < lastTemp && timerTs != null ){
ctx.timerService().deleteProcessingTimeTimer(timerTs);
timerTsState.clear();
}
// 更新温度状态
lastTempState.update(value.getTemperature());
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
// 定时器触发,输出报警信息
out.collect("传感器" + ctx.getCurrentKey().getField(0) + "温度值连续" + interval + "s上升");
timerTsState.clear();
}
@Override
public void close() throws Exception {
lastTempState.clear();
}
}
应用案例2—高低温分流(SideOut侧输出流)
ProcessFunction的分流操作—主流设置为高温流,测输出流设置为低温流
// 定义一个OutputTag,用来表示侧输出流低温流
OutputTag<SensorReading> lowTempTag = new OutputTag<SensorReading>("lowTemp") {
};
// 测试ProcessFunction,自定义侧输出流实现分流操作
SingleOutputStreamOperator<SensorReading> highTempStream = dataStream.process(new ProcessFunction<SensorReading, SensorReading>() {
@Override
public void processElement(SensorReading value, Context ctx, Collector<SensorReading> out) throws Exception {
// 判断温度,大于30度,高温流输出到主流;小于低温流输出到侧输出流
if( value.getTemperature() > 30 ){
out.collect(value);
}
else {
ctx.output(lowTempTag, value);
}
}
});
highTempStream.print("high-temp");
highTempStream.getSideOutput(lowTempTag).print("low-temp");
要注意的是要通过
getSideOutput
获取主流的测输出流,那么主流的类型必须是SingleOutputStreamOperator<>
.
总结
我们通过KeyedProcessFunction简单的看了一下ProcessFunction的强大功能,就比如说持续升温这个案例,如果要通过window操作进行的话。那么就需要持续开间隔很小的10s滑动窗口才能实现,显然那样的操作很麻烦而且不现实。所以我们如果遇到需要(1)定时器(2)分侧输出流(3)或(3)需要结合状态编程和上下文信息的操作就可以选择使用ProcessFunction,这块可能在实际项目中应用很多,形式比较简单,但是需要大家多练习一些项目和实战。