[flink]系统架构

该看 https://www.bilibili.com/video/BV1zr4y157XV/?p=27&spm_id_from=pageDriver&vd_source=51f694f71c083955be7443b1d75165e0

11:53了

书第四章看完了

边看视频边圈、或修改不需要的笔记即可。

一 、系统架构

对于一个被提交执行的作业,JobManager是真正意义上的"管理者",负责管理调度;而TaskManager是"工作者"负责执行任务、处理数据、因此可以有一个或多个。

其实客户端并不是处理系统的一部分,它只负责作业的提交,具体来说,就是调用main()方法,将代码转换成数据流图(DataflowGraph),并最终生成作业图(JobGraph),一并发送给JobManager。提交之后,任务的执行其实就跟客户端没有关系了。我们可以在客户端选择断开与JobManager的连接(命令行提交作业时加-d就是表示分离模式,即断开连接),也可以保持连接。

无论是通过WebUI还是命令行执行flink run的相关操作,都是通过命令行实现的。

  1. JobManager(作业管理器)

是Flink集群中任务管理和调度的核心,是控一个应用程序执行的主进程。也就是说每个应用都应该被唯一的JobManager控制执行。

JobManager又包含了3个不同的组件

1)JobMaster

①JobMaster是JobManager中最核心的组件,负责处理单独的作业(Job),这里我们把对数据进行处理的操作统称为任务(task),多个任务按照一定的先后顺序连接起来,就构成了Job(作业)。因此JobMaster和具体的Job是一一对应的,多个job可以同时运行在一个Flink集群上,每个job都有一个自己的JobMaster。

②在作业提交时,JobMaster会先接收到要执行的应用。一般是客户端提交来的,包括:Jar包,数据流图(dataflow graph)和作业图(JobGraph)。

③JobMaster会把JobGraph转换成一个物理层面的数据流图,这个图被叫作执行图(ExecutionGraph),包含了所有可以并发执行的任务。JobMaster会向资源管理器(ResourceManager)发出请求,申请执行任务必要的资源。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。

④在运行过程中,JobMaster会负责所有需要中央协调的操作,如检查点(checkpoint)的协调。

2)资源管理器(ResourceManager,指Flink自己的资源管理器,不是YARN的)

ResourceManager主要负责资源的分配和管理,在Flink集群中只有一个。所谓“资源”,主要就是指TaskManager的任务槽。任务槽就是Flink集群中的资源调配单元,包含了机器用来执行计算的一组CPU和内存资源。每个任务(task)都需要分配一个任务槽中执行。

这里需要注意把Flink内置的ResourceManager和yarn的ResourceManager区分开。

当新的作业申请资源时,ResourceManager会将有空闲槽位的TaskManager分配给JobMaster。如果ResourceManager没有足够的任务槽,那么它还可以向资源管理平台(yarn)发起会话,请求提供启动TaskManager进程的容器,另外,ResourceManager还负责停掉空闲的TaskManager释放计算资源。

3)分发器(Dispatcher)

Dispatcher主要负责提供一个REST接口,用来提交作业,并且负责为每一个新提交的作业启动一个新的JobMaster组件,Dispatcher也会启动一个WebUI,用来方便地展示和监控作业执行的信息,Dispatcher在架构中并不是必需的,在不同的部署模式下可能会被忽略掉。

2、TaskManager(任务管理器)

①TaskManager是Flink中的工作进程,数据流的具体计算任务(Task)就是它来做的,因此也被称为Worker,通常会有多个TaskManager,每个TaskManager都包含了一定数量的任务槽(slots)。slots(任务槽资源)是资源调度的最小单位,其数量限制了TaskManager能够并行处理的任务数量。

②启动之后,TaskManager会向ResourceManager注册它的任务槽;收到JobManager的指令后,TaskManager就会将一个或多个槽位提供给JobMaster调用,JobMaster就可以分配任务来执行了。

