Flink 架构及相关介绍
1.架构图
分层设计说明(相关术语解释)
物理部署层-deploy层
负责解决Flink的部署模式问题,
支持多种部署模式:本地部署、集群部署(Standalone/Yarn/Mesos)、云(GCE/EC2)以及kubernetes。
通过该层支持不同平台的部署,用户可以根据自身场景和需求选择使用对应的部署模式。
Runtime核心层
是Flink分布式计算框架的核心实现层,负责对上层不同接口提供基础服务。
支持分布式Stream作业的执行、JobGraph到ExecutionGraph的映射转换以及任务调度等。
将DataStream和DataSet转成统一的可执行的Task Operator,达到在流式计算引擎下同时处理批量计算和流式计算的目的。
API & Libraries层
负责更好的开发用户体验,包括易用性、开发效率、执行效率、状态管理等方面。
Flink同时提供了支撑流计算和批处理的接口,同时在这基础上抽象出不同的应用类型的组件库,如:
基于流处理的CEP(复杂事件处理库)
Table & Sql库
基于批处理的FlinkML(机器学习库)
图处理库(Gelly)
API层包括两部分
流计算应用的DataStream API
批处理应用的DataSet API
统一的API,方便用于直接操作状态和时间等底层数据
提供了丰富的数据处理高级API,例如Map、FllatMap操作等,
并提供了比较低级的Process Function API
2.运行流程
下面是从Flink官网截取的一张架构图:
在Flink运行时涉及到的进程主要有以下两个: JobManager:主要负责调度task,协调checkpoint已经错误恢复等。当客户端将打包好的任务提交到JobManager之后,JobManager就会根据注册的TaskManager资源信息将任务分配给有资源的TaskManager,然后启动运行任务。TaskManger从JobManager获取task信息,然后使用slot资源运行task; TaskManager:执行数据流的task,一个task通过设置并行度,可能会有多个subtask。 每个TaskManager都是作为一个独立的JVM进程运行的。他主要负责在独立的线程执行的operator。其中能执行多少个operator取决于每个taskManager指定的slots数量。Task slot是Flink中最小的资源单位。假如一个taskManager有3个slot,他就会给每个slot分配1/3的内存资源,目前slot不会对cpu进行隔离。同一个taskManager中的slot会共享网络资源和心跳信息。
当然在Flink中并不是一个slot只可以执行一个task,在某些情况下,一个slot中也可能执行多个task,如下:
一般情况下,flink都是默认允许共用slot的,即便不是相同的task,只要都是来同一个job即可。共享slot的好处有以下两点:
- 当Job的最高并行度正好和flink集群的slot数量相等时,则不需要计算总的task数量。例如,最高并行度是6时,则只需要6个slot,各个subtask都可以共享这6个slot; 2. 共享slot可以优化资源管理。如下图,非资源密集型subtask source/map在不共享slot时会占用6个slot,而在共享的情况下,可以保证其他的资源密集型subtask也能使用这6个slot,保证了资源分配。
3. Flink中的数据
Flink中的数据主要分为两类:有界数据流(Bounded streams)和无界数据流(Unbounded streams)。
3.1 无界数据流
顾名思义,无界数据流就是指有始无终的数据,数据一旦开始生成就会持续不断的产生新的数据,即数据没有时间边界。无界数据流需要持续不断地处理。
3.2 有界数据流
相对而言,有界数据流就是指输入的数据有始有终。例如数据可能是一分钟或者一天的交易数据等等。处理这种有界数据流的方式也被称之为批处理:
需要注意的是,我们一般所说的数据流是指数据集,而流数据则是指数据流中的数据。
4. Flink中的编程模型
4.1 编程模型
在Flink,编程模型的抽象层级主要分为以下4种,越往下抽象度越低,编程越复杂,灵活度越高。
这4层中,一般用于开发的是第三层,即DataStrem/DataSetAPI。用户可以使用DataStream API处理无界数据流,使用DataSet API处理有界数据流。同时这两个API都提供了各种各样的接口来处理数据。例如常见的map、filter、flatMap等等,而且支持python,scala,java等编程语言,后面的demo主要以scala为主。
4.2 程序结构
与其他的分布式处理引擎类似,Flink也遵循着一定的程序架构。下面以常见的WordCount为例:
val env = ExecutionEnvironment.getExecutionEnvironment
// get input data
val text = env.readTextFile("/path/to/file")
val counts = text.flatMap { _.toLowerCase.split("\\W+") filter { _.nonEmpty } }
.map { (_, 1) }
.groupBy(0)
.sum(1)
counts.writeAsCsv(outputPath, "\n", " ")
下面我们分解一下这个程序。
第一步,我们需要获取一个ExecutionEnvironment(如果是实时数据流的话我们需要创建一个StreamExecutionEnvironment)。这个对象可以设置执行的一些参数以及添加数据源。所以在程序的main方法中我们都要通过类似下面的语句获取到这个对象:
val env = ExecutionEnvironment.getExecutionEnvironment
第二步,我们需要为这个应用添加数据源。这个程序中是通过读取文本文件的方式获取数据。在实际开发中我们的数据源可能有很多中,例如kafka,ES等等,Flink官方也提供了很多的connector以减少我们的开发时间。一般都是都通addSource方法添加的,这里是从文本读入,所以调用了readTextFile方法。当然我们也可以通过实现接口来自定义source。
val text = env.readTextFile("/path/to/file")
第三步,我们需要定义一系列的operator来对数据进行处理。我们可以调用Flink API中已经提供的算子,也可以通过实现不同的Function来实现自己的算子,这个我们会在后面讨论。这里我们只需要了解一般的程序结构即可。
val counts = text.flatMap { _.toLowerCase.split("\\W+") filter { _.nonEmpty } }
.map { (_, 1) }
.groupBy(0)
.sum(1)
上面的就是先对输入的数据进行分割,然后转换成(word,count)这样的Tuple,接着通过第一个字段进行分组,最后sum第二个字段进行聚合。
第四步,数据处理完成之后,我们还要为它指定数据的存储。我们可以从外部系统导入数据,亦可以将处理完的数据导入到外部系统,这个过程称为Sink。同Connector类似,Flink官方提供了很多的Sink供用户使用,用户也可以通过实现接口自定义Sink。
counts.writeAsCsv(outputPath, "\n", " ")