一、Flink整体架构
Flink整体架构可以分为APIs&Libraries、Core和Deploy三层:
- Libraries层也被称作Flink应用组件层,是在API层之上构建满足了特定应用领域的计算框架,包括面向流处理的CEP(复杂事件处理)、类SQL操作,面向批处理的FlinkML(机器学习库)、Gelly(图处理)等;APIs层主要实现了面向流处理对应的DataStream API,面向批处理对应的DataSet API。
- Core层提供了Flink运行时的全部核心实现,例如支持分布式Stream作业执行、JobGraph到ExecutionGraph的映射和调度等,为API层提供了基础服务。
- Deploy层支持多种部署模式,包括本地、集群(Standalone、YARN、Kubernetes)及云部署(GCE/EC2)。
接下来我们从上向下,依次介绍Flink的架构设计与实现,如有不当之处欢迎交流与拍砖~
二、Flink API
Flink提供了多种抽象的编程接口,适用于不同层级用户的需求,如下图所示:
2.1 Stateful Processing Function
最底层级的抽象提供了强大且灵活的编程能力,在其中可以直接操作状态数据、TimeService等服务,同时可以注册事件时间和处理时间回调定时器,使程序能够实现更加复杂的计算。使用Stateful Processing Function需要借助DataStream API。虽然灵活度很高,但是使用复杂度也相对较高,且在DataStreamAPI中已经封装了丰富的算子可以直接使用,因此除非用户需要自定义比较复杂的算子(如直接操作状态数据等),否则无须使用Stateful Processing Function来开发Flink作业。
2.2 DataStream&DataSet API
大多数应用都是针对Core API进行编程 :DataStream API( 有界或无界流数据) 和DataSet API(有界数据集)。这些 API提供了通用的数据处理操作, 比如由用户定义的多种形式的转换( transformations)、连接( joins)、聚合( aggregations)、窗口操作( windows) 等等。DataSet API 为有界数据集提供了额外的支持, 例如循环与迭代。
值得一提的是,虽然Table和SQL API已经能够做到批流一体,但这仅是在逻辑层面上,最终还是会转换成DataSet API和DataStream API对应的作业。在未来的版本中,Flink将逐渐通过DataStream处理有界数据集和无界数据集,实现真正意义上的批流一体。
2.3 Flink SQL & Table API
Flink提供的高层级的抽象是Table API与Flink SQL 。其中Table API 是以表为中心的声明式编程,表可能会动态变化(在表达流数据时)。Table API遵循关系模型:表有二维数据结构(schema,类似于关系数据库中的表),同时API提供常用的查询操作,例如select、project、join、group-by、aggregate等。Table API 可以通过用户自定义函数( UDF)进行扩展。除此之外,Table API程序在执行之前会经过内置优化器进行优化。
Flink SQL在语法与表达能力上与Table API类似,只是以SQL查询表达式的形式编写和表达逻辑。SQL抽象与Table API交互密切,SQL查询可以直接在Table API定义的表上执行。
三、DataStream解析
DataStream API用于构建流式类型的Flink程序,处理实时无界数据流,是Flink系统中最重要的API。本节结合一个简单的示例对DataStream API进行浅析。
3.1 WordCount示例
// set up the execution environment
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// make parameters available in the web interface
env.getConfig().setGlobalJobParameters(params);
// get input data
DataStream dataStream = env.readTextFile("the_path_for_input");
DataStream> counts =
// split up the lines in pairs (2-tuples) containing: (word,1)
dataStream.flatMap(new Tokenizer())
// group by the tuple field "0" and sum up tuple field "1"
.keyBy(value -> value.f0)
.sum(1);
counts.print();
// execute program
env.execute("Streaming WordCount");
从上面WordCount代码示例可以看出,一个Flink流处理程序主要包括以下3个部分:
- StreamExecutionEnvironment初始化:该部分主要创建和初始化StreamExecutionEnvironment,提供通过DataStream API构建Flink作业需要的执行环境,包括设定ExecutionConfig、CheckpointConfig等配置信息以及StateBackend和TimeCharacteristic等变量。
- 业务逻辑转换代码:该模块是用户编写转换逻辑的区域,在streamExecutionEnvironment中提供了创建DataStream的方法,例如通过StreamExecutionEnvironment.readTextFile()方法读取文本数据并构建DataStreamSource数据集,之后所有的DataStream转换操作都会以DataStreamSource为头部节点。同时,DataStreamAPI中提供了各种转换操作,例如map、reduce、join等算子,用户可以通过这些转换操作构建完整的Flink计算逻辑。
- 执行应用程序:编写完Flink应用后,必须调用ExecutionEnvironment.execute()方法执行整个应用程序,在execute()方法中会基于DataStream之间的转换操作生成StreamGraph,并将StreamGraph结构转换为JobGraph,最终将JobGraph提交到指定的Session集群中运行。
3.2 DataStream结构
DataStream数据结构包含两个主要成员:streamExecutionEnvironment和transformation。DataStream用于表达业务转换逻辑,可以通过transformation生成新的DataStream。DataStream实际上并不存储真实数据。
如上图所示,DataStream之间的转换操作都是通过StreamTransformation进行的,例如当用户执行DataStream.map()方法转换时,底层对应的便是OneInputTransformation转换操作。在DataStream转换的过程中,不管是哪种类型的转换操作,都是按照相同方式进行:首先将用户自定义的函数(如示例中的new Tokenizer())封装到Operator中,然后将Operator封装到Transformation转换操作结构中,最后将Transformation写入StreamExecutionEnvironment提供的Transformation集合。通过DataStream之间的转换操作形成Pipeline拓扑,即StreamGraph数据结构,最终通过StreamGraph生成JobGraph并提交到集群上运行。
3.3 Transformation
由上文可以知道,DataStream之间的转换操作都是基于Transformation来实现的,每种Transformation实现都和DataStream的一个接口方法对应。
从上面的的Transformation类图可以看出,Transformation的子类涵盖了所有的DataStream转换操作。常用到的StreamMap、StreamFilter算子封装在OneInputTransformation中,即单输入类型的转换操作;常见的双输入类型算子有join、connect等,对应支持双输入类型转换的TwoInputTransformation。
在Transformation的基础上又抽象出了PhysicalTransformation类。PhysicalTransformation中提供了setChainingStrategy方法,可以将上下游算子按照指定的策略连接,从而减少网络数据传输、提高计算性能。ChainingStrategy支持如下三种策略:
- ALWAYS:代表该Transformation中的算子会和上游算子尽可能地链化,最终将多个Operator组合成OperatorChain。OperatorChain中的Operator会运行在同一个SubTask实例中,这样做的目的主要是优化性能,减少Operator之间的网络传输。
- NEVER:代表该Transformation中的Operator永远不会和上下游算子之间链化,因此对应的Operator会运行在独立的SubTask实例中。
- HEAD:代表该Transformation对应的Operator为头部算子,不支持上游算子链化,但是可以和下游算子链化,实际上就是OperatorChain中的HeaderOperator。
这里整理了常用的Transformation,如下图所示:
四、运行时架构
Flink客户端会将用户的作业转换为JobGraph结构并提交至集群的运行时中,对作业进行调度并拆分成Task继续调度和执行。运行时中的核心组件和服务会分工并协调合作,最终完成整个Job的调度和执行。
4.1 主要组件
这里先介绍几个Flink中的重要概念:
- Job: 一个Job对应一个用户提交的作业,也即对应一个jobGraph
- Task: Flink会基于jobGraph中每个算子的链化策略(见3.3)和用户设置的并发度,将一个Job拆分为多个Task,并为每个Task申请资源执行
- Slot: 是Flink中并发执行的最小单位,可以理解为Java中的线程。Slot由集群中的TaskManager提供、ResourceManager统一管理。
Dispatcher
Dispatcher主要负责接收客户端提交的JobGraph对象(例如CLI客户端或FlinkWebUI提交的任务最终都会发送至Dispatcher组件),并对JobGraph进行分发和执行。其中就包含根据JobGraph对象启动JobManager服务,专门用于管理整个任务的生命周期。
ResourceManager
ResourceManager有两个主要职责:负责管理Flink集群中的计算资源,其中计算资源主要来自TaskManager组件;以及接收来自JobManager的SlotRequest。
如果采用集群部署方式,则ResourceManager会动态地向集群资源管理器申请Container并启动TaskManager,例如HadoopYarn、Kubernetes等。对于不同的集群资源管理器,ResourceManager的实现也会有所不同。
JobManager
Dispatcher会根据接收的JobGraph对象为任务创建JobManager服务,由后者对整个任务的生命周期进行管理。JobManager会将JobGraph转换成ExecutionGraph结构,并通过内部调度程序对ExecutionGraph中的ExecutionVertex节点进行调度和执行,最终会经过ResourceManager向指定的TaskManager提交和运行Task实例。同时也会监控各个Task的运行状况,直到整个作业中所有的Task都执行完毕或停止。
和Dispatcher组件一样,JobManager组件本身也是RPC服务,因此具备RPC通信的能力,可以与ResourceManager进行RPC通信、申请任务的计算资源。当任务执行完毕后,JobManager服务也会关闭并释放任务占用的计算资源。
TaskManager
TaskManager负责向整个集群提供Slot计算资源、并管理JobManager提交的Task任务。TaskManager会向JobManager服务提供从ResourceManager中申请和分配的Slot计算资源,JobManager最终会根据分配到的Slot计算资源将Task提交到TaskManager上运行。
4.2 执行流程
接下来我们看整个集群中各个主要组件的启动流程。如上图,我们以Session类型(见第五节)的集群为例进行说明,Flink Session集群的启动流程主要包含如下步骤:
- 用户通过客户端命令启动SessionCluster,此时会触发整个集群服务的启动过程,客户端会向集群资源管理器申请Container计算资源以启动运行时中的管理节点。
- ClusterManagement会为运行时集群分配Application主节点需要的资源并启动主节点服务,例如在HadoopYarn资源管理器中会分配并启动Flink管理节点对应的Container。
- 客户端将用户提交的应用程序代码经过本地运行生成JobGraph结构,然后通过ClusterClient将JobGraph提交到集群运行时中运行。
- 此时集群运行时中的Dispatcher服务会接收到ClusterClient提交的JobGraph对象,然后根据JobGraph启动JobManagerRPC服务。JobManager是每个提交的作业都会单独创建的作业管理服务,生命周期和整个作业的生命周期一致。
- 当JobManagerRPC服务启动后,下一步就是根据JobGraph配置的计算资源向ResourceManager服务申请运行Task实例需要的Slot计算资源。
- 此时ResourceManager接收到JobManager提交的资源申请后,先判断集群中是否有足够的Slot资源满足作业的资源申请,如果有则直接向JobManager分配计算资源,如果没有则动态地向外部集群资源管理器申请启动额外的Container以提供Slot计算资源。
- 如果在集群资源管理器(例如HadoopYarn)中有足够的Container计算资源,就会根据ResourceManager的命令启动指定的TaskManager实例。
- TaskManager启动后会主动向ResourceManager注册Slot信息,即其自身能提供的全部Slot资源。ResourceManager接收到TaskManager中的Slot计算资源时,就会立即向该TaskManager发送Slot资源申请,为JobManager服务分配提交任务所需的Slot计算资源。
- 当TaskManager接收到ResourceManager的资源分配请求后,TaskManager会对符合申请条件的SlotRequest进行处理,然后立即向JobManager提供Slot资源。
- 此时JobManager会接收到来自TaskManager的offerslots消息,接下来会向Slot所在的TaskManager申请提交Task实例。TaskManager接收到来自JobManager的Task启动申请后,会在已经分配的Slot卡槽中启动Task线程。
- TaskManager中启动的Task线程会周期性地向JobManager汇报任务运行状态,直到完成整个任务运行。
五、Flink部署模式
常见的Flink部署方式有如下三种:
- Standalone:单机部署
- Flink on YARN:YARN集群部署
- Flink on Kubernetes:容器化部署
这里以Flink on YARN来说明两种常用的部署模式:
- Session-Cluster模式:Session-Cluster模式需要先启动集群,然后再提交作业,接着会向YARN申请一块空间后,资源永远保持不变。如果资源满了,下一个作业就无法提交,只能等到YARN中的其中一个作业执行完成后释放了资源,下个作业才会正常提交。所有作业共享Dispatcher和ResourceManager、共享资源,适合规模小执行时间短的作业。
- Per-Job-Cluster模式:一个Job会对应一个集群,每提交一个作业会根据自身的情况,都会单独向YARN申请资源,直到作业执行完成,一个作业的失败与否并不会影响下一个作业的正常提交和运行。独享Dispatcher和ResourceManager,按需接受资源申请,适合规模大长时间运行的作业。
参考资料
- Flink源码
- 《Flink设计与实现 核心原理与源码解析》--张利兵
- B站Flink教程