③在执行的过程中,TaskManager可以缓冲数据,还可以跟其他运行同一应用的TaskManager交换数据。

二、作业提交流程(Yarn会话模式)

1、会话模式(动态启动TaskManager)

在会话模式下,需要先启动一个YARN会话,这个会话会创建一个Flink集群。

在这里只启动了JobManager,而TaskManager可以根据需要动态地启动。在JobManager内部,由于还没有提交作业,所以只有ResourceManager和Dispatcher在运行。

接下来就是真正提交作业的流程,注:如不指明为YARN的 ResourceManager则都为Flink的ResourceManager。

1)客户端通过REST接口将作业提交给Dispatcher。

2)Dispatcher启动JobMaster,并将作业(包括作业图)提交给JobMaster。

3)JobMaster向ResourceManager请求slots。

4)ResourceManager向YARN ResourceManager请求容器。

5)YARN启动新的带有TaskManager的容器。

6)TaskManager启动之后,向ResourceManager注册自己的可用slots。

7)ResourceManager通知TaskManager为新的作业提供slots。

8)TaskManager连接到对应的JobMaster,提供slots。

9)JobMaster将需要执行的作业分发给TaskManager执行。

注:Yarn会话模式中JobManager是提前启动好的。

JobManager中的三个组件:分发器(Dispatcher)和资源管理器(ResourceManager)是提前启动好的。JobMaster是有提交任务的时候才会启动。

TaskManager是动态分配的。所以一开始就只有JobManager启动。

2、单作业模式

在单作业模式下,Flink集群不会预先启动,而是在提交作业时才启动新的JobManager,具体流程如下:

1)客户端将作业提交给YARN的ResourceManager,在这一步中,会同时将Flink的jar包和配置上传到HDFS,以便后续启动Flink集群相关组件的容器。

2)YARN的ResourceManager分配容器资源,启动Flink JobManager,并将作业提交给JobMaster(这里省略了Dispatcher组件)。

3)JobMaster向ResourceManager请求slots。

4)ResourceManager向YARN ResourceManager请求容器。

5)YARN启动带有TaskManager的容器。

6)TaskManager启动之后,向Flink的ResourceManager注册自己的可用slots。

7)ResourceManager通知TaskManager为新的作业提供slots。

8)TaskManager连接到对应的JobMater,提供slots。

9)JobMaster将需要执行的作业分发给TaskManager执行。

可见,单作业模式与会话模式的区别只在于JobManger的启动方式,以及省去了Dispatcher。在将作业提交给JobMaster之后的流程就与会话模式完全一样了。

三、数据流图

Flink是流式处理框架,它的程序结构其实就是定义了一连串的处理操作,每个数据输入之后都会依次调用每一步计算。在Flink代码中定义的每个处理转换操作都叫作“算子”(Operator),因此,我们的程序可以看作由一串算子构成的管道,而数据则像水流一样有序地流过。例如:在之前的WordCount代码中,基于执行环境调用的socketTextStream()方法就是一个读取文本流的算子;而后面的flatMap()方法则是将字符串数据进行切分的算子。

所有的Flink程序都可以归纳为由3部分组成:Source、Transformation、Sink。

  1. Source表示源算子,负责读取数据源。

  1. Transformation算子,利用各种算子进行处理加工。

  1. Sink表示下沉算子,负责数据的输出。

在运行时,Flink程序会被映射成所有算子按照逻辑顺序连接在一起的一张图,称为逻辑数据流,或者叫做数据流图。提交作业之后,打开Flink自带的Web UI,单击作业就能看到对应的数据流图。

在数据流图中,可以清楚地看到Source、Transformation、Sink这3部分。

数据流图中的每一条数据都以一个或多个Source开始,以一个或多个Sink结束。

算子的并行度有时会受到自身具体实现的影响,比如env.readTextFile他本身就是非并行的Source,因此无论怎么设置,它在运行时的并行度都是1,对应在数据流图上就只有一个并行子任务。

