概述
以Flink算子的视角为入口,解析它们是如何设计和工作的。
重点在AbstractStreamOperator
实现StreamOperator的接口和抽象类
首先是StreamOperator接口,方法1-4是生命周期相关的。方法5-6是容错与状态相关的,方法7-8是负责从StreamRecord中获取Key的信息的,应用需要shuffle的算子中。方法9-10是与flink的Operator Chaining 优化相关设置。方法11是跟监控相关。除此之外,StreamOperator还继承了CheckpointListener接口(notifyCheckpointComplete方法,checkpoint结束之后的回调方法),KeyContext接口(key的getter setter方法)。
StreamOperator
继承了StreamOperator的扩展接口则有OneInputStreamOperator,TwoInputStreamOperator(这两个接口基本上能做到顾名思义)。
实现了StreamOperator的抽象类有AbstractStreamOperator以及它的子类AbstractStreamUdfOperator。
StreamOperator
OneInputStreamOperator与TwoInputStreamOperator接口
这两个接口非常类似,本质上就是处理流上存在的三种元素StreamRecord,Watermark和LatencyMarker。一个用作单流输入,一个用作双流输入。除了StreamSource以外的所有Stream算子都必须实现并且只能实现其中一个接口。
OneInputStreamOperator
TwoInputStreamOperator
AbstractStreamOperator
AbstractStreamOperator抽象类
AbstractStreamOperator实现类(没有继承AbstractUdfStreamOperator)
public void processElement(StreamRecord<IN> element) throws Exception { for (int i = 0; i < this.numFields; i++) { outTuple.setField(((Tuple) element.getValue()).getField(fields[i]), i); } output.collect(element.replace(outTuple)); }
StreamProject实现了投影的功能,覆盖了了open方法,创建了一个输出Tuple的实例,processElement方法中将需要的field从输入Tuple中提取出来加入输出Tuple,将StreamRecord中的元素替换成输出Tuple,通过AbstractStreamOperator中定义的output发送到下游。
最后,虽然没有实现OneInputStreamOperator或TwoInputStreamOperator接口,但是对processWatermark和ProcessLatencyMarker的方法都做了实现。启动对watermark的处理是通过InternalTimeServiceManager,具体不作展开。
两个setKeyConextElement方法,借助对应的成员变量stateKeySelector将Key从StreamRecord里面提取出来. 这两个方法的调用链为processInput(StreamInputProcessor || StreamTwoInputProcessor) -> streamOperator.setKeyContextElement1. 值得注意的是,在这个方法调用后紧跟着的就是processElement方法。所以这个方法的作用就是在处理Record之前将stateBackend切换到相应的Key的状态。
snapshotState的简化的调用链是triggerCheckpoint(CheckpointCoordinater) -> triggerCheckpoint(Execution) -(AkkaMsg: TriggerCheckpoint)-> (triggerCheckpointBarrier)TaskManager->checkpointStreamOperator(StreamTask) -> snapshotState,从这条链路可以看到checkpoint是由Master节点发起,通过akka触发TaskManager对所有StreamTask做checkpoint,然后最后由算子执行snapshot,在这一层将TimerService做备份,防止触发器丢失,并且调用operatorStateBackend和keyedStateBackend的snapshot方法将stateBackend的备份到用户指定的文件系统。
initializeState是空方法,留给子类去覆盖,作用是从checkpoint中恢复状态。
getPartitionedState(无namespace)方法创建了一个partitioned state的句柄,使得聚合类的方法可以操作状态,并且这些状态会在snapshotState被调用的时候被checkpoint(AbstractKeyedStateBackend的注释里提到)。在该方法中,调用了重载方法(参数有namespace和相应Serializer)并且传入VoidNamespace相应参数。
getOrCreateKeyedState,该方法会被WindowOperator调用,由于同一个key不同的window会对应不同的值,所以每个Window就是一个namespace,在处理具体Element前不仅要切换Key,还要切换namespace。本质上与上一个方法的区别就在于取得的state是否有namespace。
notifyCheckpointComplete方法调用了keyStateBackend的同名方法,通知其执行checkpoint完成之后的逻辑。
之后是状态与容错相关的方法
setup的调用链是invoke(StreamTask) -> constructor(OperatorChain) -> setup。在调用setup方法时,StreamTask已经在各个TaskManager节点上,StreamTask在启动是会创建OperatorChain,OperatorChain会一一调用所包含的算子的setup方法。而在setup方法中,通过StreamTask的Environment和UserCodeClassLoader将上述的大部分成员变量进行初始化。
open和close方法是空方法,由继承的类实现
dispose方法是算子生命周期的最后一环,当StreamTask被取消或者出现异常时调用,负责释放与operator状态相关的资源。
首先是算子生命周期相关的方法:
在AbstractStreamOperator实现的方法中,有一部分是上述变量的getter, setter,还有就是对StreamOperator的实现。
配置相关的成员变量有chainingStrategy,决定了能否在生成JobGraph时对算子进行Chaining优化。
状态相关的成员变量:对于stateKeySelector1, stateKeySelector2这两个变量,它们是用于将Key从StreamRecord中提取出来,keySelector是否为null取决于这个算子有几个流有key。keyedStateBackend就是用户指定的state backend,是将状态放在Heap里还是RocksDB里。keyedStateStore是在state backend上做一层抽象,使其能通过StateDescriptor直接获取State。operatorStateBackend是算子相关的状态。
运行时相关的成员变量有container,该算子在运行时所属的StreamTask,以及任务配置信息StreamConfig,算子的输出句柄output,还有运行的上下文runtimeContext。
时间相关的成员变量有timeServiceManager,提供了定时触发的服务,在状态清理上有很重要的作用。
监控相关的成员变量有metrics,注册的一些关于算子的监控信息,还有latencyStats,通过LatencyMarker来获取延时信息。
在AbstractStreamOperator中有一些重要的成员变量,总体来说可以分为几类,一类是运行时相关的,一类是状态相关的,一类是配置相关的,一类是时间相关的,还有一类是监控相关的。
AbstractUdfStreamOperator抽象类
OneInputStreamOperator
ValueStateDescriptor<IN> stateId = new ValueStateDescriptor<>(STATE_NAME, serializer); values = getPartitionedState(stateId);
其中ProcessOperator对ProcessFunction的实现是只支持获取currentProcessingTime和wartermark,不支持Timer的注册。
LegacyKeyedProcessOperator对ProcessFunction的实现是支持Timer的注册的。同时它实现了Triggerable接口中的onEventTime和onProcessingTime方法,这两个方法是由InternalTimerService调用的回调方法,在这两个方法中,会去调用用户在ProcessFunction中实现的onTimer方法来实现定时触发的。LegacyKeyedProcessOperator现在被标记为Deprecated,由KeyedProcessOperator类替代,但是由于很多Function是基于ProcessFunction实现的,而KeyedProcessFunction在1.5.2中还没有什么实现类,所以还是会经常被用到。
KeyedProcessOperator实现与LegacyKeyedProcessOperator非常类似,不过UDF是KeyedProcessFunction。
ProcessOperator, LegacyKeyedProcessOperator提供了对ProcessFunction的支持, KeyedProcessOperator提供了对KeyedProcessFunction的支持。ProcessFunction是比较灵活的UDF,允许用户通过在processElement的时候可以通过传入的Conext(这个Conext是由算子实现的)操作TimerService,注册ProcessingTimeTimer和EventTimeTimer,并且通过实现方法onTimer就可以在Timer被触发的时候执行回调的逻辑。
StreamSink除了在processElement方法中调用SinkFunction方法外,还提供了SimpleContext,可以获取processingTime,watermark和element的时间戳。
GenericWriteAheadSink提供了一个可以被实现为Exactly once的sink的抽象类,这边不展开研究。
AsyncWaitOperator提供了异步处理的能力,是一个比较特殊的算子,对元素的处理和备份恢复都比较特殊。element的输出通过一个Emitter对象来实现。有机会单独针对这个算子写一篇文章。
TimestampsAndPeriodicWatermarksOperator与TimestampsAndPunctuatedWatermarksOperator通过TimestampAssigner提取timestamp并生按照规则生成watermark。
windowOperator可能是flink实现的最复杂的operator,可以参考vinoyang的Flink流处理之窗口算子分析。后续也有可能针对这个operator写一篇解析。
StreamFilter,StreamMap与StreamFlatMap算子在实现的processElement分别调用传入的FilterFunction,MapFunction, FlatMapFunction的udf将element传到下游。其中StreamFlatMap用到了TimestampedCollector,它是output的一层封装,将timestamp加入到StreamRecord中发送到下游。
StreamGroupedReduce与StreamGroupedFold算子相似的点是都涉及到了操作状态, 所以在覆盖open方法时通过创建一个状态的描述符以及调用AbstractStreamOperator实现的getPartitionedState方法获取了一个stateBackend的操作句柄。在processElement方法中借助这个句柄获取当前状态值,在用UDF将新的元素聚合进去并更新状态值,最后输出到下游。不同的是Fold的输出类型可能不一样(所以实现了OutputTypeConfigurable接口的setOutputType方法),并且有初始值。
TwoInputStreamOperator的实现类相对较少,作用于双流的各种操作。
此外它还实现了ProcessJoinFunction的Conext抽象类,提供了获取左右两个流元素timestamp的功能。关于BufferEntry的实现这里略过。
TwoInputStreamOperatorChildren.jpg
CoStreamMap, CoStreamFlatMap基本与单流的逻辑没什么区别,只是针对两个流的Function做类似的处理。
IntervalJoinOperator对双流的元素根据提供的ProcessJoinFunction做内连接,并且每个元素都有失效时间。在processElement方法中,每当一个流的元素到达,会将它加入对应流的buffer,并且遍历另一个流的buffer找到所有join的选项。最后再根据失效时间注册一个状态清理的Timer防止buffer无限增长。
CoProcessOperator和KeyedCoProcessOperator本质上与单流的处理也没有什么区别,但是提供了双流之间共享状态的可能。CoProcessOperator也被用来实现NonWindowJoin。
CoBroadcastWithKeyedOperator和CoBroadcastWithNonKeyedOperator提供了对(Keyed)BroadcastProcessFunction的支持,和CoProcess有一些类似,只是Broadcast的Stream只有读权限,没有写权限。并且可以通过context直接获得BroadcastState。
StreamSource因为没有输入,所以没有实现InputStreamOperator的接口。比较特殊的是ChainingStrategy初始化为HEAD。在运行时由SourceStreamTask调用run方法。在run方法中,他首先出事话LatencyMarksEmitter用来产生延迟监控的LatencyMarker,接着根据用户选择的时间模式(EventTime,IngestionTime和ProcessingTime)来生成相应的SourceConext(包含了产生element关联的timestamp的方法和生成watermark的方法)。最后调用SourceFunction的run方法来启动source。
AbstractStreamOperator实现类
还是算子生命周期相关的方法:首先调用了前文提到的AbstractStreamOperator相应方法,然后为userFunction提供runtimeContext,Configuration,并且调用function的open和close方法。
状态和checkpoint相关的方法: 首先调用父类相应方法,完成算子状态的别分与恢复,然后调用实现了Checkpoint相关的接口的userFunction的相关方法。
AbstractUdfStreamOperator继承了AbstractStreamOperator,对其部分方法做了增强,多了一个成员变量UserFunction,并提供get方法。此外还实现了OutputTypeConfigurable接口的setOutputType方法对输出数据的类型做了设置。
总结
本文从StreamOperator接口开始一层一层往上解析,重点关注了AbstractStreamOperator和AbstractUdfStreamOperator这两个抽象类,覆盖了大部分的算子的功能与设计。
了解算子如何工作可以对运行的任务有更深的了解,借助AbstractUdfStreamOperator,也可以较低成本的定制出特殊的算子。
作者:铛铛铛clark
链接:https://www.jianshu.com/p/2e379f242272