一、作业生成及提交整体流程
上图为一个 Flink 作业的提交流程,主要可以分为以下几个步骤:
- Client 将作业 code 生成 StreamGraph(在 Batch 模式下,生成的是一个 OptimizedPlan,这里暂不展开):StreamGraph 描述了算子和算子之间逻辑上的拓扑关系
- Client 将 StreamGraph 转换为 JobGraph:Operator chain,
- 将并不涉及到 shuffle 的算子进行合并
- 对于同一个 operator chain 里面的多个算子,会在同一个 task 中执行
- 对于不在同一个 operator chain 里的算子,会在不同的 task 中执行
- Client 中的 ClusterClient 将 JobGraph 提交给 Dispatcher,Dispatcher 根据 JobGraph 创建相应的 JobManager(代码上是 JobMaster)并运行起来
- JobManager 将 JobGraph 转换为 ExecutionGraph
- JobManager 将 ExecutionGraph 转换为物理执行计划(可执行)
接下来我们以如下例子来剖析各个步骤具体的执行流程:
public static void main(String[] args) throws Exception {
// 创建 execution environment
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置 data source
DataStream<TaxiRide> rides = env.addSource(new TaxiRideGenerator());
// 将每个 ride 转为 Tuple2(driverId, 1)
DataStream<Tuple2<Long, Long>> tuples = rides.map(new MapFunction<TaxiRide, Tuple2<Long, Long>>() {
@Override
public Tuple2<Long, Long> map(TaxiRide ride) {
return Tuple2.of(ride.driverId, 1L);
}
});
// 对 stream 根据 driverId 进行重新分区
KeyedStream<Tuple2<Long, Long>, Long> keyedByDriverId = tuples.keyBy(t -> t.f0);
// 计算每个司机的 ride 数
DataStream<Tuple2<Long, Long>> rideCounts = keyedByDriverId.sum(1);
// 将结果进行打印
rideCounts.print();
// 开始运行
env.execute("Ride Count");
}
二、执行 Transformations
该过程发生在 Client 端
上述代码可以分为三个部分,即:
- 输入的数据源是谁?用一个
DataStreamSource
表示,是该拓扑的头结点 - 对数据进行了哪些操作?用一个
List<Transformation<?>> transformations
表示 - 最终计算的结果输出到哪里?用一个
DataStreamSink
表示,是该拓扑的尾结点
我们分别就这三部分进行说明
2.1、输入的数据源是谁
add Source 相关的调用如下:
DataStream<TaxiRide> rides = env.addSource(new TaxiRideGenerator());
如上,通过 DataStreamSource<OUT> StreamExecutionEnvironment#addSource(SourceFunction<OUT> function)
设置 SourceFunction 并返回一个 DataStreamSource。关于 SourceFunction、SourceContext 等请查看
SourceFunction & SourceContext & StreamSource
-
DataStreamSource:DataStream 子类,DataStream 的起点 。详见
-
OUT:数据源中每条数据经过 SourceFunction 转换后的具体类型(说白了就是有哪些字段、字段名和类型分别是什么)
设置输入源env.addSource(new TaxiRideGenerator())
的流程如下:
总结一下,addSource 的主要流程如下:
- 使用 SourceFunction(包含 run 方法开始接受/拉取数据、cancel 方法停止接收/拉取数据) 创建 StreamSource(StreamOperator 子类)
- 使用 StreamSource 创建 SourceTransformation
- 使用 SourceTransformation 创建 DataStreamSource(表示输入源的 DataStream)
2.2、对数据进行了哪些操作
以 map 操作(如下)为例,来说明 transformation 具体流程。在这里,map 将 TaxiRide 转换为 (ride.driverId, 1L)
的二元组
DataStream<Tuple2<Long, Long>> tuples = rides.map(new MapFunction<TaxiRide, Tuple2<Long, Long>>() {
@Override
public Tuple2<Long, Long> map(TaxiRide ride) {
return Tuple2.of(ride.driverId, 1L);
}
});
主要流程如下:
map
之后的 keyBy
、sum
和 map
有类似的过程:
- 获取该 transformation 的 return type info(即各个字段名和类型)
- 分为两种情况:
- 若该 transformation 会对 elements 进行转换,则会
- 使用相应的 Function 构造相应的 AbstractUdfStreamOperator;
- 使用 return type info 和相应的 AbstractUdfStreamOperator构造 resultTransformation
- 将 resultTransformation 添加到 env 的
List<Transformation<?>> transformations
中
- 否则,创建相应的 resultTransformation(非 AbstractUdfStreamOperator 子类)
- 若该 transformation 会对 elements 进行转换,则会
- 使用 resultTransformation 构造
returenDataStream: DataStream
2.3、最终计算的结果输出到哪里
最后的 rideCounts.print()
会触发 DataStream.addSink(...)
,其流程也如上面的几步,只是最终返回的类型是 DataStreamSink,并没有直接继承 DataStream,包含一个 transformation
。我们以下图来看 DataStream 从创建、转换、输出的过程(每个 Transformation 都有 id,从 1 开始,下面 5 个 Transformations 的 id 分别是 1、2、3、4、5):
- 除了 SourceTransformation 外,其他类型 Transformation 都有 inputs 来指向其输入的
List<Transformation>
。如上图中红线所示 - DataStream 上的转换调用会最终根据调用顺序依次添加到
StreamExecutionEnvironment
的List<Transformation<?>> #transformations
中;但并不需要 DataStream 的每一个转换调用都添加一个 Transformation 到该 list 中,根据上面提到的 input 指向关系,即使不添加不包含 Function 的 Transformation 也不会丢失任何一个 Transformation
三、生成 StreamGraph
env.execute("Ride Count")
会触发真正的执行,其内部主要分为两步:
- 生成 StreamGraph:通过创建 StreamGraphGenerator 并调用
generate()
方法生成 StreamGraph - execute StreamGraph 返回 JobExecutionResult
我们先来看看 StreamGraph 和 StreamGraphGenerator 的一些概况,再来详细看看 StreamGraphGenerator 是如何生成 StreamGraph 的
3.1、StreamGraphGenerator 生成 StreamGraph
StreamGraphGenerator#generate() 生成 StreamGraph
如上流程图,关键的地方是看 translator 对 transformation 是如何 translateForStreaming 的,我们来看看 OneInputTransformationTranslator#translateForStreaming
,这也是 map、filter 操作对应的 Translator,如下:
最终,上例中的代码生成的 StreamGraph 可以用下面这个图来表示:
在上图中:
- 每个灰色底的大框都是一个 StreamNode
- 蓝色字(如 Source: Custom Source,Map 等)为 node name,其后的数字为 node id
- 每个 StreamNode 主要包含:
- 输入边(可能是多个),输出边(可能是多个);边类型为 StreamEdge:包含
sourceVertex、targetVertex
来说明边是由哪个节点指向哪个节点、StreamPartitioner outputPartitioner
表示 source 节点的数据到 target 节点的是如何分区的 - jobVertexClass:即具体的 StreamTask 具体类型;StreamTask 是不熟给 TaskManagers 用来执行的
- statePartitioners:
- operator:包含具体处理一行行数据的 Function
- parallelism:并发度,并发度是怎么计算的?优先使用用户代码指定的,若无,则使用 env 中默认的并发度,如这里是 12
- 输入边(可能是多个),输出边(可能是多个);边类型为 StreamEdge:包含
对于 StreamGraph 及其生成,以下是一些 QA
Q:为什么上图中没有 id 为 3 的节点 ?
A:有些 transform 操作并不会生成 StreamNode。virtualSideoutputNodes、virtualPartitionNodes 这几类 transform 都会被处理成虚拟节点,当下游生成 StreamNode 后,发现上游为虚拟节点会找到虚拟节点的上游,并创建 StreamEdge 与虚拟节点上游的 transform 进行连接,并把虚拟节点信息写到 StreamEdge 中
- PartitionTransformtion,表示对数据重新分区,会生成个虚拟节点,会添加到
Map<Integer, Tuple3<Integer, StreamPartitioner<?>, ShuffleMode>> virtualPartitionNodes
中 - SideOutputTransformation:表示对上游数据根据 OutoutTag 做一个 selection
Q:StreamGraph 还包含哪些需要注意的东西(除了上述的拓扑关系)?
A:主要有:
-
StateBackend stateBackend
:表示要使用哪种类型的 StateBackend,是 memory 的还是 rocksdb 的 -
Collection<Tuple2<String, DistributedCacheEntry>> userArtifacts
:job 运行依赖的 jar、文件等信息 - 配置信息:savePoint、checkPoint、execution 的配置信息
- 时间特性:使用 event time 还是 process time
-
Set<Integer> sources, Set<Integer> sinks
:表示输入(头结点)、输出(尾节点)的 ids
网友评论