天天看点

Flink 运行架构概览

作者:散文随风想
Flink 运行架构概览

1. 架构概览

Flink采用主从架构:

  • JobManager:主进程,负责调度和资源管理
  • TaskManager:从进程,由JobManager分配任务并执行

主要组件

JobManager(作业管理器)

JobManager是一个Flink集群中任务管理和调度的核心,是控制应用执行的主进程。也就是说,每个应用都应该被唯一的JobManager所控制执行。

JobManger又包含3个不同的组件:

  1. JobMaster

JobMaster是JobManager中最核心的组件,负责处理单独的作业(Job)。故JobMaster和具体的Job是一一对应的,多个Job可以同时运行在一个Flink集群中, 每个Job都有一个自己的JobMaster。

在作业提交时,JobMaster会先接收到要执行的应用。JobMaster会把JobGraph转换成一个物理层面的数据流图,这个图被叫作“执行图”(ExecutionGraph),它包含了所有可以并发执行的任务。JobMaster会向资源管理器(ResourceManager)发出请求,申请执行任务必要的资源。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。而在运行过程中,JobMaster会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。

  1. ResourceManager(资源管理器)

ResourceManager主要负责资源的分配和管理,在Flink 集群中只有一个。所谓“资源”,主要是指TaskManager的任务槽(task slots)。任务槽就是Flink集群中的资源调配单元,包含了机器用来执行计算的一组CPU和内存资源。每一个任务(Task)都需要分配到一个slot上执行。

这里注意要把Flink内置的ResourceManager和其他资源管理平台(比如YARN)的ResourceManager区分开。

  1. Dispatcher

Dispatcher主要负责提供一个REST接口,用来提交应用,并且负责为每一个新提交的作业启动一个新的JobMaster 组件。Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。Dispatcher在架构中并不是必需的,在不同的部署模式下可能会被忽略掉。

TaskManager(任务管理器)

TaskManager是Flink中的工作进程,数据流的具体计算就是它来做的。Flink集群中必须至少有一个TaskManager;每一个TaskManager都包含了一定数量的任务槽(task slots)。Slot是资源调度的最小单位,slot的数量限制了TaskManager能够并行处理的任务数量。启动之后,TaskManager会向资源管理器注册它的slots;收到资源管理器的指令后,TaskManager就会将一个或者多个槽位提供给JobMaster调用,JobMaster就可以分配任务来执行了。

在执行过程中,TaskManager可以缓冲数据,还可以跟其他运行同一应用的TaskManager交换数据。

核心概念

Parallelism(并行度)

  1. 并行子任务和并行度

当要处理的数据量非常大时,我们可以把一个算子操作,“复制”多份到多个节点,数据来了之后就可以到其中任意一个执行。这样一来,一个算子任务就被拆分成了多个并行的“子任务”(subtasks),再将它们分发到不同节点,就真正实现了并行计算。在Flink执行过程中,每一个算子(operator)可以包含一个或多个子任务(operator subtask),这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行。

一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。这样,包含并行子任务的数据流,就是并行数据流,它需要多个分区(stream partition)来分配并行任务。一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中,不同的算子可能具有不同的并行度。2. 并行度的设置 在Flink中设置作业并行度的常用方式有:

(1). 全局并行度设置:在Flink运行配置中通过参数parallelism全局设置并行度的值。

# 在Flink配置文件flink-conf.yaml中设置全局并行度参数:
parallelism.default: 8           

(2). 程序内部设置:在代码中通过ExecutionEnvironment的setParallelism()方法设置并行度。

# 1. 全局设置并行度为4
env = StreamExecutionEnvironment.get_execution_environment()
env.set_parallelism(4)            

(3). Operator级别设置:在单个操作符内部通过setParallelism()设置该操作符的并行度。

from pyflink.datastream import StreamExecutionEnvironment

env = StreamExecutionEnvironment.get_execution_environment()

# 默认并行度为1
data_stream = env.from_collection([1, 2, 3]) 

# map操作符设置并行度为2
mapped_stream = data_stream.map(lambda x: x * 2, parallelism=2)

