Flink 作业生成①:生成 StreamGraph

一、作业生成及提交整体流程

上图为一个 Flink 作业的提交流程,主要可以分为以下几个步骤:

  1. Client 将作业 code 生成 StreamGraph(在 Batch 模式下,生成的是一个 OptimizedPlan,这里暂不展开):StreamGraph 描述了算子和算子之间逻辑上的拓扑关系
  2. Client 将 StreamGraph 转换为 JobGraph:Operator chain,
    • 将并不涉及到 shuffle 的算子进行合并
    • 对于同一个 operator chain 里面的多个算子,会在同一个 task 中执行
    • 对于不在同一个 operator chain 里的算子,会在不同的 task 中执行
  3. Client 中的 ClusterClient 将 JobGraph 提交给 Dispatcher,Dispatcher 根据 JobGraph 创建相应的 JobManager(代码上是 JobMaster)并运行起来
  4. JobManager 将 JobGraph 转换为 ExecutionGraph
  5. JobManager 将 ExecutionGraph 转换为物理执行计划(可执行)

接下来我们以如下例子来剖析各个步骤具体的执行流程:

public static void main(String[] args) throws Exception {

        // 创建 execution environment
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 设置 data source
        DataStream rides = env.addSource(new TaxiRideGenerator());

        // 将每个 ride 转为 Tuple2(driverId, 1)
        DataStream> tuples = rides.map(new MapFunction>() {
                    @Override
                    public Tuple2 map(TaxiRide ride) {
                        return Tuple2.of(ride.driverId, 1L);
                    }
        });

    // 对 stream 根据 driverId 进行重新分区
        KeyedStream, Long> keyedByDriverId = tuples.keyBy(t -> t.f0);

        // 计算每个司机的 ride 数
        DataStream> rideCounts = keyedByDriverId.sum(1);

        // 将结果进行打印
        rideCounts.print();

        // 开始运行
        env.execute("Ride Count");
}

二、执行 Transformations

该过程发生在 Client 端

上述代码可以分为三个部分,即:

  • 输入的数据源是谁?用一个 DataStreamSource 表示,是该拓扑的头结点
  • 对数据进行了哪些操作?用一个 List> transformations 表示
  • 最终计算的结果输出到哪里?用一个 DataStreamSink 表示,是该拓扑的尾结点

我们分别就这三部分进行说明

2.1、输入的数据源是谁

add Source 相关的调用如下:

DataStream rides = env.addSource(new TaxiRideGenerator());

如上,通过 DataStreamSource StreamExecutionEnvironment#addSource(SourceFunction function) 设置 SourceFunction 并返回一个 DataStreamSource。关于 SourceFunction、SourceContext 等请查看

SourceFunction & SourceContext & StreamSource

  • DataStreamSource:DataStream 子类,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> tuples = rides.map(new MapFunction>() {
         @Override
         public Tuple2 map(TaxiRide ride) {
            return Tuple2.of(ride.driverId, 1L);
         }
});

主要流程如下:

map 之后的 keyBysummap 有类似的过程:

  1. 获取该 transformation 的 return type info(即各个字段名和类型)
  2. 分为两种情况:
    • 若该 transformation 会对 elements 进行转换,则会
      • 使用相应的 Function 构造相应的 AbstractUdfStreamOperator;
      • 使用 return type info 和相应的 AbstractUdfStreamOperator构造 resultTransformation
      • 将 resultTransformation 添加到 env 的 List> transformations
    • 否则,创建相应的 resultTransformation(非 AbstractUdfStreamOperator 子类)
  3. 使用 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 。如上图中红线所示
  • DataStream 上的转换调用会最终根据调用顺序依次添加到 StreamExecutionEnvironmentList> #transformations 中;但并不需要 DataStream 的每一个转换调用都添加一个 Transformation 到该 list 中,根据上面提到的 input 指向关系,即使不添加不包含 Function 的 Transformation 也不会丢失任何一个 Transformation

三、生成 StreamGraph

env.execute("Ride Count") 会触发真正的执行,其内部主要分为两步:

  1. 生成 StreamGraph:通过创建 StreamGraphGenerator 并调用 generate() 方法生成 StreamGraph
  2. 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

对于 StreamGraph 及其生成,以下是一些 QA

Q:为什么上图中没有 id 为 3 的节点 ?

A:有些 transform 操作并不会生成 StreamNode。virtualSideoutputNodes、virtualPartitionNodes 这几类 transform 都会被处理成虚拟节点,当下游生成 StreamNode 后,发现上游为虚拟节点会找到虚拟节点的上游,并创建 StreamEdge 与虚拟节点上游的 transform 进行连接,并把虚拟节点信息写到 StreamEdge 中

  • PartitionTransformtion,表示对数据重新分区,会生成个虚拟节点,会添加到 Map, ShuffleMode>> virtualPartitionNodes
  • SideOutputTransformation:表示对上游数据根据 OutoutTag 做一个 selection

Q:StreamGraph 还包含哪些需要注意的东西(除了上述的拓扑关系)?

A:主要有:

  • StateBackend stateBackend:表示要使用哪种类型的 StateBackend,是 memory 的还是 rocksdb 的
  • Collection> userArtifacts:job 运行依赖的 jar、文件等信息
  • 配置信息:savePoint、checkPoint、execution 的配置信息
  • 时间特性:使用 event time 还是 process time
  • Set sources, Set sinks:表示输入(头结点)、输出(尾节点)的 ids

你可能感兴趣的:(Flink 作业生成①:生成 StreamGraph)