在大部分情况下,数据流中的算子和程序中的转换运算是一一对应的关系。那是不是说,代码中基于DataStream API的每一个方法掉用都是一个算子(即转换操作)呢?答案并非如此。除了Source读取数据和Sink输出数据,一个中间的Transformation(即算子)必须是一个转换处理操作;而在代码中,有一些方法调用的数据是没有完成转换的,可能只是对属性做了一个设置,也可能定义的是数据的传递方式而非转换,又或者是需要几个方法合在一起才能表达一个完整的转换操作。

例如:在之前的代码中,我们用到了定义分组的方法keyBy(),它就只是一个数据分区操作,而并不是一个算子(即转换操作),所以keyBy不是一个算子。事实上,在代码中可以看到,调用其他转换操作之后返回的数据类型是DataStrem,说明这是一个算子操作。而调用keyBy()方法之后返回的类型是KeyedStream。

四、并行度

  1. 什么是并行计算

1、任务并行

第一条数据在source读取完后,在map()在转换,这时,source可以处理下一条数据。即不同的操作(任务)可以同时处理。但是这样,多条数据同时过来,需要一条一条运行,会需要等待。

2、数据并行

将一个算子分成多个子任务,这样算子可以进行同时计算。

2、并行子任务和并行度的概念

我们把一个算子操作复制多份到多个节点上,数据来了之后,就可以到其中任意一个节点去执行。这样一来,一个算子操作就被拆分成了多个并行的子任务,再将他们分发到不同的节点上,就真正实现了并行计算。

在Flink的执行过程中,每个算子(operator)可以包含一个或多个子任务(subtask),这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行。

一个特定算子的子任务(subtask)个数被称为其并行度(parallelism)这样包含并行子任务的数据流就是并行数据流,它需要多个分区来分配并行任务,一般情况下,一个流程序的并行度可以认为就是其所有算子中最大的并行度,在一个程序中,不同的算子可能具有不同的并行度。

下图,当前数据中有Source、map、keyBy/window/apply、Sink4个算子时,除最后的Sink外,其他算子的并行度都为2.整个程序包含了7个子任务,至少需要在2个分区并行执行,可以说,这段流处理程序的并行度就是2。

3、并行度设置

在Flink中,可以用不同的方法设置并行度,它们的有效范围和优先级是不同的。

  1. 在代码中设置并行度

1)设置算子的并行度,这种方式设置的并行度只针对当前算子有效。

因为keyBy()返回的不是算子,所以无法对keyBy()设置并行度

注:print是算子

words.map(word => (word, 1)).setParallelism(2)

2)设置执行环境的并行度(全局并行度),代码中所有算子的默认并行度都为2

一般不会在程序中设置全局并行度

val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(2)
2.在提交作业时设置并行度

1)在使用flink run命令提交作业时

可以增加-p参数来指定当前应用程序的并行度,它的作用类似于对执行环境进行的全局并行度的设置。

flink run \
-p 2 \
-c chapter02.StreamWordCount \
/data/yzw/flink_jars/original-flink-1.0-SNAPSHOT.jar \
--host hadoop1 --port 7777

2)webUI提交作业时

可以在对应输入框中直接添加并行度。

3.在配置文件中设置

可以在配置文件flink-conf.yml中直接更改默认并行度。这个设置对于集群上提交的所有作业都有效。

parallelism.default: 2  #默认为1
4.总结

无论在代码中设置,还是在提交应用时增加-p参数,都不是必须的,因此,在没有指定并行度的时候,就会采用配置文件中的集群默认并行度。在开发环境中(idea),没有配置文件,默认并行度就是当前机器CPU的核数。

以下是所有并行度设置的方法,优先级从高到低如下:

①算子的并行度

②全局并行度(即env设置的并行)

③提交代码时-p指定的并行度

