Flink运行时之流处理程序生成流图

流处理程序生成流图

DataStream API所编写的流处理应用程序在生成作业图(JobGraph)并提交给JobManager之前,会预先生成流图(StreamGraph)。

什么是流图

流图(StreamGraph)是表示流处理程序拓扑的数据结构,它封装了生成作业图(JobGraph)的必要信息。它的类继承关系如下图所示:

当你基于StreamGraph的继承链向上追溯,会发现它实现了FlinkPlan接口。

Flink效仿了传统的关系型数据库在执行SQL时生成执行计划并对其进行优化的思路。FlinkPlan是Flink生成执行计划的基接口,定义在Flink优化器模块中,流处理程序对应的计划是StreamingPlan,但是当前针对流处理程序没有进行优化,因此这个类可看作是一个预留设计。

一个简单的实现“word count”的流处理程序,其StreamGraph的形象化表示如下图:

Flink官方提供了一个计划可视化器来图形化执行计划,该计划可视化器基于Flink API所生成的计划的JSON格式表示绘制图形。但是需要注意的是,计划的JSON形式表示缺失了很多属性以及部分节点(比如虚拟节点等);

上面的图是由“节点”和“边”组成的。节点在Flink中对应的数据结构是StreamNode,而边对应的数据结构是StreamEdge,StreamNode和StreamEdge之间有着双向的依赖关系。StreamEdge包含了其连接的源节点sourceVertex和目的节点targetVertex:

而StreamNode中包含了与其连接的入边集合inEdges和出边集合outEdges:

StreamEdge和StreamNode都有唯一的编号进行标识,但是各自编号的生成规则并不相同。

StreamNode的编号id的生成是通过调用StreamTransformation的静态方法getNewNodeId获得的,其实现是一个静态计数器:

protected static Integer idCounter = 0;
public static int getNewNodeId() {
    idCounter++;
    return idCounter;
}

StreamEdge的编号edgeId是字符串类型,其生成的规则为:

this.edgeId = sourceVertex + "_" + targetVertex + "_" + typeNumber + "_" + selectedNames
                + "_" + outputPartitioner;

它是由多个段连接起来的,语义的文字表述如下:

源顶点_目的顶点_输入类型数量_输出选择器的名称_输出分区器

edgeId除了用来实现StreamEdge的hashCode及equals方法之外并没有其他实际意义。

StreamNode是表示流处理中算子的数据结构,source和sink在StreamGraph中也是以StreamNode表示,它们也是一种算子,只是因为它们是流的输入和输出因而有特定的称呼。

StreamNode除了存储了输入端和输出端的StreamEdge集合,还封装了算子的其他关键属性,比如其并行度、分区的键信息、输入与输出类型的序列化器等。

从直观上来看你已经知道了StreamNode和StreamEdge是StreamGraph的重要组成部分,但是为了生成JobGraph,StreamGraph很显然必须得包含更多的内容。总结一下,StreamGraph中包含的属性可分为三大类:

  • 流处理程序的执行配置;
  • 流处理程序拓扑中包含的节点和边的信息;
  • 迭代相关的信息;

当然围绕这些属性的方法非常多,比如添加边和节点,创建迭代的source/sink等。

其中的一个关键方法getJobGraph将用于生成JobGraph:

public JobGraph getJobGraph() {
    if (isIterative() && checkpointConfig.isCheckpointingEnabled()
        && !checkpointConfig.isForceCheckpointing()) {
        throw new UnsupportedOperationException(
            "Checkpointing is currently not supported by default for iterative jobs, as we cannot guarantee exactly once semantics. "
            + "State checkpoints happen normally, but records in-transit during the snapshot will be lost upon failure. "
            + "\nThe user can force enable state checkpoints with the reduced guarantees by calling: env.enableCheckpointing(interval,true)");
    }
    StreamingJobGraphGenerator jobgraphGenerator = new StreamingJobGraphGenerator(this);
    return jobgraphGenerator.createJobGraph();
}

从上面的代码段也可见,当流处理程序中包含迭代逻辑时,检查点功能暂时不被支持,在异常信息中Flink阐述了缘由:在迭代作业中无法保证“恰好一次”的语义。

