本文主要参考了flink官方文档
以及一篇博客:http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/
下图中的示例数据流由五个子任务执行,因此具有五个并行线程
Flink运行时由两种类型的进程组成:
JobManagers(也称之为masters)协调分布式执行,它们调度任务,协调checkpoints,故障恢复等
TaskManagers(也称之为workers)执行数据流的任务(更具体的说,subtasks),以缓冲和交换数据流。至少有一个TaskManagers
JobManagers和TaskManagers可以以几种方式启动:直接在机器上作为独立群集,在容器中,或者由像YARN或Mesos这样的资源框架来管理。TaskManagers连接到JobManagers,宣布自己可用,并分配工作。
Client是不运行程序执行时的部件,而是被用来准备和发送的数据流给JobManager。之后,客户端可以断开或保持连接以接收进度报告。客户端可以作为触发执行的Java / Scala程序的一部分运行,也可以在命令行进程中运行./bin/flink run ...
。
小结
当 Flink 集群启动后,首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager,JobManager 再调度任务到各个 TaskManager 去执行,然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。
Client 为提交 Job 的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交 Job 后,Client 可以结束进程(Streaming的任务),也可以不结束并等待结果返回。
JobManager 主要负责调度 Job 并协调 Task 做 checkpoint,职责上很像 Storm 的 Nimbus。从 Client 处接收到 Job 和 JAR 包等资源后,会生成优化后的执行计划,并以 Task 的单元调度到各个 TaskManager 去执行。
TaskManager 在启动的时候就设置好了槽位数(Slot),每个 slot 能启动一个 Task,Task 为线程。从 JobManager 处接收需要部署的 Task,部署启动后,与自己的上游建立 Netty 连接,接收数据并处理。
可以看到 Flink 的任务调度是多线程模型,并且不同Job/Task混合在一个 TaskManager 进程中。虽然这种方式可以有效提高 CPU 利用率,但是个人不太喜欢这种设计,因为不仅缺乏资源隔离机制,同时也不方便调试。类似 Storm 的进程模型,一个JVM 中只跑该 Job 的 Tasks 实际应用中更为合理。
每一个worker(TaskManager)都是JVM进程。可以会执行一个或者多个subtask在不同的线程中。为了控制一个 TaskManager 能接受多少个 task,Flink 提出了Task Slot 的概念。
每一个task slots代表着TaskManager的一个固定的资源子集。例如,一个拥有三个插槽的TaskManager将为每个插槽分配1/3的托管内存。
将资源 slot 化意味着来自不同job的task不会为了内存而竞争,而是每个task都拥有一定数量的内存储备。需要注意的是,这里不会涉及到CPU的隔离,slot目前仅仅用来隔离task的内存。
通过调整 task slot 的数量,用户可以定义task之间是如何相互隔离的。每个 TaskManager 有一个slot,也就意味着每个task运行在独立的 JVM 中。每个 TaskManager 有多个slot的话,也就是说多个task运行在同一个JVM中。而在同一个JVM进程中的task,可以共享TCP连接(基于多路复用)和心跳消息,可以减少数据的网络传输。也能共享一些数据结构,一定程度上减少了每个task的消耗。
每一个 TaskManager 会拥有一个或多个的 task slot,每个 slot 都能由多个连续 task 组成的一个 pipeline,比如 MapFunction 的第n个并行实例和 ReduceFunction 的第n个并行实例可以组成一个 pipeline。
默认情况下,fink允许来自不同task的subtasks共享slots,只要来源于同一个job即可。由此带来的结果可能是一个slot可能会占用整个job的pipeline。那好处是什么呢?主要有两处:
Flink 集群所需的task slots数与job中最高的并行度一致。也就是说我们不需要再去计算一个程序总共会起多少个task了。
更容易获得更充分的资源利用。如果没有slot共享,那么非密集型操作source/flatmap就会占用同密集型操作 keyAggregation/sink 一样多的资源。如果有slot共享,将基线的2个并行度增加到6个,能充分利用slot资源,同时保证每个TaskManager能平均分配到重的subtasks。
我们将 WordCount 的并行度从之前的2个增加到6个(Source并行度仍为1),并开启slot共享(所有operator都在default共享组),将得到如上图所示的slot分布图。首先,我们不用去计算这个job会其多少个task,总之该任务最终会占用6个slots(最高并行度为6)。其次,我们可以看到密集型操作 keyAggregation/sink 被平均地分配到各个 TaskManager。
SlotSharingGroup
是Flink中用来实现slot共享的类,它尽可能地让subtasks共享一个slot。相应的,还有一个CoLocationGroup
类用来强制将 subtasks 放到同一个 slot 中。CoLocationGroup
主要用于迭代流中,用来保证迭代头与迭代尾的第i个subtask能被调度到同一个TaskManager上。这里我们不会详细讨论CoLocationGroup
的实现细节。
怎么判断operator属于哪个 slot 共享组呢?默认情况下,所有的operator都属于默认的共享组default
,也就是说默认情况下所有的operator都是可以共享一个slot的。而当所有input operators具有相同的slot共享组时,该operator会继承这个共享组。最后,为了防止不合理的共享,用户也能通过API来强制指定operator的共享组,比如:someStream.filter(...).slotSharingGroup("group1");
就强制指定了filter的slot共享组为group1
。
本文主要介绍了Flink中计算资源的相关概念以及原理实现。最核心的是 Task Slot,每个slot能运行一个或多个task。为了拓扑更高效地运行,Flink提出了Chaining,尽可能地将operators chain在一起作为一个task来处理。为了资源更充分的利用,Flink又提出了SlotSharingGroup,尽可能地让多个task共享一个slot。