Flink(一)概述
一.概述
1.Flink是什么
Apache Flink 是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。Flink 被设计在所有常见的集群环境中运行,以内存执行速度和任意规模来执行计算
回到 Flink 本身的定位,它是一个大数据流式处理引擎,处理的是流式数据,也就是"数据流"(Data Flow)。顾名思义,数据流
的含义是,数据并不是收集好的,而是像水流一样,是一组有序的数据序列,逐个到来、逐个处理
。由于数据来到之后就会被即刻处理,所以流处理的一大特点就是"快速",也就是良好的实时性。Flink 适合的场景,其实也就是需要实时处理数据流的场景
2.Flink优势
数据处理有不同的方式
对于具体应用来说,有些场景数据是一个一个来的,是一组有序的数据序列,我们把它叫作"数据流";而有些场景的数据,本身就是一批同时到来,是一个有限的数据集,这就是批量数据(有时也直接叫数据集)
容易想到,处理数据流,当然应该“来一个就处理一个”,这种数据处理模式就叫作流处理
;因为这种处理是即时的,所以也叫实时处理
。与之对应,处理批量数据自然就应该一批读入、一起计算,这种方式就叫作批处理
,也叫作离线处理
那真实的应用场景中,到底是数据流更常见、还是批量数据更常见呢?
生活中,这两种形式的数据都有,比如我们日常发信息,可以一句一句地说,也可以写一大段一起发过去。一句一句的信息,就是一个一个的数据,它们构成的序列就是一个数据流;而一大段信息,是一组数据的集合,对应就是批量数据(数据集)
批处理缺点很明显,会有延迟,数据处理不够及时,实时性变差了。流处理,是真正的即时处理,没有“攒批”的等待时间,所以会更快、实时性更好,在如今的社会,实时性已经成为一种共同的追求,所以许多场景下流处理更符合现代的需求
3.分层API
越顶层越抽象,表达含义越简明;越底层越具体,表达能力越丰富
最底层级的抽象仅仅提供了有状态流,它将处理函数(Process Function)嵌入到了DataStream API 中。底层处理函数(Process Function)与 DataStream API 相集成,可以对某些操作进行抽象,它允许用户可以使用自定义状态处理来自一个或多个数据流的事件,且状态具有一致性和容错保证。除此之外,用户可以注册事件时间并处理时间回调,从而使程序可以处理复杂的计算
实际上,大多数应用并不需要上述的底层抽象,而是直接针对核心 API(Core APIs) 进
行编程,比如 DataStream API(用于处理有界或无界流数据)以及 DataSet API(用于处理有界数据集)。这些 API 为数据处理提供了通用的构建模块,比如由用户定义的多种形式的转换(transformations)、连接(joins)、聚合(aggregations)、窗口(windows)操作等。DataSet API 为有界数据集提供了额外的支持,例如循环与迭代。这些 API 处理的数据类型以类(classes)的形式由各自的编程语言所表示
Table API 是以表为中心的声明式编程,其中表在表达流数据时会动态变化。Table API 遵循关系模型:表有二维数据结构(schema)(类似于关系数据库中的表),同时 API 提供可比较的操作,例如 select、join、group-by、aggregate 等
尽管 Table API 可以通过多种类型的用户自定义函数(UDF)进行扩展,仍不如核心 API
更具表达能力,但是使用起来代码量更少,更加简洁。除此之外,Table API 程序在执行之前会使用内置优化器进行优化。我们可以在表与 DataStream/DataSet 之间无缝切换,以允许程序将 Table API 与DataStream 以及 DataSet 混合使用
Flink 提供的最高层级的抽象是SQL。这一层抽象在语法与表达能力上与 Table API 类似,但是是以 SQL 查询表达式的形式表现程序。SQL 抽象与 Table API 交互密切,同时 SQL 查询可以直接在 Table API 定义的表上执行
二.Flink集群
1.集群搭建
整个Flink组件包括客户端(Client)、作业管理器(JobManager)和任务管理器(TaskManager)
我们的代码,实际上是由客户端获取并做转换,转换成作业,然后交给JobManager
JobManager 就是 Flink 集群里的“管事人”,对作业进行中央调度管理;而它获取到要执行的作业后,会进一步处理转换,然后分发任务给众多的 TaskManager。这里的 TaskManager,就是真正“干活的人”,数据的处理操作都是它们来做的,Task Manager中含有作业所需资源的最小单位—— Task Slot
集群角色分配
具体安装部署步骤如下:
1.下载并解压安装包 官网可下载
2.修改集群配置
(1)进入 conf 目录下,修改 flink-conf.yaml 文件,修改 jobmanager.rpc.address 参数为hadoop102,如下所示:
$ cd conf/
$ vim flink-conf.yaml
配置JobManager 节点地址
jobmanager.rpc.address: hadoop102
这就指定了 hadoop102 节点服务器为 JobManager 节点
(2)修改 workers 文件,将另外两台节点服务器添加为本 Flink 集群的 TaskManager 节点,
具体修改如下:
$ vim workers
输入如下内容
hadoop103
hadoop104
这样就指定了 hadoop103 和 hadoop104 为 TaskManager 节点
3.分发安装目录
配置修改完毕后,将 Flink 安装目录发给另外两个节点服务器
4.在 hadoop102 节点服务器上执行 start-cluster.sh 启动 Flink 集群:
$ bin/start-cluster.sh
5.访问 Web UI
启动成功后,同样可以访问 http://hadoop102:8081 对 flink 集群和任务进行监控管理
很方便,我们在UI界面就可以提交任务了
关闭调用相关命令即可
2.三种部署模式
Flink 为各种场景提供了不同的部署模式,主要有以下三种:
- 会话模式(Session Mode)
- 单作业模式(Per-Job Mode)
- 应用模式(Application Mode)
它们的区别主要在于:集群的生命周期以及资源的分配方式;以及应用的 main 方法到底在哪里执行——客户端(Client)还是 JobManager
会话模式
特点:我们需要先启动一个集群,保持一个会话,在这个会话中通过客户端提交作业,如图所示。集群启动时所有资源就都已经确定,所以所有提交的作业会竞争集群中的资源
优点:我们只需要一个集群,就像一个大箱子,所有的作业提交之后都塞进去;作业结束了就释放资源,集群依然正常运行
缺点:因为资源是共享的,所以资源不够了,提交新的作业就会失败。另外,同一个 TaskManager 上可能运行了很多作业,如果其中一个发生故障导致 TaskManager 宕机,那么所有作业都会受到影响
单作业模式
特点:单作业模式就是严格的一对一,集群只为这个作业而生。同样由客户端运行应用程序,然后启动集群,作业被提交给 JobManager,进而分发给 TaskManager 执行。作业作业完成后,集群就会关闭,所有资源也会释放。这样一来,每个作业都有它自己的 JobManager 管理,占用独享的资源,即使发生故障,它的 TaskManager 宕机也不会影响其他作业
需要注意的是,Flink 本身无法直接这样运行,所以单作业模式一般需要借助一些资源管理框架来启动集群,比如 YARN、Kubernetes
应用模式
前面提到的两种模式下,应用代码都是在客户端上执行,然后由客户端提交给 JobManager的。但是这种方式客户端需要占用大量网络带宽,去下载依赖和把二进制数据发送给JobManager;加上很多情况下我们提交作业用的是同一个客户端,就会加重客户端所在节点的资源消耗
所以解决办法就是,我们不要客户端了,直接把应用提交到 JobManger 上运行。而这也就代表着,我们需要为每一个提交的应用单独启动一个 JobManager,也就是创建一个集群。这个 JobManager 只为执行这一个应用而存在,执行结束之后 JobManager 也就关闭了,这就是所谓的应用模式
总结
会话模式和单作业模式main()方法都是在client端执行的,由client来划分作业数,因此单作业模式下一个应用程序可能会启动多个集群。而应用模式的main()方法是在集群侧执行的,一个应用程序创建一个集群,显然,应用模式下,不同应用程序之间是隔离的,但一个应用程序里面的作业是不隔离的
3.Yarn结合部署模式
首先我们不要混淆,独立部署和Yarn部署是指的集群资源部署的方式,在这两个资源部署模式下又可以有上面讲的三种部署方式
独立模式 Standalone是部署 Flink 最基本也是最简单的方式:所需要的所有 Flink 组件,都只是操作系统上运行的一个 JVM 进程。独立模式是独立运行的,不依赖任何外部的资源管理平台;当然独立也是有代价的:如果资源不足,或者出现故障,没有自动扩展或重分配资源的保证,必须手动处理。所以独立模式一般只用在开发测试或作业非常少的场景下,这里不在过多赘述
注意,独立模式下,仅支持会话模式和应用模式
YARN 上部署的过程是:客户端把 Flink 应用提交给 Yarn 的 ResourceManager, Yarn 的 ResourceManager 会向 Yarn 的 NodeManager 申请容器。在这些容器上,Flink 会部署JobManager 和 TaskManager 的实例,从而启动集群。Flink 会根据运行在 JobManager 上的作业所需要的 Slot 数量动态分配 TaskManager 资源
3.1 前期准备
环境变量
不要忘记source
#HADOOP_HOME
export HADOOP_HOME=/opt/module/hadoop-3.1.3
export PATH=$PATH:$HADOOP_HOME/bin
export PATH=$PATH:$HADOOP_HOME/sbin
export HADOOP_CONF_DIR=${HADOOP_HOME}/etc/hadoop
export HADOOP_CLASSPATH=`hadoop classpath`
要保证Hadoop集群可用(仅展示一台机器)
进入 conf 目录,修改 flink-conf.yaml 文件,修改以下配置,也可不修改,若在提交命令中不特定指明,这些配置将作为默认配置,8个TaskSlots,意味着可以同时执行8个作业
特别的,我们还要在该yaml中加一个配置
classloader.check-leaked-classloader: false
3.2 Yarn-会话模式
启动一个yarn-session很简单,session的特点就是先启动集群,后提交作业
$ bin/yarn-session.sh -nm test
可用参数解读:
-d:分离模式,如果你不想让 Flink YARN 客户端一直前台运行,可以使用这个参数,
即使关掉当前对话窗口,YARN session 也可以后台运行
-jm(–jobManagerMemory):配置 JobManager 所需内存,默认单位 MB
-nm(–name):配置在 YARN UI 界面上显示的任务名
-qu(–queue):指定 YARN 队列名
-tm(–taskManager):配置每个 TaskManager 所使用内存
启动成功如下:
访问Web UI界面
可以发现可用资源都是0?这是为什么呢?
Flink1.11.0 版本不再使用-n 参数和-s 参数分别指定 TaskManager 数量和 slot 数量,YARN 会按照需求动态分配 TaskManager 和 slot。所以从这个意义上讲,YARN 的会话模式也不会把集群资源固定,同样是动态分配的
3.3 Yarn-单作业模式
在 YARN 环境中,由于有了外部平台做资源调度,所以我们也可以直接向 YARN 提交一个单独的作业,从而启动一个 Flink 集群
(1)执行命令提交作业
$ bin/flink run -d -t yarn-per-job -c com.gzhu.wordcount.StreamWordCount /opt/software/Flink-1.0-SNAPSHOT.jar
(2)在 YARN 的 ResourceManager 界面查看执行情况
点击可以打开 Flink Web UI 页面进行监控
(3)可以使用命令行查看或取消作业,命令如下
$ ./bin/flink list -t yarn-per-job -Dyarn.application.id=application_XXXX_YY
$ ./bin/flink cancel -t yarn-per-job -Dyarn.application.id=application_XXXX_YY
<jobId>
这里的 application_XXXX_YY 是当前应用的 ID,是作业的 ID。注意如果取消作业,整个 Flink 集群也会停掉
3.4 Yarn-应用模式
应用模式同样非常简单,与单作业模式类似,直接执行 flink run-application命令即可 (1)执行命令提交作业。
$ bin/flink run-application -t yarn-application -c 启动类 jar包
(2)在命令行中查看或取消作业
$ ./bin/flink list -t yarn-application -Dyarn.application.id=application_XXXX_YY
$ ./bin/flink cancel -t yarn-application
-Dyarn.application.id=application_XXXX_YY <jobId>
(3)也可以通过 yarn.provided.lib.dirs 配置选项指定位置,将 jar 上传到远程
$ ./bin/flink run-application -t yarn-application
-Dyarn.provided.lib.dirs="hdfs://myhdfs/my-remote-flink-dist-dir"
hdfs://myhdfs/jars/my-application.jar
这种方式下 jar 可以预先上传到 HDFS,而不需要单独发送到集群,这就使得作业提交更加轻量了
三.Flink架构简介
1.系统架构
Flink 的运行时架构中,最重要的就是两大组件:作业管理器(JobManager)和任务管理器(TaskManager)。对于一个提交执行的作业,JobManager 是真正意义上的“管理者(Master),负责管理调度,所以在不考虑高可用的情况下只能有一个。TaskManager 是“工作者”(Worker、Slave),负责执行任务处理数据,所以可以有一个或多个
通过前面我们知道,JM和TM可以自己启动(独立模式),也可以在Yarn中启动(Yarn模式),此外也可以在K8S中启动
客户端
它只负责作业的提交。具体来说,就是调用程序的 main 方法,将代码转换成“数据流图”(Dataflow Graph),并最终生成作业图(JobGraph),并发送给 JobManager
应用模式下main是在集群执行的,和客户端没关系
提交之后,任务的执行其实就跟客户端没有关系了;我们可以在客户端选择断开与JobManager 的连接, 也可以继续保持连接之前我们在命令提交作业时,加上的-d 参数,就是表示分离模式(detached mode),也就是断开连接
JobManager(可以配置高可用,但只有一个JM活着)
1. JobMaster
JobMaster 是 JobManager 中最核心的组件,负责处理单独的作业(Job)。所以 JobMaster和具体的 Job 是一一对应的,多个 Job 可以同时运行在一个 Flink 集群中, 每个 Job 都有一个自己的 JobMaster,主要用来把客户端的作业图转换成执行图
在作业提交时,JobMaster 会先接收到要执行的应用。这里所说“应用”一般是客户端提交来的,包括:Jar 包,数据流图(dataflow graph)和作业图(JobGraph)
JobMaster 会把 JobGraph 转换成一个物理层面的数据流图,这个图被叫作“执行图”(ExecutionGraph),它包含了所有可以并发执行的任务。 JobMaster 会向资源管理器(ResourceManager)发出请求,申请执行任务必要的资源。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的 TaskManager 上
而在运行过程中,JobMaster 会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调
2.资源管理器(ResourceManager)
ResourceManager 主要负责资源的分配和管理,在 Flink 集群中只有一个。所谓“资源”,主要是指TaskManager 的任务槽(task slots)。任务槽就是 Flink 集群中的资源调配单元,包含了机器用来执行计算的一组 CPU 和内存资源。每一个任务(Task)都需要分配到一个 slot 上执行
这里注意要把 Flink 内置的 ResourceManager 和其他资源管理平台(比如 YARN)的ResourceManager 区分开
Flink 的 ResourceManager,针对不同的环境和资源管理平台(比如 Standalone 部署,或者YARN),有不同的具体实现。在 Standalone 部署时,因为 TaskManager 是单独启动的(没有Per-Job 模式),所以 ResourceManager 只能分发可用 TaskManager 的任务槽,不能单独启动新TaskManager
而在有资源管理平台时,就不受此限制。当新的作业申请资源时,ResourceManager 会将有空闲槽位TaskManager 分配给 JobMaster。如果 ResourceManager 没有足够的任务槽,它还可以向资源提供平台发起会话,请求提供启动 TaskManager 进程的容器。另外,ResourceManager 还负责停掉空闲的 TaskManager,释放计算资源
3. 分发器(Dispatcher)
Dispatcher 主要负责提供一个 REST 接口,用来提交应用,并且负责为每一个新提交的作业启动一个新的 JobMaster 组件。Dispatcher 也会启动一个 Web UI,用来方便地展示和监控作业执行的信息。Dispatcher 在架构中并不是必需的,在不同的部署模式下可能会被忽略掉
任务管理器(TaskManager)
TaskManager 是 Flink 中的工作进程,数据流的具体计算就是它来做的,所以也被称为“Worker”。Flink 集群中必须至少有一个 TaskManager;当然由于分布式计算的考虑,通常会有多个 TaskManager 运行,每一个TaskManager 都包含了一定数量的任务槽(task slots)
slot是资源调度的最小单位,slot 的数量限制了 TaskManager 能够并行处理的任务数量
启动之后,TaskManager 会向资源管理器注册它的 slots;收到资源管理器的指令后,TaskManager 就会将一个或者多个槽位提供给 JobMaster 调用,JobMaster 就可以分配任务来执行了
在执行过程中,TaskManager 可以缓冲数据,还可以跟其他运行同一应用的 TaskManager交换数据
2.提交作业流程
抽象提交作业流程
(1) 一般情况下,由客户端(App)通过分发器提供的 REST 接口,将作业提交给JobManager
(2)由分发器启动 JobMaster,并将作业(包含 JobGraph)提交给 JobMaster
(3)JobMaster 将 JobGraph 解析为可执行的 ExecutionGraph,得到所需的资源数量,然后向资源管理器请求资源(slots)
(4)资源管理器判断当前是否由足够的可用资源;如果没有,启动新的 TaskManager(5)TaskManager 启动之后,向 ResourceManager 注册自己的可用任务槽(slots)
(6)资源管理器通知 TaskManager 为新的作业提供 slots
(7)TaskManager 连接到对应的 JobMaster,提供 slots
(8)JobMaster 将需要执行的任务分发给 TaskManager
(9)TaskManager 执行任务,互相之间可以交换数据
独立模式下的作业提交流程
Yarn-Session
(1)客户端通过 REST 接口,将作业提交给分发器
(2)分发器启动 JobMaster,并将作业(包含 JobGraph)提交给 JobMaster
(3)JobMaster 向资源管理器请求资源(slots)
(4)资源管理器向 YARN 的资源管理器请求 container 资源
(5)YARN 启动新的 TaskManager 容器
(6)TaskManager 启动之后,向 Flink 的资源管理器注册自己的可用任务槽
(7)资源管理器通知 TaskManager 为新的作业提供 slots
(8)TaskManager 连接到对应的 JobMaster,提供 slots
(9)JobMaster 将需要执行的任务分发给 TaskManager,执行任务
Yarn-单作业模式
(1)客户端将作业提交给 YARN 的资源管理器,这一步中会同时将 Flink 的 Jar 包和配置上传到 HDFS,以便后续启动 Flink 相关组件的容器
(2)YARN 的资源管理器分配 Container 资源,启动 Flink JobManager,并将作业提交给JobMaster。这里省略了 Dispatcher 组件
(3)JobMaster 向资源管理器请求资源(slots)
(4)资源管理器向 YARN 的资源管理器请求 container 资源
(5)YARN 启动新的 TaskManager 容器
(6)TaskManager 启动之后,向 Flink 的资源管理器注册自己的可用任务槽
(7)资源管理器通知 TaskManager 为新的作业提供 slots
(8)TaskManager 连接到对应的 JobMaster,提供 slots
(9)JobMaster 将需要执行的任务分发给 TaskManager,执行任务
Yarn-应用模式与单作业模式的提交流程非常相似,只是初始提交给 YARN 资源管理器的不再是具体的作业,而是整个应用。一个应用中可能包含了多个作业,这些作业都将在 Flink 集群中启动各自对应的 JobMaster
3.数据流图(StreamGraph)
Flink 是流式计算框架。它的程序结构,其实就是定义了一连串的处理操作,每一个数据输入之后都会依次调用每一步计算。在 Flink 代码中,我们定义的每一个处理转换操作都叫作“算子”(Operator),所以我们的程序可以看作是一串算子构成的管道,数据则像水流一样有序地流过
所有的 Flink 程序都可以归纳为由三部分构成:Source、Transformation 和 Sink
- Source 表示“源算子”,负责读取数据源
- Transformation 表示“转换算子”,利用各种算子进行处理加工
- Sink 表示“下沉算子”,负责数据的输出
在运行时,Flink 程序会被映射成所有算子按照逻辑顺序连接在一起的一张图,这被称为“逻辑数据流”(StreamGraph),或者叫“数据流图”(dataflow graph)。我们提交作业之后,打开 Flink 自带的 Web UI,点击作业就能看到对应的 dataflow
并不是每个API都是算子,只有对数据进行了转换才算,比如keyBy就不是,它只定义了数据的流向并没有转换
数据流图类似于任意的有向无环图(DAG),这一点与 Spark 等其他框架是一致的。图中的每一条数据流(dataflow)以一个或多个 source 算子开始,以一个或多个 sink 算子结束
4.并行度
在下图最上面的一行图形中,在同一时间内,假如任务1到了map()阶段,任务2依旧在source阶段,那么毫无疑问,在同一时间,不同任务在并行执行,也即任务并行
怎样实现数据并行呢?其实也很简单,我们把一个算子操作,“复制”多份到多个节点,数据来了之后就可以到其中任意一个执行。这样一来,一个算子任务就被拆分成了多个并行的“子任务”(subtasks),再将它们分发到不同节点,就真正实现了数据并行计算
在 Flink 执行过程中,每一个算子(operator)可以包含一个或多个子任务(operator subtask),这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行
并行度:特定算子的子任务个数即为并行度,我们在写代码时,可以对某一个算子设置其并行度
stream.map(word -> Tuple2.of(word, 1L)).setParallelism(2);
// 这种方式设置的并行度,只针对当前算子有效
// 另外,我们也可以直接调用执行环境的 setParallelism()方法,全局设定并行度
env.setParallelism(2);
// 这样代码中所有算子,默认的并行度就都为 2 了。我们一般不会在程序中设置全局并行度,
// 因为如果在程序中对全局并行度进行硬编码,会导致无法动态扩容
// 这里要注意的是,由于 keyBy 不是算子,所以无法对 keyBy 设置并行度
至此,一个作业有多少任务这个问题,现在已经基本解决了,比如上图,数据流并行视图中,一共就有7个任务
5.算子链
如图,一个数据流在算子之间传输数据的形式可以是一对一(one-to-one)的直通 (forwarding)模式,也可以是打乱的重分区(redistributing)模式,具体是哪一种形式,取决于算子的种类
(1)一对一(One-to-one,forwarding)
这种模式下,数据流维护着分区以及元素的顺序。比如图中的 source 和 map 算子,source算子读取数据之后,可以直接发送给 map 算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着 map 算子的子任务,看到的元素个数和顺序跟 source 算子的子任务产生的完全一样,保证着“一对一”的关系。map、filter、flatMap 等算子都是这种 one-to-one的对应关系
这种关系类似于 Spark 中的窄依赖
(2)重分区(Redistributing)
在这种模式下,数据流的分区会发生改变。比图中的 map 和后面的 keyBy/window 算子之间(这里的 keyBy 是数据传输算子,后面的 window、apply 方法共同构成了 window 算子),以及 keyBy/window 算子和 Sink 算子之间,都是这样的关系
每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。例如,keyBy()是分组操作,本质上基于键(key)的哈希值(hashCode)进行了重分区;而当并行度改变时,比如从并行度为 2 的 window 算子,要传递到并行度为 1 的 Sink 算子,这时的数据传输方式是再平衡(rebalance),会把数据均匀地向下游子任务分发出去。这些传输方式都会引起重分区(redistribute)的过程,这一过程类似于 Spark 中的 shuffle
总体说来,这种算子间的关系类似于 Spark 中的宽依赖
在 Flink 中,并行度相同的一对一(one to one)算子操作,可以直接链接在一起形成一个“大”的任务 task,这样原来的算子就成为了真正任务里的一部分,每个 task会被一个线程执行。这样的技术被称为“算子链”(Operator Chain)
可以看到,用了算子链技术,任务数由原来的7个变成了5个
当然,我们可以在代码中禁用算子链,也可以在某个算子后开启新的算子链
6.作业图和执行图
StreamGraph 经过优化后生成的就是作业图(JobGraph),这是提交给 JobManager 的数据结构,确定了当前作业中所有任务的划分。主要的优化为: 将多个符合条件的节点链接在一起合并成一个任务节点,形成算子链,这样可以减少数据交换的消耗。JobGraph 一般也是在客户端生成的,在作业提交时传递给 JobMaster
JobMaster 收到 JobGraph 后,会根据它来生成执行图(ExecutionGraph)。ExecutionGraph是 JobGraph 的并行化版本,是调度层最核心的数据结构。与 JobGraph 最大的区别就是按照并行度对并行子任务进行了拆分,并明确了任务间数据传输的方式
JobMaster 生成执行图后, 会将它分发给 TaskManager;各个 TaskManager 会根据执行图部署任务,最终的物理执行过程也会形成一张“图”,一般就叫作物理图(Physical Graph)。这只是具体执行层面的图,并不是一个具体的数据结构
7.任务(Tasks)和任务槽(Task Slots)
我们知道上述有5个任务,是否必须有5个slot才可以执行呢?如下图,很符合我们的预期
假如我们有13个任务,只有6个slot也可以执行,为什么?
允许子任务共享同一个slot,前提是这些子任务之间不是并行关系
如下图,红色的6个任务就是并行关系,我们的初衷就是让他们并行执行,所以必须在不同的slot才可以,但是蓝色的任务和红色任务之间不是并行的关系,那么它们就可以用一个任务槽,所以,我们所需的最小任务槽数量就是所有任务中并行度最大的那个任务
我们可以在代码中设置共享组,来决定哪些算子可以共享一个slot
8.Flink数据类型
为什么会出现“不支持”的数据类型呢?因为 Flink 作为一个分布式处理框架,处理的是以数据对象作为元素的流。如果用水流来类比,那么我们要处理的数据元素就是随着水流漂动的物体。在这条流动的河里,可能漂浮着小木块,也可能行驶着内部错综复杂的大船。要分布式地处理这些数据,就不可避免地要面对数据的网络传输、状态的落盘和故障恢复等问题,这就需要对数据进行序列化和反序列化
为了方便地处理数据,Flink 有自己一整套类型系统。Flink 使用“类型信息”(TypeInformation)来统一表示数据类型。TypeInformation 类是 Flink 中所有类型描述符的基类。它涵盖了类型的一些基本属性,并为每个数据类型生成特定的序列化器、反序列化器和比较器
Flink 支持的数据类型
简单来说,对于常见的 Java 和 Scala 数据类型,Flink 都是支持的。Flink 在内部,Flink
对支持不同的类型进行了划分,这些类型可以在 Types 工具类中找到
(1)基本类型
所有 Java 基本类型及其包装类,再加上 Void、String、Date、BigDecimal 和 BigInteger
(2)数组类型
包括基本类型数组(PRIMITIVE_ARRAY)和对象数组(OBJECT_ARRAY)
(3)复合数据类型
- Java 元组类型(TUPLE):这是 Flink 内置的元组类型,是 Java API 的一部分。最多
25 个字段,也就是从 Tuple0~Tuple25,不支持空字段 - Scala 样例类及 Scala 元组:不支持空字段
- 行类型(ROW):可以认为是具有任意个字段的元组,并支持空字段
- POJO:Flink 自定义的类似于 Java bean 模式的类
(4)辅助类型
Option、Either、List、Map 等
在项目实践中,往往会将流处理程序中的元素类型定为 Flink 的 POJO 类型
Flink 对 POJO 类型的要求如下:
- 类是公共的(public)和独立的(standalone,也就是说没有非静态的内部类)
- 类有一个公共的无参构造方法
- 类中的所有字段是 public 且非 final 的;或者有一个公共的 getter 和 setter 方法,这些方法需要符合 Java bean 的命名规范
类型提示
Flink 还具有一个类型提取系统,可以分析函数的输入和返回类型,自动获取类型信息,从而获得对应的序列化器和反序列化器。但是,由于 Java 中泛型擦除的存在,在某些特殊情况下(比如 Lambda 表达式中),自动提取的信息是不够精细的——只告诉 Flink 当前的元素由“船头、船身、船尾”构成,根本无法重建出“大船”的模样;这时就需要显式地提供类型信息,才能使应用程序正常工作或提高其性能
为了解决这类问题,Java API 提供了专门的“类型提示”(type hints)
回忆一下之前的 word count 流处理程序,我们在将 String 类型的每个词转换成(word,count)二元组后,就明确地用 returns 指定了返回的类型。因为对于 map 里传入的 Lambda 表达式,系统只能推断出返回的是 Tuple2 类型,而无法得到 Tuple2<String, Long>。只有显式地告诉系统当前的返回类型,才能正确地解析出完整数据
.map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));
这是一种比较简单的场景,二元组的两个元素都是基本数据类型。那如果元组中的一个元素又有泛型,该怎么处理呢?Flink 专门提供了 TypeHint 类,它可以捕获泛型的类型信息,并且一直记录下来,为运行时提供足够的信息。我们同样可以通过.returns()方法,明确地指定转换之后的 DataStream 里元素的类型
常用(重要)
returns(new TypeHint<Tuple2<Integer, SomeType>>(){})