1. Task and Operator Chain

Flink 应用程序是以并行的方式在 Task 的并行化算子中执行的。Flink 应用程序的性能取决于 Task 如何被调度执行。在此之前,需要了解几个概念:

  • Task:代表可以在单个线程中执行的 Operator Chain 的抽象。 诸如,keyBy(这会导致网络改组通过 Key 对流进行分区),或者 Pipeline 并行度的变化都会破坏 Chain,并迫使 Operator 分配到不同的任务中。如图 1 中,该应用程序具有 3 个 Task。
  • Operator Chain:将两个或多个算子的并行化 Task 融合到单个线程,并在该单个线程中执行。融合的 Task 通过方法调用交换数据,因此基本上没有通信成本。所以,Chain 可以提高大多数应用程序的性能,这也是 Flink 默认的配置。
  • Subtask:是任务的一个并行切片。它是可调度的、可运行的执行单元。
  • Task Slot: 一个 Task Slot 具有运行应用程序的一个并行切片的资源。 Task Slot 的总数与应用程序的最大并行度相同。

2. Slot

每个 TaskManager 是一个 JVM 进程,它可以执行一个或多个 Subtask。而 TaskManager 中的 Task 的数量,就是用 Task Slot 来控制的。在 TaskManager 中,每个 Slot 拥有相等的内存资源。如图2,在 TaskManager 中有 3 个 Slot,那么每个 Slot 拥有 1/3 的资源。需要注意的是,Flink 对 Slot 只做了内存隔离,暂时并未做 CPU 隔离

flink乱序 flink setparallelism_并行度

3. Slot and Parallelism

并行度是 TaskManager 可以并行执行的能力。Flink 默认配置一个应用程序的算子只有一个 Slot。修改应用程序并行度的方式有以下三种,且优先级依次增加:

  1. 通过修改配置文件 flink-conf.yaml 中的 taskmanager.numberOfTaskSlots,对所有应用程序设置相同的并发度;
  2. 通过调用 env.setParallelism(int) 方法,对应用程序的每个算子设置相同并发度;
  3. 在代码里通过对每个算子调用 setParallelism(int) 方法,对指定算子的并行度进行修改。

网友给了一组图进行了比较好的描述,如图 3 中,应用程序的最大并行度是 9,taskmanager.numberOfTaskSlots 表示每个 TaskManager 拥有 3 个 Slot,即每个 TaskManager 的并发能力是 3。

flink乱序 flink setparallelism_flink_02


如图 4 中,应用程序的最大并行度是 9,每个 TaskManager 拥有 3 个 Slot,而 Source→flatMap、Reduce、Sink 这三组算子的并发度是 1。

flink乱序 flink setparallelism_并行度_03


图 5 是一张对比图,其中分别把应用程序的并行度设置为 2 和 9。不难发现,并行度设置为 2 时,应用程序并没有充分使用每个 Slot,即资源没有充分利用,会使性能下降。

flink乱序 flink setparallelism_flink_04


上文提到过,我们除了可以对整个应用程序的每个算子设置同样的并行度之外,还可以分别对不同的算子设置不同的并行度。如图 6 所示,Source→flatMap 和 Reduce 的并行度设置为 9,而 Sink 的并行度设置为 1,同时也覆盖了 flink-conf.yaml 或者 env.setParallelism(int) 的设置。

flink乱序 flink setparallelism_应用程序_05

4. Slot-Sharing Group

默认情况下,Flink 的每个算子都在名为 default 的 Slot-Sharing Group 里。我们也可以显示地用 slotSharingGroup(String) 方法指定算子的 Slot-Sharing Group。如果一个算子和它的直接上游算子都属于同一个 Group,那么该算子将会继承直接上游算子的 Slot-Sharing Group。

结合前面所讲述的,我们现在看一段示例代码:

// ssg:oringe
val a: DataStream[A] = env.addSource(...)
	.slotSharingGroup("oringe")
	.setParallelism(4)
val b: DataStream[B] = a.map(...)
// ssg oringe is inherited from a	
	.setParallelism(4)

// ssg:yellow
val c: DataStream[C] = env.addSource(...)
	.slotSharingGroup("yellow")
	.setParallelism(2)

// ssg:blue
val d: DataStream[D] = b.connect(c)
	.process(...)
	.slotSharingGroup("blue")
	.setParallelism(4)
val e: DataStream[E] = d.addSink(...)
// ssg blue is inherited from d
	.setParallelism(2)

上面的栗子中,共有 5 个算子,两个 Source 是 a 和 c,两个 Transformer 是 b 和 d,一个 Sink 是 e;三个 Slot-Sharing Group 分别是 a 和 b、c、d 和 e,即 oringe、yellow、blue。该栗子的 Job Graph 如图 7 所示。

flink乱序 flink setparallelism_并行度_06


该栗子一共需要 10 个 Slot,其中 orange 和 blue 各需要 4 个 Slot,yellow 需要 2 个 Slot。

5. Conclusion

Slot 是 Flink 应用程序的资源单位,但只能做到内存隔离,暂不支持 CPU 隔离。Slot 的总数与应用程序的最大并行度相同。如果设置的并行度大于应用程序的实际的并行度时,那么会导致资源浪费,比如 Kafka Partition 小于 Flink Source 的并行度。