# filter操作符不设置并行度,继承map的2  
filtered_stream = mapped_stream.filter(lambda x: x > 2) 

# sink操作符继承filter并行度2
filtered_stream.print()           

(4). 运行时命令行设置:在提交作业时通过参数 -p 指定并行度的值。

env = StreamExecutionEnvironment.get_execution_environment()
env.from_collection([1,2,3]).map(lambda x: x + 1).print()
# 运行时命令行
flink run -py myjob.py -p 2           

(5). JSON配置文件设置:通过 parallelism 参数在json配置文件中设置并行度。

# 创建JSON配置文件parallelism-config.json,设置并行度为5
vi parallelism-config.json
# 添加内容
{
  "parallelism.default": 5
}
# 提交作业时指定该配置文件
flink run -py word_count.py --config parallelism-config.json           

主要的选择原则:

  • 全局设置用于所有作业,更方便统一管理。
  • 细粒度设置可以根据不同操作符的负载调整,但过于复杂。
  • 运行时设置可以动态调整,更灵活。

Operator Chain(算子链)

算子间的数据传输

一个数据流在算子之间传输数据的形式可以是一对一(one-to-one)的直通(forwarding)模式,也可以是打乱的重分区(redistributing)模式,具体是哪一种形式,取决于算子的种类。

  1. 一对一(One-to-one,forwarding)

这种模式下,数据流维护着分区以及元素的顺序。比如WC案例中的source和map算子,source算子读取数据之后,可以直接发送给map算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着map 算子的子任务,看到的元素个数和顺序跟source 算子的子任务产生的完全一样,保证着“一对一”的关系。map、filter、flatMap等算子都是这种one-to-one的对应关系。这种关系类似于Spark中的窄依赖。

  1. 重分区(Redistributing)

在这种模式下,数据流的分区会发生改变。比如图中的map和后面的keyBy/window算子之间,以及keyBy/window算子和Sink算子之间,都是这样的关系。

每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。这些传输方式都会引起重分区的过程,这一过程类似于Spark中的shuffle。

合并算子链

在Flink中,并行度相同的一对一(one to one)算子操作,可以直接链接在一起形成一个“大”的任务(task),这样原来的算子就成为了真正任务里的一部分,如下图所示。每个task会被一个线程执行。这样的技术被称为“算子链”(Operator Chain)。

Flink 运行架构概览

上图中Source和map之间满足了算子链的要求,所以可以直接合并在一起,形成了一个任务;因为并行度为2,所以合并后的任务也有两个并行子任务。这样,这个数据流图所表示的作业最终会有5个任务,由5个线程并行执行。

将算子链接成task是非常有效的优化:可以减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。

env = StreamExecutionEnvironment.get_execution_environment()
# 通过is_chaining_enabled()可以再次开启自动算子链合并
env.is_chaining_enabled()
# 通过disable_operator_chaining方法全局禁用算子链合并
env.disable_operator_chaining()
# 也可以在具体的算子上调用disable_chaining()/start_new_chain()来控制是否合并
mapped_stream = data_stream.map(...).disable_chaining() # 禁用算子链
filtered_stream = mapped_stream.filter(...).start_new_chain() # 从当前算子开始新链           

Task Slots(任务槽)

Flink中每一个TaskManager都是一个JVM进程,它可以启动多个独立的线程,来并行执行多个子任务(subtask)。

很显然,TaskManager的计算资源是有限的,并行的任务越多,每个线程的资源就会越少。那一个TaskManager到底能并行处理多少个任务呢?为了控制并发量,我们需要在TaskManager上对每个任务运行所占用的资源做出明确的划分,这就是所谓的任务槽(task slots)。

每个任务槽(task slot)其实表示了TaskManager拥有计算资源的一个固定大小的子集。这些资源就是用来独立执行一个子任务的。

假如一个TaskManager有3个slot,那么它会将管理的内存平均分成三份,每个slot独自占据一份。这样一来,我们在slot上执行一个子任务时,相当于划定了一块内存“专款专用”,就不需要跟来自其他作业的任务去竞争内存资源了。

  1. 任务槽数量的设置

