前言
Apache Flink 作为新一代分布式流处理框架,其运行时架构是理解任务调度、资源管理与并行执行的核心基础。本文以 Standalone 会话模式 为例,系统剖析 Flink 集群的两大核心进程------JobManager(主进程) 与 TaskManager(工作进程) 的内部组件与协作机制。内容涵盖 JobManager 中的 Dispatcher、JobMaster、ResourceManager 等关键角色,TaskManager 中的任务槽(Task Slot)、并行度(Parallelism)、算子链(Operator Chain)等核心概念,并通过具体图解与示例,深入阐释任务槽与并行度的区别及实际配置方法。通过本文,读者能够建立对 Flink 运行时架构的清晰认知,为后续性能调优与集群部署打下坚实基础。
一、Flink运行时架构------Standalone会话模式为例

Flink的分布式执行由两个重要进程组成,即:主进程和工作进程
二、系统架构
(一)作业管理器(JobManager)

1.定义
- JobManager是一个Flink集群中任务管理和调度的核心 ,是控制应用执行的主进程。也就是说,每个应用都应该被唯一的JobManager所控制执行。
2.组件
- JobManger又包含不同的组件:
- JobMaster
- 资源管理器(ResourceManager)
- 分发器(Dispatcher)
- Actor通信系统
- 检查点(checkpoint)
(1)Actor通信系统
- 一个包含多个角色的容器。提供调度、配置、日志记录等服务,所有Actor按层级结构分布。
- Actor是相应消息的轻量级并发实体,在同一个JVM进程中,Actor之间以父子层级组织并进行监督,通过消息系统相互通信,接受所有专属消息。
- 本地实例通过共享内存传输,远程实例则通过RPC传递信息负责监督子进程。若出现任何问题,父进程则将收到通知。若Actor能自行解决子问题,则可重启子进程。
(2)分发器(dispatcher)
- Dispatcher主要负责提供一个REST接口,用来提交应用,并且负责为每一个新提交的作业启动一个新的JobMaster 组件。Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。
(3)JobMaster
-
JobMaster是JobManager中最核心的组件,负责处理单独的作业(Job)。所以JobMaster和具体的Job是一一对应的,多个Job可以同时运行在一个Flink集群中, 每个Job都有一个自己的JobMaster。
-
在作业提交时,JobMaster会先接收到要执行的应用。JobMaster会把JobGraph转换成一个物理层面的数据流图 ,这个图被叫作"执行图"(ExecutionGraph),它包含了所有可以并发执行的任务。JobMaster会向资源管理器(ResourceManager)发出请求 ,申请执行任务必要的资源。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。
-
而在运行过程中,JobMaster会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。
(4)资源管理器(ResourceManager)
这里注意要把Flink内置的ResourceManager和其他资源管理平台(比如YARN)的ResourceManager区分开。
- ResourceManager主要负责资源的分配和管理 ,在Flink 集群中只有一个。所谓"资源",主要是指TaskManager的任务槽(task slots) 。任务槽就是Flink集群中的资源调配单元,包含了机器用来执行计算的一组CPU和内存资源。每一个任务(Task)都需要分配到一个slot上执行。
检查点后续第七章会讲,到时候跟状态一起说
(二)任务管理器(TaskManager)

1.核心概念
(1)并行度(Parallelism)
①并行子任务
- 当要处理的数据量非常大时,我们可以把一个算子操作,"复制"多份到多个节点,数据来了之后就可以到其中任意一个执行 。这样一来,一个算子任务就被拆分成了多个并行的"子任务"(subtasks),再将它们分发到不同节点 真正实现并行计算。
- 在Flink执行过程中,每一个算子(operator)可以包含一个或多个子任务(operator subtask),这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行。

②并行度
- 一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism) 。这样,包含并行子任务的数据流,就是并行数据流,它需要多个分区(stream partition) 来分配并行任务。
- 一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中,不同的算子可能具有不同的并行度。
- 例如:如上图所示,当前数据流中有source、map、window、sink四个算子,其中sink算子的并行度为1,其他算子的并行度都为2。所以这段流处理程序的并行度就是2。
③并行度的设置
- 在Flink中,可以用不同的方法来设置并行度,它们的有效范围和优先级别也是不同的。
a.代码中设置
- 我们在代码中,可以很简单地在算子后跟着调用
setParallelism()方法,来设置当前算子的并行度,这种方式设置的并行度,只针对当前算子有效。 如:
java
stream.map(word -> Tuple2.of(word, 1L)).setParallelism(2);
- 另外,我们也可以直接调用执行环境的setParallelism()方法,全局设定并行度:
java
env.setParallelism(2);
- 这样代码中所有算子,默认的并行度就都为2 了。我们一般不会在程序中设置全局并行度 ,因为如果在程序中对全局并行度进行硬编码,会导致无法动态扩容。这里要注意的是,由于
keyBy不是算子,所以无法对keyBy设置并行度。
b.代码中设置
- 在使用
flink run命令提交应用时,可以增加-p参数来指定当前应用程序执行的并行度,它的作用类似于执行环境的全局设置:
java
bin/flink run -p 2 -c com.atguigu.wc.SocketStreamWordCount
./FlinkTutorial-1.0-SNAPSHOT.jar
- 如果我们直接在Web UI上提交作业 ,也可以在对应输入框中直接添加并行度。如图所示。