流处理程序依赖StreamingJobGraphGenerator来生成JobGraph,至于如何生成,后续会进行剖析。

生成流图的源码分析

了解了什么是流图(StreamGraph)之后,我们来分析它是如何生成的。流图的生成是通过StreamExecutionEnvironment的getStreamGraph实例方法触发的:

public StreamGraph getStreamGraph() {
    if (transformations.size() <= 0) {
        throw new IllegalStateException("No operators defined in streaming topology. Cannot execute.");
    }
    return StreamGraphGenerator.generate(this, transformations);
}

从代码段中可见,StreamGraph的生成依赖于一个名为transformations的集合对象,它是环境对象所收集到的所有的转换对象的集合,该集合中存储着一个流处理程序中所有的转换操作对应的StreamTransformation对象。

每当在DataStream对象上调用transform方法或者调用已经被实现了的一些内置的转换函数(如map、filter等,这些转换函数在内部也调用了transform方法),这些调用都会使得其对应的转换对象被加入到transformations集合中去。StreamTransformation表示创建DataStream对象的转换,流处理程序中存在多种DataStream,每种底层都对应着一个StreamTransformation。DataStream持有执行环境对象的引用,当调用transform方法时,它会调用执行环境对象的addOperator方法,将特定的StreamTransformation对象加入到transformations集合中去,这就是transformations集合中元素的来源。

DataStream API的设计存在着多重对象的封装,我们以flatMap转换操作为例图示各种对象之间的构建关系:

在Flink的源码中,这些对象的命名也并不是那么准确,比如上图中的SingleOutputStreamOperator其实是一种DataStream,但却以Operator结尾,让人匪夷所思。因此较为准确的鉴定它们类型的方式是通过查看它们的继承链来进行识别。

StreamGraph的生成依赖于生成器StreamGraphGenerator,每调用一次静态方法generate才会在内部创建一个StreamGraphGenerator的实例,一个实例对应着一个StreamGraph对象。StreamGraphGenerator调用内部的实例方法generateInternal来遍历transformations集合的每个对象:

private StreamGraph generateInternal(List<StreamTransformation<?>> transformations) {
    for (StreamTransformation<?> transformation: transformations) {
        transform(transformation);
    }
    return streamGraph;
}

在transform方法中,它枚举了Flink中每一种转换类型,并对当前传入的转换类型进行判断,然后将其分发给特定的转换方法进行转换,最终返回当前StreamGraph对象中跟该转换有关的节点编号集合。

这里我们以常用的单输入转换方法transformOnInputTransform为例来进行分析:

private <IN, OUT> Collection<Integer> transformOnInputTransform(
    OneInputTransformation<IN, OUT> transform) {
    //递归地对该转换的输入端进行转换
    Collection<Integer> inputIds = transform(transform.getInput());
    // 递归调用可能会产生重复,这里需要以转换过的对象进行检查
    if (alreadyTransformed.containsKey(transform)) {
        return alreadyTransformed.get(transform);
    }

    //结合输入端对应的节点编号来判断并得出槽共享组的名称
    String slotSharingGroup = determineSlotSharingGroup(transform.getSlotSharingGroup(), inputIds);
    //将当前算子(节点)加入到流图中
    streamGraph.addOperator(transform.getId(),
        slotSharingGroup,
        transform.getOperator(),
        transform.getInputType(),
        transform.getOutputType(),
        transform.getName());
    //如果有键选择器,则进行设置
    if (transform.getStateKeySelector() != null) {
        TypeSerializer<?> keySerializer =
            transform.getStateKeyType().createSerializer(env.getConfig());
        streamGraph.setOneInputStateKey(transform.getId(),
            transform.getStateKeySelector(), keySerializer);
    }
    streamGraph.setParallelism(transform.getId(), transform.getParallelism());
    //构建从当前转换对应的节点到输入转换对应的节点之间的边
    for (Integer inputId: inputIds) {
        streamGraph.addEdge(inputId, transform.getId(), 0);
    }
    //返回当前转换对应的节点编号
    return Collections.singleton(transform.getId());
}