在Flink的$FLINK_HOME/conf/flink-conf.yaml配置文件中,可以设置TaskManager的slot数量,默认是1个slot。

taskmanager.numberOfTaskSlots: 8           

需要注意的是,slot目前仅仅用来隔离内存,不会涉及CPU的隔离。在具体应用时,可以将slot数量配置为机器的CPU核心数,尽量避免不同任务之间对CPU的竞争。这也是开发环境默认并行度设为机器CPU数量的原因。

  1. 任务对任务槽的共享

同一个作业中,不同任务节点的并行子任务,就可以放到同一个slot上执行。

默认情况下,Flink是允许子任务共享slot的。当我们将资源密集型和非密集型的任务同时放到一个slot中,它们就可以自行分配对资源占用的比例,从而保证最重的活平均分配给所有的TaskManager。

slot共享另一个好处就是允许我们保存完整的作业管道。这样一来,即使某个TaskManager出现故障宕机,其他节点也可以完全不受影响,作业的任务可以继续执行。

当然,Flink默认是允许slot共享的,如果希望某个算子对应的任务完全独占一个slot,或者只有某一部分算子共享slot,我们也可以通过设置“slot共享组”手动指定:ds.map(lambda x: x * 10).slot_sharing_group('1')

这样,只有属于同一个slot共享组的子任务,才会开启slot共享;不同组之间的任务是完全隔离的,必须分配到不同的slot上。在这种场景下,总共需要的slot数量,就是各个slot共享组最大并行度的总和。

任务槽和并行度的关系

任务槽和并行度都跟程序的并行执行有关,但两者是完全不同的概念。简单来说任务槽是静态的概念,是指TaskManager具有的并发执行能力,可以通过参数taskmanager.numberOfTaskSlots进行配置;而并行度是动态概念,也就是TaskManager运行程序时实际使用的并发能力,可以通过参数parallelism.default进行配置。

假设集群一共有3个TaskManager,每一个TaskManager中的slot数量设置为4个,那么一共有12(3*4)个task slot,表示集群最多能并行执行12个同一算子的子任务。

案例

# 共同条件
taskmanager.numberOfTaskSlots:3
3个TaskManager共9个Slot
           
  1. 默认并行度 如果我们没有任何并行度设置,而配置文件中默认parallelism.default=1,那么程序运行的默认并行度为1,总共有3个任务。由于不同算子的任务可以共享任务槽,所以最终占用的slot只有1个。9个slot只用了1个,有8个空闲。
  2. 设置并行度为2
# 设置任务并行度的方式有以下三种:
Flink-conf.yaml:parallelism.default: 2
# Flink客户端:
./bin/flink run –p 2
# 执行环境:
env.set_parallelism(2)
           

作业并行度设置为2,那么总共有6个任务,共享任务槽之后会占用2个slot。同样,就有7(9-2)个slot空闲,计算资源没有充分利用。所以可以看到,设置合适的并行度才能提高效率。

  1. 设置并行度为9
# 设置任务并行度的方式有以下三种:
Flink-conf.yaml:parallelism.default: 9
# Flink客户端:
./bin/flink run –p 9
# 执行环境:
env.set_parallelism(9)

           

怎样设置并行度效率最高呢?当然是需要把所有的slot都利用起来。考虑到slot共享,我们可以直接把并行度设置为9,这样所有27个任务就会完全占用9个slot。这是当前集群资源下能执行的最大并行度,计算资源得到了充分的利用。

  1. 全局并行度为9,并单独设置sink并行度为1

另外再考虑对于某个算子单独设置并行度的场景。例如,如果我们考虑到输出可能是写入文件,那会希望不要并行写入多个文件,就需要设置sink算子的并行度为1。这时其他的算子并行度依然为9,所以总共会有19个子任务。根据slot共享的原则,它们最终还是会占用全部的9个slot,而sink任务只在其中一个slot上执行。

结论:整个流处理程序的并行度,就是所有算子并行度中最大的那个,这代表了运行程序需要的slot数量。