④集群的默认并行度 flink-conf.yaml中的parallelism.default: 1

实际生产中应该只对算子进行并行度的设置,在提交代码时指定并行度。

五、算子链

注:此图指定-p并行度为2。socket文本流的并行度只能为1,无法改变。

  1. 算子任务间的数据传输

一个程序中,不同的算子可能具有不同的并行度。

一个数据流在算子之间的传输数据的方式可以是一对一(one by one)的直通模式,也可以是打乱的重分区(redistributing)模式,具体是哪一种取决于算子的种类。

1)一对一(one to one)的直通模式

在一对一的直通模式下,数据流维护着分区及元素的顺序例如,上图的Source和map()算子,Source算子读取数据之后,可以直接发送给map()算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着map()算子的子任务看到的元素个数和顺序跟Source算子的子任务产生的完全相同,保证着一对一的关系。map()、filter()、flatMap()等都是one-to-one的对应关系。

相当于spark中的窄依赖。

2)重分区(redistributing)模式

①并行度改变会发生重分区。如从并行度为1的Source到并行度为2的map。这时数据传输方式是再平衡,会把数据均匀地向下游子任务分发出去。

②算子之间会引起重分区操作。例如keyBy()+sum(),sum()是分组操作,本质上基于键(key)和哈希值(hashCode)进行了重分区。

这些传输方式都会引起重分区的过程,这一过程类似于spark中的shuffle。

2、合并算子链(Operator Chains)

算子之间是on-to-one的关系且并行度相同,就可以合并成算子链。

在Flink中,并行度相同的一对一算子操作可以直接连接在一起形成一个Task,这样,原来的算子就成了真正任务里的一部分(子任务subtask)。每个Task的并行子任务会被一个线程执行。这样的技术称为算子链(Oprator Chain)。

在上图中,Source和map()算子之间满足算子链的需求,因此可以直接并行在一起,形成一个Task,因为并行度为2,所以合并后的Task也有两个并行子任务。这样,这个数据流图表示的作业最终会有5个任务,由5个线程并行执行。

Flink为什么要有算子链这样一个设计呢?这是因为将算子链接成Task是非常有效的优化,可以减少线程之间的切换和基于缓存区的数据交换,在降低时延的同时提升了吞吐量。

Flink默认会按照算子链的原则进行连接合并,如果要禁止合并或自行定义,则可以在代码中对算子做一些特定的设置:

//当前这个map不能和前后的任何任务合并算子链(完全断开)
words.map(word => (word, 1)).disableChaining();

//前面断,后面不断,即:从当前算子开始开启一个新的算子链
words.map(word => (word, 1)).startNewChain()

//全局禁用算子链
env.disableOperatorChaining()

六、作业图与执行图

由Flink程序执行映射成的数据流图也被称为逻辑流图,因为它表示的是计算逻辑的高级视图。当到具体执行环节时,我们还要考虑并行子任务的分配、数据在任务间的传输,以及合并算子链的优化。为了说明最终应该怎样执行一个流处理程序,Flink需要将逻辑视图进行解析,转换为物理图(Physica Graph)。

在这个转换过程中,有几个不同的阶段,会生成不同层级的图,其中最重要的就是作业图和执行图。Flink中任务调度执行的图按照生成顺序可以分成4层:逻辑流图->作业图->执行图->物理图。

wordcount程序的处理过程:

env.socketTextStream().flatMap().map().keyBy().sum(1).print()

如果在提交应用时设置并行度为2:

flink run \
-p 2 \
-c chapter02.StreamWordCount \
/data/yzw/flink_jars/original-flink-1.0-SNAPSHOT.jar \

根据之前的分析,除了env.socketTextStream()是非并行的Source算子(它的并行度始终为1),其他算子的并行度都为2。

接下来分析一下程序对4层调度图的演变过程

1、逻辑流图(StreamGraph)

