前面我们讲了Flink运行时的核心组件和提交流程,但有些细节需要进一步的思考,一个具体的作业是怎样从编写的代码转换成TaskManager可以执行的任务的呢?JobManager在收到提交的作业之后,又是如何确定总共有多少任务、需要配置多少资源的呢?本文将从一些重要的概念入手,对上述问题做详细的讲解。
一、数据流图
Flink是流式计算框架,它的程序结构其实就是定义了一连串的处理操作,会对输入数据一次调用一系列的操作,每个处理转换操作都叫做算子。因此,Flink程序可以看作是一串算子构成的管道,而数据则像水流一样有序地流过。
所有Flink程序都可以抽象出以下三部分:Source、Transformation、Sink。
- Source表示源算子,负责读取数据源;
- Transformation表示转换算子。利用各种算子对数据进行加工处理;
- Sink表示下沉算子,负责数据的输出。
Flink程序会被映射成所有算子按照逻辑顺序连接在一起的图,称作逻辑数据流,或者数据流图。数据流图类似于任意的有向无环图(DAG),数据流图中的每一条数据流都是以一个或者多个Source开始,以一个或多个Sink结束。
二、并行度
实现数据并行的方式很简单,把一个算子操作复制多份到多个节点上,数据来了之后,就可以到其中任意一个节点上去执行了。这样,一个算子操作就被拆分成了多个并行的子任务,再将它们分发到不同的节点上,就真正实现了并行计算。
在Flink的执行过程中,每个算子可以包含一个或者多个子任务,这些子任务可以在不同的线程、不同的物理机或不同的容器中完全独立地执行。
一个特定算子的子任务的个数被称为其并行度,不同的算子可能具有不同的并行度,一个流程序的并行度可以认为就是其所有算子中最大的并行度。
如下所示,当前数据流中有Source、map()、keyBy()/window()/apply()、Sink等4个算子时,除了Sink算子的并行度是1,其余算子并行度均为2。整个程序包含了7个子任务,至少需要2个分区并行执行,可以说,这段流处理程序的并行度就是2。
三、算子链
3.1 算子任务间的数据传输
一个数据流在算子之间传输数据的方式可以是一对一的直通模式,也可以是打乱的重分区模式,具体是哪一种取决于算子的种类。
(1)一对一的直通模式
在一对一的直通模式下,数据流会维护着分区及元素的顺序,比如Source和map()算子。Source算子读取数据之后,可以直接发送给map()算子处理,它们之间不需要重新分区,也不需要调整数的顺序。map()、filter()、flatMap()等算子都是这种一对一的直通关系。
(2)重分区模式
在重分区模式下,数据流的分区会发生改变。比如map()和后面的keyBy()/window()/apply()算子之间。这里的keyBy()是数据传输方法,后面的window()/apply()方法共同构成了窗口算子。
每个算子的子任务都会根据数据传输的策略把数据发送到不同的下游目标任务中,例如keyBy()是分组操作,本质上基于key的哈希值进行了重分区。
3.2 合并算子链
在Flink中,并行度相同的一对一算子操作可以直接链接在一起而形成一个大任务(Task),这样原来的算子就成了真正任务里的一部分。如下图所示:
Source和map()算子之间满足算子链的要求,因此可以将它们直接合并在一起,形成一个Task,因为并行度是2,因此合并后的Task也有两个并行子任务。这个数据流表示的作业最终会有5个任务,并由5个线程执行。
为什么会有算子链的设计呢?
这是因为将算子链接成Task可以减少线程之间的切换和基于缓存区的数据交换,在降低时延的同事提升了吞吐量。
四、任务槽
Flink中的每一个TaskManager都是一个JVM进程,可以启动多个独立的线程,从而并行执行多个子任务。但是每个TaskManager的计算资源是有限的,并不是所有任务都可以放在一个TaskManager上并行执行,并行的任务越多,每个线程的资源就会越少。那么一个TaskManager到底能并行处理多少个任务呢?
为了控制并发量,需要在TaskManager上对每个任务运行所占用的资源做出明确的划分,这就是所谓的任务槽(task slot)。每个任务槽其实表示了TaskManager拥有计算资源的一个固定大小的子集,是用来独立执行一个子任务的。
假如一个TaskManager有3个任务槽,那么它会将管理的内存平均分为3份,每个任务槽独自占据一份,在任务槽中执行一个子任务时,相当于划定了一块专属内存,CPU通过轮询方式共享。那么一个被划分了5个子任务的作业,只需要两个TaskManager就可以了。
Flink是允许子任务共享任务槽的,只要属于同一个作业,不同任务节点(可以理解为不同算子/算子链)的并行子任务就可以放在同一个任务槽中执行。
相反,同一个任务节点的并行子任务是不能共享任务槽的,因此运行作业所需的任务槽数量正好就是作业中所有算子并行度的最大值。