八、Flink运行时架构
8.1 Flink运行时组件
8.1.1 作业管理器(JobManager)
- 控制一个应用程序执行的主进程,也就是说,每个应用程序 都会被一个不同的Jobmanager所控制执行
- Jobmanager会先接收到要执行的应用程序,这个应用程序会包括:作业图( Job Graph)、逻辑数据流图( ogical dataflow graph)和打包了所有的类、库和其它资源的JAR包。
- Jobmanager会把 Jobgraph转换成一个物理层面的 数据流图,这个图被叫做 “执行图”(Executiongraph),包含了所有可以并发执行的任务。Job Manager会向资源管理器( Resourcemanager)请求执行任务必要的资源,也就是( 任务管理器( Taskmanager)上的插槽slot。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的 Taskmanager上。而在运行过程中Jobmanagera会负责所有需要中央协调的操作,比如说检查点( checkpoints的协调。
8.1.2 任务管理器(Taskmanager)
- Flink中的工作进程。通常在 Flink中会有多个 Taskmanageria运行, 每个 Taskmanageri都包含了一定数量的插槽( slots)。插槽的数量限制了Taskmanageri能够执行的任务数量。
- 启动之后, Taskmanager会向资源管理器注册它的插槽;收到资源管理器的指令后, Taskmanageri就会将一个或者多个插槽提供给Jobmanageri调用。Jobmanager就可以向插槽分配任务( tasks)来执行了。
- 在执行过程中, 一个 Taskmanagera可以跟其它运行同一应用程序的Taskmanager交换数据。
8.1.3 资源管理器(Resource Manager)
- 主要负责管理任务管理器( Task Manager)的 插槽(slot)Taskmanger插槽是 Flink中定义的处理资源单元。
- Flink 为不同的环境和资源管理工具提供了不同资源管理器,比如YARNMesos、K8s,以及 standalone部署。
- 当 Jobmanagerl申请插槽资源时, Resourcemanager会将有空闲插槽的Taskmanager?分配给Jobmanager。如果 Resourcemanagery没有足够的插槽来满足 Jobmanagerf的请求, 它还可以向资源提供平台发起会话,以提供启动 Taskmanageri进程的容器。
8.1.4 分发器(Dispatcher)
- 可以跨作业运行,它为应用提交提供了REST接口。
- 当一个应用被提交执行时,分发器就会启动并将应用移交给Jobmanage
- Dispatcher他会启动一个 WebUi,用来方便地 展示和监控作业执行的信息。
- Dispatcher?在架构中可能 并不是必需的,这取決于应用提交运的方式。
8.2 任务提交流程
1. 提交应用 2. 启动并提交应用 3. 请求slots 4. 任务启动 5. 注册slots 6. 发出提供slot的指令 7. 提供slots 8. 提交要在slots中执行的任务 9. 交换数据
8.3 Job提交流程
8.3.1 YARN
1. Flink任务提交后,Client向HDFS上传Flink的Jar包和配置 2. 随后向Yarn ResourceManager 提交任务,ResourceManager分配Container资源并通知对应的NodeManager启动 3. ApplicationMaster,ApplicationMaster 启动后加载Flink的Jar包和配置构建环境 4. 然后启动JobManager,之后ApplicationMaster向ResourceManager申请资源启动TaskManager 5. ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager 6. NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager 7. TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务。
8.3.2 会话(Session)模式
在会话模式下,我们需要先启动一个YARN session,这个会话会创建一个Flink集群。
这里只启动了JobManager,而TaskManager可以根据需要动态地启动。在JobManager内部,由于还没有提交作业,所以只有ResourceManager和Dispatcher在运行。
可见,整个流程除了请求资源时要“上报”YARN的资源管理器,其他与7.2.1节所述抽象流程几乎完全一样。
8.3.3 单作业(Per-Job)模式
在单作业模式下,Flink集群不会预先启动,而是在提交作业时,才启动新的JobManager。
可见,区别只在于JobManager的启动方式,以及省去了分发器。当第2步作业提交给JobMaster,之后的流程就与会话模式完全一样了。
8.3.4 应用(Application)模式
应用模式与单作业模式的提交流程非常相似,只是初始提交给YARN资源管理器的不再是具体的作业,而是整个应用。一个应用中可能包含了多个作业,这些作业都将在Flink集群中启动各自对应的JobMaster。
8.4 任务调度原理
8.5 TaskManager 和 Slots
1.Flink中每一个Taskmanageri都是一个JMM进程,它可能会在独立的线程上执行一个或多个subtask 2.为了控制一个Taskmanageri能接收多少个task, Taskmanager通过task slot来进行控制(一个Taskmanager至少有一个slot) 假如一个TaskManager有三个slot,那么它会将管理的内存平均分成三份,每个slot独自占据一份。这样一来,我们在slot上执行一个子任务时,相当于划定了一块内存“专款专用”,就不需要跟来自其他作业的任务去竞争内存资源了。所以现在我们只要2个TaskManager,就可以并行处理分配好的5个任务了。
3.默认情况下,Fink允许子任务共享slot,即使它们是不同任务的子任务。这样的结果是,一个slot可以保存作业的整个管道。 4.Task Slot是静态的概念,是指Taskmanager具有的并发执行能力
两者关系:
Slot和并行度都跟程序的并行执行有关,但两者是完全不同的概念。简单来说,task slot是静态的概念,是指TaskManager具有的并发执行能力,可以通过参数taskmanager.numberOfTaskSlots进行配置;而并行度(parallelism)是动态概念,也就是TaskManager运行程序时实际使用的并发能力,可以通过参数parallelism.default进行配置。
下面我们再举一个具体的例子。假设一共有3个TaskManager,每一个TaskManager中的slot数量设置为3个,那么一共有9个task slot,表示集群最多能并行执行9个任务。
而我们定义wordcount程序的处理操作是四个转换算子:
“source→ flatmap→ reduce→ sink
”
当所有算子并行度相同时,容易看出source和flatmap可以合并算子链,于是最终有三个任务节点。
如果我们没有任何并行度设置,而配置文件中默认parallelism.default=1,那么程序运行的默认并行度为1,总共有3个任务。由于不同算子的任务可以共享任务槽,所以最终占用的slot只有1个。9个slot只用了1个,有8个空闲,如图中的Example 1所示。
8.6 程序与数据流(DataFlow)
1. 所有的Flink程序都是由三部分组成的:Source、 Transformation和Sink 2. Source负责读取数据源,Transformation利用各种算子进行处理加工,Sink负责输出 3. 在运行时,Flink上运行的程序会被映射成 “逻辑数据流”( dataflows),它包含了这三部分 4. 每一个dataflow以一个或多个Sources开始以一个或多个sinks结束。dataflow类以于任意的有向无环图(DAG) 5. 在大部分情况下,程序中的转换运算( transformations)跟 dataflow中的算子
8.7 执行图(ExecutionGraph)
Flink中的执行图可以分成四层:
“Streamgraph -> Jobgraph -> Executiongraph -> 物理执行图
”
1.Streamgraph:是根据用户通过Stream API编写的代码生成的最初的图。用来表示程序的拓扑结构。 2.Jobgraph: Streamgraph经过优化后生成了Jobgraph,提交给Jobmanager的数据结构。主要的优化为,将多个符合条件的节点chain在一起作为一个节点Execution Graph: Jobmanager根据Jobgraph生成 3.ExecutiongraphExecution Graph是 Job Graphi的并行化版本,是调度层最核心的数据结构。 4.物理执行图:Jobmanager根据Executiongraph对Job进行调度后,在各个Taskmanager上部署Task后形成的“ 图”,并不是一个具体的数据结构。
8.8 并行度(Parallelism)
1. 特定算子的子任务( subtask)的个数被称之为其并行度( parallelism)般情况下,一个 stream的并行度,可以认为就是其所有算子中最大的并行度。 2. 一个程序中,不同的算子可能具有不同的并行度 3. 算子之间传输数据的形式可以是 one-to-one( (forwarding)的模式也可以是 redistributing的模式,具体是哪一种形式,取決于算子的种类 4. One-to-one: stream维护着分区以及元素的顺序(比如 Sources和map之间)。这意味着map算子的子任务看到的元素的个数以及顺序跟 Source算子的子任务生产的元素的个数、顺序相同。map、 fliter、flatmap等算子都是one-to-one的对应关系 5. Redistributing: stream的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如keyby基于 hash Code重分区、而broadcast和rebalance会随机重新分区,这些算子都会引起distributer过程,而redistribute过程就类似于Spark中的shuffle过程。
并行度的设置
- 代码中设置我们在代码中,可以很简单地在算子后跟着调用setParallelism()方法,来设置当前算子的并行度:
“stream.map(word -> Tuple2.of(word, 1L)).setParallelism(2);
”
这种方式设置的并行度,只针对当前算子有效。
另外,我们也可以直接调用执行环境的setParallelism()方法,全局设定并行度:
“env.setParallelism(2);
”
这样代码中所有算子,默认的并行度就都为2了。我们一般不会在程序中设置全局并行度,因为如果在程序中对全局并行度进行硬编码,会导致无法动态扩容。
这里要注意的是,由于keyBy不是算子,所以无法对keyBy设置并行度。
- 提交应用时设置在使用flink run命令提交应用时,可以增加-p参数来指定当前应用程序执行的并行度,它的作用类似于执行环境的全局设置:
bin/flink run –p 2 –c com.atguigu.wc.StreamWordCount ./FlinkTutorial-1.0-SNAPSHOT.jar
如果我们直接在Web UI上提交作业,也可以在对应输入框中直接添加并行度。
- 配置文件中设置我们还可以直接在集群的配置文件flink-conf.yaml中直接更改默认并行度:
“parallelism.default: 2这个设置对于整个集群上提交的所有作业有效,初始值为1。无论在代码中设置、还是提交时的-p参数,都不是必须的;所以在没有指定并行度的时候,就会采用配置文件中的集群默认并行度。在开发环境中,没有配置文件,默认并行度就是当前机器的CPU核心数。
”
8.9 任务链(Operator Chains)
1. Flink采用了一种称为任务链的优化技术,可以在特定条件下减少本地通信的开销。为了满足任务链的要求 ,必须将两个或多个算子设为相同的并行度,并通过本地转发( ocal forward)的方式进行连接 2. 相同并行度的one-to-one操作, Flink这样相连的算子链接在一起形成一个task,原来的算子成为里面的subtask并行度相同、并且是one-to-one操作,两个条件缺一不可
8.9.1 算子链(Operator Chain)
算子间的数据传输
一个数据流在算子之间传输数据的形式可以是一对一(one-to-one)的直通 (forwarding)模式,也可以是打乱的重分区(redistributing)模式,具体是哪一种形式,取决于算子的种类。
- 一对一(One-to-one,forwarding)
这种模式下,数据流维护着分区以及元素的顺序。比如图中的source和map算子,source算子读取数据之后,可以直接发送给map算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着map 算子的子任务,看到的元素个数和顺序跟source 算子的子任务产生的完全一样,保证着“一对一”的关系。map、filter、flatMap等算子都是这种one-to-one的对应关系。 这种关系类似于Spark中的窄依赖。
- 重分区(Redistributing)
在这种模式下,数据流的分区会发生改变。比图中的map和后面的keyBy/window算子之间(这里的keyBy是数据传输算子,后面的window、apply方法共同构成了window算子),以及keyBy/window算子和Sink算子之间,都是这样的关系。 每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。例如,keyBy()是分组操作,本质上基于键(key)的哈希值(hashCode)进行了重分区;而当并行度改变时,比如从并行度为2的window算子,要传递到并行度为1的Sink算子,这时的数据传输方式是再平衡(rebalance),会把数据均匀地向下游子任务分发出去。这些传输方式都会引起重分区(redistribute)的过程,这一过程类似于Spark中的shuffle。
总体说来,这种算子间的关系类似于Spark中的宽依赖。
8.9.2 合并算子链
在Flink中,并行度相同的一对一(one to one)算子操作,可以直接链接在一起形成一个“大”的任务(task),这样原来的算子就成为了真正任务里的一部分。每个task会被一个线程执行。这样的技术被称为“算子链”(Operator Chain)。
比如在图中的例子中,Source和map之间满足了算子链的要求,所以可以直接合并在一起,形成了一个任务;因为并行度为2,所以合并后的任务也有两个并行子任务。这样,这个数据流图所表示的作业最终会有5个任务,由5个线程并行执行。
将算子链接成task是非常有效的优化:可以减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。
Flink默认会按照算子链的原则进行链接合并,如果我们想要禁止合并或者自行定义,也可以在代码中对算子做一些特定的设置:
- 禁用算子链
.map(word -> Tuple2.of(word, 1L)).disableChaining();
- 从当前算子开始新链
.map(word -> Tuple2.of(word, 1L)).startNewChain()
8.10 作业图(JobGraph)与执行图(ExecutionGraph)
我们已经彻底了解了由代码生成任务的过程,现在来做个梳理总结。
“由Flink程序直接映射成的数据流图(dataflow graph),也被称为逻辑流图(logical StreamGraph),因为它们表示的是计算逻辑的高级视图。到具体执行环节时,我们还要考虑并行子任务的分配、数据在任务间的传输,以及合并算子链的优化。为了说明最终应该怎样执行一个流处理程序,Flink需要将逻辑流图进行解析,转换为物理数据流图。
”
在这个转换过程中,有几个不同的阶段,会生成不同层级的图,其中最重要的就是作业图(JobGraph)和执行图(ExecutionGraph)。Flink中任务调度执行的图,按照生成顺序可以分成四层
“逻辑流图(StreamGraph)→ 作业图(JobGraph)→ 执行图(ExecutionGraph)→ 物理图(Physical Graph)。
”
我们可以回忆一下之前处理socket文本流的WordCountL程序:
env.socketTextStream().flatMap(…).keyBy(0).sum(1).print();
如果提交时设置并行度为2:
bin/flink run –p 2 –c com.liuhao.WordCountL ./FlinkTutorial-1.0-SNAPSHOT.jar
那么根据之前的分析,除了socketTextStream()是非并行的Source算子,它的并行度始终为1,其他算子的并行度都为2。
- 逻辑流图(StreamGraph)
这是根据用户通过 DataStream API编写的代码生成的最初的DAG图,用来表示程序的拓扑结构。这一步一般在客户端完成。 逻辑流图中的节点,完全对应着代码中的四步算子操作: 源算子Source(socketTextStream())→扁平映射算子Flat Map(flatMap()) →分组聚合算子Keyed Aggregation(keyBy/sum()) →输出算子Sink(print())
- 作业图(JobGraph)
StreamGraph经过优化后生成的就是作业图(JobGraph),这是提交给 JobManager 的数据结构,确定了当前作业中所有任务的划分。主要的优化为: 将多个符合条件的节点链接在一起合并成一个任务节点,形成算子链,这样可以减少数据交换的消耗。JobGraph一般也是在客户端生成的,在作业提交时传递给JobMaster。 分组聚合算子(Keyed Aggregation)和输出算子Sink(print)并行度都为2,而且是一对一的关系,满足算子链的要求,所以会合并在一起,成为一个任务节点。
- 执行图(ExecutionGraph)
JobMaster收到JobGraph后,会根据它来生成执行图(ExecutionGraph)。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。与JobGraph最大的区别就是按照并行度对并行子任务进行了拆分,并明确了任务间数据传输的方式。
- 物理图(Physical Graph)
JobMaster生成执行图后, 会将它分发给TaskManager;各个TaskManager会根据执行图部署任务,最终的物理执行过程也会形成一张“图”,一般就叫作物理图(Physical Graph)。这只是具体执行层面的图,并不是一个具体的数据结构。 物理图主要就是在执行图的基础上,进一步确定数据存放的位置和收发的具体方式。有了物理图,TaskManager就可以对传递来的数据进行处理计算了。 所以我们可以看到,程序里定义了四个算子操作:源(Source)->转换(flatmap)->分组聚合(keyBy/su