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<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 的起点 。详见

    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 之后的 keyBysummap 有类似的过程:

  1. 获取该 transformation 的 return type info(即各个字段名和类型)
  2. 分为两种情况:
    • 若该 transformation 会对 elements 进行转换,则会
      • 使用相应的 Function 构造相应的 AbstractUdfStreamOperator;
      • 使用 return type info 和相应的 AbstractUdfStreamOperator构造 resultTransformation
      • 将 resultTransformation 添加到 env 的 List<Transformation<?>> 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<Transformation> 。如上图中红线所示
  • DataStream 上的转换调用会最终根据调用顺序依次添加到 StreamExecutionEnvironmentList<Transformation<?>> #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<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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容