一、Flink架构概览
1、Flink介绍
Apache Flink是一个分布式系统,需要有效地分配和管理计算资源,以便执行流式应用程序。它可以与所有常见的集群资源管理器集成,例如Hadoop YARN和Kubernetes,但也可以设置为作为独立集群或甚至作为库运行。
Apache Flink 是一个针对无界和有界数据流进行有状态计算的框架。Flink 自底向上在不同的抽象级别提供了多种 API,并且针对常见的使用场景开发了专用的扩展库。
Flink官网关于Flink架构的文档地址:https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/concepts/flink-architecture/
此处是基于官网资料,做个自我归纳和总结。
2、Flink架构图
Flink采用主从架构,运行时由两种类型的进程组成:一个 JobManager 和一个或者多个 TaskManager。
- JobManager:主进程,负责调度和资源管理
- TaskManager:从进程,由JobManager分配任务并执行
Client (客户端)不是运行时和程序执行的一部分,而是用于准备数据流并将其发送给 JobManager。之后,客户端可以断开连接(分离模式),或保持连接来接收进程报告(附加模式)。客户端可以作为触发执行 Java/Scala 程序的一部分运行,也可以在命令行进程./bin/flink run ...
中运行。
可以通过多种方式启动 JobManager 和 TaskManager:直接在机器上作为standalone 集群启动、在容器中启动、或者通过YARN等资源框架管理并启动。TaskManager 连接到 JobManagers,宣布自己可用,并被分配工作。
2.2 JobManager(作业管理器)
JobManager是一个Flink集群中任务管理和调度的核心,是控制应用执行的主进程。也就是说,每个应用都应该被唯一的JobManager所控制执行。
JobManager 协调和管理Flink 应用程序的分布式执行:任务的调度管理、任务的完成或失败处理、协调 checkpoint、任务异常恢复等等。
JobManger又包含三个不同的组件:
2.2.1 ResourceManager(资源管理器)
ResourceManager主要负责资源的分配、回收和管理,在Flink 集群中只有一个。所谓“资源”,主要是指TaskManager的任务槽(task slots)。任务槽就是Flink集群中的资源调配单元,包含了机器用来执行计算的一组CPU和内存资源。每一个任务(Task)都需要分配到一个slot上执行。
Flink 为不同的环境和资源提供者(例如 YARN、Kubernetes 和 standalone 部署)实现了对应的 ResourceManager。这里注意要把Flink内置的ResourceManager和其他资源管理平台(比如YARN)的ResourceManager区分开。
2.2.2 JobMaster
JobMaster是JobManager中最核心的组件,负责处理单独的作业(Job)。故JobMaster和具体的Job是一一对应的,Flink 集群中可以同时运行多个作业,每个作业都有自己的 JobMaster。
在作业提交时,JobMaster会先接收到要执行的应用。JobMaster会把JobGraph转换成一个物理层面的数据流图,这个图被叫作“执行图”(ExecutionGraph),它包含了所有可以并发执行的任务。JobMaster会向资源管理器(ResourceManager)发出请求,申请执行任务必要的资源。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。而在运行过程中,JobMaster会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。
2.2.3 Dispatcher
Dispatcher主要负责提供一个REST接口,用来提交 Flink 应用程序执行,并且负责为每一个新提交的作业启动一个新的JobMaster 组件。
Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。Dispatcher在架构中并不是必需的,在不同的部署模式下可能会被忽略掉。
2.3 TaskManager(任务管理器)
TaskManager(也称为 worker)是Flink中的工作进程。在执行过程中,TaskManager可以缓存数据,还可以跟其他运行同一应用的TaskManager交换数据。
Flink集群中必须至少有一个TaskManager;每一个TaskManager都包含了一定数量的任务槽(task slots)。Slot是资源调度的最小单位,slot的数量限制了TaskManager能够并行处理的任务数量。启动之后,TaskManager会向资源管理器注册它的slots;收到资源管理器的指令后,TaskManager就会将一个或者多个槽位提供给JobMaster调用,JobMaster就可以分配任务来执行了。
二、Flink核心概念
1 Parallelism(并行度)
1.1 什么是parallelism?
一个Flink程序是由多个任务组成(source、transformation和sink)。一个任务由多个并行的实例(线程)来执行,一个任务的并行实例(线程)数目就被称为该任务的并行度。
当要处理的数据量非常大时,我们可以把一个算子操作,“复制”多份到多个节点,数据来了之后就可以到其中任意一个执行。这样一来,一个算子任务就被拆分成了多个并行的“子任务”(subtasks),再将它们分发到不同节点,就真正实现了并行计算。在Flink执行过程中,每一个算子(operator)可以包含一个或多个子任务(operator subtask),这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行。
一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。这样,包含并行子任务的数据流,就是并行数据流,它需要多个分区(stream partition)来分配并行任务。一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中,不同的算子可能具有不同的并行度。
1.2 并行度的设置
在Flink中设置作业并行度的常用方式有:
全局并行度设置:Flink的配置文件(conf/flink-conf.yaml),在flink配置文件中可以看到其默认并行度是1。
# The parallelism used for programs that did not specify and other parallelism.
parallelism.default: 1
程序内部设置:在代码中通过ExecutionEnvironment的setParallelism()方法设置并行度。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
算子Operator级别设置:在单个操作符内部通过setParallelism()设置该操作符的并行度。
.flatMap(new XxxFlatMapFunction()).setParallelism(5)
.map(new XxxMapFunction).setParallelism(5)
运行时命令行设置:在提交作业时通过参数 -p 指定并行度的值。
./bin/flink run -p 10 ../examples/WordCount-java.jar
1.3 主要的选择原则
- 全局设置用于所有作业,更方便统一管理。
- 细粒度设置可以根据不同操作符的负载调整,但过于复杂。
- 运行时设置可以动态调整,更灵活。
- 并行度设置的优先级别,颗粒度越细,优先级越高。
2 Operator Chain(算子链)
2.1 算子间的数据传输
一个数据流在算子之间传输数据的形式可以是一对一(one-to-one)的直通(forwarding)模式,也可以是打乱的重分区(redistributing)模式,具体是哪一种形式,取决于算子的种类。
2.1.1 一对一(One-to-one,forwarding)
这种模式下,数据流维护着分区以及元素的顺序。比如WC案例中的source和map算子,source算子读取数据之后,可以直接发送给map算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着map 算子的子任务,看到的元素个数和顺序跟source 算子的子任务产生的完全一样,保证着“一对一”的关系。map、filter、flatMap等算子都是这种one-to-one的对应关系。这种关系类似于Spark中的窄依赖。
2.1.2 重分区(Redistributing)
在这种模式下,数据流的分区会发生改变。比如图中的map和后面的keyBy/window算子之间,以及keyBy/window算子和Sink算子之间,都是这样的关系。
每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。这些传输方式都会引起重分区的过程,这一过程类似于Spark中的shuffle。
2.2 合并算子链
算子链(Operator Chain)是一种将多个算子(Operator)链接在一起的优化技术,可以提高Flink作业的性能和吞吐量。在Operator Chain中,多个算子被串联在一起,形成一个大的算子链,从而减少算子之间的通信和数据传输开销。同时,由于算子之间的数据不需要序列化和反序列化,可以减少CPU和内存的开销、减少网络传输,提高作业的性能和吞吐量。
上图中Source和map之间满足了算子链的要求,所以可以直接合并在一起,形成了一个任务;因为并行度为2,所以合并后的任务也有两个并行子任务。这样,这个数据流图所表示的作业最终会有5个任务,由5个线程并行执行。
2.3 算子链的应用
2.3.1 自动Operator Chain
Flink会自动检测算子之间的依赖关系,自动将多个算子链接在一起形成Operator Chain。在使用Flink的DataStream API或者Table API时,默认情况下会启用自动Operator Chain优化,无需额外的配置或代码修改。
用户能够通过禁用全局作业链的操作来关闭整个Flink的作业链,但是这个操作会影响到这个作业的执行情况,除非我们非常清楚作业的执行过程,否则不建议这么做。
StreamExecutionEnvironment.disableOperatorChaining();
2.3.2 手动Operator Chain
启动Operator Chain
注意该方法只对当前操作符及之后的操作符有效,下面的例子中,两个map(),会启用算子链合并,对filter()算子不起作用。
someStream.filter(...).map(...).startNewChain().map(...);
禁用Operator Chain
如果我们只想对某个算子执行禁用作业链,只需调用disableChaining()方法,该方法只会禁用当前算子的链条(上述代码中就是map),对其他算子操作不产生影响。
someStream.map(...).disableChaining().filter();
3 Task Slots(任务槽)
3.1 什么是任务槽(task slots)
任务槽(task slots)用来控制TaskManager同时运行任务的数量。
TaskManager 是一个 JVM 进程,并会以独立的线程来执行一个task。为了控制一个 TaskManager 能接受多少个 task,Flink 提出了 Task Slot 的概念,通过 Task Slot 来定义Flink 中的计算资源。solt 对TaskManager内存进行平均分配,每个solt内存都相同,加起来和等于TaskManager可用内存,但是仅仅对内存做了隔离,并没有对cpu进行隔离。将资源 slot 化意味着来自不同job的task不会为了内存而竞争,而是每个task都拥有一定数量的内存储备。
通过调整 task slot 的数量,用户可以定义task之间是如何相互隔离的。
每个 TaskManager 有一个slot,也就意味着每个task运行在独立的 JVM 中。每个 TaskManager 有多个slot的话,也就是说多个task运行在同一个JVM中。
而在同一个JVM进程中的task,可以共享TCP连接(基于多路复用)和心跳消息,可以减少数据的网络传输。也能共享一些数据结构,一定程度上减少了每个task的消耗。
图解:两个TaskManager,每个TaskManager有三个Task Slot。在任务槽不分享的情况下,并行度为2,有5个Task Slot在使用,一个Task Slot空闲。
3.2 任务槽数量的设置
在Flink的$FLINK_HOME/conf/flink-conf.yaml配置文件中,可以设置TaskManager的slot数量,默认是1个slot。
taskmanager.numberOfTaskSlots: 8
需要注意的是,slot目前仅仅用来隔离内存,不会涉及CPU的隔离。在具体应用时,可以将slot数量配置为机器的CPU核心数,尽量避免不同任务之间对CPU的竞争。这也是开发环境默认并行度设为机器CPU数量的原因。
3.3 任务对任务槽的共享
同一个作业中,不同任务节点的并行子任务,就可以放到同一个slot上执行。
默认情况下,Flink 允许 subtask 共享 slot,即便它们是不同的 task 的 subtask,只要是来自于同一作业即可。结果就是一个 slot 可以持有整个作业管道。允许 slot 共享有两个主要优点:
1)Flink 集群所需的 task slot 和作业中使用的最大并行度恰好一样。无需计算程序总共包含多少个 task(具有不同并行度)。
2)容易获得更好的资源利用。如果没有 slot 共享,非密集 subtask(source/map())将阻塞和密集型 subtask(window) 一样多的资源。通过 slot 共享,我们示例中的基本并行度从 2 增加到 6,可以充分利用分配的资源,同时确保繁重的 subtask 在 TaskManager 之间公平分配。
图解:两个TaskManager,每个TaskManager有三个Task Slot。在任务槽不分享的情况下,并行度最高可设置为6,6个Task Slot都处于运行状态,每个Task Slot运行整个作业管道(Source->map->keyBy->window->apply->Sink)。
可见,在任务槽的共享的情况下,并行运算效率得到了明细提升,资源更好被利用。