处理函数
文章目录
下一章: Flink 1.13 多流转换
一、基本处理函数(ProcessFunction)
DataStream API 是 Flink 编程的核心。为了让代码有更强大的表现力和易用性,Flink 本身提供了多层 API,DataStream API 只是中间的一环。
![](https://i-blog.csdnimg.cn/blog_migrate/e07c73e85f7bf13268c5d0fb965717e8.png)
在更底层,我们可以不定义任何具体的算子(比如 map,filter,或者 window),而只是提炼出一个统一的“处理”(process)操作,它是所有转换算子的一个概括性的表达,可以自定义处理逻辑,叫作“处理函数”(process function)。
在处理函数中,我们直面的就是数据流中最基本的元素:数据事件(event)、状态(state)以及时间(time)。这就相当于对流有了完全的控制权。
我们通常把处理函数和富函数RichFunction做对比,常见的转换算子,如MapFunction、FlatMapFunction都有对应的富函数。
1.RichFunction的功能与作用
AbstractRichFunction,提供了获取运行时上下文的方法getRuntimeContext()和生命周期方法,可以拿到状态,还有并行度、任务名称之类的运行时信息。
public abstract class AbstractRichFunction implements RichFunction, Serializable {
// 运行时上下文
private transient RuntimeContext runtimeContext;
public void setRuntimeContext(RuntimeContext t) { this.runtimeContext = t;}
public RuntimeContext getRuntimeContext() {}
public IterationRuntimeContext getIterationRuntimeContext() {}
// 生命周期方法
public void open(Configuration parameters) throws Exception {}
public void close() throws Exception {}
}
//运行时上下文
public interface RuntimeContext {
//并行度、任务名称等基本的运行时信息
/** returned ID should NOT be used for any job management tasks. */
JobID getJobId();
/** The name of the task in which the UDF runs. */
String getTaskName();
/** The metric group for this parallel subtask. */
MetricGroup getMetricGroup();
/** The parallelism with which the parallel task runs. */
int getNumberOfParallelSubtasks();
/** The max-parallelism with which the parallel task runs. */
int getMaxNumberOfParallelSubtasks();
/** The index of the parallel subtask. */
int getIndexOfThisSubtask();
/** Attempt number of the subtask.尝试次数 */
int getAttemptNumber();
/** The name of the task, with subtask indicator. */
String getTaskNameWithSubtasks();
ExecutionConfig getExecutionConfig();
/** The ClassLoader for user code classes. */
ClassLoader getUserCodeClassLoader();
/** Registers a custom hook for the user code class loader release. */
void registerUserCodeClassLoaderReleaseHookIfAbsent( String releaseHookName, Runnable releaseHook);
...
// 状态
<T> ValueState<T> getState(ValueStateDescriptor<T> stateProperties);
<T> ListState<T> getListState(ListStateDescriptor<T> stateProperties);
<T> ReducingState<T> getReducingState(ReducingStateDescriptor<T> stateProperties);
<IN, ACC, OUT> AggregatingState<IN, OUT> getAggregatingState(
AggregatingStateDescriptor<IN, ACC, OUT> stateProperties);
<UK, UV> MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV> stateProperties);
}
2. ProcessFunction的功能与作用
处理函数(ProcessFunction)继承了AbstractRichFunction抽象类,拥有富函数类的所有特性。
除此之外,其当前运行的上下文可以直接将数据输出到侧输出流(side output)中;另外提供了一个“定时服务”,访问流中的时间戳、水位线,甚至可以注册“定时事件”。
public abstract class ProcessFunction<I, O> extends AbstractRichFunction {
public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;
//只有KeyedStream才支持设置定时器的操作
public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {}
//上下文可以直接将数据输出到侧输出流(side output)中;提供了一个“定时服务”
public abstract class Context {
/** TimeCharacteristic#ProcessingTime 是个null */
public abstract Long timestamp();
public abstract TimerService timerService();
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
public abstract class OnTimerContext extends Context {
/** The {@link TimeDomain} of the firing timer. */
public abstract TimeDomain timeDomain();
}
}
//定时服务中可以时间戳、水位线,注册和删除“闹钟”
public interface TimerService {
long currentProcessingTime(); //processing time
long currentWatermark(); //event-time watermark
void registerProcessingTimeTimer(long time);
void registerEventTimeTimer(long time);
void deleteProcessingTimeTimer(long time);
void deleteEventTimeTimer(long time);
}
3. 处理函数的分类
- ProcessFunction
最基本的处理函数,基于 DataStream 直接调用.process()时作为参数传入。 - KeyedProcessFunction
对流按键分区后的处理函数,基于 KeyedStream 调用.process()时作为参数传入。要想使用定时器,比如基于 KeyedStream。 - ProcessWindowFunction
开窗之后的处理函数,也是全窗口函数的代表。基于 WindowedStream 调用.process()时作为参数传入。 - ProcessAllWindowFunction
同样是开窗之后的处理函数,基于 AllWindowedStream 调用.process()时作为参数传入。 - CoProcessFunction
合并(connect)两条流之后的处理函数,基于 ConnectedStreams 调用.process()时作为参数传入。关于流的连接合并操作,在多流转换里面介绍。 - ProcessJoinFunction
间隔连接(interval join)两条流之后的处理函数,基于 IntervalJoined 调用.process()时作为参数传入。 - BroadcastProcessFunction
广播连接流处理函数,基于 BroadcastConnectedStream 调用.process()时作为参数传入。这里的“广播连接流”BroadcastConnectedStream,是一个未 keyBy 的普通 DataStream 与一个广播流(BroadcastStream)做连接(conncet)之后的产物。关于广播流的相关操作,也在多流转换里面详细介绍。 - KeyedBroadcastProcessFunction
按键分区的广播连接流处理函数,同样是基于 BroadcastConnectedStream 调用.process()时作为参数传入。与 BroadcastProcessFunction 不同的是,这时的广播连接流,是一个 KeyedStream 与广播流(BroadcastStream)做连接之后的产物。
二、按键分区处理函数(KeyedProcessFunction)
1. 定时器(TimerService)
只有在 KeyedStream 中才支持使用 TimerService 设置定时器的操作。
定时器(timers)是处理函数中进行时间相关操作的主要机制。在.onTimer()方法中可以实现定时处理的逻辑,而它能触发的前提,就是之前曾经注册过定时器、并且现在已经到了触发时间。注册定时器的功能,是通过上下文中提供的“定时服务”(TimerService)来实现的。
对于处理时间和事件时间这两种类型的定时器,TimerService 内部会用一个优先队列将它们的时间戳保存起来,排队等待执行。可以认为,定时器其实是 KeyedStream 上处理算子的一个状态,它以时间戳作为区分。所以 TimerService 会以键(key)和时间戳为标准,对定时器进行去重;也就是说对于每个 key 和时间戳,最多只有一个定时器,如果注册了多次,onTimer()方法也将只被调用一次。
Flink 对.onTimer()和.processElement()方法是同步调用的(synchronous),所以也不会出现状态的并发修改。
Flink 的定时器同样具有容错性,它和状态一起都会被保存到一致性检查点中。当发生故障时,Flink 会重启并读取检查点中的状态,恢复定时器。如果是处理时间的定时器,有可能会出现已经“过期”的情况,这时它们会在重启时被立刻触发。
2. KeyedProcessFunction的功能与示例
KeyedProcessFunction用于KeyedStream中,功能和基本处理函数ProcessFunction类似,但相比于ProcessFunction,它可以设置定时器的操作。
stream.keyBy(data -> true) // 基于KeyedStream定义事件时间定时器
.process(new KeyedProcessFunction<Boolean, Event, String>() {
@Override
public void processElement(Event value, Context ctx,
Collector<String> out) throws Exception {
out.collect("数据到达,时间戳为:" + ctx.timestamp());
out.collect("数据到达,水位线为:" + ctx.timerService().currentWatermark());
// 注册一个10秒后的定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp()+10*1000L);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx,
Collector<String> out) throws Exception {
out.collect("定时器触发,触发时间:" + timestamp);
}
})
三、窗口处理函数(ProcessWindowFunction、ProcessAllWindowFunction)
1. ProcessWindowFunction的功能
与基本处理函数ProcessFunction相比,ProcessWindowFunction在功能和使用上有以下变化:
- 因为全窗口函数不是逐个处理元素的,所以处理数据的方法在这里并不是.processElement(),而是改成了.process()。不再是一个输入数据,而是窗口中所有数据的集合。
- 除了.process()方法外,没有了.onTimer()方法,而是多出了一个.clear()方法。如果我们自定义了窗口状态,那么必须在.clear()方法中进行显式地清除,避免内存溢出。
- Context变化:
- 由于当前不是只处理一个数据,所以Context也不再提供.timestamp()方法。
- Context也不再持有TimerService对象,失去了设置定时器的功能,只能通过currentProcessingTime和currentWatermark来获取当前时间;没有了定时器,可以使用窗口触发器(Trigger)获取当前时间、注册和删除定时器,还可以获取当前的状态。具体操作见时间和窗口章节中。
- Context增加了一些获取其他信息的方法:
- 可以通过.window()直接获取当前的窗口对象,有getStart(),getEnd(),maxTimestamp();
- 可以通过.windowState()和.globalState()获取到当前自定义的窗口状态和全局状态。
这样设计无疑会让处理流程更加清晰——定时操作也是一种“触发”,所以我们就让所有的触发操作归触发器管,而所有处理数据的操作则归窗口函数管。
stream.keyBy( t -> t.f0 )
.window( TumblingEventTimeWindows.of(Time.seconds(10)) )
.process(new ProcessWindowFunction(){...})
public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window>
extends AbstractRichFunction {
public abstract void process(KEY key, Context context, Iterable<IN> elements,
Collector<OUT> out) throws Exception;
public void clear(Context context) throws Exception {}
/** The context holding window metadata. */
public abstract class Context implements java.io.Serializable {
public abstract W window();
public abstract long currentProcessingTime();
public abstract long currentWatermark();
public abstract KeyedStateStore windowState();
public abstract KeyedStateStore globalState();
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
}
2. ProcessAllWindowFunction的功能
相比于ProcessWindowFunction,它的Context没有了currentProcessingTime()和currentWatermark()。
sounce.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new ProcessAllWindowFunction(){...})
public abstract class ProcessAllWindowFunction<IN, OUT, W extends Window>
extends AbstractRichFunction {
public abstract void process(Context context, Iterable<IN> elements,
Collector<OUT> out) throws Exception;
public void clear(Context context) throws Exception {}
public abstract class Context {
public abstract W window();
public abstract KeyedStateStore windowState();
public abstract KeyedStateStore globalState();
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
}
四、其他处理函数
1. CoProcessFunction:
相比于基本处理函数ProcessFunction,processElement有变化,变为了两个方法,processElement1和processElement2。
2. KeyedCoProcessFunction:
相比于基础CoProcessFunction,Context和OnTimerContext有变化,都增加了getCurrentKey()方法。
3. ProcessJoinFunction:
其更像RichJoinFunction,多了侧输出流和当前时间戳。
public abstract class ProcessJoinFunction<IN1, IN2, OUT> extends AbstractRichFunction {
public abstract void processElement(IN1 left, IN2 right, Context ctx, Collector<OUT> out) throws Exception;
public abstract class Context {
public abstract long getLeftTimestamp();
public abstract long getRightTimestamp();
/** @return The timestamp of the joined pair. */
public abstract long getTimestamp();
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
}
4. BroadcastProcessFunction:
相比于处理函数ProcessFunction,不同如下:
-
多了processBroadcastElement方法,没有.onTimer()方法,
-
context里面多了 getBroadcastState() 方法。不再持有TimerService对象, 只能通过currentProcessingTime和currentWatermark来获取当前时间
-
ReadOnlyContext与context方法一样,但其调用时不能改。
public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction { ... public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception; public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception; ... }
5. KeyedBroadcastProcessFunction:
相比于函数BroadcastProcessFunction,不同如下:
- 多了onTimer方法
- context里面多了applyToKeyedState()
- ReadOnlyContext持有timerService()对象,多了getCurrentKey()
- OnTimerContext继承于ReadOnlyContext,拥有timeDomain()方法。
五、侧输出流(Side Output)
其功能时分流,从主流里面分出来测流,可以与主流的数据类型不同。
OutputTag<String> outputTag = new OutputTag<String>("side-output") {};
SingleOutputStreamOperator<Long> longStream = stream.process(
new ProcessFunction<Integer, Long>() {
@Override
public void processElement(Integer value, Context ctx,
Collector<Integer> out) throws Exception {
out.collect(Long.valueOf(value)); // 转换成Long,输出到主流中
// 转换成String,输出到侧输出流中
ctx.output(outputTag, "side-output: " + String.valueOf(value));
}
});
DataStream<String> stringStream = longStream.getSideOutput(outputTag);
六、Top N
1. 使用 ProcessAllWindowFunction 实现 Top N
不推荐,一方面会将并行度强制改为1,另一方面没有了预聚合,攒一个窗口的数据处理一次,类似批处理。具体示例如下:
stream.windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.process(new ProcessAllWindowFunction<String, String, TimeWindow>(){
@Override
public void process(Context context, Iterable<String> elements,
Collector<String> out) throws Exception {
HashMap<String, Long> urlCountMap = new HashMap<>();
// 遍历窗口中数据,将浏览量保存到一个 HashMap 中
for (String url : elements){
if(urlCountMap.containsKey(url)) {
urlCountMap.put(url, urlCountMap.get(url) + 1L);
} else {
urlCountMap.put(url, 1L);
}
}
ArrayList<Tuple2<String,Long>> mapList=new ArrayList<>();
// 将浏览量数据放入ArrayList,进行排序
for (String key : urlCountMap.keySet()) {
mapList.add(Tuple2.of(key, urlCountMap.get(key)));
}
mapList.sort(new Comparator<Tuple2<String, Long>>() {
@Override
public int compare(Tuple2<String,Long> o1,Tuple2<String,Long> o2) {
return o2.f1.intValue() - o1.f1.intValue();
}
});
// 取排序后的前两名,构建输出结果
StringBuilder result = new StringBuilder();
result.append("========================================\n");
for (int i = 0; i < 2; i++) {
Tuple2<String, Long> temp = mapList.get(i);
String info = "浏览量No." + (i + 1) + "url:" + temp.f0 + "浏览量:" + temp.f1 + "窗口结束时间:" + new Timestamp(context.window().getEnd())+"\n";
result.append(info);
}
result.append("========================================\n");
out.collect(result.toString());
}
});
2. 使用 KeyedProcessFunction 实现 Top N
// 第一步:按key分组,在给定时间窗口内求每个key的个数
SingleOutputStreamOperator<Tuple3<String, Long, Long>> aggregate = sounce.keyBy(data -> data.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.aggregate(new AggregateFunction<Tuple2<String, Long>, Long, Long>() {
@Override
public Long createAccumulator() { return 0L; }
@Override
public Long add(Tuple2<String, Long> value, Long accumulator) {
return accumulator + 1;
}
@Override
public Long getResult(Long accumulator) { return accumulator; }
@Override
public Long merge(Long a, Long b) { return a + b;}
},
new ProcessWindowFunction<Long, Tuple3<String, Long, Long>,
String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<Long> elements,
Collector<Tuple3<String, Long, Long>> out) throws Exception {
Long num = elements.iterator().next();
long end = context.window().getEnd();
out.collect(Tuple3.of(s, end, num));
}
});
// 第一步:按窗口结束时间分组,在给定时间窗口内求 Top N
aggregate.keyBy(data -> data.f1)
.process(new KeyedProcessFunction<Long, Tuple3<String, Long, Long>, String>{
private final int value = 2;
private ListState<Tuple3<String,Long,Long>> listState;
@Override
public void open(Configuration parameters) throws Exception {
listState = getRuntimeContext().getListState(new ListStateDescriptor<>
("count", Types.TUPLE(Types.STRING, Types.LONG, Types.LONG)));
}
@Override
public void processElement(Tuple3<String, Long,Long> value, Context ctx,
Collector<String> out) throws Exception {
listState.add(value);
ctx.timerService().registerEventTimeTimer(ctx.getCurrentKey() + 1);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx,
Collector<String> out) throws Exception {
ArrayList<Tuple3<String, Long, Long>> tuple3s = new ArrayList<>();
for (Tuple3<String, Long, Long> element : listState.get()) {
tuple3s.add(element);
}
tuple3s.sort(new Comparator<Tuple3<String, Long, Long>>() {
@Override
public int compare(Tuple3<String, Long, Long> o1,
Tuple3<String, Long, Long> o2) {
return (int)(o2.f2 - o1.f2);
}
});
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("----------------------------\n窗口结束时间:");
stringBuilder.append(new TimeStamp(ctx.getCurrentKey()) + "\n");
for(int i = 0; i < value; i++) {
Tuple3<String, Long, Long> stringLongLongTuple3 = tuple3s.get(i);
String info = "No."+(i+1)+"FirstName:"+stringLongLongTuple3.f0 +
"访问量:" + stringLongLongTuple3.f2 + "\n";
stringBuilder.append(info);
}
stringBuilder.append("----------------------------\n");
out.collect(stringBuilder.toString());
}
});
下一章:Flink 1.13 多流转换