逻辑流图是根据用户通过DataStream API编写的代码生成的最初DAG,用来表示程序的拓扑结构。这一步一般在客户端完成。

可以看到,逻辑流图中的节点完全对应着代码中的4步算子操作:源算子Source(socketTextStream())->扁平映射算子FlatMap(flatMap())->分组算子Keyed Aggregation(keyBy()/sum())->输出算子Sink(print())。

2、作业图(JobGraph)

逻辑流图经过优化后生成的就是作业图,这就是交给JobManager的数据结构,确定了当前作业所有任务的划分。主要的优化为:将多个符合条件的节点链接在一起合并成一个任务点,形成算子链,这样可以降低数据交换的消耗。作业图一般也是在客户端生成的(执行main方法),在作业提交时传递给JobManager。

在第二个小图内,分组算子(Keyed Aggregation)和输出算子Sink的并行度都为2,而且是一对一的关系,满足算子链的要求,因此会合并在一起,成为一个任务节点。

3、执行图(ExecutionGraph)(核心)

JobMaster收到作业图后,会根据它来生成执行图。执行图是作业图的并行化版本,是调度层最核心的数据结构。

在第三个小图内,执行图与作业图的最大区别就是按照并行度对并行子任务进行了拆分,并明确了算子任务间数据传输的方式。

4、物理图

JobManager生成执行图后,会将它分发给TaskManager;各个TaskManager会根据执行图部署任务,最终的物理执行过程也会形成一张图,一般叫作物理图,这只是具体执行层面的图,并不是一个具体的数据结构。

在第四个小图内,物理图主要就是在执行图的基础上进一步确定数据存放的位置和收发的具体方式。有了物理图,TaskManager就可以对传递来的数据进行处理计算了。

因此可以看到,程序里定义了4个算子操作:源(Source)->转换(flatMap())->分组聚合(keyBy()/sum())->输出(print());合并算子链进行优化之后,就有3个任务节点了;考虑并行度后,一共有5个并行子任务,最终需要5个线程来执行。

注:前3个图:逻辑流图(StreamGraph)、作业图(JobGraph)、执行图(ExecutionGraph)在代码中都有对的类。物理图没有。

七、任务和任务槽

  1. 任务槽

Flink中的每一个TaskManager都是一个JVM进程,可以启动多个独立的线程(每个线程可以运行一个子任务),从而并行执行多个子任务。

因此,如果想要执行5个子任务,那么并不一定非要5个TaskManager,可以让TaskManager多线程执行任务。如果可以同时运行5个线程,那么只要一个TaskManager就可以满足之前程序的运行需求了。

很显然,TaskManager的计算资源是有限的,并不是所有任务都可以放在一个TaskManager上并行执行。并行的任务越多,每个线程的资源就会越少。那一个TaskManager到底能并行处理多少个任务呢?为了控制并发量,需要在TaskManager上对每个任务运行所占用的资源做出明确的划分,这就是所谓的任务槽。

每个任务槽(task slot)其实表示了TaskManager拥有计算资源的一个固定大小的子集。这些资源就是用来独立执行一个子任务的。

假如一个TaskManager有3个任务槽,那么它会将管理的内存平均分为3份,每个任务槽独自占据一份,这样一来,在任务槽中执行一个子任务时,相当于划定了一块内存“专款专用”,就不需要跟来自其他作业的任务去竞争内存资源了。因此,现在只要2个TaskManager就可以并行处理分配好的5个子任务了。

2、任务槽数量的设置

我们可以通过集群的配置文件来设定TsakManager的任务槽数量。

taskmanager.numberOfTaskSlots: 8

通过调整任务槽的数量,可以控制子任务之间的隔离级别。