每遍历完一个转换对象,就离构建完整的流图更近一步。不同的转换操作类型,它们为流图提供的“部件”并不完全相同,有的转换只构建节点(如SourceTransformation),有的转换除了构建节点还构建边(如SinkTransformation),有的只构建虚拟节点(如PartitionTransformation、SelectTransformation等)。

关于虚拟节点,这里需要说明的是并非所有转换操作都具有实际的物理意义(即物理上对应具体的算子)。有些转换操作只是逻辑概念(例如select,split,partition,union),它们不会构建真实的StreamNode对象。比如某个流处理应用对应的转换树如下图:

但在运行时,其生成的StreamGraph却是下面这种形式:

从图中可以看到,转换树中对应的一些逻辑操作在StreamGraph中并不存在,Flink将这些逻辑转换操作转换成了虚拟节点,它们的信息会被绑定到从source到map转换的边上。

Flink当前对于流处理的程序是不作优化的,所以StreamGraph就是它的执行计划。你可以通过Flink提供的执行计划的可视化器将StreamGraph所表述的信息以图形化的方式展示出来,就像上文我们展示的那幅图一样。那么我们如何查看我们自己所编写的程序的执行计划呢?其实很简单,我们以Flink源码中flink-examples-streaming模块中的SocketTextStreamWordCount为例,来看一下如何生成执行计划。

我们将SocketTextStreamWordCount最后一行代码注释掉:

env.execute("WordCount from SocketTextStream Example");

然后将其替换成下面这句:

System.out.println(env.getExecutionPlan());

这行语句的作用是打印当前这个程序的执行计划,它将在控制台产生该执行计划的JSON格式表示:

{"nodes":[{"id":1,"type":"Source: Socket Stream","pact":"Data Source","contents":"Source: Socket Stream",
"parallelism":1},{"id":2,"type":"Flat Map","pact":"Operator","contents":"Flat Map","parallelism":2,
"predecessors":[{"id":1,"ship_strategy":"REBALANCE","side":"second"}]},{"id":4,"type":"Keyed Aggregation",
"pact":"Operator","contents":"Keyed Aggregation","parallelism":2,"predecessors":[{"id":2,
"ship_strategy":"HASH","side":"second"}]},{"id":5,"type":"Sink: Unnamed","pact":"Data Sink",
"contents":"Sink: Unnamed","parallelism":2,"predecessors":[{"id":4,"ship_strategy":"FORWARD",
"side":"second"}]}]}

把上面这段JSON字符串复制到Flink的执行计划可视化器的输入框中,然后点击下方的“Draw”按钮,即可生成。


微信扫码关注公众号:Apache_Flink


QQ扫码关注QQ群:Apache Flink学习交流群(123414680)

时间: 2024-10-11 10:24:04

Flink运行时之流处理程序生成流图的相关文章

Flink执行时之流处理程序生成流图

流处理程序生成流图 DataStream API所编写的流处理应用程序在生成作业图(JobGraph)并提交给JobManager之前,会预先生成流图(StreamGraph). 什么是流图 流图(StreamGraph)是表示流处理程序拓扑的数据结构.它封装了生成作业图(JobGraph)的必要信息.它的类继承关系例如以下图所看到的: 当你基于StreamGraph的继承链向上追溯,会发现它实现了FlinkPlan接口. Flink效仿了传统的关系型数据库在运行SQL时生成运行计划并对其进行优

Flink运行时之客户端提交作业图-下

