Professional Documents
Culture Documents
com
see more please visit: https://homeofpdf.com
see more please visit: https://homeofpdf.com
see more please visit: https://homeofpdf.com
see more please visit: https://homeofpdf.com
本书既讲解了Flink的入门、安装、流计算开发入门、类型和序列
化系统、监控运维、安全管理配置等基础知识,又讲解了Flink的时间
概念、Window的实现原理及其代码解析,Flink的容错机制原理,
Flink容错的关键设计、代码实现分析,Flink Job从源码到执行整个
过程的解析,Flink Job的调度策略、资源管理策略、内存管理、数据
交换的关键设计和代码实现分析,Flink的RPC通信框架等深度内容。
本书适合对实时计算感兴趣的大数据开发、运维领域的从业人员
阅读,此外对机器学习工程技术人员也有所帮助。
图书在版编目(CIP)数据
Flink内核原理与实现/冯飞,崔鹏云,陈冠华编著.—北京:机械工
业出版社,2020.8
ISBN 978-7-111-66189-4
Ⅰ.①F… Ⅱ.①冯…②崔…③陈… Ⅲ.①数据处理软件 Ⅳ.
①TP274
中国版本图书馆CIP数据核字(2020)第134178号
电话服务
客服电话:010-88361066
010-88379833
010-68326294
网络服务
机 工 官 网:www.cmpbook.com
机 工 官 博:weibo.com/cmp1952
金 书 网:www.golden-book.com
机工教育服务网:www.cmpedu.com
封底无防伪标均为盗版
1.1 核心特点
1.1.1 批流一体
所有的数据都天然带有时间的概念,必然发生在某一个时间点。
把事件按照时间顺序排列起来,就形成了一个事件流,也叫作数据
流。例如信用卡交易事务,传感器收集设备数据、机器日志数据以及
网站或移动应用程序上的用户交互行为数据等,所有这些数据都是数
据流。
数据时时刻刻都在产生,如同江河奔流不息。例如,每一个人每
天都在处理各种各样的事情(事件),解决问题(响应),一年结束
之时,人们往往会坐下来总结这一年的得失,并制订新一年的计划。
在总结过去的时候,其实就默认给了一个时间范围。再举一个例子,
企业进行年终总结时,会统计当年完成了多少业绩,而不是考虑每一
笔业务的业绩。由此可以引出两个概念:无界数据(流)、有界数据
(批)。
1.无界数据
无界数据是持续产生的数据,所以必须持续地处理无界数据流。
数据是无限的,也就无法等待所有输入数据到达后处理,因为输入是
无限的,没有终止的时间。处理无界数据通常要求以特定顺序(例如
事件发生的顺序)获取,以便判断事件是否完整、有无遗漏。
2.有界数据
有界数据,就是在一个确定的时间范围内的数据流,有开始有结
束,一旦确定了就不会再改变。
Flink的设计思想与谷歌Cloud Dataflow的编程模型较为接近,都
以流为核心,批是流的特例。Flink擅长处理无界和有界数据。Flink
提供的精确的时间控制能力和有状态计算的机制,让它可以轻松应对
任何类型的无界数据流,同时Flink还专门设计了算法和数据结构来高
效地处理有界数据流。
1.2 架构
从概念上来说,所有的计算都符合“数据输入—处理转换—数据
输出”的过程,这个过程有时候叫作数据处理流水线(Pipeline),
流水线的概念来自生产制造中的流水线。以WordCount为例,其处理过
程抽象如图1-1所示。
图1-3 Flink技术架构
对于应用开发者而言,直接使用API层和应用框架层,两者的差别
在于API的层次不同,API层是Flink对外提供的核心API,应用框架层
是在核心API之上提供的面向特定计算场景、更加易用的API。
1.应用框架层
图1-4 Flink运行时架构
3. TaskManager
TaskManager接收JobManager分发的子任务,根据自身的资源情
况,管理子任务的启动、停止、销毁、异常恢复等生命周期阶段。
作业启动后开始从数据源消费数据、处理数据,并写入外部存储
中。
无论使用哪种资源集群,以上所介绍的角色是必不可少的,其作
用一样。
从 图 1-4 中 可 以 看 到 , JobManager 是 一 个 单 点 的 部 署 模 式 , 在
Flink中支持JobManager的HA部署,在后续章节中会介绍Flink HA的部
1.3 Flink的未来
Flink的理想是构建以流为核心的批流一体的计算框架,但是目前
还没有完全实现以流为核心的计算。在Flink1.9版本之前,批流是两
套 体 系 , 批 处 理 的 API 是 DataSet API , 流 计 算 的 API 是 DataStream
API。从Flink1.9版本开始,开启了批流一体的进程。首先在Flink
Table&SQL 模 块 的 API 和 算 子 层 面 上 实 现 了 统 一 , 在 不 远 的 未 来 ,
DataSet API将会被废弃,同样DataSet的算子也会被废弃,从而完全
使用DataStream API及其算子来实现批流的统一。
随着AI的持续发展,Flink在其1.9及以后的版本中设计了新的
PyFlink,提供Python API,逐渐将AI生态融合进来,AI开发者可以充
分使用Flink的分布式计算能力。在Flink1.10版本中,PyFlink还不是
特别成熟,所以本书中暂不对此进行介绍,待Flink的后续版本发布之
后,再详细介绍其原理和实现。
1.4 准备工作
在阅读过程中,除了了解必要的概念之外,深入了解Flink的源码
细节,可以帮助大家更好地理解Flink。
建议安装IntelliJ IDEA,该工具集成了Git版本管理、代码调
试、源码调用链分析、逆向类图等工具,便于阅读Flink的源码。
1.使用Git导入源码
Flink 工 程 源 码 的 GitHub 路 径 是
https://github.com/apache/flink.git , 可 以 直 接 使 用 IntelliJ
IDEA的Git工具导入,然后切换到release-1.10分支。
2.从源码包导入项目
从https://github.com/apache/flink下载Flink 1.10版本的源码
包,解压后从本地导入Maven工程即可。源码包下载如图1-5所示。
1.5 总结
本章介绍了Flink的核心特点,如批流一体、可靠的容错能力、高
吞吐低延迟、大规模复杂计算、多平台部署等,以及为了实现这些特
点,Flink的技术架构和运行架构所进行的有针对性的设计。Blink的
引入提升了批流一体化和性能,使得Flink的未来更值得期待。
对于Flink,应该从代码层面上进行深度了解,所以建议读者一边
阅读本书,一边了解Flink的源码,必定会有不同的收获。
2.1 Flink应用开发
上一章简要地介绍了Flink的设计理念、特点、生态与技术栈、运
行架构,在深入Flink之前,还有必要对Flink内部的一些核心概念、
流程进行总体性的阐述。
Flink作为批流一体的计算引擎,其面对的是业务场景,面向的使
用者是开发人员和运维管理人员。
Flink应用程序(简称Flink应用),也叫作Flink作业、Flink
Job。Flink作业包含了两个基本的块:数据流(DataStream)和转换
(Transformation)。DataStream是逻辑概念,为开发者提供了API接
口,Transformation是处理行为的抽象,包含了数据的读取、计算、
写出。所以Flink的作业中的DataStream API调用,实际上构建了多个
由Transformation组成的数据处理流水线(Pipeline)。
DataStream API和Transformation的转换如图2-1所示。
2.2 API层次
API面向的是开发者,从纵向来看Flink中的API分为4个层次,从
下而上,API层次越高,抽象程度越高,使用起来越方便,灵活性则会
2.3 数据流
对Flink这种以流为核心的分布式计算引擎而言,数据流是核心数
据 抽 象 , 表 示 一 个 持 续 产 生 的 数 据 流 , 与 Apache Beam 中 的
PCollection 的 概 念 类 似 。 在 Flink 中 使 用 DataStream 表 示 数 据 流 ,
DataStream是一种逻辑概念,并不是底层执行的概念。DataStream上
定义了常见的数据处理操作API(转换为Transformation),同时也具
备自定义数据处理函数的能力,当DataStream提供的常见操作不满足
需求的时候,可以自定义数据处理的逻辑。
DataStream体系如图2-3所示。
2.4 数据流API
DataStream API是Flink流计算应用中最常用的API,相比Table &
SQL API更加底层、更加灵活。
图2-4 内存数据读取API
2.文件读取数据
内置的从文件中读取数据的API如图2-5所示。
图2-5 读取文件API
图2-6 Socket接入数据API
socketTextStream()的参数比较简单,需要提供hostname(主
机名)、port(端口号)、delimiter(分隔符)和maxRetry(最大重
试次数)。
4.自定义读取
自定义数据读取就是使用Flink连接器、自定义数据读取函数,与
外部存储交互,读取数据,如从Kafka、JDBC、HDFS等读取。自定义数
据读取的API如图2-7所示。
3. Filter
过滤数据,如果返回true则该元素继续向下传递,如果为false则
将 该 元 素 过 滤 掉 。 该 类 运 算 应 用 在 DataStream 上 , 输 出 结 果 为
DataStream。
DataStream#filter 接 口 对 应 的 是 FilterFunction , 其 类 泛 型 为
FilterFunction<T>,T代表输入和输出元素的数据类型,如代码清单
2-3所示。
4. KeyBy
将数据流元素进行逻辑上的分组,具有相同Key的记录将被划分到
同 一 分 组 。 KeyBy ( ) 使 用 Hash Partitioner 实 现 。 该 运 算 应 用 在
DataStream上,输出结果为KeyedStream。
输 出 的 数 据 流 的 类 型 为 KeyedStream<T,KEY> , 其 中 T 代 表
KeyedStream中元素数据类型,KEY代表逻辑Key的数据类型,如代码清
单2-4所示。
代码清单2-4 KeyBy代码示例
以下两种数据不能作为Key。
1 ) POJO 类 未 重 写 hashCode ( ) , 使 用 了 默 认 的
Object.hashCode()。
2)数组类型。
5. Reduce
按照KeyedStream中的逻辑分组,将当前数据与最后一次的Reduce
结果进行合并,合并逻辑由开发者自己实现。该类运算应用在
KeyedStream上,输出结果为DataStream。
ReduceFunction<T>中的T代表KeyedStream中元素的数据类型,如
代码清单2-5所示。
代码清单2-5 Reduce代码示例
FoldFunction<O, T>已经被标记为Deprecated废弃,替代接口是
AggregateFunction<IN, ACC, OUT>。
7. Aggregation
渐进聚合具有相同Key的数据流元素,以min和minBy为例,min返
回的是整个KeyedStream的最小值,minBy按照Key进行分组,返回每个
分 组 的 最 小 值 。 在 KeyedStream 上 应 用 聚 合 运 算 输 出 结 果 为
DataStream,如代码清单2-7所示。
代码清单2-7 内置聚合运算代码示例
关于窗口,第4章会有详细讲解。
9. WindowAll
对一般的DataStream进行时间窗口切分,即全局1个窗口,如每5
秒 钟 一 个 滚 动 窗 口 。 应 用 在 DataStream 上 , 输 出 结 果 为
AllWindowedStream,如代码清单2-9所示。
代码清单2-9 WindowAll代码示例
注意:在一般的DataStream上进行窗口切分,往往会导致无法并
行计算,所有的数据会集中到WindowAll算子的一个Task上。
(2)AllWindowedStream
在AllWindowedStream上应用的是AllWindowFunction,输出结果
为DataStream。该类运算对应的是AllWindowFunction,其类泛型定义
为AllWindowFunction<IN, OUT, W extends Window>,IN表示输入值
的类型,OUT表示输出值的类型,W表示窗口的类型,如代码清单2-11
所示。
代码清单2-11 AllWindowFunction代码示例
14. Union
把两个或多个DataStream合并,所有DataStream中的元素都会组
合成一个新的DataStream,但是不去重。如果在自身上应用Union运
算,则每个元素在新的DataStream出现两次,如代码清单2-15所示。
代码清单2-15 Union运算示例
15. Window Join
在相同时间范围的窗口上Join两个DataStream数据流,输出结果
为DataStream。
Join核心逻辑在JoinFunction<IN1,IN2,OUT>中实现,IN1为第一
个DataStream中的数据类型,IN2为第二个DataStream中的数据类型,
OUT为Join结果的数据类型,如代码清单2-16所示。
代码清单2-16 Join代码示例
Join的核心逻辑在ProcessJoinFunction<IN1,IN2,OUT>中实现,
IN1为第一个DataStream中元素数据类型,IN2为第二个DataStream中
的元素数据类型,OUT为结果输出类型,如代码清单2-17所示。
代码清单2-17 Interval Join代码示例
17. WindowCoGroup
两个DataStream在相同时间窗口上应用CoGroup运算,输出结果为
DataStream,CoGroup和Join功能类似,但是更加灵活。
CoGroup 接 口 对 应 的 是 CoGroupFunction , 其 类 泛 型 为
CoGroupFunction<IN1, IN2, O>,IN1代表第一个DataStream中的元素
类型,IN2代表第二个DataStream中的元素类型,O为输出结果类型,
如代码清单2-18所示。
代码清单2-18 CoGroup代码示例
19. CoMap和CoFlatMap
在 ConnectedStream 上 应 用 Map 和 FlatMap 运 算 , 输 出 流 为
DataStream。其基本逻辑类似于在一般DataStream上的Map和FlatMap
运算,区别在于CoMap转换有2个输入,Map转换有1个输入,CoFlatMap
同理,如代码清单2-20所示。
代码清单2-20 CoMap和CoFlatMap代码示例
22. Iterate
在 API 层 面 上 , 对 DataStream 应 用 迭 代 会 生 成 1 个
IteractiveStream,然后在IteractiveStream上应用业务处理逻辑,
最终生成1个新的DataStream,IteractiveStream本质上来说是一种中
间数据流对象。
在数据流中创建一个迭代循环,即将下游的输出发送给上游重新
处理。如果一个算法会持续地更新模型,这种情况下反馈循环比较有
用,如代码清单2-23所示。
2.4.3 数据写出
数据读取的API绑定在StreamExecutionEnvironment上,数据写出
的API绑定在DataStream对象上。在现在的版本中,只有写到Console
控制台、Socket网络端口、自定义三类,写入文本文件、CSV文件等文
件接口都已被标记为废弃了。接口使用的详细介绍参照官方文档即
可。
自定义数据写出接口是DataStream.addSink,对于Sink的详细介
绍参见连接器和输出函数章节。
2.4.4 旁路输出
旁 路 输 出 在 Flink 中 叫 作 SideOutput , 用 途 类 似 于
DataStream#split,本质上是一个数据流的切分行为,按照条件将
DataStream切分为多个子数据流,子数据流叫作旁路输出数据流,每
个旁路输出数据流可以有自己的下游处理逻辑。如图2-9所示,通过旁
路输出将正常和异常的数据分别记录到不同的外部存储中。
2.5 总结
3.1 环境对象
StreamExecutionEnvironment是Flink应用开发时的概念,表示流
计算作业的执行环境,是作业开发的入口、数据源接口、生成和转换
DataStream的接口、数据Sink的接口、作业配置接口、作业启动执行
的入口。
Environment 是 运 行 时 作 业 级 别 的 概 念 , 从
StreamExecutionEnvironment中的配置信息衍生而来。进入到Flink作
业执行的时刻,作业需要的是相关的配置信息,如作业的名称、并行
度、作业编号Job ID、监控的Metric、容错的配置信息、IO等,用
StreamExecutionRuntime对象就不合适了,很多API是不需要的,所以
在Flink中抽象出了Environment作为运行时刻的上下文信息。
图3-1 3种环境对象的关系
对 于 开 发 者 而 言 , StreamExecutionEnvironment 在 作 业 开 发 的
Main函数中使用,RuntimeContext在UDF开发中使用,Environment则
起到衔接StreamExecutionEnvironment和RuntimeContext的作用。
3.1.1 执行环境
执行环境是Flink作业开发、执行的入口,当前版本Flink的批流
在API并没有统一,所以有流计算(StreamExecutionEnvironment)和
批处理(ExecutionEnvironment)两套执行环境。在本书中,主要介
绍流计算应用执行环境。
流计算执行环境体系如图3-2所示。
图3-3 Flink执行计划
5. ScalaShellStreamEnvironment
这是Scala Shell执行环境,可以在命令行中交互式开发Flink作
业。
其基本工作流程如下。
1)校验部署模式,目前Scala Shell仅支持attached模式。
2)上传每个作业需要的Jar文件。
其余步骤与RemoteStreamEnvironment类似。
3.1.2 运行时环境
运行时环境在Flink中叫作Environment,是Flink运行时的概念,
该接口定义了在运行时刻Task所需要的所有配置信息,包括在静态配
置和调度器调度之后生成的动态配置信息。
Environment类体系如图3-4所示。
3.2 数据流元素
数 据 流 元 素 在 Flink 中 叫 作 StreamElement , 有 数 据 记 录
StreamRecord、延迟标记Latency Marker、Watermark、流状态标记
StreamStatus这4种,分别有各自不同的用途。
在执行层面上,4种数据流元素都被序列化成二进制数据,形成混
合的数据流,在算子中将混合数据流中的数据流元素反序列化出来,
图3-6 StreamElement类体系
1. StreamRecord
StreamRecord表示数据流中的一条记录(或者叫作一个事件),
也叫作数据记录。
StreamRecord包含如下内容。
1)数据的值本身。
2)事件戳(可选)。
2. LatencyMarker
LatencyMarker用来近似评估延迟,LatencyMarker在Source中创
建 , 并 向 下 游 发 送 , 绕 过 业 务 处 理 逻 辑 , 在 Sink 节 点 中 使 用
LatencyMarker估计数据在整个DAG图中流转花费的时间,用来近似地
评估总体上的处理延迟。
LatencyMarker包含如下信息。
1)周期性地在数据源算子中创造出来的时间戳。
2)算子编号。
3)数据源算子所在的Task编号。
3. Watermark
Watermark 是 一 个 时 间 戳 , 用 来 告 诉 算 子 所 有 时 间 早 于 等 于
Watermark的事件或记录都已经到达,不会再有比Watermark更早的记
录,算子可以根据Watermark触发窗口的计算、清理资源等。后边有详
细介绍。
4. StreamStatus
3.3 数据转换
数据转换在Flink中叫作Transformation,是衔接DataStream API
和Flink内核的逻辑结构。DataStream面向开发者,Transformation面
向Flink内核,调用DataStream API的数据处理流水线,最终会转换为
Transformation流水线,Flink从Transformation流水线开始执行,后
边章节会详细介绍。
从DataStream流水线到Transformation流水线的转换示意如图3-7
所示。
Transformation 有 两 大 类 : 物 理 Transformation 和 虚 拟
Transformation。在运行时刻,DataStream的API调用都会被转换为
Transformation,然后从Transformation转换为实际运行的算子,而
虚拟的Transformation则不会转换为具体的算子,如图3-8所示。
Reblance、Union、Split、Select最终并没有形成实体的算子,
那么它们去哪儿了?在后边的作业提交章节中会阐述。
Flink内置的Transformation类体系如图3-9所示。
从类图中可以看到,Transformation类是顶层的抽象,所有的物
理 Transformation 继 承 了 PhysicalTransformation , 其 他 类 型 的
Transformation均为虚拟Transformation。
Transformation包含了Flink运行时的一些关键参数:
1)name:转换器的名称,主要用于可视化。
2)uid: 用户指定的uid,该uid的主要目的是在job重启时再次分
配跟之前相同的uid,可以持久保存状态。
图3-8 虚拟Transformation被优化后的算子树
图3-9 Transformation类体系
图3-11 SideOutput示例
(2)SplitTransformation
用来按条件切分数据流,该转换用于将一个流拆分成多个流(通
过OutputSelector来达到这个目的),当然这个操作只是逻辑上的拆
分(它只影响上游的流如何跟下游的流连接)。
构造该转换器,同样也依赖于其输入转换器(input)以及一个输
出 选 择 器 ( outputSelector ) , 但 会 实 例 化 其 父 类
(StreamTransformation,没有提供自定义的名称,而是固定的常量
值Split)。
(3)SelectTransformation
与 SplitTransformation 配 合 使 用 , 用 来 在 下 游 选 择
SplitTransformation切分的数据流。
(4)PartitionTransformation
该转换器用于改变输入元素的分区,其名称为Partition。因此,
工作时除了提供一个StreamTransformation作为输入外,还需要提供
一个StreamPartitioner的实例来进行分区。
图3-12 UnionTransformation示例
Union运算要求其直接上游输入的数据的结构必须是完全相同的。
(6)FeedbackTransformation
表示Flink DAG中的一个反馈点。简单来说,反馈点就是把符合条
件的数据重新发回上游Transformation处理,一个反馈点可以连接一
个或者多个上游的Transformation,这些连接关系叫作反馈边。处于
反馈点下游的Transformation将可以从反馈点和反馈边获得元素输
入。符合反馈条件并交给上游的Transformation的数据流叫作反馈流
(Feedback DataStream),如图3-13所示。
图3-14 CoFeedbackTransformation示意图
在图3-14中,可以看到TwoInputTransformation有两个输入,第1
输 入 的 类 型 为 Tuple<String,Long> , 反 馈 流 的 输 入 类 型 为
图3-15 Transformation、算子、UDF的关系
图 3-15 是 OneInputTransformation 的 示 例 ,
TwoInputTransformation与此相同,接下来介绍算子、UDF。
3.4 算子
算子在Flink中叫作StreamOperator。StreamOperator是流计算的
算子。Flink作业运行时由Task组成一个Dataflow,每个Task中包含一
个或者多个算子,1个算子就是1个计算步骤,具体的计算由算子中包
装的Function来执行。除了业务逻辑的执行算子外,还提供了生命周
期的管理。
(2)TowInputStreamOperator
TwoInputStreamOperator是双流输入算子,对于两个输入流提供
了6个关键处理接口:
1)数据处理processElement1、processElement2接口,分别对应
上游两个输入流的数据记录。
2)Watermark处理processWatermark1、processWatermark2接口
分别对应上游两个输入流的Watermark。
3 ) LatencyMark 处 理 processLatencyMarker1 、
processLatencyMarker2 接 口 分 别 对 应 上 游 两 个 输 入 流 的
LatencyMarker,如代码清单3-3所示。
(3)算子融合优化策略
算子中还定义了OperatorChain的策略,具体规则在JobGraph优化
相关章节中介绍。
3.4.2 Flink算子
Flink流计算算子体系最开始是围绕DataStream API设计的,同时
也是Flink SQL流计算的底层执行框架,其整个体系如图3-16和图3-17
所示。
在设计的时候分为两条线:
1 ) 数 据 处 理 。 主 要 是 OneInputStreamOperator 、
TwoInputStreamOperator接口。
2)生命周期、状态与容错。主要是AbstractStreamOperator抽象
类及其子实现类。AbstractUdfStreamOperator是主要的实现类,目前
所有的算子都继承自此类。SourceReaderOperator是抽象类,目前还
没有实现类,其在Flip-27中引入,未来要做Source接口的重构,实现
流和批Source在实现层面上的统一。
DataStream的算子跟DataStrea API几乎是对应的,从行为上来说
分为4类:
(1)单流输入算子
图3-17 Flink算子体系2
3.4.3 Blink算子
Blink Runtime中的算子是阿里巴巴Blink引入的流批统一的算
子,在当前阶段用来支撑Blink的Table & SQL的运行。正因为要实现
流 批 统 一 、 动 态 代 码 生 成 等 高 级 能 力 特 性 , 所 以 在 目 前 的 Blink
Runtime中重新实现了很多与SQL相关的算子,但是仍然使用了Flink-
Streaming-java中定义的Transformation来包装这些算子。
Blink算子体系如图3-18和图3-19所示。
Blink Runtime中用到的算子大概可以分为3类:
1)Blink Runtime内置算子。
2 ) 其 他 模 块 内 置 的 算 子 , 如 CEP 算 子 ( CepOperator ) 、
ProcessOperator等。
图3-20 同步IO与异步IO
既然是异步请求,那么就存在后调用的请求先返回的情况,所以
为了更好地适应实际场景,Flink在异步算子中提供了两种输出模式。
(1)顺序输出模式
先收到的数据元素先输出,后续数据元素的异步函数调用无论是
否先完成,都需要等待。顺序输出模式可以保证消息不乱序,但是可
能增加延迟、降低算子的吞吐量,其原理如图3-21所示。
图3-23 Function分类与关系
2. SinkFunction
无下游Function,SinkFunction直接将数据写入外部存储,所以
Sink函数所在的算子是作业的重点,没有下游算子。
3. 一般Function
一 般 的 UDF 函 数 用 在 作 业 的 中 间 处 理 步 骤 中 , 其 接 口 定 义 与
SourceFunction和SinkFunction不同。一般UDF所在的算子有上游算
子,也有下游算子。
Flink的一般UDF有单流输入和双流输入两种,从UDF输入、输出的
模型来说,多流输入可以通过多个双流输入串联而成,这种设计比较
简单实用,如图3-24所示。
图3-25 Function层次
Flink 内 置 的 DataStream 上 的 API 接 口 , 如 DataStream#map 、
DataStream#flatMap、DataStreamFilter#filter等,使用的都是高阶
函数,开发者使用高阶函数的时候,无须关心定时器之类的底层概
念,只需要关注业务逻辑即可。低阶函数即ProcessFunction。
无 状 态 Function 用 来 做 无 状 态 计 算 , 使 用 比 较 简 单 , 如
MapFunction 。 无 状 态 Function 和 RichFunction 是 一 一 对 应 的 , 如
MapFunction对应RichMapFunction,如代码清单3-5所示。
代码清单3-5 MapFunction代码示例
图3-28 双流延迟Join
其逻辑如下。
1)创建2个State对象,分别缓存输入流1和输入流2的事件。
2)创建1个定时器,等待数据的到达,定时延迟触发Join计算。
3)接收到输入流1事件后更新State。
图3-29 延迟计算过程
触发器在算子层面上提供支持,所有支持延迟计算的算子都继承
了Triggerable接口。Triggerable接口主要定义了基于事件时间和基
于处理时间的两种触发行为,如代码清单3-6所示。
代码清单3-6 Triggerable接口
图3-30 广播函数体系
代码清单3-7 BroadcastProcessFunction抽象类
上 面 的 两 个 广 播 函 数 BroadcastProcessFunction 和
KeyedBroadcastProcessFunction都是抽象类,所以在实际使用中,开
发者需要实现其定义的抽象方法。
processElement()方法和processBroadcastElement()方法的
区别在于:processElement只能使用只读的上下文ReadOnlyContext,
而processBroadcastElement()方法则可以使用支持读写的上下文
Context。这么设计看起来很奇怪,但是合理的。广播状态模式下,要
求所有算子上的广播状态应完全一致,如果也允许processElement方
法更新、删除广播状态中的数据,那么会使得算子之间的广播状态变
得不一致,导致系统行为不可预测。在后边会介绍数据分区,数据分
区会将数据流进行分流,交给下游的不同算子,那么不同算子接收的
图3-31 异步函数类体系
3.5.5 数据源函数
数据源函数在Flink中叫作SourceFunction,Flink是一个计算引
擎,其需要从外部读取数据,所以在Flink中设计了SourceFunction体
系,专门用来从外部存储读取数据。SourceFunction是Flink作业的起
点,一个作业中可以有多个起点,即读取多个数据源的数据。
SourceFunction体系如图3-32所示。
图3-34 SinkFunction类体系
3.5.7 检查点函数
ListCheckpointed接口的行为跟Checkpointed行为类似,除了提
供状态管理能力之外,修改作业并行度的时候,还提供了状态重分布
的支持。ListCheckpointed接口定义如代码清单3-13所示。
代码清单3-13 ListCheckpointed接口
3.6 数据分区
数据分区在Flink中叫作Partition。本质上来说,分布式计算就
是把一个作业切分成子任务Task,将不同的数据交给不同的Task计
算。在分布式存储中,Partition分区的概念就是把数据集切分成块,
在该接口中可以看到,每一个分区器都知道下游通道数量,通道
数量在一次作业运行中是固定的,除非修改作业的并行度,否则该值
是不会改变的(此处跟后边容错章节模型中提到的Flink DAG有关系,
Flink DAG的拓扑关系是静态的)。
数据分区类体系如图3-35所示。
2. ForwardPartitioner
在API层面上,ForwardPartitioner应用在DataStream上,生成一
个新的DataStream。
该Partitioner比较特殊,用于在同一个OperatorChain中上下游
算子之间的数据转发,实际上数据是直接传递给下游的。
3. ShufflePartitioner
在API层面上,ShufflePartitioner应用在DataStream上,生成一
个新的DataStream。
图3-36 Rescaling分区效果示意
其使用如代码清单3-18所示。
代码清单3-18 DataStream中使用RescalingPartitioner
3.7 连接器
连接器在Flink中叫作Connector。Flink本身是计算引擎,并不提
供数据存储能力,所以需要访问外部数据,外部数据源类型繁多,连
接器因此应运而生,它提供了从数据源读取数据和写入数据的能力。
基于SourceFunction和SinkFunction构建出了种类繁多的连接器。
Flink在Flink-Connectors模块中提供了内置的Connector,包含
常 见 的 数 据 源 , 如 HDFS 、 Kafka 、 HBase 等 , 同 时 结 合 Source &
SinkFunction体系也能够自定义连接器。也有一部分第三方实现的连
接器,如GitHub的Bahir项目。
1.流内置连接器(见表3-1)
表3-1 Flink流内置连接器
连接器中有两个关键行为,即读取和写入,分别对应Flink中的
SourceFunction和SinkFunction。根据外部存储类型的不同,实现逻
辑各不相同。
下面以KafkaConnector为例说明连接器是如何构建和运转的。
Kafka是一个分布式的高性能消息队列,是流计算中最常用的数据
存储。Kafka在概念上与传统的消息中间件类似,有Topic、消费者、
图3-37 Kafka连接器与Kafka集群交互
图3-37中,Kafka连接器使用SinkFunction向Kafka集群的Topic写
入 数 据 , SinkFunction 中 使 用 了 Kafka 的 Producer 。 使 用
SourceFunction从Kafka集群读取数据,SourceFunction的实现中使用
了KafkaConsumerProducer。
3.8 分布式ID
在分布式计算中,Flink对所有需要进行唯一标识的组件、对象提
供了抽象类AbstractID,因为需要跨网络进行传递,所以该类实现了
Serializable 接 口 , 需 要 比 较 唯 一 标 识 是 否 相 同 , 所 以 也 实 现 了
Comparable接口。
分布式ID类体系如图3-38所示。
从图中可以看到,基本上Flink作业、资源管理、作业管理器、资
源管理器、TaskManger等都有各自的身份标识实现。
3.9 总结
Flink作业的API只是面向开发者层面的抽象,在底层Flink做了重
要的核心运行时抽象,首先是数据流和在数据流上的操作,这是API的
核心实现,然后从API层逐渐向下,抽象了数据转换作为API层到执行
层转换的中间层,抽象了算子、函数、数据分区体系作为运行时业务
逻辑的载体,抽象了数据IO屏蔽外部数据存储的差异性。
4.1 时间类型
在Flink中定义了3种时间类型:事件时间(Event Time)、处理
时间(Processing Time)和摄取时间(Ingestion Time)。
3种时间类型如图4-1所示。
图4-1 3种时间类型
(1)事件时间
4.3 窗口原理与机制
窗口算子负责处理窗口,数据流源源不断地进入算子,每一个数
据元素进入算子时,首先会被交给WindowAssigner。WindowAssigner
决定元素被放到哪个或哪些窗口,在这个过程中可能会创建新窗口或
者合并旧的窗口。在Window Operator中可能同时存在多个窗口,一个
元素可以被放入多个窗口中。
数据进入窗口时,分配窗口和计算的逻辑如图4-3所示。
4.3.2 WindowTrigger
Trigger触发器决定了一个窗口何时能够被计算或清除,每一个窗
口都拥有一个属于自己的Trigger,Trigger上会有定时器,用来决定
一个窗口何时能够被计算或清除。每当有元素加入该窗口,或者之前
注册的定时器超时时,Trigger都会被调用。
Trigger触发的结果如下。
1)Continue:继续,不做任何操作。
2)Fire:触发计算,处理窗口数据。
4.4 水印
水印(Watermark)用于处理乱序事件,而正确地处理乱序事件,
通常用Watermark机制结合窗口来实现。
图4-9 DataStream中TimestampAssigner接口体系
AssignerWithPeriodicWatermarks是周期性生成Watermark策略的
顶层抽象接口,该接口的实现类周期性地生成Watermark,而不会针对
每一个事件都生成。
AssignerWithPunctuatedWatermarks对每一个事件都会尝试进行
Watermark的生成,但是如果生成的Watermark是null或者Watermark小
于之前的Watermark,则该Watermark不会发往下游,因为发往下游也
不会有任何效果,不会触发任何窗口的执行。
4.4.2 Flink SQL Watermark生成
图4-11 Watermark处理逻辑
Apache Flink内部实现每一个边上只能有一个递增的Watermark,
当出现多流携带EventTime汇聚到一起(GroupBy或Union)时,Apache
图4-12 多流Watermark示例
在图4-12中,Source算子产生各自的Watermark,并随着数据流流
向下游的map算子,map算子是无状态计算,所以会将Watermark向下透
传。window算子收到上游两个输入的Watermark后,选择其中较小的一
个发送给下游,window(1)算子比较Watermark 29和Watermark 14,
选择Watermark 14作为算子当前Watermark,并将Watermark 14发往下
游,window(2)算子也采用相同的逻辑。
4.5 时间服务
时间是Flink中极其重要的概念,在Flink的开发层面上,会在
KeyedProcessFunction中和Window中使用到时间概念。一般情况下,
处理时间也是类似的逻辑,区别在于,处理时间是从处理时间
Timer优先级队列中找到Timer。处理时间因为依赖于当前系统,所以
其使用的是周期性调度。
4.5.3 优先级队列
直接使用Java的PriorityQueue看起来也能实现InternalTimer的
需求,但是Flink在优先级队列中使用了KeyGroup,是按照KeGroup去
重的,并不是按照全局的Key去重,如图4-13所示。
4.6 窗口实现
在Flink中窗口有两套实现,分别位于flink-streaming-java和
flink-table-runtime-blink模块中,其会话窗口、时间窗口实现基本
是一样的,计数窗口的实现不同。Flink-streaming-java中计数窗口
依赖于GlobalWindow来实现,在flink-table-runtime-blink中,计数
窗口与时间窗口一样有类定义和窗口分配器。
图4-14 WindowOperator中数据处理过程
4.6.1 时间窗口
按照时间类型,窗口分为两类:处理时间窗口和事件时间窗口。
按照窗口行为,时间窗口分为两类:滚动窗口和滑动窗口。
滚动窗口(TumbleWindow)的关键属性有两个:
1)Offset:窗口的起始时间。
2)Size:窗口的长度。
滑动窗口(SlidingWindow)的关键属性有3个:
1)Offset:窗口的起始时间。
2)Size:窗口的长度。
3)Slide:滑动距离。
滚动窗口、滑动窗口其本质上是类似的,滚动窗口可以看作是滑
动距离与窗口长度相同的滑动窗口。
在数据元素分配窗口的时候,对于滚动窗口,一个数据元素只属
于一个窗口,但可能属于多个滑动窗口。
从上边的代码中可以看到,Flink会为每个数据元素分配一个或者
多个TimeWindow对象,然后使用TimeWindow对象作为Key来操作窗口对
应的State。
注:TimeWindow的equals方法,使用类型和窗口的起止时间进行
相等比较,所以使用TimeWindow作为Key没有问题。
4.6.2 会话窗口
图4-15 会话窗口分割示例
3)EventTimeSessionWindows:事件时间会话窗口,使用固定会
话间隔时长。
4)DynamicEventTimeSessionWindows:事件时间会话窗口,使用
自定义会话间隔时长。
不同类型的会话窗口使用示例如代码清单4-9所示。
代码清单4-9 会话窗口使用示例
4.7 总结
本章中介绍了Flink中的时间类型,包括事件时间、处理时间、摄
取时间,不同的时间类型有其各自的适用场景。窗口是流上的重要概
念,在Flink有计数窗口、时间窗口、会话窗口3大类,其原理与机制
类似。
基于时间,使用窗口进行数据流切分,按照窗口进行计算,触发
窗口统计的是Watermark机制,Watermark在DataStream和SQL中有各自
的生成机制。在Flink内部,算子的Watermark可能来自上游的多个算
图5-1 逻辑类型/序列化的承上启下作用
Flink 目 前 有 两 套 逻 辑 类 型 系 统 : TypeInfomation 类 型 系 统 和
Flink SQL中的LogicalTypes类型系统。
TypeInformation类型系统是为DataStream/DataSet API设计的,
用来描述对象的类型信息,在运行时根据TypeInfomation的类型描述
来序列化对象。在1.9版本引入Blink Planner之前,Flink SQL依赖于
TypeInfomation 类 型 系 统 定 义 表 的 元 信 息 ( Schema ) 。 在
5.1 DataStream类型系统
DataStream是面向开发者的比较偏底层的API,在Flink SQL推出
之前是Flink主要的开发接口。开发者在开发DataStream应用的时候,
就像在编写Java或者Scala的程序一样,在定义数据类型的时候,使用
的是Java或者Scala的类型,如代码清单5-1所示。
代码清单5-1 WindowWordCount代码示例
图5-2 Flink物理类型分类
如果开发者在编写Flink应用过程中使用了自定义类型,并且又没
有提供类型的注册和序列化/反序列化方法,Flink就无法对该类型进
行该自定义序列化/反序列化。此时为了Flink的正常运行,对于这一
类的数据类型,无法识别的类型就会交给Kryo进行序列化。Kryo可以
对任意类型的Java对象进行序列化,是一种Java中的通用序列化方
式,缺点是序列化/反序列化效率相对较低。
5.1.2 逻辑类型
从上述代码中可以看到,在使用DataStream#map接口的时候,就
会触发类型的提取。
2. 自动类型推断
Flink首先会自动进行类型推断,但是对于一些带有泛型的类型,
Java泛型的类型擦除机制会导致Flink在处理Lambda表达式的类型推断
时不能保证一定能提取到类型。
Java泛型(Generic)的引入加强了参数类型的安全性,减少了类
型的转换,但有一点需要注意:Java的泛型机制是在编译级别实现
的。编译器生成的字节码在运行期间并不包含泛型的类型信息。
此时就需要为Flink的应用提供类型信息,TypeHint的用途即在于
此,使用TypeHint的匿名类来获取泛型的类型信息,如代码清单5-3所
示。
代码清单5-3 使用TypeHint在运行时进行类型提取
上 述 代 码 中 使 用 匿 名 内 部 类 来 获 取 泛 型 信 息 , 其 中 new
TypeHint<Tuple3<String, String, Double>>(){}就是用来在类型
擦除的情况下来获取泛型信息的。
3. Lambda函数的类型提取
Flink类型提取依赖于继承等机制,但Lambda函数比较特殊,其类
型提取是匿名的,也没有与之相关的类,所以其类型信息较难获取。
5.2 SQL类型系统
在 Flink 1.9 版 本 之 前 , Flink SQL 使 用 TypeInfomation 类 型 系
统,从1.9版本开始引入了Blink,同时引入了新的SQL类型系统,解决
了DataStream类型系统在SQL中使用的问题。
SQL类型系统是一种类型的逻辑表示,跟Java/Scala的物理类型并
不 是 一 一 对 应 的 关 系 。 在 DataStream 的 类 型 系 统 中 , 使 用
TypeInformation来描述类型信息,而在Flink SQL中则使用DataType
中的LogicalType类型系统来描述类型信息,LogicalType类型系统与
SQL标准基本保持一致,同时增加了一些额外的信息,如是否可以为
null等,目的是提高scala expression(标量表达式)的处理效率。
Flink SQL中支持的SQL逻辑类型如图5-4所示。
图5-7 BinaryRow的结构
BinaryRow存储结构中包含两个部分: 定长部分和变长部分。
(1)定长部分
定长部分包含3个内容:头信息区(Header)、空值索引(Null
Bit Set)、字段值区(Field Values)。
1)头信息区(Header):占用一个字节。
2)空值索引(Null Bit Set):用于标记行中Null值字段,在内
存中使用8字节进行对齐。在实际的存储中,该区域的第1个字节就是
行的头信息区,剩下的才是Null值字段标识位。
3)字段值区(Field Values):保存基本类型和8个字节长度以
内的值,如果某个字段值超过了8个字节,则保存该字段的长度与
offset偏移量。在目前的实现中,一般的Bool类型、数值类型和长度
较短的时间类型、精度低一些的Decimal类型可以保存在定长部分,如
代码清单5-4所示。
代码清单5-4 数据类型是否为定长判断
图5-8 ColumnarRow组织数据的结构示意图
默认情况下,一个ColumnarRow示例中保存2048行的数据,2048是
个经验数字。
图5-9 序列化和反序列化过程
TypeSerializer的实现类比较多,感兴趣的读者可以查看官方的
Java API文档或者源代码了解更多细节。
对于嵌套类型的数据结构,从最内层的原子字段开始进行序列
化,外层的TypeSerializer负责将内层的序列化结果组装到一起。
图5-10 嵌套类型序列化示例
注意:反序列化的时候,Tuple中的每个子序列化器能够自
动识别应该读取多少字节的数据,如对于int类型,读取32字节,对于
最 终 的 实 际 序 列 化 的 动 作 交 给 StringValue.class 执 行 , 写 入
String的长度和String的值到java.io.DataOutput,实际上就是写入
MemorySegment中,如代码清单5-6所示。
代码清单5-6 String类型数据实际序列化过程
5.4 总结
DataStream类型系统分为物理类型和逻辑类型,在Flink UDF中需
要使用类型推断将用户代码中的类型转换为DataStream的逻辑类型,
在运行时刻需要使用逻辑类型信息来实现数据的序列化和反序列化。
DataStream的类型系统对SQL的类型系统的支持不够完善,所以Blink
SQL引入了新的类型系统,执行层面使用DataStream类型系统。
对于Flink类型系统没有覆盖的类型,使用Kryo来实现序列化。对
于Flink类型系统支持的类型,则会使用类型描述信息,将数据序列化
为二进制数据和从二进制反序列化成对象。
6.1 自主内存管理
Flink从一开始就选择了使用自主的内存管理,避开了JVM内存管
理在大数据场景下的问题,提升了计算效率。
1. JVM内存管理的不足
基于JVM的数据分析引擎需要将大量数据存到内存中,这就不得不
面对JVM存在的几个问题。
(1)有效数据密度低
Java的对象在内存中的存储包含3个主要部分:对象头、实例数
据、对齐填充部分。32位和64位的虚拟机中对象头分别需要占用32bit
和64bit。实例数据是实际的数据存储。为了提高效率,内存中数据存
储不是连续的,而是按照8byte的整数倍进行存储。例如,只有一个
boolean字段的类实例占16byte:头信息占8byte,boolean占1byte,
为了对齐达到8的倍数会额外占用7byte。这就导致在JVM中有效信息的
存储密度很低。
(2)垃圾回收
JVM的内存回收机制的优点和缺点同样明显,优点是开发者无须资
源回收,可以提高开发效率,减少了内存泄漏的可能。但是内存回收
是不可控的,在大数据计算的场景中,这个缺点被放大,TB、PB级的
数据计算需要消耗大量的内存,在内存中产生海量的Java对象,一旦
出现Full GC,GC会达到秒级甚至分钟级,直接影响执行效率。
6.2 内存模型
Flink 1.10以前的版本中内存模型存在一些缺陷,导致优化资源
充分利用率比较困难,例如:
● 流和批处理内存占用的配置模型不同,配置参数多、关系乱。
6.3 内存数据结构
Flink的内存管理像操作系统管理内存一样,将内存划分为内存
段、内存页等结构。
6.3.1 内存段
内存段在Flink内部叫作MemorySegment,是Flink的内存抽象的最
小分配单元。默认情况下,一个MemorySegment对应着一个32KB大小的
内存块。这块内存既可以是堆上内存(Java的byte数组),也可以是
堆外内存(基于Netty的DirectByteBuffer)。
图6-4 MemorySegment类体系
HeapMemorySegment用来分配堆上内存,HybridMemorySegment用
来分配堆外内存和堆上内存。实际上在2017年之后的Flink中,并没有
使用HeapMemorySegment,而是使用HybridMemorySegment这个类来同
时实现堆上和堆外内存的分配。
之所以在后续的版本中只使用HybridMemorySegment,涉及了JIT
编译优化的问题。如果同时使用了两个类,那么在运行的时候,每一
次调用都需要去查询函数表,确定调用哪个子类中的方法,无法提前
优化。但是如果只使用一个类,那么JIT编译时,自动识别方法的调用
都可以被去虚化(de-virtualized)和内联(inlined),可以极大地
提高性能,调用越频繁,优化效果就越高。一般实际测试的性能差距
在2.7倍左右。
早期的实现中,Flink使用了工厂模式,确保每次运行只用到了1
个子类,这样JIT通过运行分析就能确定,只有某一个子类实例化了,
可以动态地优化提高效率。
在现在的实现中,Flink使用HybridMemorySegment同时实现堆上
堆 外 内 存 访 问 , 则 JIT 针 对 性 优 化 就 可 以 实 现 高 效 的 调 用 。
HybridMemorySegment借助sun.misc.Unsafe提供的一系列方法同时操
作堆上和堆外内存。
6.3.2 内存页
MemorySegment 是 Flink 内 存 分 配 的 最 小 单 元 , 对 于 跨
MemorySegment保存的数据,如果需要上层的使用者,需要考虑所有的
图6-5 DataInputView接口继承关系
6.4 内存管理器
MemoryManager是Flink中管理托管内存的组件,其管理的托管内
存只使用堆外内存。在批处理中用在排序、Hash表和中间结果的缓存
中,在流计算中作为RocksDBStateBackend的内存。
流计算中,当Task停止执行的时候RocksDBStateBackend负责释放
物 理 内 存 , 并 将 资 源 归 还 给 MemoryManager 。 资 源 归 还 给
MemoryManager只是更新其可用资源的大小数值,并不是对内存的物理
操作。
6.5 网络缓冲器
网络缓冲器(NetworkBuffer)是网络交换数据的包装,其对应于
MemorySegment内存段,当结果分区(ResultParition)开始写出数据
的 时 候 , 需 要 向 LocalBufferPool 申 请 Buffer 资 源 , 使 用
BufferBuilder将数据写入MemorySegment。当MemorySegment都分配完
后,则会持续等待Buffer的释放。
Network的使用如图6-9所示。
LocalBufferPool 首 先 从 自 身 持 有 的 MemorySegment 中 分 配 可 用
的 , 如 果 没 有 可 用 的 , 则 从 TaskManager 的 NetworkBufferPool 中 申
请,如果没有,则阻塞等待可用的MemorySegment,如代码清单6-5所
示。
代码清单6-5 LocalBuffer分配MemorySegment
6.5.2 内存回收
Buffer 回 收 之 后 , 并 不 会 释 放 MemorySegment , 此 时
MemorySegment仍然在LocalBufferPool的资源池中,除非TaskManager
级别内存不足,才会释放回TaskManager持有的全局资源池。
释放MemorySegment的时候,同样要根据MemorySegment的类型来
进行,并且要在不低于保留内存的情况下,将内存释放回内存段中,
变为可用内存,后续申请MemorySegment的时候,可以重复利用该内存
片段。
2. MemorySegment释放
当NetworkBufferPool关闭的时候进行内存的释放,交还给操作系
统,相关介绍参见MemoryManager内存章节的内存释放。
6.6 总结
大数据场景下,使用Java的内存管理会带来一系列的问题,所以
Flink从一开始就选择自主管理内存。为了实现内存管理,Flink对内
存进行了一系列的抽象,内存段MemorySegment是最小的内存分配单
7.1 状态类型
按照数据结构的不同,Flink中定义了多种State,应用于不同的
场景,具体如下。
(1)ValueState<T>
即类型为T的单值状态。这个状态与对应的Key绑定,是最简单的
状态。可以通过update方法更新状态值,通过value()方法获取状态
值。
(2)ListState<T>
即Key上的状态值为一个列表。可以通过add方法往列表中附加
值;也可以通过get()方法返回一个Iterable<T>来遍历状态值。
(3)ReducingState<T>
这种状态通过用户传入的reduceFunction,每次调用add方法添加
值时,会调用reduceFunction,最后合并到一个单一的状态值。
(4)AggregatingState<IN,OUT>
1. KeyedState
在 KeyedStream 中 使 用 。 状 态 是 跟 特 定 的 Key 绑 定 的 , 即
KeyedStream流上的每一个Key对应一个State对象。KeyedState可以使
用所有的State。KeyedState保存在StateBackend中。
可以使用ValueState进行求和运算,其使用如代码清单7-1所示。
代码清单7-1 ValueState求和运算示例
7.2 状态描述
State既然是暴露给用户的,那么就有一些属性需要指定,如
State名称、State中类型信息和序列化/反序列化器、State的过期时
间等。在对应的状态后端(StateBackend)中,会调用对应的create
方 法 获 取 到 StateDescriptor 中 的 值 。 在 Flink 中 状 态 描 述 叫 作
StateDescriptor,其体系如图7-1所示。
7.4 状态接口
在Flink中使用状态,有两种典型场景:
● 使用状态对象本身存储、写入、更新数据。
● 从StateBackend获取状态对象本身。
前者叫作状态操作接口,后者叫作状态访问接口。
7.4.1 状态操作接口
Flink中的State面向两类用户,即应用开发者和Flink框架本身。
两者对State的操作是不同的,所以在Flink中设计了两套接口:面向
应用开发者的State接口和内部State接口。面向开发者的接口要保持
稳定,考虑Flink升级的兼容性。内部的State接口则是Flink内部引擎
使用的,提供了更多的State操作方法,可以根据需要灵活地扩展改
进。
图7-3 面向开发者的State接口体系
以MapState为例,其提供了添加、获取、删除、遍历的API接口。
其接口定义如代码清单7-4所示。
代码清单7-4 MapState接口
图7-6 KeyedStateStore接口原理
从 图 7-6 中 可 以 看 到 , KeyedStateStore 中 使 用
RocksDBStateBackend 或 者 HeapKeyedStateBackend 来 保 存 数 据 ,
KeyedStateStore中获取/创建状态都交给了具体的StateBackend来处
理,KeyedSateStore本身更像是一个代理。
在后续算子中的初始化/恢复状态中会用到OperatorStateStore和
KeyedStateStore。
7.5 状态存储
Flink中无论是哪种类型的State,都需要被持久化到可靠存储
中 , 才 具 备 应 用 级 的 容 错 能 力 , State 的 存 储 在 Flink 中 叫 作
StateBackend。StateBackend需要具备如下两种能力。
1)在计算过程中提供访问State的能力,开发者在编写业务逻辑
中能够使用StateBackend的接口读写数据。
2)能够将State持久化到外部存储,提供容错能力。
根据使用场景的不同,Flink内置了3种StateBackend,其体系如
图7-7所示。
图7-8 3种StateBackend的关系
图7-9 StateTable类体系
7.5.2 基于RocksDB的StateBackend
RocksDBStateBackend跟内存型和文件型StateBackend不同,其使
用嵌入式的本地数据库RocksDB将流计算数据状态存储在本地磁盘中,
不会受限于TaskManager的内存大小,在执行检查点的时候,再将整个
RocksDB中保存的State数据全量或者增量持久化到配置的文件系统
7.6 状态持久化
StateBackend中的数据最终需要持久化到第三方存储中,确保集
群故障或者作业故障能够恢复,针对不同类型的快照策略如图7-10所
示。
HeapSnapshotStrategy 策 略 对 应 于 HeapKeyedStateBackend ,
RocksDBStateBackend 的 持 久 化 策 略 有 两 种 : 全 量 持 久 化 策 略
( RocksFullSnapshotStratey ) 和 增 量 持 久 化 策 略
(RocksIncementalSnapshotStrategy)。
图7-10 快照保存策略类体系
在执行持久化策略的时候,使用异步机制,每个算子启动1个独立
的线程,将自身的状态写入分布式存储可靠存储中。在做持久化的过
程中,状态可能会被持续修改,基于内存的状态后端使用
CopyOnWriteStateTable来保证线程安全,RocksDBStateBackend则使
用RockDB的快照机制,使用快照来保证线程安全。
(2)增量持久化策略
增 量 持 久 化 就 是 每 次 持 久 化 增 量 的 State , 只 有
RocksDBStateBackend支持增量持久化。
Flink增量式的检查点以RocksDB为基础,RocksDB是一个基于LSM-
Tree的KV存储,新的数据保存在内存中,称为memtable。如果Key相
同,后到的数据将覆盖之前的数据,一旦memtable写满了,RocksDB就
会将数据压缩并写入到磁盘。memtable的数据持久化到磁盘后,就变
成了不可变的sstable。
因为sstable是不可变的,Flink对比前一个检查点创建和删除的
RocksDB sstable 文 件 就 可 以 计 算 出 状 态 有 哪 些 改 变 。 为 了 确 保
7.7 状态重分布
在实际的生产环境中,作业预先设置的并行度很多时候并不合
理,太多则浪费资源,太少则资源不足,可能导致数据积压延迟变大
或者处理时间太长,所以在运维过程中,需要根据作业的运行监控数
据调整其并行度。调整并行度的关键是处理State。回想一下前文中的
内容,State位于算子中,改变了并行度,则意味着算子个数改变了,
需要将State重新分配给算子。下面从OperatorState和KeyedState两
种State角度,介绍如何将State重新分配给算子。
7.7.1 OperatorState重分布
1. ListState
并行度在改变的时候,会将并发上的每个List都取出,然后把这
些List合并到一个新的List,根据元素的个数均匀分配给新的Task。
如图7-11所示,如果刚开始算子的并行度为2,那么在重新分区之
后 ,会将所有的元素平均分配给每一个算子的State。
2. UnionListState
比ListState更加灵活,把划分的方式交给用户去做,当改变并发
的时候,会将原来的List拼接起来,然后不做划分,直接交给用户。
图7-11 ListState重分布
图7-12 UnionListState重分布
3. BroadcastState
在函数UDF章节提到过,操作BroadcastState的UDF需要保证不可
变性,所以各个算子的同一个BroadcastState完全一样。变并发的时
候,把这些数据分发到新的Task即可,如图7-13所示。
图7-13 BroadcastState重分布
7.7.2 KeyedState重分布
基于Key-Group,每个Key隶属于唯一的Key-Group。Key-Group分
配给Task实例,每个Task至少有1个Key-Group。
7.8 状态过期
流计算应用肯定会用到State。流上的数据处理是永无止境的,假
如状态不自动清除,并且随着作业运行的时间越来越久,就会累积越来
越多的状态,进而影响任务的性能,甚至可能会因为状态太大,导致
整个作业的崩溃。
另一方面,数据是有时效性的,一般情况下,历史数据在业务上
的价值会随着时间流逝不断下降,在State持有大量低价值的数据是否
真的有必要?
7.8.1 DataStream中状态过期
在DataStream作业中,对于复杂的逻辑而言,有时需要精细的控
制,此时可以通过API来进行,如代码清单7-7所示,对每一个State可
以设置清理的策略StateTtlConfig,可以设置的内容如下。
● 过期时间:超过多长时间未访问,视为State过期,类似于缓
存。
● 过期时间更新策略:创建和写时更新、读取和写时更新。
● State的可见性: 未清理可用,超期则不可用。
代码清单7-7 API控制设置State过期策略
根据阿里巴巴的实践经验,过期时间一般为1.5天左右。
7.8.3 状态过期清理
默认情况下,只有在明确读出过期值时才会删除过期值,如通过
调用ValueState#value(),具体如代码清单7-9所示。
代码清单7-9 启用过期清理
注意:此选项不适用于RocksDBStateBackend中的增量检查
点。
通过增量触发器渐进清理State。一种设计是当进行状态访问或者
处理数据时,在回调函数中进行处理。当每次增量清理触发时,遍历
StateBackend中的状态,清理掉过期的,如代码清单7-11所示。
代码清单7-11 增量清理
7.9 总结
8.1 提交流程
根据Flink Client提交作业之后是否可以退出Client进程,提交
模式又可分为Detached模式和Attached模式。Detached模式下,Flink
Client 创 建 完 集 群 之 后 , 可 以 退 出 命 令 行 窗 口 , 集 群 独 立 运 行 。
Attached模式下,Flink Client创建完集群后,不能关闭命令行窗
口,需要与集群之间维持连接,好处是能够感知集群的退出,集群退
出之后有机会做一些资源清理等动作,此处的清理是Flink作业可能占
图8-2 PipelineExecutor类体系
除了上述两种部署模式外,在IDE环境中运行Flink MiniCluster
进行调试的时候,使用LocalExecutor。
1. Session模式
该模式下,作业共享集群资源,作业通过Http协议进行提交。
在Flink 1.10版本中提供了3种会话模式:Yarn会话模式、K8s会
话模式、Standalone。Standalone模式比较特别,Flink安装在物理机
上,不能像在资源集群上一样,可以随时启动一个新集群,所有的作
业共享Standalone集群,本质上就是一种Session模式,所以不支持
Per-Job模式。
在Session模式下,Yarn作业提交使用yarn-session.sh脚本,K8s
作业提交使用kubernetes-session.sh脚本。两者的具体实现不同,但
逻辑是类似的,在启动脚本的时候就会检查是否存在已经启动好的
Flink Session模式集群,如果没有,则启动一个Flink Session模式
集群,然后在PipelineExecutor中,通过Dispatcher提供的Rest接口
8.2 Graph总览
在Flink中可以使用高阶的Table & SQL API进行开发,也可以使
用底层的DataStream和DataSet API进行开发,从应用开发完毕、打包
执行到具体的调度执行,需要经过多层抽象转换。早期,Batch和
Stream 的 图 结 构 和 优 化 方 法 有 很 大 的 区 别 , 所 以 批 处 理 使 用
OptimizedPlan来做Batch相关的优化,使用StreamGraph表达流计算的
逻辑,最终都转换为JobGraph,实现了流批的统一。
综上而言,在当前版本中,不同的Flink API、不同层次Graph的
对应关系略微复杂,如图8-6所示。
8.3 流图
使 用 DataStream API 开 发 的 应 用 程 序 , 首 先 被 转 换 为
Transformation,然后被映射为StreamGraph,该图与具体的执行无
关,核心是表达计算过程的逻辑。WordCount的StreamGraph如图8-7所
示。
StreamGraph 实 际 上 是 在 StreamGraphGenerator 中 生 成 的 , 从
SinkTransformatiom(输出)向前追溯到SourceTransformation。在
遍历过程中一边遍历一边构建StreamGraph,如代码清单8-2所示。
代码清单8-2 StreamGraphGenerator生成StreamGraph
从上边的代码可以看出,对PartitionTransformation的转换没有
生 成 具 体 的 StreamNode 和 StreamEdge , 而 是 通 过
streamGraph.addVirtualPartitionNode ( ) 方 法 添 加 了 一 个 虚 拟 节
点 。 当 数 据 分 区 的 下 游 Transformation 添 加 StreamEdge 时 ( 调 用
streamGraph.addEdge ( ) ) , 会 把 Partitioner 分 区 器 封 装 进 到
StreamEdge中,如代码清单8-6所示。
代码清单8-6 在StreamGraph添加边
图8-9 StreamNode向JobVertex复制的StreamConfig信息
在构建JobEdge的时候,很重要的一点是确定上游JobVertex和下
游 JobVertext 的 数 据 交 换 方 式 。 此 时 根 据 ShuffleMode 来 确 定
ResultPartition的类型(在执行算子写出数据和数据交换中使用),
用前边介绍的Flink Partition来确定JobVertext的连接方式(在生成
ExecutionGraph中使用)。
ShuflleMode 确 定 了 ResultParition , 那 么 就 可 以 确 定 上 游
JobVertext 输 出 的 IntermediateDataSet 的 类 型 了 , 也 就 知 道 该
如上代码清单中所示的WordCount代码,其生成的调用链如代码清
单8-12所示。
后文在数据传递的部分会详细解释这个调用链。
8.5 执行图
ExecutionGraph是调度Flink作业执行的核心数据结构,包含了作
业中所有并行执行的Task的信息、Task之间的关联关系、数据流转关
系。
StreamGraph、JobGraph在Flink客户端中生成,然后提交给Flink
集群。JobGraph到ExecutionGraph的转换在JobMaster中完成,转换过
程中的重要变化如下。
1)加入了并行度的概念,成为真正可调度的图结构。
2 ) 生 成 了 与 JobVertex 对 应 的 ExecutionJobVertex 和
ExecutionVertex , 与 IntermediateDataSet 对 应 的
IntermediateResult和IntermediateResultPartition等,并行将通过
这些类实现。
生成后的ExecutionGraph如图8-10所示。
ExecutionGraph已经可以用于调度任务。从图8-10中可以看到
Flink根据该图生成了一一对应的ExecutionVertex,每个Task对应一
个 ExecutionGraph 的 一 个 ExecutionVertex 。 Task 用 InputGate 、
InputChannel 和 ResultPartition 对 应 了 图 8-10 中 的
IntermediateResult和ExecutionEdge。
8.5.1 ExecutionGraph核心对象
ExecutionGraph 的 核 心 对 象 有 ExecutionJobVertex 、
ExecutionVertex 、 IntermediateResult 、
IntermediateResultPartition、ExecutionEdge和Execution。
1. ExecutionJobVertex
该对象和JobGraph中的JobVertex一一对应。该对象还包含一组
ExecutionVertex,数量与该JobVertex中所包含的StreamNode的并行
度一致,假设StreamNode的并行度为5,那么该ExecutionJobVertex中
也会包含5个ExecutionVertex。
ExecutionJobVertex 用 来 将 一 个 JobVertex 封 装 成
ExecutionJobVertex , 并 依 次 创 建 ExecutionVertex 、 Execution 、
IntermediateResult 和 IntermediateResultPartition , 用 于 丰 富
ExecutionGraph。
在 ExecutionJobVertex 的 构 造 函 数 中 , 首 先 是 依 据 对 应 的
JobVertex的并发度,生成对应个数的ExecutionVertex。其中,一个
ExecutionVertex代表一个ExecutionJobVertex的并发子Task。然后是
将 原 来 JobVertex 的 中 间 结 果 IntermediateDataSet 转 化 为
ExecutionGraph中的IntermediateResult。
2. ExecutionVertex
在从JobGraph向ExecutionGraph转换的核心逻辑中,主要完成的
两件事情:
1 ) 构 造 ExecutionGraph 的 节 点 , 将 JobVertex 封 装 成
ExecutionJobVertex。
2)构造ExecutionEdge,建立ExecutionGraph的节点之间的相互
联系,把节点通过ExecutionEdge连接。
核心过程如代码清单8-14所示。
代码清单8-14 构建ExecutionGraph的核心逻辑
图8-12 多对一等量点对点连接
b)numSources % parallelism != 0,即每个Task消费的上游结
果分区数量不均,如上游有3个结果分区,下游有两个Task,那么一个
Task分配两个结果分区消费,另一个Task分配1个Task消费,如图8-13
所示。
3)一对多连接。parallelism>numSources,即下游的Task数量多
于上游的分区数,此时分为两种情况:
a) parallelism % numSources == 0,即每个结果分区的下游消
费Task数据量相同,如上游有两个结果分区,下游有4个Task,每个结
果分区被两个Task消费,如图8-14所示。
b)parallelism %numSources != 0,即每个结果分区的下游消费
Task数据量不相同,如上游有两个结果分区,下游有3个Task,那么1
图8-13 多对一不等量点对点连接
图8-14 一对多等量点对点连接
图8-16 全连接
9.1 资源抽象
Flink涉及的资源分为两级:集群资源和Flink自身资源。
集群资源管理的是硬件资源,包括CPU、内存、GPU等,由资源管
理框架(yarn、K8s、Mesos)来管理,Flink从资源管理框架申请和释
放资源。
Flink从资源管理框架申请资源容器(Yarn的Container或者K8s的
Pod),1个容器中运行1个TaskManager进程。容器的资源对于Flink来
说也是比较粗粒度的,如单个容器可以使用1个CPU核心,8GB内存,因
为计算类型的不同,一个任务占用一个容器可能无法充分利用资源,
所以单个容器会被多个Flink的任务共享。
图9-1 Flink资源抽象
9.2 资源管理器
资源管理器在Flink中叫作ResourceManager。Flink同时支持不同
的 资 源 集 群 类 型 , ResourceManager 位 于 Flink 和 资 源 管 理 集 群
(Yarn、K8s等)之间,是Flink集群级资源管理的抽象,其主要作用
如下。
1)申请容器启动新的TM,或者为作业申请Slot。
2)处理JobManager和TaskManager的异常退出。
3)缓存TaskManager(即容器),等待一段时间之后再释放掉不
用的容器,避免资源反复地申请释放。
4 ) JobManager 和 TaskManager 的 心 跳 感 知 , 对 JobManager 和
TaskManger的退出进行对应的处理。
在Flink中内置了4种ResourceManager,分别对应于不同的资源管
理框架,资源管理体系如图9-3所示。
(1)YarnResourceManager
图9-3 ResourceManager类体系
(2)KubernetesResourceManager(K8s)
K8s资源管理器,用来对接K8s,在K8s环境中启动和运行Flink集
群。Flink的未来目标是云原生的大数据计算引擎,K8s资源管理器是
未来的重要改进方向。
(3)StandaloneReousrceManager
Flink集群自身的管理器,用于资源确定的部署模式,难以在大规
模的数据中心中共享资源,一般会选择使用Yarn模式,所以在实际生
产环境中使用得不多。
(4)MesosResourceManager
Mesos资源管理器,用来对接Mesos,在Mesos上启动和运行Flink
集群,用得较少。
9.3 Slot管理器
Slot管理器在Flink中叫作SlotManager,是ResourceManager的组
件,从全局角度维护当前有多少TaskManager、每个TaskManager有多
9.4 SlotProvider
SlotProvider接口定义了Slot的请求行为,支持两种请求模式。
● 立即响应模式:Slot请求会立即执行。
● 排队模式:排队等待可用Slot,当资源可用时分配资源。
最 终 的 实 现 在 SchedulerImpl 中 , 其 中 Scheduler 接 口 增 加 了
SlotSelectionStrategy。
9.5 Slot选择策略
Flink在决定Task运行在哪个TaskManager上时,会根据策略进行
选择,选择Slot的时候有不同的选择策略,SlotSelectionStrategy就
是策略定义的接口,其类体系如图9-4所示。
9.6 Slot资源池
Slot资源池在Flink中叫作SlotPool,是JobMaster中记录当前作
业 从 TaskManager 获 取 的 Slot 的 集 合 。 JobMaster 的 调 度 器 首 先 从
SlotPool中获取Slot来调度任务,SlotPool在没有足够的Slot资源执
行 作 业 的 时 候 , 首 先 会 尝 试 从 ResourceManager 中 获 取 资 源 , 如 果
9.7 Slot共享
每个TaskManager都是一个Java进程,TaskManager为每个Task分
配独立的执行线程,一个TaskManager中可能执行一个或者多个Task。
TaskManager 通 过 Slot 来 控 制 ( 一 个 TaskManager 至 少 有 一 个 Slot )
TaskManager能够接收多少个Task。
Slot表示TaskManager拥有资源的一个固定大小的子集。假如一个
TaskManager 有 3 个 Slot, 那 么 它 会 将 其 管 理 的 内 存 分 成 3 份 给 各 个
Slot,在没有Slot共享的情况下,并行度为2的作业部署之后,Slot与
Task的分配关系如图9-5所示。Slot的资源化意味着一个作业的Task将
不需要跟来自其他作业的Task竞争内存、CPU等计算资源。
图9-5 无Slot共享并行度2的Task与Slot分配关系
图9-7 TaskSlot类体系
SingleTaskSlot表示运行单个Task的Slot,每个SingleTaskSlot
对应于一个LogicalSlot。MultiTaskSlot中包含了一组TaskSlot。
借助SingleTaskSlot和MultiTaskSlot,Flink实现了一般Slot共
享和CoLocationGroup共享,两者的数据结构如图9-8所示。
9.8 总结
本章主要介绍了Flink中的资源抽象。资源抽象分为两个层面:集
群 资 源 抽 象 和 Flink 自 身 资 源 抽 象 。 集 群 级 使 用 资 源 管 理 器
( ResourceManager ) 来 解 耦 Flink 和 资 源 管 理 集 群 ( Yarn 、 K8s 、
Mesos等),Flink自身的资源使用Slot精细地划分其计算资源,作业
10.1 调度
调度器是Flink作业执行的核心组件,管理作业执行的所有相关过
程,包括JobGraph到ExecutionGraph的转换、作业生命周期管理(作
业的发布、取消、停止)、作业的Task生命周期管理(Task的发布、
取消、停止)、资源申请与释放、作业和Task的Failover等。
调度器的类体系如图10-1所示。
图10-1 调度器类体系
在调度相关的体系中有几个非常重要的组件。
10.2 执行模式
流计算作业的数据执行模式毫无疑问是推送的模式,但是批处理
作业的情况则比较复杂,Flink在底层统一批流作业的执行,执行模式
指定批处理程序在数据交换方面的执行方式在Flink中有两类执行模
1. 流水线模式
即Pipelined,此模式以流水线方式(包括Shuffle和广播数据)
执行作业,但流水线可能会出现死锁的数据交换除外。如果可能会出
现数据交换死锁,则数据交换以Batch方式执行。
当数据流被多个下游分支消费处理,处理后的结果再进行Join
时,如果以Pipelined模式运行,则可能出现数据交换死锁。
代码清单10-3 数据交换死锁代码示例
2. 强制流水线模式
即 Pipelined_Forced , 此 模 式 以 流 水 线 方 式 ( 包 括 shuffle 和
broadcast数据)执行作业,即便流水线可能会出现死锁的数据交换时
仍然执行。
一般情况下,Pipelined模式是优先选择,确保不会出现数据死锁
的情况下才会使用Pipelined_Forced模式。
3.流水线优先模式
10.3 数据交换模式
执行模式的不同决定了数据交换行为的不同,为了能够实现不同
的数据交换行为,Flink在ResultPartitionType中定义了4种类型的数
据分区模式,与执行模式一起完成批流在数据交换层面的统一,如代
码清单10-4所示。
代码清单10-4 ResultPartitionType的数据交换模式定义
1. BLOCKING
BLOCKING类型的数据分区会等待数据完全处理完毕,然后才会交
给下游进行处理,在上游处理完毕之前,不会与下游进行数据交换。
该类型的数据分区可以被多次消费,也可以并发消费。被消费完毕之
后不会自动释放,而是等待调度器来判断该数据分区无人再消费之
后,由调度器发出销毁指令。
10.4 作业生命周期
Flink作业的生命周期状态非常多,状态之间的变化关系非常复
杂,所以Flink作业生命周期的管理参考了有限状态机。
有限状态机又叫作Finite State Machine(FSM),是表示有限个
状态及在这些状态之间的转移和动作等行为的数学模型,在计算机领
域有着广泛的应用。
状态机可归纳为4个要素,即现态、条件、动作、次态。这样归纳
主要是出于对状态机的内在因果关系的考虑。“现态”和“条件”是
因,“动作”和“次态”是果,详解如下。
10.5 关键组件
10.5.1 JobMaster
在新版本的Flink中已经没有JobManager这个类对象,取而代之的
是JobMaster。而JobManagerRunner则依然延续旧的名称,用于启动
JobMaster , 提 供 作 业 级 别 的 leader 选 举 、 处 理 异 常 。 旧 版 本 中
JobManager的作业调度、管理等逻辑现在由JobMaster实现。在之前的
实 现 中 所 有 的 作 业 共 享 JobManager , 引 入 JobMaster 之 后 , 一 个
JobMaster 对 应 于 一 个 作 业 , 一 个 JobManager 中 可 以 有 多 个
JobMaster,这样的实现能够更好地隔离作业,减少相互影响的可能
性。
现在提到JobManager的时候,其实是说Flink的JobManager角色,
是一个独立运行的进程,该进程中包含了一系列的服务,如
Dispatcher、ResourceManager等。
JobMaster负责单个作业的管理,提供了对作业的管理行为,允许
通过外部的命令干预作业的运行,如提交、取消等。同时JobMaster也
维护了整个作业及其Task的状态,对外提供对作业状态的查询功能。
JobMaster负责接收JobGraph,并将其转换为ExecutionGraph,启动调
度器执行ExecutionGraph。
1.调度执行和管理
将 JobGraph 转 化 为 ExecutionGraph , 调 度 Task 的 执 行 , 并 处 理
Task的异常,进行作业恢复或者中止。根据TaskManager汇报的状态维
护ExecutionGraph。
图10-6 StreamTask体系
StreamTask的实现分为几类:
(1)TwoInputStreamTask
两个输入的StreamTask,对应于TowInputStreamOperator。
(2)OneInputStreamTask
图10-7 StreamTask生命周期
1.初始化阶段
10.6 作业启动
Flink 作 业 被 提 交 之 后 , JobManager 中 会 为 每 个 作 业 启 动 一 个
JobMaster,并将剩余的工作交给JobMaster。JobMaster负责整个作业
生命周期中的资源申请释放、调度、容错等细节。
在 作 业 启 动 过 程 中 , JobMaster 会 与 ResourceManager 、
TaskManager频繁交互,经过一系列复杂的过程之后,作业才真正在
Flink集群中运行起来,进入执行阶段,开始读取、处理、写出数据的
过程。
10.6.1 JobMaster启动作业
作 业 启 动 涉 及 JobMaster 和 TaskManager 两 个 位 于 不 同 进 程 的 组
件。在JobMaster中完成作业图的转换,为作业申请资源、分配Slot,
将作业的Task交给TaskManager,TaskManager初始化和启动Task。通
过JobMaster管理作业的取消、检查点保存等,Task在执行过程中持续
地向JobMaster汇报自身的状态,以便监控和异常时重启作业或者
Task,总体如图10-8所示。
根据调度器,启动不同调度器的调度。批流调度选择在
DefaultScheduler中,通过多态的方式交给调度策略执行具体的调
度,如代码清单10-6所示。
代码清单10-6 多态调度策略
DefaultScheduler中根据调度策略,选择不同的调度方法,对于
流作业启动而言,最终调用方法如代码清单10-8所示。
流 计 算 作 业 中 , 需 要 一 次 性 部 署 所 有 Task , 所 以 会 对 所 有 的
Execution异步获取Slot,申请到所有需要的Slot之后,经过一系列的
过程,最终调用Execution#deploy进行实际的部署,实际上就是将
Task部署相关的信息通过TaskMangerGateway交给TaskManager,如代
码清单10-9所示。
代码清单10-9 部署所有的Task
至此Task启动完毕。下节介绍Task启动过程中,启动StreamTask
的相关细节。
2. StreamTask启动
StreamTask是算子的执行容器。在JobGraph中将算子连接在一起
进行了优化,在执行层面上对应的是OperatorChain。在Task启动前,
无论是单个算子还是连接在一起的一组算子,都会首先被构造成
OperatorChain , 构 造 OperatorChain 的 过 程 中 , 包 含 了 算 子 的 实 例
化,同时也构造了算子的输出(Output)。
因为不符合链接条件,所以Source算子和FlatMap算子都是单个的
算子,单个算子构成的OperatorChain如图10-10所示。
图10-11 KeyedAgg和Sink组成的多算子OperatorChain示意
两 个 或 以 上 算 子 构 成 的 OperatorChain , 算 子 之 间 包 含 了 两 层
Output,其中CountingOutput用来统计上游算子(KeyedAgg)输出的
数 据 元 素 个 数 。 ChaingOutput 提 供 了 Watermark 的 统 计 和 下 游 算 子
(Sink)的输入数据元素个数。
Task启动完毕之后,就进入了作业执行的阶段。作业执行的详细
内容见第11章内容讲解。
10.7 作业停止
当作业执行完毕(批处理作业)、执行失败无法恢复时就会进入
停止状态。同时在一些场景中,也需要将作业手动停止,如集群升
级、作业升级、作业迁移等,此时作业都会进入停止状态。与作业的
启动相比,作业的停止要简单许多,主要是资源的清理和释放。
10.8 作业失败调度
一个分布式系统涉及上千台服务器,服务上运行着成千上万个进
程,同时还要依赖网络设备进行通信。角色越多,整个系统出错的概
率就越高,在如此大规模的分布式系统中,每天都在发生着硬件故
障、软件故障,网络波动等异常、故障,一个可靠的分布式系统必须
能够应对各种异常对系统稳定性带来的冲击。
Flink作为低延迟的分布式计算引擎,在流计算中引入了分布式快
照容错机制,以满足低延迟和高吞吐的要求。Flink所使用的容错机制
使用分布式快照保存作业状态,与Flink的作业恢复机制相结合,确保
数据不丢失、不重复处理。发生错误时,Flink作业能够根据重启策略
自动从最近一次成功的快照中恢复状态。
Flink对作业失败的原因做了归纳定义,有如下4种类型。
● NonRecoverableError: 不可恢复的错误。此类错误意味着即
便是重启也无法恢复作业到正常状态,一旦发生此类错误,则作业执
行失败,直接退出作业执行。
● PartitionDataMissingError:分区数据不可访问错误。下游
Task无法读取上游Task的产生的数据,需要重启上游的Task。
● EnvironmentError: 环境的错误,问题本身来自机器硬件、外
部服务等。这种错误需要在调度策略上进行改进,如使用黑名单机
制,排除有问题的机器、服务,避免将失败的Task重新调度到这些机
器上。
● RecoverableError: 可恢复的错误。
目前Flink有两套调度策略:默认的作业失败调度和遗留的作业失
败调度。默认的作业失败调度是Flip-1中提出Failover的改进调度策
图10-12 Flip1失败调度策略体系
● RestartAllStarttegy : 若 Task 发 生 异 常 , 则 重 启 所 有 的
Task,恢复成本高,但其是恢复作业一致性的最安全策略。
● RestartPipelinedRegionStrategy:分区恢复策略,若Task发
生异常,则重启该分区的所有Task,恢复成本低,实现逻辑复杂。
Flip1引入的作业Failover机制,将整个作业的物理执行拓扑Task
DAG切分为不同的FailoverRegion。FailoverRegion本质上是一组有相
互关系的Task,失败恢复的时候按照FailoverRegion回溯,重新启动
需要启动的Task,其过程如图10-13所示。
1. FailoverRegion切分
实现细粒度的Failover,首先需要对作业进行FailoverRegion的
切分,切分策略如下。
2.作业恢复时的FailoverRegion回溯
下面使用一个示例来说明作业恢复时如何回溯FailoverRegion找
到需要重启的Task。如图10-15所示,图中是一个作业的物理执行拓扑
示 例 , 正 方 形 和 长 方 形 表 示 一 个 FailoverRegion , 圆 形 表 示 一 个
Task。
图10-14 FailoverRegion切分示例
图10-15 FailoverRegion回溯示例
假 设 Task C1 由 于 错 误 执 行 失 败 , 那 么 需 要 重 新 调 度 执 行 的
FailoverRegion分析步骤如下。
(1)Task所在FailoverRegion整体恢复
由 于 FailoverRegion 内 的 Task 被 视 作 一 个 整 体 , 因 此
FailoverRegion中的任何一个Task执行失败,整个FailoverRegion中
根据Flink抽象的错误类型判断Task是否可以恢复。如果Task可恢
复则恢复(可恢复的异常类型在前文提到了);如果不可恢复则整个
作业失败,尝试重启整个作业。如代码清单10-18所示。
代码清单10-18 重启或者失败选择
重启Task的过程中需要在ExecutionGraph中重置ExecutionVertex
状态,然后调度Task的重新执行,执行过程与作业发布类似,区别在
如果Task是不可重启的,那么就需要走重启作业的流程。
10.8.2 遗留的作业失败调度
LegacyScheduler是遗留的调度器,未来以NG调度器默认调度器。
该调度器分为Task Failover和作业Failover。
在Task执行错误时,首先会进行Task Failover,如果Task错误无
法恢复到正常状态,最终触发了Full Restart,此时作业Restart策略
将会控制是否需要恢复作业。Flink提供了3种作业具体的重启策略:
10.9 组件容错
对于分布式系统来说,守护进程的容错是基本要求而且已经比较
成熟,基本包括故障检测和故障恢复两个部分:故障检测通常通过心
跳的方式来实现,心跳可以在内部组件间实现或者依赖于zookeeper等
外部服务;故障恢复则通常要求将状态持久化到外部存储,然后在故
障出现时用于初始化新的进程。
以 最 为 常 用 的 YARN 的 部 署 模 式 为 例 , Flink 的 关 键 守 护 进 程 有
JobManager和TaskManager两个,其中JobManager的主要职责——协调
资源和管理作业——的执行分别由ResourceManager和JobMaster两个
守护线程承担。
在容错方面,3个角色两两之间相互发送心跳来进行共同的故障检
测 。 此 外 在 HA 场 景 下 , ResourceManager 和 JobMaster 都 会 注 册 到
ZooKeeper节点上以实现Leader锁。
10.9.1 容错设计
Flink 中 的 Dispatcher 、 JobMaster 、 ResourceManager 、
TaskManager都是非常重要的组件,所以需要进行高可用设计,使组件
具备容错能力,防止单个组件故障导致整个集群宕机。
在生产环境中,推荐使用HA模式部署Flink,Flink中提供了两种
HA模式:基于ZooKeeper的HA和Standalone HA,如图10-16所示。
10.9.2 HA服务
容错的基本原理是提供两个或以上的相同组件,选择其中一个作
为Leader,其他的作为备选,当Leader出现问题的时候,各备选组件
能够感知到Leader宕机,重新选举,并通知各相关组件新的Leader。
1. Leader服务
对于JobMaster和ResourceManager,Leader选举服务和Leader变
更服务是两项基本服务。前者用于切换到新的Leader,后者用于通知
相关的组件Leader的变化,进行相应的处理。
(1)Leader选举服务
Leader选举最重要的行为是为竞争者提供选举和确认Leader。在
Flink中有3种Leader选举服务的实现,其类体系如图10-17所示。
图10-18 Leader变更通知类体系
LeaderRetrievalListener用来通知选举了新的Leader,在选举过
程中可能顺利选举出新的Leader,也可能因为内部或者外部异常,导
致无法选举出新的Leader,此时也需要通知各相关组件。对于无法处
理 的 故 障 , 无 法 进 行 恢 复 , 作 业 进 入 停 止 状 态 。
LeaderRetrievalListener通知行为的定义如代码清单10-21所示。
代码清单10-21 Leader选举通知
(2)心跳管理器(HeartbeatManager)
通用的心跳管理器用来启动或停止监视HeartbeatTarget,并报告
该 目 标 心 跳 超 时 事 件 。 通 过 monitorTarget 来 传 递 并 监 控
HeartbeatTarget,这个方法可以看成整个服务的输入,告诉心跳服务
去管理哪些目标。心跳管理器的行为定义如代码清单10-23所示。
图10-19 HA服务类体系
(1)ZooKeeperHAServices
基于ZooKeeper高可用服务实现,使用ZooKeeper作为集群信息的
存储,能够实现真正的高可用。集群信息在Zookeeper的信息存储结构
如图10-20所示。
10.10 总结
本章主要介绍了Flink作业的调度模式以及调度执行的过程,在调
度执行过程中作业和Task有各自的生命周期转换。在执行过程中,作
业可能因为某个Task执行异常或者Flink集群组件故障导致执行失败,
需要对作业进行Failover。同时,各个集群组件也会出现故障,为了
应对此类问题,Flink实现了HA服务。
11.1 作业执行图
经过Flink的多层Graph转换之后,作业进入调度阶段,开始分发
任务执行,最终会在集群中形成如图11-1所示的物理拓扑结构,借用
前边的Graph的概念,此处称为物理执行图。
当一个作业执行起来之后,其拓扑关系如图11-1所示,但是在作
业真正执行之前需要经历作业调度和启动的过程。
11.2 核心对象
11.2.1 输入处理器
输 入 处 理 器 在 Flink 中 叫 作 StreamInputProcessor , 是 对
StreamTask中读取数据行为的抽象,在其实现中要完成数据的读取、
处理、输出给下游的过程。对应于单流输入和双流输入,
StreamInputProcessor也提供了两种实现,如图11-2所示。
图11-2 StreamInputProcessor类体系
● StreamOneInputProcessor:用在OneInputStreamTask中,只
有1个上游输入。
● StreamTwoInputProcessor:用在TwoInputStreamTask中,有2
个上游输入。
以 StreamOneInputProcessor 为 例 , 核 心 方 法 是
StreamOneInputProcessor#processInput ( ) , 该 方 法 中 调 用
StreamTaskInput#emit 触 发 数 据 的 读 取 , 将 数 据 反 序 列 化 为
StreamRecord , 交 给 StreamTaskNetworkOutput , 由 其 触 发
StreamOperator ( 算 子 ) 的 处 理 , 最 终 触 发 UDF 的
processElemen(),执行用户在DataStream API中编写用户逻辑,处
理数据,然后交给下游,整体过程如图11-3所示。
11.2.2 Task输入
Task输入在Flink中叫作StreamTaskInput,是StreamTask的数据
输入的抽象,其类体系如图11-4所示。
对于Flink中的StreamTask而言,数据读取的行为有两种:
1 ) StreamTaskNetworkInput 负 责 从 上 游 Task 获 取 数 据 , 使 用
InputGate作为底层读取数据。
2)StreamTaskSourceInput负责从外部数据源获取数据,本质上
是使用SourceFunction读取数据,交给下游的Task。
11.2.3 Task输出
Task 输 出 在 Flink 中 叫 作 StreamTaskNetworkOutput , 是
StreamTask的数据输出的抽象,其类体系如图11-5所示。
图11-5 StreamTaskNetworkOutput类体系
虽 然 从 字 面 含 义 上 看 起 来 , StreamTaskNetworkOutput 跟
StreamTaskNetworkInput 是 对 应 的 , 但 是 两 者 职 责 差 别 很 大 ,
StreamTaskNetworkOutput只是负责将数据交给算子来进行处理,实际
的数据写出是在算子层面上执行的。
同样,StreamTaskNetworkOutput也有对应于单流输入和双流输入
的 两 种 实 现 , 作 为 私 有 内 部 类 定 义 在 OneInputStreamTask 和
StreamTwoInputProcessor中。
11.2.4 结果分区
结果分区在Flink中叫作ResultPartition,用来表示作业的单个
Task 产 生 的 数 据 。 ResultPartition 是 运 行 时 的 实 体 , 与
ExecutionGraph 中 的 中 间 结 果 分 区 对 应
( IntermediateResultPartition ) , 一 个 ResultPartition 是 一 组
Buffer 实 例 , ResultPartition 由 ResultSubPartition 组 成 ,
ResultSubPartition用来进一步将ResultPartition进行切分,切分成
多少个ResultSubPartition取决于直接下游子任务的并行度和数据分
发模式。
图11-6 ResultPartition结果分区类体系
前 文 提 到 过 ResultPartition 有 4 种 类 型 : Blocking 、
Blocking_persisted、Pipeline_bounded、Pipelined。对于流上的计
算而言,ResultPartition在作业的执行过程中会一直存在,但是对于
批处理而言,上游Task输出ResultPartition,下游Task消费上游的
ResultPartition,消费完毕之后,上游的ResultPartition就没有什
么 用 了 , 需 要 进 行 资 源 回 收 , 所 以 Flink 增 加 了 新 的
ReleaseOnConsumptionResultPartition。
11.2.5 结果子分区
结果子分区在Flink中叫作ResultSubPartition,结果子分区是结
果分区的一部分,负责存储实际的Buffer。
ResultPartition是一个Task的输出,上游Task跟下游Task之间的
数 据 交 换 经 常 是 一 对 多 的 关 系 , 所 以 Flink 在 输 出 端 将
图11-7 ResultPartition结果子分区类体系
从 图 中 可 以 看 到 , 结 果 子 分 区 有 PipelinedSubPartition 和
BoundedBlockingSubPartition两种,分别对应于流的数据消费模式和
批处理中的数据消费模式。
1. PipelinedSubPartition
PipelinedSubPartition是纯内存型的结果子分区,只能被消费1
次。
当向PipelinedSubPartition中添加1个完成的BufferConsumer或
者 添 加 下 一 个 BufferCon sumer 时 ( 这 种 情 况 下 , 默 认 前 一 个
BufferConsumer是完成的),会通知PipelinedSubPartitionView新数
据到达,可以消费了。
2. BoundedBlockingSubPartition
用作对批处理Task的计算结果的数据存储,其行为是阻塞式的,
需要等待上游所有的数据处理完毕,然后下游才开始消费数据,可以
消费1次或者多次。此种ResultSubPartition有多种存储形式,可以保
存在文件中或者内存映射文件中等。
注意:BoundedBlockingSubPartition不是线程安全的,Flink网
络栈是单线程模型,添加、刷新、完成都由单个Writer完成,如果在
写入阶段需要release,则同样由该Writer的线程负责执行,所以能确
保安全。在实现中,支持多个并发的Reader,但是需要确保Reader是
单线程的。
图11-8 BoundedData类体系
(1)FileChannelBoundedData
使用Java NIO的FileChannel写入数据和读取文件。
(2)FileChannelMemoryMappedBoundedData
使用FileChannel写入数据到文件,使用内存映射文件读取数据。
(3)MemoryMappedBoundedData
使用内存映射文件写入、读取,全部是内存操作。
内存映射文件(Memory-mapped File)是将一段虚拟内存逐字节
映射于一个文件,使得应用程序处理文件如同访问主内存(但在真正
使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操
作),内存映射文件与磁盘的真正交互由操作系统负责。
图11-9 InputGate类体系
图11-10 JobGraph简要示例
Flink作业在集群中并行执行时,其并行状态下的逻辑关系如图
11-11所示,中间结果集会按照并行度切分为与并行度个数相同的结果
分区,每个结果分区又进一步切分为一个或者多个结果子分区。
图11-11 InputGate与ResultPartition的对应关系
图11-11中,作业的并行度为2,有两个map子任务分别产生数据,
生 成 两 个 ResultPartition ( ResultSubPartition 1 和
ResultSubPartition 2 ) , 每 个 ResultPartition 又 被 切 分 为 两 个
ResultSubPartition(ResultSubPartition的数量与下游Reduce任务
的子任务个数相同)。每个Reduce的子任务都有一个InputGate,负责
图11-12 InputChannel类体系
(1)LocalInputChannel
对应于本地结果子分区的输入通道,用来在本地进程内不同线程
之间的数据交换。
LocalInputChannel 实 际 调 用
SingleInputGate.notifyChannelNonEmpty ( InputChannel
channel),这个方法调用inputChannelsWithData.notifyAll , 唤醒
阻塞在inputChannelsWithData对象实例的所有线程。上文提到的阻塞
在CheckPointBarrierHandler.getNextNonBlocked()方法的线程也
会被唤醒,返回数据。
(2)RemoteInputChannel
对应于远程的结果子分区的输入通道,用来表示跨网络的数据交
换,底层基于Netty。
(3)UnknownInputChannel
一种用于占位目的的输入通道,需要占位通道是因为暂未确定相
对于Task生产者的位置,在确定上游Task位置之后,如果位于不同的
TaskManager 则 替 换 为 RemoteInputChannel , 如 果 位 于 相 同 的
TaskManager则转换为LocalInputChannel。
至此,读取到数据,对于数据记录(StreamRecord),会在算子
中包装用户的业务逻辑,即使用DataStream编写的UDF,如代码清单
至此,进入到算子内部,由算子去执行用户编写的业务逻辑,以
WordCount示例中的flatMap处理,调用flatMap方法执行用户编写的具
体业务逻辑,如代码清单11-6所示。
代码清单11-6 StreamFlatMap算子触发用户UDF
算子真正执行的是WordCount示例的Tokenizer分词器进行分词,
然后通过Output将数据输出给下游的算子(如果算子在OperatorChain
中且不是最后一个)或者下游Task,如代码清单11-7所示。
代码清单11-7 WordCount示例中Tokenizer
双 流 输 入 从 上 游 两 个 算 子 中 接 收 到 两 个 Watermark ,
inputWatermark1表示第一个输入流的Watermark,inputWatermark2表
示 第 2 个 输 入 流 的 Watermark , 选 择 其 中 较 小 的 那 一 个
Min(inputWatermrk1,inputWatermark2)作为当前的Watermark。之
后的处理逻辑与单流输入一致,如代码清单11-9所示。
代码清单11-9 双流输入Watermark处理
11.4 总结
Flink作业真正执行起来之后,会在物理上构成Task相互连接的
DAG,在执行过程中上游Task结果写入结果分区,结果分区又分成结果
子 分 区 , 下 游 的 Task 通 过 InputGate 与 上 游 建 立 数 据 传 输 通 道 ,
InputGate中的InputChannel对应于结果子分区,将数据交给Task执
行。
Task 执 行 的 时 候 , 根 据 数 据 的 不 同 类 型 ( StreamRecord 、
Watermark、LatencyMarker)进行不同的处理逻辑,处理完后再交给
下游的Task。
12.1 数据传递模式
在分布式计算过程中,不同计算节点之间传送数据,一般有PULL
模式和PUSH模式两种选择。
1. Flink Batch模式
Batch的计算模型采用PULL模式,与Spark类似,将计算过程分成
多个阶段,上游完全计算完毕之后,下游从上游拉取数据开始下一阶
段计算,直到最终所有的阶段都计算完毕,输出结果,Batch Job结束
退出。
2. Flink Stream模式
Stream的计算模型采用的是PUSH模式,上游主动向下游推送数
据,上下游之间采用生产者-消费者模式,下游收到数据触发计算,没
有数据则进入等待状态。PUSH模式的数据处理过程也叫作Pipeline,
提到Pipeline或者流水线的时候,一般是指PUSH模式的数据处理过
程。
总结来看,PUSH和PULL的模式的特性见表12-1。
表12-1 PUSH和PULL模式的特性对比
图12-2 ChannelSelectorWriter选路和数据输出
2. 广播
图12-3 BroadcastRecordWriter广播数据输出
代码清单12-1 单播数据写入
图12-4 序列化器类体系
SpanningRecordSerializer是一种支持跨内存段的序列化器,其
实现借助于中间缓冲区来缓存序列化后的数据,然后再往真正的目标
Buffer里写,在写的时候会维护两个“指针”:
1)一个是表示目标Buffer内存段长度的limit。
2)一个是表示其当前写入位置的position。
因为一个Buffer对应着一个内存段,当将数据序列化并存入内存
段时,其空间可能有剩余也可能不够。因此,RecordSerializer定义
了一个表示序列化结果的SerializationResult枚举。
在序列化数据写入内存段的过程中,存在3种可能的结果:
● PARTIAL_RECORD_MEMORY_SEGMENT_FULL:内存段已满但记录的
数据只写入了一部分,没有完全写完,需要申请新的内存段继续写
入。
● FULL_RECORD_MEMORY_SEGMENT_FULL:内存段写满,记录的数
据已全部写入。
● FULL_RECORD:记录的数据全部写入,但内存段并没有满。
序列化过程中遇到以上3种情况的处理逻辑如代码清单12-2所示。
代码清单12-2 数据序列化过程
图12-5 RecordDeserializer类体系
跟RecordSerializer类似,考虑到记录的数据大小以及Buffer对
应的内存段的容量大小,在反序列化时也存在不同的反序列化结果,
以枚举DeserializationResult表示。
● PARTIAL_RECORD: 表示记录并未完全被读取,但缓冲中的数
据已被消费完成。
图12-6 结果子分区视图类体系
● PipelinedSubPartitionView : 用 来 读 取
PipelinedSubPartition中的数据。
● BoundedBlockingSubPartitionReader : 用 来 读 取
BoundedBlockingSubPartition中的数据。
12.2.5 数据输出
数据输出(Output)是算子向下游传递的数据抽象,定义了向下
游 发 送 StreamRecord 、 Watermark 、 LetencyMark 的 行 为 , 对 于
StreamRecord,多了一个SideOutput的行为定义。
图12-7 Output数据输出类体系
1. WatermarkGaugeExposingOutput
此接口定义了统计Watermark监控指标计算行为,将最后一次发送
给下游的Watermark作为其指标值。其实现类负责计算指标值,在
Flink Web UI中,通过可视化StreamGraph看到的Watermark监控信息
即来自于此。
2. RecordWriterOutput
包 装 了 RecordWriter , 使 用 RecordWriter 把 数 据 交 给 数 据 交 换
层。RecordWriter主要用来在线程间、网络间实现数据序列化、写
入。
3. ChainingOutput & CopyingChainingOutput
这两个类是在OperatorChain内部的算子之间传递数据用的,并不
会 有 序 列 化 的 过 程 。 直 接 在 Output 中 调 用 下 游 算 子 的
processElement()方法。在同一个线程内的算子直接传递数据,跟
普通的Java方法调用一样,这样就直接省略了线程间的数据传送和网
络间的数据传送的开销。
4. DirectedOutput & CopyingDirectedOutput
12.3 数据传递
在Flink中,数据处理的业务逻辑位于UDF的processElement()
方法中,算子调用UDF处理数据完毕之后,需要将数据交给下一个算
子。Flink的算子使用Collector接口进行数据传递。
Flink中有3种数据传递的方式:
● 本地线程内的数据交换。
● 本地线程之间的数据传递。
● 跨网络的数据交换。
下边分别对这3种数据交换方式进行详细介绍。
12.3.1 本地线程内的数据传递
本地线程内的数据交换是最简单、效率最高的传递形式,其本质
是属于同一个OperatorChain的算子之间的数据传递,如图12-8所示。
图12-9 本地Task之间的数据交换
4 ) 唤 醒 FlatMap 所 在 的 线 程 ( 通 过
inputChannelWithData.notifyAll()方法唤醒)。
5)FlatMap线程首先调用LocalInputChannel从LocalBuffer中读
取数据,然后进行数据的反序列化。
FlatMap将反序列化之后的数据交给算子中的用户代码进行业务处
理。
12.3.3 跨网络的数据传递
跨网络数据传递,即运行在不同TaskManager JVM中的Task之间的
数据传递,与本地线程间的数据交换类似。不同点在于,当没有
Buffer 可 以 消 费 时 , 会 通 过 PartitionartitionRequestClient 向
FlatMap Task 所 在 的 进 程 发 起 RPC 请 求 , 远 程 的
PartitionRequestServerHandler 接 收 到 请 求 后 , 读 取
ResultPartition管理的Buffer,并返回给Client。
跨网络的FlatMap算子和KeyedAgg/Sink算子数据交换如图12-10所
示。
12.4 数据传递过程
从总体上来说,数据在Task之间传递分为如下几个大的步骤。
1)数据在本算子处理完后,交给RecordWriter。每条记录都要选
择下游节点,所以要经过ChannelSelector,找到对应的结果子分区。
2)每个结果子分区都有一个独有的序列化器(避免多线程竞
争),把这条数据记录序列化为二进制数据。
3)数据被写入结果分区下的各个子分区中,此时该数据已经存入
DirectBuffer(MemorySegment)。
4)单独的线程控制数据的flush速度,一旦触发flush,则通过
Netty的nio通道向对端写入。
5)对端的Netty Client接收到数据,解码出来,把数据复制到
Buffer中,然后通知InputChannel。
6)有可用的数据时,下游算子从阻塞醒来,从InputChannel取出
Buffer,再反序列化成数据记录,交给算子执行用户代码(UDF)。
12.4.1 数据读取
ResultSubPartitionView接收到数据可用通知之后,有两类对象
会接收到该通知,如图12-11所示。
LocalInputChannel 其 实 是 本 地 JVM 线 程 之 间 的 数 据 传 递 ,
CreditBasedSequenceNumberingViewReader用来对本机跨JVM或者跨网
图12-11 数据可用通知类体系
其 基 本 过 程 为 :
StreamTask#processInput→StreamOneInputStreamOperator#process
Input→StreamTaskNetworkInput#emitNext→SpillingAdataptiveSpa
nningRecordDeserializer。
从NetworkInput中读取数据反序列化为StreamRecord,然后交给
DataOutput向下游发送。如代码清单12-3所示。
代码清单12-3 StreamTaskNetworkInput读取数据元素
2. BoundedBlockingSubPartition
如果是立即刷新,则相当于1条记录向文件中写入1次,否则认为
是延迟刷新,每隔一定时间周期将该时间周期内的数据进行批量处
理,默认是100ms刷新一次数据刷新的时候根据ResultPartition中
图12-12 Task之间的逻辑连接
Flink的Task在物理层面上是基于TCP连接,且TaskManager上的所
有Task共享一个TCP连接,所以Flink 1.5版本之前的流控机制是基于
连接的。
前面提到过在同一个Flink作业中不同的Task可以通过Slot共享发
布到同一个TaskManager上执行,同时根据Slot选择策略,同一个作业
的不同Task也可能会被分配到同一个TaskManager上执行。
以图12-12中Task之间的逻辑连接为例,假设其并行度为4,部署
中 有 两 个 TaskManager , 各 有 两 个 Slot 。 TaskManager 1 执 行 子 任 务
A.1、A.2、B.1和B.2,TaskManager 2执行子任务A.3、A.4、B.3和
B.4。在任务A和任务B之间的Shuffle类型连接(如通过keyBy())
中,在每个TaskManager上有2×4个逻辑连接,其中一些是本地的,一
些是远程的,见表12-2。
表12-2 Task之间的逻辑连接关系
图12-13 Task之间的物理连接
TaskManager之间是通过网络进行通信的,无论两个TaskManager
是否位于同一个物理服务器上。Task A1、A2在Flink网络栈上获得各
自的TCP通道,与下游位于相同TaskManager上的B.3、B.4建立连接,
A1、A2通过多路复用,共享TaskManager 1与TaskManager 2之间的TCP
连接,节省资源。
每个Task产生各自的结果分区(ResultParition),每个结果分
区拆分成多个单独的结果子分区(ResultSubPartition),与下游的
图12-14 基于连接的流控
12.6 总结
Flink 进 行 数 据 交 换 有 3 种 模 式 : 本 地 线 程 内 的 数 据 交 换 , 即
OperatorChain内算子之间的数据交换;本地线程间的数据交换;跨网
络的数据交换。不同交换模式的工作流程不同。
Flink是基于推送的数据传输模型,在网络层面的数据交换必须使
用流控机制确保集群的稳定并实现高效的数据处理,所以在现在的版
本中使用了基于信用的流控机制。
13.1 容错保证语义
按照数据处理保证的可靠程度,从低到高包含4个不同的层次。
1. 最多一次
最低级别的数据处理保证,即At-Most-Once,数据不重复处理,
但可能会丢失,在Flink中,不开启检查点就是最多一次的处理保证。
2. 最少一次
13.2 检查点与保存点
检查点在Flink中叫作Checkpoint,是Flink实现应用容错的核心
机制,根据配置周期性通知Stream中各个算子的状态来生成检查点快
照,从而将这些状态数据定期持久化存储下来,Flink程序一旦意外崩
溃,重新运行程序时可以有选择地从这些快照进行恢复,将应用恢复
到最后一次快照的状态,从此刻开始重新执行,避免数据的丢失、重
复。
默认情况下,如果设置了检查点选项,则Flink只保留最近成功生
成的一个检查点,而当Flink程序失败时,可以从最近的这个检查点来
进行恢复。但是,如果希望保留多个检查点,并能够根据实际需要选
择其中一个进行恢复,会更加灵活。
默认情况下,检查点不会被保留,取消程序时即会删除它们,但
是可以通过配置保留定期检查点,根据配置,当作业失败或者取消的
时候,不会自动清除这些保留的检查点。
如果想保留检查点,那么Flink也设计了相关实现,可选项如下。
13.3 作业恢复
Flink提供了应用自动容错机制,可以减少人为干预,降低运维复
杂度。同时为了提高灵活度,也提供了手动恢复。Flink提供了如下两
种手动作业恢复方式。
(1)外部检查点
检查点完成时,在用户给定的外部持久化存储保存。当作业
Failed(或者Cancled)时,外部存储的检查点会保留下来。用户在恢
复时需要提供用于恢复的作业状态的检查点路径。
(2)保存点
用户通过命令触发,由用户手动创建、清理。使用了标准化格式
存储,允许作业升级或者配置变更。用户在恢复时需要提供用于恢复
作业状态的保存点路径。
13.3.1 检查点恢复
1. 自动检查点恢复
自动恢复可以在配置文件中提供全局配置,也可以在代码中为Job
特别设定。自动恢复的内容在第10章已有讲解,其中作业失败调度就
是自动恢复的实现,见表13-1。
表13-1 作业调拨重启策略及说明
13.3.2 保存点恢复
从保存点恢复作业并不简单,尤其是在作业变更(如修改逻辑、
修复bug)的情况下,需要考虑如下几点。
(1)算子的顺序改变
如果对应的UID没变,则可以恢复,如果对应的UID变了则恢复失
败。
(2)作业中添加了新的算子
如果是无状态算子,没有影响,可以正常恢复,如果是有状态的
算子,跟无状态的算子一样处理。
(3)从作业中删除了一个有状态的算子
13.4 关键组件
在介绍检查点的执行过程之前,首先介绍一下跟检查点生成相关
的几个关键内容。
13.4.1 检查点协调器
在Flink中检查点协调器叫作CheckpointCoordinator,负责协调
Flink 算 子 的 State 的 分 布 式 快 照 。 当 触 发 快 照 的 时 候 ,
CheckpointCoordinator向Source算子中注入Barrier消息,然后等待
图13-1 检查点消息类体系
检查点消息中有3个重要信息:该检查点所属的作业标识
(JobID)、检查点编号、Task标识(ExecutionAttemptID)。
(1)AcknowledgeCheckpoint消息
该消息从TaskExecutor发往JobMaster,告知算子的快照备份完
成。
(2)DeclineCheckpoint消息
该消息从TaskExecutor发往JobMaster,告知算子无法执行快照备
份,如Task了Running状态但是内部还没有准备好执行快照备份。
13.5 轻量级异步分布式快照
Flink采用轻量级分布式快照实现应用容错,采用此种实现方式基
于如下基本假设条件。
1)作业异常和失败极少发生,因为一旦发生异常,作业回滚到上
一个状态的成本很高。
2)为了低延迟,快照需要很快就能完成。
3)Task与TaskManager之间的关系是静态的,即分配完成之后,
在作业运行过程中不会改变(至少是一个快照周期内),除非手动改
图13-2 Barrier切分数据流
Barrier会在数据流源头被注入并行数据流中。Barrier n所在的
位置就是恢复时数据重新处理的起始位置。例如,在kafka中,这个位
置就是最后一个记录在分区内的偏移量(offset),作业恢复时,会
根据这个位置从这个偏移量之后向kafka请求数据。这个偏移量就是
State中保存的内容之一。
Barrier接着向下游传递。当一个非数据源算子从所有的输入流中
收到了快照n的Barrier时,该算子就会对自己的State保存快照,并向
自己的下游广播送快照n的Barrier。一旦Sink算子接收到Barrier,有
两种情况:
1)如果是引擎内严格一次处理保证,当Sink算子已经收到了所有
上游的Barrier n时,Sink算子对自己的State进行快照,然后通知检
1)在图13-3a中,算子收到输入通道1的Barrier,输入通道2的
Barrier尚未到达算子。
2)在图13-3b中,算子收到输入通道1的Barrier,会继续从输入
通道1接收数据,但是并不处理,而是保存在输入缓存中,等待输入通
道2的Barrier到达。在输入通道2 Barrier到达前,缓存了3条数据
(3,2,1)。
3)在图13-3c中,输入通道2的Barrier到达,算子开始对其State
进行异步快照,并将Barrier向下游广播,并不等待快照执行完毕。
4)在图13-3d,算子在做异步快照,首先处理缓存中积压的数
据,然后再从输入通道中获取数据。
13.6 检查点执行过程
JobMaster作为作业的管理者,是作业的控制中枢,在JobMaster
中的CheckpointCoordinator组件专门负责检查点的管理,包括何时触
发检查点、检查点完成的确认。检查点的具体执行者则是作业的各个
Task,各个Task再将检查点的执行交给算子,算子是最底层的执行
者,如图13-4所示。
图13-4 检查点过程示意
13.6.2 TaskExecutor执行检查点
JobMaster通过TaskManagerGateway触发TaskManager的检查点执
行,TaskManager则转交给Task执行。
1. Task层面的检查点执行准备
Task类中的部分,该类创建了一个CheckpointMetaData的对象,
确保Task处于Running状态,把工作转交给StreamTask,如代码清单
13-3所示。
代码清单13-3 Task处理检查点
代码清单13-5 Barrier触发检查点
在向JobMaster汇报的消息中,TaskStateSnapshot中保存了本次
检查点的State数据,如果是内存型的StateBackend,那么其中保存的
是真实的State数据,如果是文件型的StateBackend,其中保存的则是
状态的句柄(StateHandle)。在分布式文件系统中的保存路径也是通
过TaskStateSnapshot中保存的信息恢复回来的。
状态的句柄分为OperatorStateHandle和KeyedStateHandle,分别
对应于OperatorState和KyedState,同时也区分了原始状态和托管状
13.7 检查点恢复过程
在作业发生异常自动恢复、从保存点恢复作业时,都会涉及从快
照中恢复作业状态。本书第10章介绍了作业的容错,但是只介绍了作
业调度层面的恢复,并没有深入到恢复的细节。
JobMaster会将恢复状态包装到Task的任务描述信息中,在上节执
行检查过程中提到,Task使用TaskStateSnapshot向JobMaster汇报自
身的状态信息,恢复的时候也是使用TaskStateSnapshot对象。
从上面的代码中可以看出UDF算子在初始化的时候,复用了非UDF
算子的状态恢复,然后又做了函数状态的恢复。
1. OperatorState恢复
在 初 始 化 算 子 状 态 的 时 候 , 从 OperatorStateStore 中 获 取
ListState 类 型 的 状 态 , 由 OperatorStateStore 负 责 从 对 应 的
StateBackend中读取状态重新赋予算子中的状态变量,如代码清单13-
12所示。
代码清单13-12 异步算子恢复状态示例
13.8 端到端严格一次
在分布式环境下,保证端到端严格一次是一件非常困难的事情,
尤其是并行输出的时候,如图13-5所示。
图13-6 Flink作业示例
本例中的Flink作业包含以下算子。
1)一个Source算子,从Kafka中读取数据(即KafkaConsumer)。
2)一个窗口算子,基于时间窗口化的聚合运算(即window +
window函数)。
3)一个Sink算子,将结果写回到Kafka(即KafkaProducer)。
如果要实现端到端的严格一次,Sink必须以事务的方式写数据到
Kafka,这样当提交事务时两次检查点间的所有写入操作作为一个事务
被提交,确保出现故障或崩溃时这些写入操作能够被回滚。
如果只是一个Sink实例,比较容易实现端到端严格一次,然而在
一个分布式且含有多个并发执行Sink的应用中,仅仅执行单次提交或
回滚是不够的,因为所有组件都必须对这些提交或回滚达成共识,这
样才能保证得到一个一致的结果。Flink使用两阶段提交协议以及预提
交(Pre-commit)阶段来解决这个问题。两阶段提交协议分为预提交
(Pre-Commit)阶段和提交(Commit)阶段。
1. 预提交阶段
当开始执行检查点的时候进入预提交阶段,JobMaster向Soure
Task注入CheckpointBarrier,Source Task将CheckpointBarrier插入
数据流,向下游广播开启本次快照。
CheckpointBarrier除了起到触发检查点的作用,在两阶段提交协
议中,还负责将流中所有消息分割成属于本次检查点的消息以及属于
下次检查点的两个集合,每个集合表示一组需要提交的数据,即属于
同一个事务。
图13-7 检查点过程示例
在检查点开始的时候,Kafka Source在快照中保存KafkaTopic的
offset偏移量,下游的算子一直到Kafka Sink依次完成快照的保存,
JobMaster再确认完成,一旦失败就回滚到最后一次成功完成的检查点
中保存的状态,从备份的Kafka Offset开始去读数据重新执行。
这种方式只能在数据源Kafka到Flink内部保证严格一次,一旦涉
及从Sink写入到外部Kafka就会出现问题了。假设Checkpoint 3完成之
后,Source从Topic偏移量位置65536读取了1000条数据,Topic偏移量
为66536,Sink写入了1000条数据到外部Kafka,此时Flink应用的1个
Sink并行实例因为未处理的异常崩溃,进入Failover阶段,应用自动
从Checkpoint 3恢复,重新从Topic的偏移量65536开始读取数据,这
就会导致65536~66536之间的1000条数据被重复处理,写入到了Kafka
中。
这种情况下需要避免重复写入这1000条数据到Kafka中。幂等性是
一种解决方案,如对HBase按照主键插入可能有效,第2次插入是对第1
次的更新。
对于支持幂等性操作的外部存储,这比较简单,无论写入多少
次,外部存储可以保证不会出现重复数据,使用Flink现有的检查点机
制就能保证端到端的严格一次。但是对于支持事务的外部存储,现有
的检查点机制就不够了,重复写入的问题无法解决,检查点在Flink内
部是可控的,但是外部存储Flink无法控制。所以需要扩展Flink,使
Sink与外部存储通过事务关联起来,在出现异常作业需要恢复的时
图13-8 两阶段协议-预提交阶段
倘若在预提交阶段任何一个算子发生异常,导致检查点没有备份
到状态后端存储,所有其他算子的检查点也必须被终止,Flink回滚到
最近成功完成的检查点。
2. 提交阶段
预提交阶段完成之后,下一步就是通知所有的算子,确认检查点
已成功完成。然后进入第二阶段——提交阶段。该阶段中JobMaster会
为作业中每个算子发起检查点已完成的回调逻辑。
本例中的Source和Window窗口操作不需要与外部存储交互,因此
在该阶段,这两个算子无须执行任何逻辑,但是Sink需要与外部交
互,所以此时需要将预提交开启的外部事务提交,如图13-9所示。
13.9 总结
Flink有不同可靠性级别的容错语义保证,底层依赖于轻量级异步
快照。轻量级异步快照机制通过在数据流中注入检查点Barrier,随着
数据的流动,在各个算子上执行检查点,保存状态到外部的可靠存储
中,当作业发生异常的时候,从最后一次成功的检查点中恢复状态。
同时在检查点基础上设计了保存点,使得在作业迁移、集群升级
等过程中也能保证作业执行结果的精确性。
另外,基于检查点机制,在框架级别支持两阶段提交协议,实现
了端到端严格一次的语义保证,这是Flink相比其他计算引擎的独特之
处。
当上层编程语言(如SQL)转换为关系表达式后,就会被送到
Calcite的逻辑规划器进行规则匹配。在这个过程中,Calcite查询优
化引擎会循环使用规划规则对SQL逻辑计划树进行迭代优化。Calcite
中提供了RBO(基于规则)和CBO(基于代价)两种优化器,在保证语
义等价的基础上,生成执行成本最低的SQL逻辑树。
使用逻辑规划规则等同于对关系表达式进行等价变换,比如将一
个过滤器推到Join运算之前执行。如图14-1所示,将Filter操作下推
到Join之前执行,这样做的好处是减少了Join操作记录的数量,同时
降低了CPU、内存、网络等各方面的开销,极端情况下,Join效率可能
会提升上百倍。
图14-1 Filter下推到Join之前
Calcite提供了灵活的机制,任何使用Calcite的框架都可以自定
义关系运算符、优化规则、代价模型、相关优化需要的统计信息,如
数据统计直方图等,从而能够灵活地应用在不同的场景中,这符合
Calcite作为SQL优化框架的目标。
传统SQL比较成熟,且应用广泛,如果能在流上使用SQL进行开
发,那么会带来极大的便利性。将SQL应用于流计算,需要将流和表两
个差异比较大的概念进行融合。在本书开篇的时候讲到有界数据集是
无界数据集的特例,如果把有界数据集当作表,那么无界数据集
(流)就是一个随着时间变化持续写入数据的表,Flink中使用动态表
(Dynamic Table)来表示流,用静态表表示传统的批处理中的数据
集。流是DataStream API中的概念,动态表是Flink SQL中的概念,本
质上来说,两者都表示无界数据集。
2. 动态表与连续查询
引入了动态表的概念之后,将SQL作用于流,实际上变成了将SQL
作用于动态表。Flink的Table API和SQL围绕着动态表构建。动态表在
Flink中抽象为Table接口。
与表示批处理数据的静态表相比,动态表随时间而变化。将SQL查
询作用于动态表,查询会持续执行而不会终止,因此叫作连续查询。
因为数据会持续产生、没有尽头,所以连续查询不会给出一个最终的
不变的结果。以Count运算为例,对于静态表而言,Count是一个确定
图14-2 流、动态表、连续查询的关系
从概念上来说,图14-2示意的过程如下。
1)流转换为动态表。
2)在动态表上执行连续查询,生成新的动态表。
3)生成的动态表将转换回流。
从Flink开发的逻辑来说,图14-2示意的过程如下。
1)将DataStream注册为Table。
2)在Table上应用SQL查询语句,结果为一个新的Table。
3)将Table转换为DataStream。
注意:数据流(DataStream)、动态表(Table)都是逻辑概
念,都是API层面的概念。
为了更好地理解动态表和连续查询的概念,接下来通过一个示例
来进行更详细的说明,假设一个点击事件流,模式如代码清单14-2所
示。
代码清单14-2 点击事件流例子
注意:此处的动态表是逻辑概念,开发时用在Table API和SQL
中,在实际执行的时候,Flink内部动态表的运算仍然会变成DAG,与
数据库中的表不同,数据库中的表虽然也是逻辑概念,但是表最终会
保存到磁盘上。
图14-3 流表映射
14.2.2 连续查询
上面的示例中提到cnt的维护代价太大,其成本最主要是State,
如果使用内存State(MemoryStateBackend),内存占用会很大,最终
可 能 超 过 内 存 上 限 , 如 果 使 用 基 于 磁 盘 的
State(RocksDBStatebackend),随着State的增长,磁盘IO会成为瓶
颈。
注意:State提供了过期清理的功能,跟Flink SQL中的配置参数
相结合,可以降低State大小带来的影响。
2. 计算更新代价
对于一些流上的SQL查询来说,即使只添加或更新一行记录,也可
能会导致重新计算和更新大部分结果表中的行,通常这样的查询不适
合作为流上的SQL查询。
如下查询示例中,会根据最后一次点击的时间为每个用户计算
RANK。一旦Clicks表收到新行,用户的lastAction就会被更新并计算
新的RANK。然而由于不存在两行相同的RANK,所以所有较低RANK的行
也需要被更新。
所以并不是所有的传统SQL运算都适合于流。
14.2.4 表到流的转换
数据库中表的DML行为有3种: INSERT、UPDATE、DELETE。动态表
作为类似于数据库的表的概念,也支持这3种DML操作,但是有些不
同。
14.3 TableEnvironment
TableEnvironment是Flink Table API和Flink SQL中使用的执行
环境,向上对开发者提供了Flink SQL使用的相关接口,向下连接
Flink的SQL运行时。
TableEnvironment包含如下职责。
1)连接到外部数据源。
2)注册Table和获取元数据信息。
3)执行SQL语句。
4)提供SQL执行的配置。
14.3.1 TableEnvironment体系
Flink中有5个TableEnvironment,在实现上是5个面向用户的接
口,在接口底层进行了不同的实现。5个接口包括一个
TableEnvironment 接 口 , 两 个 BatchTableEnvironment 接 口 , 两 个
StreamTableEnvironment接口,5个接口文件的完整路径如下。
图14-8 TableEnvironment接口体系
TableEnvironment 是 顶 级 接 口 , 是 所 有 TableEnvironment 的 基
类,其有两个BatchTableEnvironment子接口(分别在Flink Table &
SQL中用作批处理)和两个StreamTableEnvironment子接口(分别在
Flink都提供了Java实现和Scala实现,各有两个接口)。
2.场景二
用户使用Old Planner进行批处理的Table程序的开发。这种场景
下,用户只能使用BatchTableEnvironment,因为在使用Old Planner
时,批处理程序操作的数据是DataSet,只有BatchTableEnvironment
提供了面向DataSet的接口实现。
如代码清单14-4所示。
代码清单14-4 Flink Planner批处理
图14-9 Table体系
如同DataStream,Table也有各种不同的变体,在Flink中的DQL查
询语义中有7种可以相互转换的Table,不同类型的Table之间的转换关
系如图14-10所示。
5. OverWindowTable
使用开窗函数分组之后的Table,如代码清单14-10所示。
代码清单14-10 Table API OverWindow示例
6. AggregatedTable
7. FlatAggregateTable
对分组之后的Table(如GroupedTable和WindowedGroupTable)执
行TableAggregationFunction(表聚合函数)的结果,如代码清单14-
12所示。
代码清单14-12 Table API GroupBy后表聚合示例
14.6 元数据
与其他的数据仓库、数据库系统类似,Flink Table API和Flink
SQL也需要依赖于元数据。元数据描述了Flink处理的读取和写出的数
据的结构以及数据的访问方法等信息,是Flink中实现SQL处理数据的
重要组成部分,没有元数据SQL就无法校验、优化。
接下来将对元数据的结构、管理、集成、使用等方面一一进行介
绍。
14.6.1 元数据管理
图14-11 Catalog元数据目录体系
Catalog并不是孤立的,其上对开发人员、下对实际元数据的存储
的整体关系如图14-12所示。
注意:Flink1.9版本之前用ExternalCatalog对接外部元数据,
现在已经移除。
14.6.2 元数据分类
Catalog定义了对4种元数据类型的接口,每种元数据的用途和操
作不同,所以Flink定义了4类接口分别对应于4种元数据类型,元数据
类型之间的层次关系如图14-13所示。
最顶层的Catalog是元数据的容器,从Catalog向下是实际的不同
类型元数据的定义。
1.数据库
图14-13 元数据层次关系
图14-14 数据库实例元数据类体系
2.表和视图
CatalogTable对应于数据库中的表,CatalogView对应于数据库中
的视图,两者相似,所以继承了共同的CatalogBaseTable接口,其类
体系如图14-15所示。
14.7 数据访问
为 了 使 Flink 能 够 访 问 外 部 数 据 源 ,Flink 内 置 了 大 量 的
Connector,也提供了自定义Connector的机制。但是Connector提供的
是Function接口,服务于DataStream API的开发,Table API和SQL并
不能直接使用Connector体系。Table API和SQL是关系型API,提供了
校验和优化能力,要求数据源能够提供其元数据描述,这也是
Connector体系所不能提供的。基于前边两点,Flink为Table API和
SQL设计了Table Source和Table Sink体系,满足对Table API和SQL数
据源访问的需求。
Table Source用来读取外部存储中的数据,Table Sink用来将
Table API和SQL计算的结果写出到外部存储。
1. TableSchema
Table Source和Table Sink需要具备对外部数据源的描述能力,
所以Flink定义了TableSchema对象来定义表的字段名称和字段类型、
表的存储形式(CSV、Parquet、JSON等),同时还具备字段类型和格
式的转换能力,如代码清单14-14所示。
代码清单14-14 TableSchema Java代码示例
2.时间属性
(2)SQL语句
使用SQL语句同样也能在表上设定Watermark策略,所有使用这张
表的SQL语句共享此Watermark策略,如代码清单14-17所示。
代码清单14-17 SQL创建Watermark示例
图14-19 SQL自定义函数体系
(2)聚合函数
在Flink中叫作Aggregation Function,简称UDAF。
14.9 Planner关键抽象
Table API和SQL API面向的是开发者,Planner是Flink引擎内部
的组件,是用户编写的代码和Flink运行时的中介,负责将用户代码转
换到Flink运行时可以识别的Transformation。
其主要为Flink中的Planner定义了两个关键行为:
● SQL解析:将SQL字符串解析为对Table API调用的Operation
树。
● 关 系 代 数 到 Flink 执 行 计 划 : 将 Operation 树 转 换 为
Transformation。
在Blink Table模块和Flink Table模块中,各自实现了不同的优
化器,其类体系如图14-20所示。
图14-22 ResolvedExpression包含的实现
(1)CallExpression
CallExpression表示一个解析、验证后的函数调用表达式。其基
本属性如下。
1)输出类型。
图14-23 Operation类体系
● CreateOperation : 创 建 操 作 , 可 以 定 义 Database 、 表 、 视
图、函数等的元数据。
● AlterOperation:修改操作,可以修改Database、表、视图、
函数等的元数据。
● DropOperation:删除操作,可以删除Database、表、视图、
函数等的元数据。
图14-24 Blink物理执行计划抽象
图14-25 Flink物理执行计划抽象
DataStreamRel表示流上的物理计划树的节点,DataSetRel表示批
上的物理计划树的节点。
14.11 Blink与Calcite关系
在Blink中的Table API和SQL语句两者表达的都是关系型运算,在
面向开发者API的层面不同,但是底层对于Flink引擎而言,本质上是
一样的,在Operation层面上进行了统一。如图14-26所示,Table API
的调用被转换成Operation树,SQL语句经过解析、验证最终也变成了
一 棵 Operation 树 , Table API 和 SQL Query 在 Operation 上 完 成 了 统
一,接下来的过程是一样的,Blink Planner对Operation树进行优
化、转换,最终变成了流计算作业。
至此,SQL语句转换为Operation树,接下来进入优化和执行换测
过程。
14.12.2 Operation到Transformation
前 文 中 介 绍 了 从 SQL 到 Operation 的 过 程 , 本 节 中 主 要 介 绍 从
Operation到Transformation的过程。
(1)DQL转换
DQL 语 句 , 即 查 询 语 句 , 在 Flink 中 基 本 上 是 作 为 中 间 运 算
( QueryOperation→RelNode→FlinkPhysicalRel→ExecNode→Transf
ormation;)使用的。
(2)DML、DQL转换
具 体 路 径 为
ModifyOperation→RelNode→FlinkPhysicalRel→ExecNode→Transfo
rmation。
转换的入口在TableEnvironment# translate中,如代码清单14-
21所示。
代码清单14-21 转换入口
2.将Filter添加到QueryOperation树
14.13.2 Operation到Transformation
在 Flink Table 和 SQL 的 Blink 实 现 中 , Table API 和 SQL 在
Operation层进行了统一,之后的转换、处理、执行的过程是相同的,
不再赘述。
14.14 Flink与Calcite的关系
在1.9版本之前的Flink Table和SQL模块的旧实现中,Table API
和SQL在Calcite RelNode层面进行了语义的统一,但是在执行层面又
14.17 SQL优化
图14-28 Blink优化器类体系
1. CommonSubGraphBasedOptimizer
基于DAG的公共子图优化将原始的RelNode DAG优化为语义等价的
RelNode DAG。什么是公共子图?子图就是一个逻辑计划树(Calcite
RelNode Tree)的子树,公共子图也叫公共子树。公共子树就是多棵
逻辑计划树相同的子树。Calcite Planner不支持DAG(包含了多个
Sink)的优化,所以RelNode DAG需要被分解为多棵子树(每棵子树一
个 根 , 即 只 有 一 个 Sink ) , 每 棵 子 树 使 用
org.apache.calcite.plan.RelOptPlanner单独优化。
从总体上来说,优化分为两个阶段,逻辑优化阶段和物理优化阶
段,优化过程如图14-29所示:
Blink中的定制了优化过程,并不是全部托管给Calcite的优化
器,而是混合使用了Calcite的Hep规则优化器和Volcano代价优化器。
逻辑优化使用的是Calcite的Hep优化器(基于规则),物理优化阶段
使用了Calcite的Hep规则优化器和Volcano优化器(基于代价)。
在 Blink SQL 的 CommonSubGraphBasedOptimizer 中
FlinkOptimzeProgram 将 具 体 的 优 化 交 给 Calcite 来 执 行 。
FlinkOptimzeProgram将不同的优化规则组合到优化器中,按照顺序应
用优化规则。
FlinkOptimizedProgram类体系如图14-30所示。
1. FlinkChainedProgram
优化程序链、容器类型,将一组FlinkOptimizeProgram顺序组织
起 来 , 当 执 行 优 化 的 时 候 , 依 照 顺 序 依 次 执 行 。 在
FlinkStreamProgram中就是使用该Program作为顶层的容器,将优化规
则集按照顺序组织起来的。
代码清单14-29 SQL物理计划
14.19 Flink优化
Flink SQL的优化在1.9版本之前完全依赖于Calcite,使用了自定
义的批流优化规则,直接使用Calcite Volcano的优化器。而当前版本
则与Blink SQL一样,混合使用Calcite的优化器。
14.19.1 优化器
Flink Planner中的优化与Blink Planner的优化最大的不同点在
于优化规则和类型系统。在1.9版本之前,Flink Planner的优化使用
的单一的优化器,默认使用基于代价的优化器,1.9版本及之后会根据
14.19.2 优化过程
Flink Planner的优化过程如图14-36所示。
14.20 代码生成
优化器负责全局的优化,从提升全局资源利用率、消除数据倾
斜、降低IO等角度进行优化,包括Join重写等。代码生成负责局部优
图14-37 表达式a+b+1的求值过程
首先需要定义add函数,add函数层层调用。在这个过程中涉及虚
函数调用、对象创建等额外逻辑,这些额外成本远超对表达式求值本
身 。 而 通 过 代 码 生 成 , 则 可 以 将 过 程 简 化 为 row.get ( a )
+row.get(b)+1,Java执行简单的加法效率要高得多。Spark在其
Tungsten项目中也引入了代码生成技术,Spark SQL的执行效率获得了
极大的提升。
14.20.2 代码生成范围
2.生成OneInputStreamOperator子类Java代码
生成了处理片段之后,需要将处理代码封装到算子的实现类中,
如代码清单14-36所示。
代码清单14-36 生成OneInputStreamOperator算子
14.21 总结
Flink Table & SQL依托Apache Calcite提供的SQL解析、优化框
架,并没有像Spark一样自己做SQL解析、优化器。从用户视角,Flink
提供了Table API和SQL两种不同层次的开发接口,提供了元数据管
理、SQL函数、数据源等基础构建模块。
在目前的状态下,Flink中包含了Flink SQL和Blink SQL两套体
系。Flink Planner是旧的Planner,Blink Planner是新的Planner,
未来Flink Planner将会被废弃。SQL语句经过Calcite解析构建成为逻
辑计划树,通过Planner逻辑计划树经过层层优化转换为Flink可以运
行的内部结构,Table API同样会转换为Calcite识别的逻辑节点,与
SQL一样经历优化、转换,然后进入执行阶段。
15.1 监控指标
Flink提供了Counter、Gauge、Histogram和Meter 4类监控指标。
指标接口体系如图15-1所示。
图15-1 指标分类
1. Counter计数器
用来统计一个指标的总量。以Flink中的指标为例,算子的接收记
录 总 数 ( numRecordsIn ) 和 发 送 记 录 总 数 ( numRecordsOut ) 属 于
15.2 指标组
Flink的指标体系是一个树形结构,域相当于树上的顶层分支,表
示指标的大的分类。
每个指标被分配一个标识符,该标识符将基于3个组件进行汇报:
注册指标时用户提供的名称、可选的用户自定义域和系统提供的域。
例如,如果A.B是系统域,C.D是用户域,E是名称,那么指标的标识符
将 是 A.B.C.D.E. 可 以 通 过 设 置 conf/flink-conf.yam 里 面 的
metrics.scope.delimiter参数来配置标识符的分隔符,默认为“.”
(点)。
以算子的指标组结构为例,其默认为:
算子的输入记录数指标为:
关于指标组的配置参数的使用参见官方手册。
在Flink中使用MetricReporter的时候,有两种实例化的方式:
1 ) 使 用 Java 反 射 机 制 实 例 化 MetricReporter , 要 求
MetricReporter的实现类必须是public的访问修饰符,不能是抽象
类,必须有一个无参构造函数。
2 ) 使 用 MetricReporterFactory 工 厂 模 式 实 例 化
MetricReporter,推荐使用这种实例化的方式,相比发射机制,限制
更少。
Flink 提 供 了 JMX 、 Graphite 、 InfluxDB 、 Prometheus 、
PrometheusPushGateway、StatsD、Datadog和Slf4j共8种Reporter,
在配置文件中进行配置就可以直接使用。具体配置参数的使用参见官
方文档。
2. Rest API监控接口被动集成
15.4 指标注册中心
指标注册中心在Flink中叫作MetricRegistry,追踪所有已注册的
指标(Metric)。指标组(MetricGroup)用来对指标进行分组管理,
指标汇报器(MetricReporter)用来对外披露指标,而指标注册中心
就是这两者的中介,通过指标注册中心就可以让指标汇报器感知到在
指标组中有哪些指标、指标的值是多少,然后指标汇报器可以采集指
标数据,并写入第三方监控系统中。
指标组、指标注册中心、指标汇报器之间的关系如图15-2所示。
图15-2 指标组、指标注册中心、指标汇报器的关系
以 在 指 标 组 添 加 指 标 为 例 , 其 过 程 为
AbstractMetricGroup.addGroup→AbstractMetricGroup.addMetric→
MetricRegistry.register→MetricReporter.notifyOfAddedMetric 。
删除指标的过程也是类似的。
15.5 指标查询服务
指标查询服务在Flink中的接口是MetricQueryService,其通过继
承 RpcEndpoint 提 供 了 堆 外 调 用 的 能 力 , 实 现 了
MetricQueryService底层使用Akka的Actor实现,能够响应如下事
件。
1 ) 添 加 指 标 : 调 用 addMetric ( String metricName 、 Metric
metric、AbstractMetricGroup group)方法。
2)删除指标:调用removeMetric(Metric metric)方法。
3)指标查询:调用queryMetrics(Time timeout)方法。
15.6 延迟跟踪实现原理
Flink这样的流计算引擎面对的主要是低延迟的场景。在运行时
刻,各个计算子任务分布在大规模的物理机上,跟踪全链路的计算延
迟是一件复杂且重要的事情。Flink通过LatencyMarker(延迟标记)
的特性实现了全链路延迟的计算,延迟最终作为Histogram类型的指标
进行呈现。该功能默认是关闭的。
延迟追踪的原理如图15-3所示。
15.7 总结
Flink的监控指标分为4类,为了更好地区分指标,采用指标分组
组织指标体系。监控数据提供了被动读取和主动汇报两种方式,可以
非常方便地与其他运维管理系统进行集成。
16.1 Akka简介
16.1.1 Akka是什么
Akka是一个开发并发、容错和可伸缩应用的框架,是Actor模型的
一个实现,类似于Erlang的并发模型。在Actor模型中,所有的实体被
认 为 是 独 立 的 Actor 。 Actor 和 其 他 Actor 通 过 发 送 异 步 消 息 通 信 。
Actor模型的强大来自异步。也可以使用同步模式执行同步操作,但不
建议使用同步消息,因为它们限制了系统的伸缩性。每个Actor有一个
邮箱(Mailbox),用于存储所收到的消息。另外,每一个Actor维护
自身单独的状态。一个Actor网络如图16-1所示。
图16-1 Actor网络
每个Actor是一个单一的线程,它不断地从其邮箱中拉取消息,并
且连续不断地处理。对于已经处理过的消息的结果,Actor可以改变它
自身的内部状态,或者发送一个新消息,或者孵化一个新的Actor。尽
1. Actor路径
其接口继承体系如图16-2所示。
图16-2 RpcGateway接口继承体系
从图16-2可以看到,Flink的几个重要的角色都实现了RpcGateway
接 口 。 JobMasterGateway 接 口 是 JobMaster 提 供 的 对 外 服 务 接 口 ,
TaskExecutorGateway是TaskManager(其实现类是TaskExecutor)提
供的对外服务接口,ResourceManagerGateway是ResourceManager资源
管理器提供的对外服务接口,DispatcherGateway是Flink提供的作业
提交接口。组件之间的通信行为都是通过RpcGateway进行交互的。
前面在RPC消息类型中提到过Fenced消息,专门用来解决集群脑裂
问题,JobMaster、ResourceManager、DispatcherGateway在高可用模
式下,因为涉及Leader的选举,可能导致集群的脑裂问题,所以这几
个涉及选举的组件,都继承了FencedRpcGateway。
图16-3 RpcEndpoint类体系
在Flink的设计中,同一个RpcEndpoint中的所有调用只有一个线
程处理,叫作Endpoint的主线程。与Akka的Actor模型一样,所有对状
态数据的修改在同一个线程中执行,所以不存在并发的问题。
上面的重要子类都使用了RpcService作为参数,用来启动RPC通信
服 务 , RpcEndpoint 提 供 远 程 通 信 特 性 , RpcService 负 责 管 理
RpcEndpoint生命周期。RPCEndpoint是RpcService、RPCServer的结合
之处,如代码清单16-5所示。
代码清单16-5 RpcEndpoint构造函数
图16-5 RpcServer类体系
RpcServer的启动实质上是通知底层的AkkaRpcActor切换到START
状态,开始处理远程调用请求,如代码清单16-7所示。
16.3.5 AkkaRpcActor
Flink集群内部的通信依赖于Akka,AkkaRpcActor是其具体的实
现,负责处理如下类型消息。
(1)本地Rpc调用LocalRpcInvocation
LocalRpcInvocation类型的调用指派给RpcEndpoint进行处理,如
果有响应结果,则将响应结果返还给Sender。
(2)RunAsync & CallAsync
RunAsync、CallAsync类型的消息带有可以执行的代码,直接在
Actor的线程中执行。
(3)控制消息ControlMessages
ControlMessage用来控制Actor的行为,ControlMessages#START
启动Actor开始处理消息,ControlMessages#STOP停止处理消息,停止
后收到的消息会被丢弃掉。
AkkaRpcActor类体系如图16-6所示。
16.4 RPC交互过程
不同组件之间在运行时,需要频繁地进行RPC通信,如JobMaster
向TaskManager发布Task、JobMaster向ResourceManager请求Slot资源
等,在底层的RPC过程分为请求和响应两类。
16.4.1 RPC请求发送
在RpcService中调用connect()方法与对端的RpcEndpoint建立
连 接 , connect ( ) 方 法 根 据 给 的 地 址 返 回
InvocationHandler ( AkkaInvocationHandler 或 者
FencedAkkaInvocationHandler)。
在上文中客户端会提供代理对象,而代理对象会调用
AkkaInvocationHandler的invoke方法并传入RPC调用的方法和参数信
息,如代码清单16-8所示。
代码清单16-8 AkkaInvocationHandler处理调用
(2)控制消息
例 如 , 在 RpcEndpoint 调 用 start 方 法 后 , 会 向 自 身 发 送 一 条
Processing.START消息来转换当前Actor的状态为STARTED,STOP也类
似,并且只有在Actor状态为STARTED时才会处理RPC请求。控制消息处
理如代码清单16-12所示。
(3)RPC消息
通 过 解 析 RpcInvocation 获 取 方 法 名 和 参 数 类 型 , 并 从
RpcEndpoint类中找到Method对象,通过反射调用该方法。如果有返回
结果,会以Akka消息的形式发送回发送者。RPC消息处理如代码清单
16-13所示。
代码清单16-13 RPC消息处理