目录
作业图(JobGraph)与执行图(ExecutionGraph)
我们现在已经了解
Flink
运行时的核心组件和整体架构,也明白了不同场景下作业提交的
具体流程。但有些细节还需要进一步思考:一个具体的作业,是怎样从我们编写的代码,转换
成
TaskManager
可以执行的任务的呢?
JobManager
收到提交的作业,又是怎样确定总共有多
少任务、需要多少资源呢?接下来我们就从一些重要概念入手,对这些问题做详细的展开讲解。
数据流图(Dataflow Graph)
Flink
是流式计算框架。它的程序结构,其实就是定义了一连串的处理操作,每一个数据
输入之后都会依次调用每一步计算。在
Flink
代码中,我们定义的每一个处理转换操作都叫作
“
算子
”
(
Operator
),所以我们的程序可以看作是一串算子构成的管道,数据则像水流一样有序
地流过。比如在之前的
WordCount
代码中,基于执行环境调用的
socketTextStream()
方法,就
是一个读取文本流的算子;而后面的
flatMap()
方法,则是将字符串数据进行分词、转换成二
元组的算子。
所有的
Flink
程序都可以归纳为由三部分构成:
Source
、
Transformation
和
Sink
。
⚫
Source
表示
“
源算子
”
,负责读取数据源。
⚫
Transformation
表示
“
转换算子
”
,利用各种算子进行处理加工。
⚫
Sink
表示
“
下沉算子
”
,负责数据的输出。
在运行时,
Flink
程序会被映射成所有算子按照逻辑顺序连接在一起的一张图,这被称为
“
逻辑数据流
”
(
logical dataflow
),或者叫
“
数据流图
”
(
dataflow graph
)。我们提交作业之后,
打开
Flink
自带的
Web UI
,点击作业就能看到对应的
dataflow
,如图
4-7
所示。在数据流图中,
可以清楚地看到
Source
、
Transformation
、
Sink
三部分。
数据流图类似于任意的有向无环图(
DAG
),这一点与
Spark
等其他框架是一致的。图中
的每一条数据流(
dataflow
)以一个或多个
source
算子开始,以一个或多个
sink
算子结束。
在大部分情况下,
dataflow
中的算子,和程序中的转换运算是一一对应的关系。那是不是
说,我们代码中基于
DataStream API
的每一个方法调用,都是一个算子呢?
并非如此。除了
Source
读取数据和
Sink
输出数据,一个中间的转换算子(
Transformation
Operator
)必须是一个转换处理的操作;而在代码中有一些方法调用,数据是没有完成转换的。
可能只是对属性做了一个设置,也可能定义的是数据的传递方式而非转换,又或者是需要几个方法合在一起才能表达一个完整的转换操作。例如,在之前的代码中,我们用到了定义分组的
方法
keyBy
,它就只是一个数据分区操作,而并不是一个算子。事实上,代码中我们可以看到
调用其他转换操作之后返回的数据类型是
SingleOutputStreamOperator
,说明这是一个算子操
作;而
keyBy
之后返回的数据类型是
KeyedStream
。感兴趣的读者也可以自行提交任务在
Web
UI
中查看。
并行度(Parallelism)
我们已经清楚了算子和数据流图的概念,那最终执行的任务又是什么呢?容易想到,一个
算子操作就应该是一个任务。那是不是程序中的算子数量,就是最终执行的任务数呢?
1. 什么是并行计算
要解答这个问题,我们需要先梳理一下其他框架分配任务、数据处理的过程。对于
Spark
而言,是把根据程序生成的
DAG
划分阶段(
stage
)、进而分配任务的。而对于
Flink
这样的流
式引擎,其实没有划分
stage
的必要。因为数据是连续不断到来的,我们完全可以按照数据流
图建立一个“流水线”,前一个操作处理完成,就发往处理下一步操作的节点。如果说
Spark
基于
MapReduce
架构的思想是“数据不动代码动”,那么
Flink
就类似“代码不动数据流动”,
原因就在于流式数据本身是连续到来的、我们不会同时传输所有数据,这其实是更符合数据流
本身特点的处理方式。
在大数据场景下,我们都是依靠分布式架构做并行计算,从而提高数据吞吐量的。既然处
理完一个操作就可以把数据发往别处,那我们就可以将不同的算子操作任务,分配到不同的节
点上执行了。这样就对任务做了分摊,实现了并行处理。
但是仔细分析会发现,这种“并行”其实并不彻底。因为算子之间是有执行顺序的,对一
条数据来说必须依次执行;而一个算子在同一时刻只能处理一个数据。比如之前
WordCount
,
一条数据到来之后,我们必须先用
source
算子读进来、再做
flatMap
转换;一条数据被
source
读入的同时,之前的数据可能正在被
flatMap
处理,这样不同的算子任务是并行的。但如果多
条数据同时到来,一个算子是没有办法同时处理的,我们还是需要等待一条数据处理完、再处
理下一条数据——这并没有真正提高吞吐量。
所以相对于上述的“任务并行”,我们真正关心的,是“数据并行”。也就是说,多条数据
同时到来,我们应该可以同时读入,同时在不同节点执行
flatMap
操作。
2. 并行子任务和并行度
怎样实现数据并行呢?其实也很简单,我们把一个算子操作,“复制”多份到多个节点,
数据来了之后就可以到其中任意一个执行。这样一来,一个算子任务就被拆分成了多个并行的
“子任务”(
subtasks
),再将它们分发到不同节点,就真正实现了并行计算。
在
Flink
执行过程中,每一个算子(
operator
)可以包含一个或多个子任务(
operator subtask
), 这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行。
一个特定算子的子任务(
subtask
)的个数被称之为其并行度(
parallelism
)。这样,包含并
行子任务的数据流,就是并行数据流,它需要多个分区(
stream partition
)来分配并行任务。
一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中, 不同的算子可能具有不同的并行度。 如图 4-8
所示,当前数据流中有
source
、
map
、
window
、
sink
四个算子,除最后
sink
,其 他算子的并行度都为 2
。整个程序包含了
7
个子任务,至少需要
2
个分区来并行执行。我们可 以说,这段流处理程序的并行度就是 2
。
3. 并行度的设置
在
Flink
中,可以用不同的方法来设置并行度,它们的有效范围和优先级别也是不同的。
(
1
)代码中设置
我们在代码中,可以很简单地在算子后跟着调用
setParallelism()
方法,来设置当前算子的
并行度:
stream.map(word -> Tuple2.of(word, 1L)).setParallelism(2);
这种方式设置的并行度,只针对当前算子有效。
另外,我们也可以直接调用执行环境的
setParallelism()
方法,全局设定并行度:
env.setParallelism(2);
这样代码中所有算子,默认的并行度就都为
2
了。
我们一般不会在程序中设置全局并行度,
因为如果在程序中对全局并行度进行硬编码,会导致无法动态扩容。
这里要注意的是,由于
keyBy
不是算子,所以无法对
keyBy
设置并行度。
(2)提交应用时设置
在使用
flink run
命令提交应用时,可以增加
-p
参数来指定当前应用程序执行的并行度,
它的作用类似于执行环境的全局设置:
bin/flink run –p 2 –c com.atguigu.wc.StreamWordCount./FlinkTutorial-1.0-SNAPSHOT.jar
如果我们直接在
Web UI
上提交作业,也可以在对应输入框中直接添加并行度。
(3)配置文件中设置
我们还可以直接在集群的配置文件
flink-conf.yaml
中直接更改默认并行度:
parallelism.default: 2
这个设置对于整个集群上提交的所有作业有效,初始值为 1
。无论在代码中设置、还是提
交时的
-p
参数,都不是必须的;所以在没有指定并行度的时候,就会采用配置文件中的集群
默认并行度。在开发环境中,没有配置文件,默认并行度就是当前机器的
CPU
核心数。这也
就解释了为什么我们在第二章运行
WordCount
流处理程序时,会看到结果前有
1~4
的分区编
号——运行程序的电脑是
4
核
CPU
,那么开发环境默认的并行度就是
4
。
我们可以总结一下所有的并行度设置方法,它们的优先级如下:
(
1
)对于一个算子,首先看在代码中是否单独指定了它的并行度,这个特定的设置优先级
最高,会覆盖后面所有的设置。
(2)如果没有单独设置,那么采用当前代码中执行环境全局设置的并行度。
(3)如果代码中完全没有设置,那么采用提交时
-p
参数指定的并行度。
(4)如果提交时也未指定
-p
参数,那么采用集群配置文件中的默认并行度。
这里需要说明的是,算子的并行度有时会受到自身具体实现的影响。比如之前我们用到的
读取
socket
文本流的算子
socketTextStream
,它本身就是非并行的
Source
算子,所以无论怎么
设置,它在运行时的并行度都是
1
,对应在数据流图上就只有一个并行子任务。这一点大家可
以自行在
Web UI
上查看验证。
那么实践中怎样设置并行度比较好呢?那就是在代码中只针对算子设置并行度,不设置全
局并行度,这样方便我们提交作业时进行动态扩容。
算子链(Operator Chain)
关于“一个作业有多少任务”这个问题,现在已经基本解决了。但如果我们仔细观察 Web
UI
上给出的图,如图
4-9
所示,上面的节点似乎跟代码中的算子又不是一一对应的。
很明显,这里的一个节点,会把转换处理的很多个任务都连接在一起,合并成了一个“大
任务”。这又是怎么回事呢?
1. 算子间的数据传输
回到上一小节的例子,我们先来考察一下算子任务之间数据传输的方式。
如图 4-10
所示,一个数据流在算子之间传输数据的形式可以是一对一(
one-to-one
)的直
通
(forwarding)
模式,也可以是打乱的重分区(
redistributing
)模式,具体是哪一种形式,取决
于算子的种类。
(1)一对一(One-to-one,forwarding)
这种模式下,数据流维护着分区以及元素的顺序。比如图中的 source
和
map
算子,
source
算子读取数据之后,可以直接发送给
map
算子做处理,它们之间不需要重新分区,也不需要
调整数据的顺序。这就意味着
map
算子的子任务,看到的元素个数和顺序跟
source
算子的子
任务产生的完全一样,保证着
“
一对一
”
的关系。
map
、
filter
、
flatMap
等算子都是这种
one-to-one
的对应关系。
这种关系类似于 Spark 中的窄依赖。
(2)重分区(Redistributing)
在这种模式下,数据流的分区会发生改变。比图中的 map
和后面的
keyBy/window
算子之
间(这里的
keyBy
是数据传输算子,后面的
window
、
apply
方法共同构成了
window
算子)
,
以及
keyBy/window
算子和
Sink
算子之间,都是这样的关系。
每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。例如,
keyBy()
是分组操作,本质上基于键(
key
)的哈希值(
hashCode
)进行了重分区;而当并行度
改变时,比如从并行度为
2
的
window
算子,要传递到并行度为
1
的
Sink
算子,这时的数据
传输方式是再平衡(
rebalance
),会把数据均匀地向下游子任务分发出去。这些传输方式都会
引起重分区(
redistribute
)的过程,这一过程类似于
Spark
中的
shuffle
。
总体说来,这种算子间的关系类似于 Spark 中的宽依赖。
2. 合并算子链
在 Flink
中,并行度相同的一对一(
one to one
)算子操作,可以直接链接在一起形成一个
“大”的任务(
task
),这样原来的算子就成为了真正任务里的一部分,如图
4-11
所示。每个
task
会被一个线程执行。这样的技术被称为“算子链”(
Operator Chain
)。
比如在图 4-11
中的例子中,
Source
和
map
之间满足了算子链的要求,所以可以直接合并
在一起,形成了一个任务;因为并行度为
2
,所以合并后的任务也有两个并行子任务。这样,
这个数据流图所表示的作业最终会有
5
个任务,由
5
个线程并行执行。
Flink 为什么要有算子链这样一个设计呢?这是因为将算子链接成 task
是非常有效的优
化:可以减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。
Flink 默认会按照算子链的原则进行链接合并,如果我们想要禁止合并或者自行定义,也
可以在代码中对算子做一些特定的设置:
// 禁用算子链.map(word -> Tuple2.of(word, 1L)).disableChaining();// 从当前算子开始新链.map(word -> Tuple2.of(word, 1L)).startNewChain()
作业图(JobGraph)与执行图(ExecutionGraph)
至此,我们已经彻底了解了由代码生成任务的过程,现在来做个梳理总结。
由 Flink
程序直接映射成的数据流图(
dataflow graph
),也被称为逻辑流图(
logical
StreamGraph
),因为它们表示的是计算逻辑的高级视图。到具体执行环节时,我们还要考虑并
行子任务的分配、数据在任务间的传输,以及合并算子链的优化。为了说明最终应该怎样执行
一个流处理程序,
Flink
需要将逻辑流图进行解析,转换为物理数据流图。
在这个转换过程中,有几个不同的阶段,会生成不同层级的图,其中最重要的就是作业图
(
JobGraph
)和执行图(
ExecutionGraph
)。
Flink
中任务调度执行的图,按照生成顺序可以分成
四层:
逻辑流图(StreamGraph)→ 作业图(JobGraph)→ 执行图(ExecutionGraph)→ 物理
图(Physical Graph)。
我们可以回忆一下之前处理 socket
文本流的
StreamWordCount
程序:
env.socketTextStream().flatMap(…).keyBy(0).sum(1).print();
如果提交时设置并行度为 2
:
bin/flink run –p 2 –c com.atguigu.wc.StreamWordCount./FlinkTutorial-1.0-SNAPSHOT.jar
那么根据之前的分析,除了 socketTextStream()
是非并行的
Source
算子,它的并行度始终
为
1
,其他算子的并行度都为
2
。
接下来我们分析一下程序对应四层调度图的演变过程,如图 4-12
所示。
1. 逻辑流图(StreamGraph)
这是根据用户通过 DataStream API
编写的代码生成的最初的
DAG
图,用来表示程序的拓
扑结构。这一步一般在客户端完成。
我们可以看到,逻辑流图中的节点,完全对应着代码中的四步算子操作:
源算子 Source(socketTextStream())→扁平映射算子 Flat Map(flatMap()) →分组聚合算子
Keyed Aggregation(keyBy/sum()) →输出算子 Sink(print())。
2. 作业图(JobGraph)
StreamGraph 经过优化后生成的就是作业图(
JobGraph
),这是提交给
JobManager
的数据
结构,确定了当前作业中所有任务的划分。
主要的优化为: 将多个符合条件的节点链接在一起
合并成一个任务节点,形成算子链,这样可以减少数据交换的消耗。JobGraph 一般也是在客
户端生成的,在作业提交时传递给 JobMaster。
在图 4-12
中,分组聚合算子(
Keyed Aggregation
)和输出算子
Sink(print)
并行度都为
2
,
而且是一对一的关系,满足算子链的要求,所以会合并在一起,成为一个任务节点。
3. 执行图(ExecutionGraph)
JobMaster 收到
JobGraph
后,会根据它来生成执行图(
ExecutionGraph
)。
ExecutionGraph 是 JobGraph
的并行化版本,是调度层最核心的数据结构。
从图 4-12
中可以看到,与
JobGraph
最大的区别就是按照并行度对并行子任务进行了拆分,
并明确了任务间数据传输的方式。
4. 物理图(Physical Graph)
JobMaster 生成执行图后, 会将它分发给
TaskManager
;各个
TaskManager
会根据执行图
部署任务,最终的物理执行过程也会形成一张“图”,一般就叫作物理图(Physical Graph)。
这只是具体执行层面的图,并不是一个具体的数据结构。
对应在上图 4-12
中,物理图主要就是在执行图的基础上,进一步确定数据存放的位置和
收发的具体方式。有了物理图,
TaskManager
就可以对传递来的数据进行处理计算了。
所以我们可以看到,程序里定义了四个算子操作:源(Source
)
->
转换(
flatMap
)
->
分组
聚合(
keyBy/sum
)
->
输出(
print
);合并算子链进行优化之后,就只有三个任务节点了;再考
虑并行度后,一共有
5
个并行子任务,最终需要
5
个线程来执行。
任务(task)和任务槽(slots)
1. 任务槽(Task Slots)
之前已经提到过,Flink
中每一个
worker(
也就是
TaskManager)
都是一个
JVM
进程,它可
以启动多个独立的线程,来并行执行多个子任务(
subtask
)。 所以如果想要执行 5
个任务,并不一定非要
5
个
TaskManager
,我们可以让
TaskManager 多线程执行任务。如果可以同时运行 5
个线程,那么只要一个
TaskManager
就可以满足我们之 前程序的运行需求了。
很显然,TaskManager
的计算资源是有限的,并不是所有任务都可以放在一个
TaskManager
上并行执行。并行的任务越多,每个线程的资源就会越少。那一个
TaskManager
到底能并行处
理多少个任务呢?为了控制并发量,我们需要在
TaskManager
上对每个任务运行所占用的资源
做出明确的划分,这就是所谓的任务槽(
task slots
)。
每个任务槽(task slot
)其实表示了
TaskManager
拥有计算资源的一个固定大小的子集。
这些资源就是用来独立执行一个子任务的。
![](https://i-blog.csdnimg.cn/blog_migrate/567b7a8d41a3ec0c828e38fb9afade16.png)
假如一个 TaskManager
有三个
slot
,那么它会将管理的内存平均分成三份,每个
slot
独自
占据一份。这样一来,我们在
slot
上执行一个子任务时,相当于划定了一块内存
“
专款专用
”
,
就不需要跟来自其他作业的任务去竞争内存资源了。所以现在我们只要
2
个
TaskManager
,就
可以并行处理分配好的
5
个任务了,如图
4-14
所示。
2. 任务槽数量的设置
我们可以通过集群的配置文件来设定 TaskManager
的
slot
数量:
taskmanager.numberOfTaskSlots: 8
通过调整 slot
的数量,我们就可以控制子任务之间的隔离级别。
具体来说,如果一个 TaskManager
只有一个
slot
,那将意味着每个任务都会运行在独立的
JVM
中(当然,该
JVM
可能是通过一个特定的容器启动的);而一个
TaskManager
设置多个
slot
则意味着多个子任务可以共享同一个
JVM
。它们的区别在于:前者任务之间完全独立运行,
隔离级别更高、彼此间的影响可以降到最小;而后者在同一个
JVM
进程中运行的任务,将共
享
TCP
连接和心跳消息,也可能共享数据集和数据结构,这就减少了每个任务的运行开销,
在降低隔离级别的同时提升了性能。
需要注意的是,slot 目前仅仅用来隔离内存,不会涉及 CPU 的隔离。在具体应用时,可
以将 slot 数量配置为机器的 CPU 核心数,尽量避免不同任务之间对 CPU 的竞争。这也是开发
环境默认并行度设为机器 CPU 数量的原因。
3. 任务对任务槽的共享
这样看来,一共有多少任务,我们就需要有多少 slot
来并行处理它们。不过实际提交作业
进行测试就会发现,我们之前的
WordCount
程序设置并行度为
2
提交,一共有
5
个并行子任
务,可集群即使只有
2
个
task slot
也是可以成功提交并运行的。这又是为什么呢?
我们可以基于之前的例子继续扩展。如果我们保持 sink
任务并行度为
1
不变,而作业提
交时设置全局并行度为
6
,那么前两个任务节点就会各自有
6
个并行子任务,整个流处理程序
则有
13
个子任务。那对于
2
个
TaskManager
、每个有
3
个
slot
的集群配置来说,还能否正常
运行呢
![](https://i-blog.csdnimg.cn/blog_migrate/a9ae3d73935aaff8908bc3579fa7c5ad.png)
完全没有问题。这是因为默认情况下,Flink
是允许子任务共享
slot
的。如图
4-15
所示,
只要属于同一个作业,那么对于不同任务节点的并行子任务,就可以放到同一个
slot
上执行。
所以对于第一个任务节点
source
→
map
,它的
6
个并行子任务必须分到不同的
slot
上(如果在
同一
slot
就没法数据并行了),而第二个任务节点
keyBy/window/apply
的并行子任务却可以和
第一个任务节点共享
slot
。
于是最终结果就变成了:每个任务节点的并行子任务一字排开,占据不同的 slot
;而不同
的任务节点的子任务可以共享
slot
。一个
slot
中,可以将程序处理的所有任务都放在这里执行,
我们把它叫作保存了整个作业的运行管道(
pipeline
)。
这个特性看起来有点奇怪:我们不是希望并行处理、任务之间相互隔离吗,为什么这里又
允许共享
slot
呢?
我们知道,一个 slot
对应了一组独立的计算资源。在之前不做共享的时候,每个任务都平
等地占据了一个
slot
,但其实不同的任务对资源的占用是不同的。例如这里的前两个任务,
source/map
尽管是两个算子合并算子链得到的,但它只是基本的数据读取和简单转换,计算耗
时极短,一般也不需要太大的内存空间;而
window
算子所做的窗口操作,往往会涉及大量的
数据、状态存储和计算,我们一般把这类任务叫作“资源密集型”(
intensive
)任务。当它们
被平等地分配到独立的
slot
上时,实际运行我们就会发现,大量数据到来时
source/map
和
sink
任务很快就可以完成,但
window
任务却耗时很久;于是下游的
sink
任务占据的
slot
就会等待
闲置,而上游的
source/map
任务受限于下游的处理能力,也会在快速处理完一部分数据后阻
塞对应的资源开始等待(相当于处理背压)。这样资源的利用就出现了极大的不平衡,“忙的忙
死,闲的闲死”。
解决这一问题的思路就是允许 slot
共享。当我们将资源密集型和非密集型的任务同时放到
一个
slot
中,它们就可以自行分配对资源占用的比例,从而保证最重的活平均分配给所有的
TaskManager
。
slot 共享另一个好处就是允许我们保存完整的作业管道。这样一来,即使某个
TaskManager
出现故障宕机,其他节点也可以完全不受影响,作业的任务可以继续执行。
另外,同一个任务节点的并行子任务是不能共享 slot
的,所以允许
slot
共享之后,运行作
业所需的
slot
数量正好就是作业中所有算子并行度的最大值。这样一来,我们考虑当前集群需
要配置多少
slot
资源时,就不需要再去详细计算一个作业总共包含多少个并行子任务了,只看
最大的并行度就够了。
当然,Flink
默认是允许
slot
共享的,如果希望某个算子对应的任务完全独占一个
slot
,
或者只有某一部分算子共享
slot
,我们也可以通过设置
“slot
共享组
”
(
SlotSharingGroup
)手动
指定:
.map(word -> Tuple2.of(word, 1L)).slotSharingGroup(“1”);
这样,只有属于同一个 slot 共享组的子任务,才会开启 slot 共享;不同组之间的任务是完
全隔离的,必须分配到不同的 slot 上。在这种场景下,总共需要的 slot 数量,就是各个 slot
共享组最大并行度的总和。
默认情况下,所有算子都在default共享组下,所以slot可以共享,但是某一个算子单独设置共享组,在他之后的算子操作默认都和他一样在同一个共享组;除非后面的算子也单独设置共享组了。
4. 任务槽和并行度的关系
直观上看,slot
就是
TaskManager
为了并行执行任务而设置的,那它和之前讲过的并行度
(
Parallelism
)是不是一回事呢?
Slot 和并行度确实都跟程序的并行执行有关,但两者是完全不同的概念。简单来说,
task
slot
是 静 态 的 概 念 , 是 指
TaskManager
具 有 的 并 发 执 行 能 力 , 可 以 通 过 参 数
taskmanager.numberOfTaskSlots
进行配置;而并行度(
parallelism
)是动态概念,也就是
TaskManager
运行程序时实际使用的并发能力,可以通过参数
parallelism.default
进行配置。换
句话说,并行度如果小于等于集群中可用
slot
的总数,程序是可以正常执行的,因为
slot
不一
定要全部占用,有十分力气可以只用八分;而如果并行度大于可用
slot
总数,导致超出了并行
能力上限,那么心有余力不足,程序就只好等待资源管理器分配更多的资源了。
下面我们再举一个具体的例子。假设一共有 3
个
TaskManager
,每一个
TaskManager
中的
slot
数量设置为
3
个,那么一共有
9
个
task slot
,如图
4-16
所示,表示集群最多能并行执行
9
个任务。
而我们定义 WordCount
程序的处理操作是四个转换算子:
source→
flatMap
→
reduce
→
sink
当所有算子并行度相同时,容易看出 source
和
flatMap
可以合并算子链,于是最终有三个
任务节点。
如果我们没有任何并行度设置,而配置文件中默认 parallelism.default=1
,那么程序运行的
默认并行度为
1
,总共有
3
个任务。由于不同算子的任务可以共享任务槽,所以最终占用的
slot
只有
1
个。
9
个
slot
只用了
1
个,有
8
个空闲,如图
4-17
中的
Example 1
所示。
![](https://i-blog.csdnimg.cn/blog_migrate/5a29588d8daaef9b71963480de707ec1.png)
如果我们更改默认参数,或者提交作业时设置并行度为 2
,那么总共有
6
个任务,共享任
务槽之后会占用
2
个
slot
,如图
4-18
中
Example 2
所示。同样,就有
7
个
slot
空闲,计算资源
没有充分利用。所以可以看到,设置合适的并行度才能提高效率。
那对于这个例子,怎样设置并行度效率最高呢?当然是需要把所有的 slot
都利用起来。考
虑到
slot
共享,我们可以直接把并行度设置为
9
,这样所有
27
个任务就会完全占用
9
个
slot
。
这是当前集群资源下能执行的最大并行度,计算资源得到了充分的利用,如图
4-19
中
Example
3
所示。
另外再考虑对于某个算子单独设置并行度的场景。例如,如果我们考虑到输出可能是写入
文件,那会希望不要并行写入多个文件,就需要设置
sink
算子的并行度为
1
。这时其他的算子
68
并行度依然为
9
,所以总共会有
19
个子任务。根据
slot
共享的原则,它们最终还是会占用全
部的
9
个
slot
,而
sink
任务只在其中一个
slot
上执行,如图
4-20
中
Example 4
所示。通过这个
例子也可以明确地看到,整个流处理程序的并行度,就应该是所有算子并行度中最大的那个,
这代表了运行程序需要的
slot
数量。