c.配置文件中设置
- 我们还可以直接在集群的配置文件
flink-conf.yaml中直接更改默认并行度:
java
parallelism.default: 2
- 这个设置对于整个集群上提交的所有作业有效,初始值为1 。无论在代码中设置、还是提交时的
-p参数,都不是必须的;所以在没有指定并行度的时候,就会采用配置文件中的集群默认并行度。在开发环境中,没有配置文件,默认并行度就是当前机器的CPU核心数。
(2)算子链(Operator Chain)
①算子间的数据传输
- 一个数据流在算子之间传输数据的形式可以是一对一(one-to-one) 的直通(forwarding)模式,也可以是打乱的**重分区(redistributing)**模式,具体是哪一种形式,取决于算子的种类。
a.一对一(One-to-one,forwarding)
- 这种模式下,数据流维护着分区以及元素的顺序。 比如:图中的source和map算子,source算子读取数据之后,可以直接发送给map算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着map 算子的子任务,看到的元素个数和顺序跟source算子的子任务产生的完全一样,保证着"一对一"的关系 。
map、filter、flatMap等算子都是这种one-to-one的对应关系。这种关系类似于Spark中的窄依赖。
b.重分区(Redistributing)
- 在这种模式下,数据流的分区会发生改变。 比如图中的map和后面的
keyBy/window算子之间,以及keyBy/window算子和Sink算子之间,都是这样的关系。 - 每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。这些传输方式都会引起重分区的过程,这一过程类似于Spark中的shuffle。

②合并算子链
- 在Flink中,并行度相同的一对一(one to one) 算子操作,可以直接链接在一起形成一个"大"的任务(task),这样原来的算子就成为了真正任务里的一部分,如下图所示。每个task会被一个线程执行。这样的技术被称为"算子链"(Operator Chain)。

- 上图中Source和map之间满足了算子链的要求,所以可以直接合并在一起,形成了一个任务;因为并行度为2,所以合并后的任务也有两个并行子任务。这样,这个数据流图所表示的作业最终会有5个任务,由5个线程并行执行。
- 将算子链接成task是非常有效的优化:可以减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。
- Flink默认会按照算子链的原则进行链接合并,如果我们想要禁止合并或者自行定义,也可以在代码中对算子做一些特定的设置:
java
// 禁用算子链
.map(word -> Tuple2.of(word, 1L)).disableChaining();
java
// 从当前算子开始新链
.map(word -> Tuple2.of(word, 1L)).startNewChain();
2.任务槽(Task Slots)
(1)定义
- Flink中每一个TaskManager都是一个JVM进程,它可以启动多个独立的线程,来并行执行多个子任务(subtask)。
- 很显然,TaskManager的计算资源是有限的,并行的任务越多,每个线程的资源就会越少。那一个TaskManager到底能并行处理多少个任务呢?为了控制并发量,我们需要在TaskManager上对每个任务运行所占用的资源做出明确的划分,这就是所谓的任务槽(task slots)。
- 每个任务槽(task slot)其实表示了TaskManager拥有计算资源的一个固定大小的子集。这些资源就是用来独立执行一个子任务的。
(2)实例
- 假如一个TaskManager有三个slot,那么它会将管理的内存平均分成三份,每个slot独自占据一份。这样一来,我们在slot上执行一个子任务时,相当于划定了一块内存"专款专用",就不需要跟来自其他作业的任务去竞争内存资源了。
- 所以现在我们只要2个TaskManager,就可以并行处理分配好的5个任务了。

