flink-1.12.1源码阅读之计算流程
这里以源码工程中提供的一个完整实例,来看flink流计算的完成流程,在此过程中会结合其核心概念进行展开解析.
1 调试
这里使用flink源码工程中的examples子工程,直接在streaming工程里的WordCount(org.apache.flink.streaming.examples.wordcount)类中打断点,如下
2 flink流计算的整体概述
2.1 flink流计算相关的任务概念
Flink的流计算针对计算本身分为job和task,task由不同的算子组成.
先从最小的算子说起.算子代表一个完整的计算逻辑,比如map等,这些算子本身有并行度的概念,比如map的并行度为4,那么在同一时刻最多就是在4个线程中并行执行该map操作.算子分为shuffle算子和普通算子,这个和spark的操作是一样的.
Task可以是单个算子,也可以是一串算子组成的链条,这个划分的依据就是当算子的并行度发生变化或者出现shuffle算子(例如分组算子keyBy)时,就分为前后两个task,同时还可以手动通过API明确设置,在这前后也会产生两个task。
subtask或者是子task,就是指这些task的并行度,每个为一个subtask.
Job为一个作业,由不同的task组成,比如上述的wordCount计算,读取一个字符串然后计算出其中每个单词的个数,该job就由source任务、flatMap任务、keyBy、keyBy后的sum算子和sink这3个任务组成,整个过程就是一个job.
2.2 flink流计算相关的阶段概念
每个flink的执行作业(job)都可以在时间线上分为三个阶段:
1 读如数据源
2 transform操作,包括所有的算子
3 持久化数据
数据源就是source相关的操作,transform就是在source之后,sink之前的所有操作,最后就是sink操作,也就是数据的持久化.
3 第一个断点fromElements方法
该方法就是读如数据源,这里很简单,就是一个字符串,其调用的方法树为
其方法比较简单,最后返回一个DataStreamSource对象.这里需要注意的是其上标有五角星形状的FromElementsFunction的构造方法,其使用了序列化器把输入的字符串序列化了,进入该方法
可见,在构造方法内已经完成了序列化.
4 第二个断点execute方法
这是具体的执行方法,里面会涉及flink的一些核心概念,在遇到时会展开分析.
其调用的方法树为
可见,首先是创建streamGraph.
4.1 getStreamGraph方法
其调用的方法树为
核心方法为generate方法,其调用的方法树为
重要方法为transform方法,此方法根据程序进行了很多流的转换,同时生成每个转换的之前的输入和转换之后的输出,组成带有定点和边的dag图,flink提供了一个StreamGraph可视化显示工具,可以进行查看生成的dag图.
4.2 execute(StreamGraph)方法
其调用的方法树为
重要方法executeAsync的调用方法树为
重要方法为execut,其调用的方法树为
重要方法为getJobGraph和submitJob.
4.3 getJobGraph方法
该方法就是生成job图,进入该方法,其调用的方法树为
最后调用的是FlinkPipelineTranslationUtil的getJobGraph方法,其调用的方法树为
最终调用的是StreamingJobGraphGenerator的createJobGraph方法,其调用的方法树为
从被标注的第一个方法开始,一共有5个重要方法,这些方法的逻辑如下:
第一个方法,设置改job图的每个计算阶段的hash值,用作唯一标识每个计算阶段
第二个方法,设置dag图的边
第三个方法,设置dag图的顶点
第四个方法,设置每个算子的内存使用比例,这个比例非0即1,不存在0和1之间的小数
第五个方法,设置保存点
该方法最终创建了jobGraph,然后提交到执行器去执行.
4.4 submitJob方法
submitJob方法的调用方法树为
可见,最后是调用了Dispatcher的internalSubmitJob方法,其调用的方法树为
最后调用的就是persistAndRunJob方法,其调用的方法树为
4.5 生成executionGraph
执行图就是在上面的runJob中生成的,其调用方法树为:
重要方法为JobManagerRunnerImpl的构造方法,其调用的方法树为
重要方法为JobMaster的构造方法,其调用的方法树为
其中createScheduler方法的调用方法树为
重要方法createInstance的调用方法树为
DefaultScheduler的构造方法的调用方法树为
至此,由SchedulerBase的createExecutionGraph方法开始创建executionGraph,其具体过程就是依据job图,对应定点和边创建execution的顶点和边.
4.6 根据executionGraph执行task
上面提到的在创建jobMaster的同时也启动了jobMaster,进入其start方法,调用树为
重要方法resetAndStartScheduler的调用树为
重要方法startSchedulingInternal的调用树为
重要方法allocateSlotsAndDeploy的调用树为
重要方法deployOrHandleError的调用树为
重要方法deploy的调用树为
至此,由TaskManagerGateway的submitTask方法就把task提交了,调用的方法就是TaskExecutor类的submitTask方法,进入该方法,其调用方法树为
可见,首先创建了task,其次启动了task的run方法,进入其run方法,调用的方法树为
主要就是doRun方法,可以预见,此方法很长,其调用的方法树为
主要方法就是loadAndInstantiateInvokable和invoke方法,这很明显,就是要执行task中的逻辑了.
至此,就剩下executionGraph是怎么划分为task的部分没有说了.这部分并不难,根据源码可以看到,其实主要就是根据定点来确定task的划分,由于边代表了数据的输入和输出,所以再结合定点就可以把每一个task都给确定了,读者可以按照本文中给出的源码阅读流程找到该Task的构造方法进去一窥究竟.
4.7 flink的rpc
Flink的rpc和spark还有hadoop等都不同,其使用了akka来实现,具体各个业务逻辑交由actor来执行.
Actor的写法已经很固定了,而且flink并没有使用过多的包装来使用actor,整体上也非常直白和简洁,所以这里不再过多的展开叙述.
Actor是一个非常好的并发框架,本人在有时间时会作为补充做一个原理的介绍和说明.
4.8 slot的概念和共享的解释
在flink的领域内,slot更接近于近代可插拔的cpu插槽,所以在jvm进程中,flink有意根据slot数目把可使用内存按照内存页的形式进行了平分.这样以来,原本更接近于cpu概念的slot就转变为了可以代表一定内存资源的资源单位,由jvm进程根据task或subtask启动的线程只能占用其slot的内存,不能共享同一进程下其它slot的内存,实现了同一进程内线程的内存隔离,但没有限制cpu的使用,也就是说cpu对这线程并不隔离.
Slot共享的概念其实容易使人产生疑惑.上面所说,slot是隔离线程使用的内存的,这里的共享并不是指内存共享,而是指不同的task或subTask可以在同一个slot中起线程来执行.比如本来有一个slot只为map类型的task起线程,在共享后也可以为sum类型的task起线程来执行了,这种不再区分task的slot就是共享.
5 总结
Flink的流计算整体上要经过三层图和最后一步执行来完成,总结起来就是:
1 flink由客户端提交后,交由dispatcher来分发任务,交由taskExecutor来执行
2 flink的执行管理由jobManager和taskManager组成,jobManager负责生成executionGraph,之后部署到taskManager上执行
3 flink使用了akka作为并行框架,整体上来看还是很符合flink的设计实现
从flink的流计算过程可以看到,其整体设计是非常好的,同时很多概念也是很清晰的,代码质量也是目前我所看过的源码中最好的.