submitJob方法分析 JobClientActor通过向JobManager的Actor发送SubmitJob消息来提交Job,JobManager接收到消息对象之后,构建一个JobInfo对象以封装Job的基本信息,然后将这两个对象传递给submitJob方法: case SubmitJob(jobGraph, listeningBehaviour) => val client = sender() val jobInfo = new JobInfo(client, listeningB

使用Visual VM监控运行时的项目

1.1 Visual VM简介 VisualVM 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息.在 VisualVM 的图形用户界面中,您可以方便.快捷地查看多个 Java 应用程序的相关信息.(摘自官方) 简单说来,VisualVM是一种集成了多个JDK命令行工具的可视化工具,它能为您提供强大的分析能力.所有这些都是免费的!它囊括的命令行工具包括jstat, JConsole, jstack, jmap 和 jinfo

Java虚拟机学习--记录运行时数据区域

为方便后面学习的理解,记录一下! 运行时数据区 1.线程共享 1.1方法区(Method Area) 1.1.1运行时常量池(Runtime Constant Pool) 1.2堆(Heap) 2.线程私有 2.1虚拟机栈(VM Stack) 2.2本地方法栈(Native Method Stack) 2.3程序计数器(Program Counter Register) 3.直接内存(Direct Memory) 虚拟机栈: 线程私有,生命周期与线程同步,用来执行Java方法. 每个java方法

BI - ETL运行时监控

对于任何事物而言,监督都是提高自身的有效手段,BI也是如此.从我个人的经验而言,BI的监督可以分为两类(欢迎拍砖讨论):运行时监督(Runtime Monitoring)与数据仓库健康状况的监督(DW Healthy Monitoring): 1. 运行时监督 所谓运行时监督是指监督数据从数据源到流到数据仓库的过程,通俗来讲就是监督ETL的执行过程.我相信绝大多数的BI系统都具有该功能,区别只在于实现监控的方式以及监控信息的详细程度 2. 数据仓库健康状况监督 所谓数据仓库的健康状况监督,其实就

第24章 运行时序列化

什么是序列化和反序列化 序列化(serialization)是将一个对象或者对象图(对象在特定的时间点的一个视图)转换成一个字节流的过程.反序列化(deserialization)是将一个字节流转换回对象图的过程. 应用场景: 应用程序的状态(对象图)可以保存到磁盘文件或数据库中,并在应用程序下次运行时恢复. 一组对象可以轻松复制到Windows 窗体的剪贴板中,再粘贴回同一个或者另一个应用程序. 将对象按值从一个应用程序域中发送到另一个程序域 24.1 序列化/反序列化快速入门 http://

Spring XD简介:大数据应用的运行时环境

简介 Spring XD(eXtreme Data,极限数据)是Pivotal的大数据产品.它结合了Spring Boot和Grails,组成Spring IO平台的执行部分.尽管Spring XD利用了大量现存的Spring项目,但它是一种运行时环境,而不是一个类库或者框架,它包含带有服务器的bin目录,你可以通过命令行启动并与之交互.运行时可以运行在开发机上.客户端自己的服务器上.AWS EC2上或者Cloud Foundry上. Spring XD中的关键组件是管理和容器服务器(Admin

高铁在高速运行时的电力是如何提供的?

高铁在高速运行时的电力是如何提供的? 铁路机车是个庞大的家族,高铁只是这个大家庭的一个新成员,如果要连篇累牍赘述其他车辆,恐怕这个答案是写不下的,故本文针对高速铁路进行讨论. 一. 高铁列车的动力来源是交流电还是直流电? 各国高铁基本采用交流电作为高铁列车的牵引网络的电流制式. (萌萌的意呆立除外.在高铁电流制式这个问题上,全世界都摸着意呆立过河) 二. 高速列车如何获取电能作为动力? (从电路角度来看,高铁采取AT(自耦变压器)供电方式. ) 高铁能够跑起来,依靠的是牵引供电系统给高速列车提供

JVM运行时的内存结构

我们都知道,JVM的垃圾收集机制能够帮开发者自动管理内存,了解JVM运行时的内存结构是理解垃圾收集机制的前提.本文主要简单介绍JVM运行时的内存结构. [JVM运行时内存中不同的数据区域] 一.PC寄存器 做过嵌入式开发或者底层编程的朋友应该都知道PC寄存器的作用,它就相当于在程序流中的游标,指示线程当前的运行位置.但是要注意,如果虚拟机执行的是本地native方法,则PC寄存器为undefined状态. 一个线程持有一个独立的PC寄存器. 二.java栈 每一个JVM线程都会拥有一个java栈