(3)任务槽数量的设置
-
在Flink的
/opt/module/flink-1.17.0/conf/flink-conf.yaml配置文件中,可以设置TaskManager的slot数量,默认是1个slot。
taskmanager.numberOfTaskSlots: 8 -
需要注意的是,slot目前仅仅用来隔离内存,不会涉及CPU的隔离 。在具体应用时,可以将slot数量配置为机器的CPU核心数,尽量避免不同任务之间对CPU的竞争。这也是开发环境默认并行度设为机器CPU数量的原因。
(4)任务对任务槽的共享
-
默认情况下,Flink是允许子任务共享slot。如果我们保持sink任务并行度为1不变,而作业提交时设置全局并行度为6,那么前两个任务节点就会各自有6个并行子任务,整个流处理程序则有13个子任务。
-
当我们将资源密集型和非密集型的任务同时放到一个slot中,它们就可以自行分配对资源占用的比例,从而保证最重的活平均分配给所有的TaskManager。
-
slot共享另一个好处就是允许我们保存完整的作业管道。 这样一来,即使某个TaskManager出现故障宕机,其他节点也可以完全不受影响,作业的任务可以继续执行。
-
Flink默认是允许slot共享的,如果希望某个算子对应的任务完全独占一个slot,或者只有某一部分算子共享slot,我们也可以通过设置"slot共享组"手动指定:
java
.map(word -> Tuple2.of(word, 1L)).slotSharingGroup("1");
- 只有属于同一个slot共享组的子任务,才会开启slot共享;不同组之间的任务是完全隔离的,必须分配到不同的slot上。在这种场景下,总共需要的slot数量,就是各个slot共享组最大并行度的总和。

三、任务槽与并行度的关系
(一)基本概念
任务槽和并行度都跟程序的并行执行有关 ,但两者是完全不同的概念。简单来说任务槽是静态的概念 ,是指TaskManager 具有的并发执行能力 ,可以通过参数taskmanager.numberOfTaskSlots进行配置;而并行度是动态概念 ,也就是TaskManager运行程序时实际使用的并发能力 ,可以通过参数parallelism.default进行配置。
(二)具体实例
- 举例说明( Standalone运行模式):假设一共有3个TaskManager,每一个TaskManager中的slot数量设置为3个,那么一共有9个task slot,表示集群最多能并行执行9个同一算子的子任务。
- 而我们定义word count程序的处理操作是四个转换算子:
source→ flatmap→ reduce→ sink - 当所有算子并行度相同时,容易看出source和flatmap可以合并算子链 ,于是最终有三个任务节点。
通过这个例子也可以明确地看到,整个流处理程序的并行度,就应该是所有算子并行度中最大的那个,这代表了运行程序需要的slot数量。
1.parallelism.default=1

9个slot只有1个,有8个空闲。
2.parallelism.default=2

3.parallelism.default=9

4.全局并行度为9,并单独设置Sink并行度为1
只有sink会在其中一个slot上执行。
三、计算公式
所需TaskManager数量=⌈作业并行度单个TaskManager的Slot数⌉ \text{所需TaskManager数量} = \left\lceil \frac{\text{作业并行度}}{\text{单个TaskManager的Slot数}} \right\rceil 所需TaskManager数量=⌈单个TaskManager的Slot数作业并行度⌉

- 举例说明( Yarn运行模式):假设每一个TaskManager中的slot数量设置为3个,因为Yarn运行模式是动态分配资源的,所以TaskManager数量的计算公式为
- 示例:并行度 10,每个 TaskManager 3 个 Slot → 10/3≈3.33,向上取整为 4 个 TaskManager
总结
本文围绕 Flink Standalone 会话模式,完整呈现了分布式执行的核心设计:
-
1.主从架构:JobManager 负责任务调度与协调,TaskManager 负责实际执行。JobManager 内部通过 Dispatcher 接收作业、JobMaster 转换执行图、ResourceManager 管理任务槽资源,三者协同完成作业生命周期管理。
-
2.并行机制:算子可拆分为多个并行子任务,并行度决定子任务数量。Flink 支持在代码、提交命令、配置文件等多层级设置并行度,优先级依次升高。算子链优化将并行度相同且为"一对一"数据传输的算子合并为一个大任务,减少线程切换与数据交换开销。
-
3.任务槽:TaskManager 中的 slot 代表固定的内存资源子集,用于隔离任务执行。默认允许不同子任务共享 slot,从而提升资源利用率并保证作业管道的完整性。slot 数量为静态配置,而并行度为动态运行参数。
-
4.关系与计算:任务槽是静态并发能力,并行度是实际使用量。在 Standalone 模式下,所需 TaskManager 数量 = ⌈作业全局并行度 / 单 TaskManager slot 数⌉;在 YARN 动态分配模式下,TaskManager 数量按相同公式向上取整确定。
通过明确 slot 与并行度的区别、合理设置算子链与共享组,开发者可以更高效地利用集群资源,实现高吞吐、低延迟的流处理应用。