具体来说,如果一个TaskManager只有一个任务槽,就意味着每个任务都会运行在独立的JVM进程(当然,该JVM可能是通过一个特定的容器启动的)中;而一个TaskManager若设置多个任务槽,则意味着多个子任务可以共享同一个JVM进程。它们的区别在于:前者任务之间完全独立运行,隔离级别更高、彼此间的影响可以降到最小;而后者在同一个JVM进程中运行的任务共享TCP连接和心跳消息,也可能共享数据集和数据结构,这就降低了每个任务的运行开销,在降低隔离级别的同时提升了性能。

需要注意的是,任务槽目前仅仅用来隔离内存,不会涉及CPU的隔离在具体应用时,可以将任务槽数量配置为机器的CPU核数,尽量避免不同任务之间对CPU的竞争。这也是将开发环境默认并行度设置为CPU核数的原因。

3、任务对任务槽的共享

这样看来,一共有多少任务,就需要有多少任务槽来并行处理它们。不过,实际提交作业进行测试就会发现,我们之前的WordCount程序设置并行度为2提交,一共有有5个并行子任务,可集群即使只有2个任务槽也是可以成功提交并行运行的,这又是为什么呢?

我们可以基于之前的例子继续扩展。如果保持Sink子任务的并行度为1不变,而在作业提交时设置全局并行度为6,那么前两个任务节点各自有6个并行子任务,整个流处理程序有13个子任务。那对于有2个TaskManager(每个有3个任务槽)的集群配置来说,还能否正常运行呢?

答案是完全没有问题。这是因为在默认情况下,Flink是允许子任务共享任务槽的,如上图所示,只要属于同一个作业,不同任务节点的并行子任务就可以放在同一个任务槽中执行。因此,对于第一个任务节点Source->map(),它的6个并行子任务必须分到不同的任务槽(如果在同一个任务槽中,就没法数据并行了)中,而第二个任务节点keyBy()/window()/apply()的并行子任务则可以和第一个任务节点共享任务槽。

于是最终结果就变成了每个任务节点的并行子任务一字排开,占据不同的任务槽;而不同的任务节点的子任务可以共享任务槽。在一个任务槽中,可以将程序处理的所有任务都放在这里执行,我们把它叫作保存了整个作业的运行管道。

这个特性看起来很奇怪:我们不是希望并行处理且任务之间相互隔离吗,那为什么这里又允许共享任务槽呢?

我们知道,一个任务槽对应了一组独立的计算资源。在之前不做共享的时候,每个任务都平等地占据了一个任务槽,但其实不同的任务对资源的占用是不同的。例如这里的前两个任务:Source/map()尽管是两个算子合并算子链得到的,但它只是基本的数据读取和简单转换,计算耗时极短,一般也不需要太大的内存空间;而后面的窗口操作往往会涉及大量的数据、状态存储和计算,一般把这类任务叫作资源密集型任务。当他们被平等地分配到独立的任务槽上时,在实际运行时就会发现,当大量数据到来时,Source/map()和Sink任务很快就可以完成,但窗口任务却耗时很久,于是下游的Sink任务占据的任务槽就会等待闲置,而上游的Source/map()任务受限于下游的处理能力,也会在快读处理完一部分数据后阻塞对应的资源而开始等待(相当于处理背压)。这样,资源的利用就出现了极大的不平衡。

解决这一问题的思路就是允许任务槽共享。当我们将资源密集型和非密集型的任务同时放到一个任务槽中时,它们就可以自行分配对资源占用的比例,从而保证将最重的任务平均分配给所有的TaskManager。

任务槽共享的另一个好处就是允许我们保存完整的作业运行管道。这样一来,即使某个TaskManager出现故障而宕机,其他节点也可以完全不受影响,作业的任务可以继续执行。

另外,同一个任务节点的并行子任务是不能共享任务槽的,因此,在允许任务槽共享之后,运行作业所需的任务槽数量正好就是作业中所有算子并行度的最大值。这样一来,在我们考虑当前集群需要配置多少任务槽资源时,就不需要再去详细计算一个作业总共包含多少个并行子任务了,只要看最大的并行度就够了。

