Flink源码解析-从API到JobGraph

首先,本文假定读者对流计算思想已经有基本的认识、对Flink的API已经熟练使用、对Flink的设计思想已经有初步了解,本文着重介绍从api到flinkjob的详细过程。

Streaming API

通常情况下,如果想要使用flink进行并行计算,开发者会把自己的业务逻辑抽象成流式计算的模型,使用flink提供的api定义Job来实现该模型,因此一个flinkjob的生命是从api开始的。我们从一个官网的word count的例子开始,稍做一些修改,代码如下:

如上便是一个flinkJob编写的方式:

首先,代码段1获取一个类StreamExecutionEnvironment的对象,我们稍后会详细介绍这个类;代码段2~7便是利用streamming Api来定义自己的Job,方法:

addSource( ... ),flatMap( ... ), keyBy(0),
timeWindow( ... ), sum( ... ), print( ... )

就是flink提供的API;代码段7是提交上面定义的job 。


StreamExecutionEnvironment, API, transformation

我们来看一下类:StreamExecutionEnvironment,有下面一个属性:

protected final List<StreamTransformation<?>> transformations = new ArrayList<>();
容易看出,transformations是个ArrayList, 元素是StreamTransformation的对象,StreamTransformation把用户通过Streaming API提交的udf(如FlatMapFunction 的对象)作为自己的operator属性存储,同时还把上游的transformation作 为input属性存储。streaming api, transformation, StreamExecutionEnvironment,三者的关系用一句话概括就是:用户通过api构造transformation存储到StreamExecutionEnvironment,如下图:

有以下几点需要指出:

  1. StreamExecutionEnvironment不存储SourceTransformation, 因为flink不允许提交只有Source的job,而根据其他类型的Transformation的input引用可以回溯到SourceTransformation。
  2. Stream可以分为两种类型,一种是继承DataStream类,另一种不继承;功能上的区别在于,前者产生transformation(flink会根据transformation的组织情况构建DAG),后者不产生transformation,但是会赋予Stream一些特殊的功能,例如:window, iterate, union等。下图反映的flink所有的stream类。


    从类名字上也能推断每个类型Stream的功能。
  3. 一个job可以没有sink,其他operator也能良好工作。

从Transformation到StreamGraph

StreamGraph是flink客户端把用户定义的Transformation(就是存储在里面的那个ArrayList< StreamTransformation>)组织成StreamGraph——逻辑上的DAG。代码段:

env.execute("word count on flink");

做了两件事情:

  1. 把Transformation转化成StreamGraph。
  2. 把StreamGraph按照一定的原则切分成JobGraph。

此部分我们先来讨论1,首先Flink到底有多少类型的transformation呢?如下图:

Transformation的结构相对简单,所有的Transformation均继承自StreamTransformation。先来介绍两个概念:StreamNode(DAG图的顶点), StreamEdge(DAG图的边)。两者的关系十分清晰,如下图:

Flink Client会遍历StreamExecutionEnvironment中的transformations数组,按照用户定义构建上面的DAG图。其中:

StreamNode对象里面存储了用户的udf对象、输入输出的序列化方法、所有输入和输出的StreamAdge对象、该StreamNode对应的transformationID等信息。

StreamAdge对象存储了上游的StreamNode、下游的StreamNode还有自身的一个edgeId。

从StreamGraph到JobGraph

上文提到过,StreamGraph会被切分成JobGraph,这里来介绍一下:首先,StreamGraph和JobGraph有什么区别,StreamGraph是逻辑上的DAG图,不需要关心jobManager怎样去调度每个Operator的调度和执行;JobGraph是对StreamGraph进行切分,因为有些节点可以打包放在一起被JobManage安排调度,因此JobGraph的DAG每一个顶点就是JobManger的一个调度单位。假如StreamGraph切分如下图:

那么JobGraph的DAG如下图,绿色实心的两个顶点是上图打包在一起的StreamNode:
可见,StreamGraph的切分,实际上是逐条审查每一个StreamAdge和改SteamAdge两头连接的两个StreamNode的特性,来决定改StreamAdge两头的StreamNode是不是可以打包在一起,flink给出了明确的规则,看面的代码段:

该方法返回true的时候,两端的StreamNode才能打包在一起,几个有趣的条件需要指出:

  • 下游的StreamNode的输入StreamAdge的个数必须是1。
  • 上游的StreamNode和下游的StreamNode必须有相同的SlotSharingGroup(可以在Api中指定该变量)。
  • 上游的StreamNode和下游的StreamNode的必须有相同的并行度(Api可以指定该变量)。

当然其他条件也非常重要,但是在本文,暂不展开。这里我们来着重介绍一下第一条规则:下游的StreamNode的输入StreamAdge的个数必须是1。根据这条规则我们至少可以得到下面两条结论:

  1. 在一个JobGraphNode里可以包含该的多个StreamNode,这些StreamNode是以树状结构组织在一起且只有一颗。
  2. 两个StreamNode V_{1}  V_{2},假设V_{1}V_{2}的上游,从V_{1} V_{2}可能存在多条路径,下面命题成立:如果从某条路径上来看V_{1}V_{2}不能打包在一起,那么肯定不存在一条其他的路径使二者能够打包在一起。换句话说,最终JobGraph的结构和遍历StreamEdge的顺序无关,是唯一的。

至此Flink客户端的工作基本完成,接下来Flink客户的通过Akka把生成的JobGraph提交给JobManager,JobManager开始根据JobGraph部署工作,接下来详细介绍下改过程。

从JobGraph到ExecutionGraph

上文提到,在客户的完成JobGraph的构建之后,将其通过akka提交给JobManager,接下来我们介绍下JobManager怎样按照JobGraph的规划进行任务调度。

JobManager收到客户端提交的JobGraph之后,会构建ExecutionGraph;ExecutionGraph的拓扑结构和JobGraph保持一致,只是把JobGraph重构成ExecutionJobGraph,其中按照JobVertex将顶点分装成ExecutionJobVertex,按照JobEdge将边封装成ExecutionAdge,还构建IntermediateResult(中间数据)用来描述节点之间的Data shuffle 。如下图:

上游节点会把产生的数据写到IntermediateReslut(下文称:中间数据集)中,是中间数据集的生产者;下游节点会处理中间数据集产生的数据,是中间数据集的消费者,具体的可能有下面两种情况:

ExecutionJobGraph有下面几个特点:

  1. Partition的数量和上游节点的并行度保持一致。
  2. 下游节点在和上游节点建立连接时,只有POINTWISE和ALL_TO_ALL两种模式,事实上只有RescalePartitioner和ForwardPartitioner是POINTWISE模式,其他的都是ALL_TO_ALL。默认情况下如果不指定partitioner,如果上游节点和下游节点并行度一样为ForwardPartitioner,否则为RebalancePartioner ,前者POINTWISE,后者ALL_TO_ALL。
编辑于 2016-11-06