flink学习笔记(二)分布式运行环境
本文主要参考了flink官方文档
以及一篇博客:http://wuchong.me/blog/2016/05/09/flink-internals-understanding-execution-resources/
Task和Operator Chains
下图中的示例数据流由五个子任务执行,因此具有五个并行线程
Job Managers, Task Managers, Clients
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 实际应用中更为合理。
Task Slots (任务槽)和Resources(资源)
每一个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。