当然,Flink默认是允许任务槽共享的,如果希望某个算子对应的任务完全独占一个任务槽,或者只有某一部分算子共享任务槽,则可以通过设置“任务槽共享组”(slotSharingGroup)手动指定:

//在一个共享组里的算子才是一个共享一个slot的,不在一个共享组中的算子,彼此之间必须隔离
//默认情况下,所有的算子都在default共享组里
//当前算子的slot共享组是"1",那么当前算子后面的所有算子都属于共享组"1"
lines.flatMap(line => line.split("\\s+")).slotSharingGroup("1")

在一个共享组里的算子才是一个共享一个slot的,不在一个共享组中的算子,彼此之间必须隔离。

默认情况下,所有的算子都在default共享组里。

当前算子的slot共享组是"1",那么当前算子后面的所有算子都属于共享组"1"。

这样,只有属于同一个任务槽共享组的子任务才会开启任务槽共享功能;不同组之间的任务是完全隔离的,必须分配到不同的任务槽中。这种场景下,总共需要的任务槽数量就是各个任务槽共享组最大并行度的总和。

4、任务槽和并行度的关系

直观上看,任务槽就是TaskManager为了并行执行任务而设置的,那它和之前讲过的并行度(Parallelism)是不是一回事呢?

任务槽和并行度确实都跟程序的并行执行有关,但两者是完全不同的概念。简单来说,任务槽是静态的概念,是指TaskManager具有的并发执行的能力,可以通过参数taskmanager.numberOfTaskSlots进行配置;而并行度是动态概念,即TaskManager运行程序时实际使用的并发能力,可以通过参数parallelism.default进行配置。换句话说,并行度如果小于或等于集群中可用任务槽的总数,那么程序是可以正常执行的,因为任务槽不一定要全部占用;而如果并行度大于可用任务槽的总数,超出并行能力上限,那么程序就只能等待ResourceManager(资源管理器)分配更多的资源了。

推荐将slot个数设置成cpu与核数相等的个数。

下面再举一个例子。假设一共有3个TaskManager,每个TaskManager中的任务槽数量设置为3个,即一共有9个任务槽,如下图,表示集群最多能并行执行9个任务。

而我们定义WordCount程序处理操作是4个转换算子:Source->FlatMap->Reduce->Sink。

当所有的并行度相同时,容易看出Source和FlatMap可以合并成算子链,于是最终有3个任务节点

如果我们没有进行任何并行度的设置,而配置文件parallelism.default=1,那么程序运行的默认并行度为1,总共有3个任务。由于不同算子的任务可以共享任务槽,所以最终占用的任务槽只有1个,即有8个空闲,如下图所示。

如果我们更改默认参数,或者在提交作业时设置并行度为2,那么总共有6个任务,共享之后会占用2个任务槽,如下图。因此,就有7个任务槽空闲,计算资源没有得到充分利用。可以看到,只有设置合适的并行度才能提交效率。

那么对于这个例子,怎杨设置并行度才能使效率最高呢?当然是需要把所有的任务槽都利用起来。考虑到任务槽共享,可以直接把并行度设置为9,这样所有27个任务就会完全占用9个任务槽,这是当前集群资源下能执行的最大并行度,计算资源得到了充分的利用,如下图。

再考虑对某个算子单独设置并行度的场景。例如,如果我们考虑到输出可能是写入文件,则希望不要并行写入文件,此时就是需要设置Sink算子的并行度为1。这时其他算子的并行度依然为9,因此,总共会有19个子任务(2(前两个算子)*9(并行度为9)+1(最后一个算子并行度为1))。根据共享任务槽的原则,它们最终还是会占用全部的9个任务槽的,而Sink算子任务只在其中一个任务槽中执行,如下图。通过这个例子也可以明确地看到,整个流处理程序的并行度就应该是所有算子并行度中最大的那个,这代表了运行程序需要的任务槽数量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值