You are on page 1of 544

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
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号

机械工业出版社(北京市百万庄大街22号 邮政编码 100037)


策划编辑:张淑谦 责任编辑:张淑谦
责任校对:张艳霞 责任印制:李 昂
北京机工印刷厂印刷

see more please visit: https://homeofpdf.com


2020年8月第1版·第1次印刷
184mm×260mm·21.25印张·526千字
0001-2500册
标准书号:ISBN 978-7-111-66189-4
定价:119.00元

电话服务
客服电话:010-88361066
     010-88379833
     010-68326294
网络服务
机 工 官 网:www.cmpbook.com
机 工 官 博:weibo.com/cmp1952
金  书  网:www.golden-book.com
机工教育服务网:www.cmpedu.com
封底无防伪标均为盗版

see more please visit: https://homeofpdf.com



数据,已经渗透到当今各行各业的价值创造过程中,成为核心生
产要素之一。海量数据的挖掘和运用,已初见成效,预示着新一波生
产率增长和消费者盈余浪潮的到来。“大数据”在物理学、生物学、
环境生态学等领域以及军事、金融、通信等行业存在已有时日,却因
为近年来互联网和信息行业的发展而引起人们关注。依托大数据、云
计算、人工智能等技术的发展,人类社会从信息时代跨入智能时代,
5G成为第四次工业革命的技术基石。
随着网络建设的快速推进,万物互联时代已经开启,5G作为移动
通信技术制高点,将推动蓬勃发展的消费互联网进入崭新的工业互联
网、产业互联网时代。作为数字经济增长新引擎,5G与人工智能
( AI ) 、 物 联 网 ( IoT ) 、 云 计 算 ( Cloud Computing ) 、 大 数 据
(BigData)、边缘计算(Edge Computing)等技术的深度融合,将为
社会和经济发展注入新动能、开创新模式。信息技术在各行业转型升
级过程中的渗透力不断加强,成为社会信息流动的主动脉,承载着海
量实时数据流。毋庸置疑,数据越实时价值越大,秒级甚至毫秒级的
实时流式大数据计算场景层出不穷,这与5G高带宽、低延迟的业务特
点也是紧密契合的。海量实时流计算技术是最为重要的底层支撑技术
之一。
市场上,各大厂都在不遗余力地试用新的流计算框架,实时流计
算 引 擎 和 API , 诸 如 Spark Streaming 、 Kafka Streaming 、 Beam 和
Flink将持续火爆。随着5G万物互联互通带来的新一轮数据量的爆发,
越来越多的政府、企业等机构开始意识到实时数据正在成为最重要的
资产,实时数据分析能力正在成为新的核心竞争力。对于这一与时俱
进的大数据实时处理引擎——Flink,我们也许可以看到更多可能的未
来。
Flink作为行业顶级架构师、程序员的智慧结晶,毫无疑问是复杂
的,在理解其设计和实现时,亦有“只在此山中,云深不知处”的感
觉。而如何从根本上了解Flink的设计思路、原理、最新的动态及未来
发展趋势,阅读本书或许是一个捷径。

see more please visit: https://homeofpdf.com


本书的亮点可以概括为以下三个方面。
1.高屋建瓴、融会贯通
大数据处理技术领域,分布式计算引擎百花齐放。面对如此复杂
的技术领域,其首要之务是构建认知体系,而宏观认知则是认知体系
中的最重要环节。作者从Flink面向的人员角色、计算引擎的设计与抽
象层次、运行环境和外部交互等角度阐述,第1、2章帮助各位读者从
宏观视角认识Flink。不同的计算引擎虽各有特色、概念不同,然其设
计思路、技术原理皆有相通之处,一通则百通。
2.知其然,知其所以然
Flink毫无疑问是复杂的,本书将带领各位读者深入其中,系统性
地阐述Flink的核心原理、重要组件、关键工作流程,本书利用5个章
节主要介绍Flink的基础原理,从第8章开始,以执行过程的视角介绍
Flink的工作流程,使各位读者“知其然”,其间穿插关键代码片段分
析,梳理组件之间的协同关系,使各位读者“知其所以然”。
3.顺风而呼、开卷有益
读完本书,无论是开发者、架构师、运维人员、测试人员,还是
对Flink感兴趣的技术爱好者,相信各位读者会有一种豁然开朗的感
觉,从自己的视角获得不同的理解。技术的应用变化无穷,有了深入
全面的理解之后,无论是对Flink进行改进优化、性能调优、运维管理
等,都能够准确地抓住要点,直指根源。
为了让公众更好地了解Flink,让产业更全面地把握Flink,这本
书由资深专家执笔,从源码级别剖析了Flink的内核原理与实现,深入
浅出,值得我们学习、参考和借鉴。在此,我将它推荐给各位读者。
中国移动信息技术中心
大数据平台部副总经理

see more please visit: https://homeofpdf.com


前言
关于本书
随着Flink的应用越来越广泛,关于Flink的书籍、文章也越来越
多,但是系统性地阐述Flink设计原理和实现方法的书籍却很少。本书
的核心目标是对Flink的设计与原理做一个比较系统的介绍,尽量将
Flink的核心原理与其实现细节呈现给读者,但是由于篇幅有限,加之
Flink体系庞大且复杂,本书难以将其细节一一呈现,只能选择重点部
分加以阐述,如有疏漏、谬误之处还请包涵。各个大数据计算引擎在
原理上类似,但在设计取向和实现方法上会有不同。希望通过阅读本
书,读者能够对分布式计算引擎有更加深入的理解,开拓视野。
关于如何使用Flink,业内已经有相关书籍、官方文档、网络技术
文章可以参考,因此本书不是介绍如何开发Flink应用,而是以Word
Count经典案例贯穿本书,作为讲解和演示。本书涉及的Hadoop、
Yarn、K8s、Mesos、Kafka等Flink之外的大数据领域的组件,不是本
书主要介绍的内容,读者可以阅读相应的书籍,也可以参考网上的技
术文章。
适合人群
本书特别适合“穷理以致其知,反躬以践其实”的人阅读,具体
有以下人群。
● 愿意深入了解Flink设计与实现原理的Flink开发者。
● 对流计算感兴趣的大数据开发人员、技术爱好者。
● 对性能优化和部署感兴趣的运维工程师与架构师。
● 对Flink感兴趣的Spark开发人员、架构师。
阅读建议
建议首先通读本书,对Flink建立一个基本的认识,了解其核心流
程,不同的组件及其作用、相互之间的关联关系,避免沉浸在细节
中,窥一斑不见全貌。有了总体的认识之后,再有针对性地了解细
节。

see more please visit: https://homeofpdf.com


本书内容大概分为三大部分:基础知识、核心执行、运维管理。
基础知识包含第1~7章。第1章是总体性的介绍;第2章介绍Flink
应用中的基本概念及其API层;第3~7章介绍Flink底层运行的核心抽象
及其实现,如内存管理、时间与窗口、类型与序列化、状态原理等。
核心执行包含第8~14章。其中第8~13章是Flink作业提交、执行、
应用容错等方面实现原理的介绍;第14章是Flink SQL实现原理的介
绍,未来SQL是比较重要的应用开发方式。
运维管理包含第15、16章。第15章是Flink运维监控原理的介绍;
第16章是Flink集群内部的通信框架介绍。
强烈建议各位读者,不要从学习的角度去阅读本书,而是从设计
一个批流一体的大数据计算引擎的角度来进行阅读,思考作为设计者
必须要解决哪些问题,如何解决这些问题。
本书以Flink 1.10版本为基础编写,随着Flink的演进,后续将会
持续更新,敬请期待。
读者沟通
在阅读本书的过程中,读者若遇到任何问题、有任何建议,都可
以 向 deep_in_flink@126. com 发 送 邮 件 , 或 者 在 https : //github.
com/ffly1985/deep-in-flink上提交issue,对于读者比较关注的内
容,编者将在后续版本中丰富完善。
致谢
感谢我们所生活的时代,这是一个信息爆炸的时代,数据量呈指
数级增长,大数据的技术快速发展,数据处理的手段也在不断进化,
实时智能时代的到来使得本书有了面世的机会。
感谢为Flink的发展壮大付出辛苦努力的社区,为了Flink的完
善、推广付出巨大努力的阿里Flink团队,还有其他分享Flink经验的
各行业领军企业,正是有了它们共同的努力,才使得Flink成为流计算
事实上的标准。
感谢中国移动信息技术中心的领导尚晶、郭志伟、武智晖、刘辉
等,在本书编写过程中,他们给了很多思路和意见。同时与中国移动
各省分公司的集中交流、研讨,也使得Flink在运营商领域的位置计

see more please visit: https://homeofpdf.com


算、业务信息补全、复杂事件处理等实时计算场景下的适用性得到了
印证。
感谢编者所在公司给予的良好技术氛围和工作环境,使得作者能
够全心全意投入到技术的研究中,同时要感谢同事张文霞、孙得强、
李运波、王茂均、赵红岩花费了大量时间分享Flink的实战经验、提出
建议、书稿勘误,使得本书的内容得以持续完善。
编者

see more please visit: https://homeofpdf.com


目录

前言
第1章 Flink入门
1.1 核心特点
1.1.1 批流一体
1.1.2 可靠的容错能力
1.1.3 高吞吐、低延迟
1.1.4 大规模复杂计算
1.1.5 多平台部署
1.2 架构
1.2.1 技术架构
1.2.2 运行架构
1.3 Flink的未来
1.4 准备工作
1.5 总结
第2章 Flink应用
2.1 Flink应用开发
2.2 API层次
2.3 数据流
2.4 数据流API
2.4.1 数据读取
2.4.2 处理数据
2.4.3 数据写出
2.4.4 旁路输出
2.5 总结

see more please visit: https://homeofpdf.com


第3章 核心抽象
3.1 环境对象
3.1.1 执行环境
3.1.2 运行时环境
3.1.3 运行时上下文
3.2 数据流元素
3.3 数据转换
3.4 算子
3.4.1 算子行为
3.4.2 Flink算子
3.4.3 Blink算子
3.4.4 异步算子
3.5 函数体系
3.5.1 函数层次
3.5.2 处理函数
3.5.3 广播函数
3.5.4 异步函数
3.5.5 数据源函数
3.5.6 输出函数
3.5.7 检查点函数
3.6 数据分区
3.7 连接器
3.8 分布式ID
3.9 总结
第4章 时间与窗口
4.1 时间类型
4.2 窗口类型
4.3 窗口原理与机制

see more please visit: https://homeofpdf.com


4.3.1 WindowAssigner
4.3.2 WindowTrigger
4.3.3 WindowEvictor
4.3.4 Window函数
4.4 水印
4.4.1 DataStream Watermark生成
4.4.2 Flink SQL Watermark生成
4.4.3 多流的Watermark
4.5 时间服务
4.5.1 定时器服务
4.5.2 定时器
4.5.3 优先级队列
4.6 窗口实现
4.6.1 时间窗口
4.6.2 会话窗口
4.6.3 计数窗口
4.7 总结
第5章 类型与序列化
5.1 DataStream类型系统
5.1.1 物理类型
5.1.2 逻辑类型
5.1.3 类型推断
5.1.4 显式类型
5.1.5 类型系统存在的问题
5.2 SQL类型系统
5.2.1 Flink Row
5.2.2 Blink Row
5.2.3 ColumnarRow

see more please visit: https://homeofpdf.com


5.3 数据序列化
5.3.1 数据序列化/反序列化
5.3.2 String序列化过程示例
5.3.3 作业序列化
5.3.4 Kryo序列化
5.4 总结
第6章 内存管理
6.1 自主内存管理
6.2 内存模型
6.2.1 内存布局
6.2.2 内存计算
6.3 内存数据结构
6.3.1 内存段
6.3.2 内存页
6.3.3 Buffer
6.3.4 Buffer资源池
6.4 内存管理器
6.4.1 内存申请
6.4.2 内存释放
6.5 网络缓冲器
6.5.1 内存申请
6.5.2 内存回收
6.6 总结
第7章 状态原理
7.1 状态类型
7.1.1 KeyedState与OperatorState
7.1.2 原始和托管状态
7.2 状态描述

see more please visit: https://homeofpdf.com


7.3 广播状态
7.4 状态接口
7.4.1 状态操作接口
7.4.2 状态访问接口
7.5 状态存储
7.5.1 内存型和文件型状态存储
7.5.2 基于RocksDB的StateBackend
7.6 状态持久化
7.7 状态重分布
7.7.1 OperatorState重分布
7.7.2 KeyedState重分布
7.8 状态过期
7.8.1 DataStream中状态过期
7.8.2 Flink SQL中状态过期
7.8.3 状态过期清理
7.9 总结
第8章 作业提交
8.1 提交流程
8.1.1 流水线执行器PipelineExecutor
8.1.2 Yarn Session提交流程
8.1.3 Yarn Per-Job提交流程
8.1.4 K8s Session提交流程
8.2 Graph总览
8.3 流图
8.3.1 StreamGraph核心对象
8.3.2 StreamGraph生成过程
8.3.3 单输入物理Transformation的转换示例
8.3.4 虚拟Transformation的转换示例

see more please visit: https://homeofpdf.com


8.4 作业图
8.4.1 JobGraph核心对象
8.4.2 JobGraph生成过程
8.4.3 算子融合
8.5 执行图
8.5.1 ExecutionGraph核心对象
8.5.2 ExecutionGraph生成过程
8.6 总结
第9章 资源管理
9.1 资源抽象
9.2 资源管理器
9.3 Slot管理器
9.4 SlotProvider
9.5 Slot选择策略
9.6 Slot资源池
9.7 Slot共享
9.8 总结
第10章 作业调度
10.1 调度
10.2 执行模式
10.3 数据交换模式
10.4 作业生命周期
10.4.1 作业生命周期状态
10.4.2 Task的生命周期
10.5 关键组件
10.5.1 JobMaster
10.5.2 TaskManager
10.5.3 Task

see more please visit: https://homeofpdf.com


10.5.4 StreamTask
10.6 作业启动
10.6.1 JobMaster启动作业
10.6.2 流作业启动调度
10.6.3 批作业调度
10.6.4 TaskManger启动Task
10.7 作业停止
10.8 作业失败调度
10.8.1 默认作业失败调度
10.8.2 遗留的作业失败调度
10.9 组件容错
10.9.1 容错设计
10.9.2 HA服务
10.9.3 JobMaster的容错
10.9.4 ResourceManager容错
10.9.5 TaskManager的容错
10.10 总结
第11章 作业执行
11.1 作业执行图
11.2 核心对象
11.2.1 输入处理器
11.2.2 Task输入
11.2.3 Task输出
11.2.4 结果分区
11.2.5 结果子分区
11.2.6 有限数据集
11.2.7 输入网关
11.2.8 输入通道

see more please visit: https://homeofpdf.com


11.3 Task执行
11.3.1 Task处理数据
11.3.2 Task处理Watermark
11.3.3 Task处理StreamStatus
11.3.4 Task处理LatencyMarker
11.4 总结
第12章 数据交换
12.1 数据传递模式
12.2 关键组件
12.2.1 RecordWriter
12.2.2 数据记录序列化器
12.2.3 数据记录反序列化器
12.2.4 结果子分区视图
12.2.5 数据输出
12.3 数据传递
12.3.1 本地线程内的数据传递
12.3.2 本地线程间的数据传递
12.3.3 跨网络的数据传递
12.4 数据传递过程
12.4.1 数据读取
12.4.2 数据写出
12.4.3 数据清理
12.5 网络通信
12.5.1 网络连接
12.5.2 无流控
12.5.3 基于信用的流控
12.6 总结
第13章 应用容错

see more please visit: https://homeofpdf.com


13.1 容错保证语义
13.2 检查点与保存点
13.3 作业恢复
13.3.1 检查点恢复
13.3.2 保存点恢复
13.3.3 恢复时的时间问题
13.4 关键组件
13.4.1 检查点协调器
13.4.2 检查点消息
13.5 轻量级异步分布式快照
13.5.1 基本概念
13.5.2 Barrier对齐
13.6 检查点执行过程
13.6.1 JobMaster触发检查点
13.6.2 TaskExecutor执行检查点
13.6.3 JobMaster确认检查点
13.7 检查点恢复过程
13.8 端到端严格一次
13.8.1 两阶段提交协议
13.8.2 两阶段提交实现
13.9 总结
第14章 Flink SQL
14.1 Apache Calcite
14.1.1 Calcite是什么
14.1.2 Calcite的技术特点
14.1.3 Calcite的主要功能
14.1.4 Calcite的核心原理
14.2 动态表

see more please visit: https://homeofpdf.com


14.2.1 流映射为表
14.2.2 连续查询
14.2.3 流上SQL查询限制
14.2.4 表到流的转换
14.3 TableEnvironment
14.3.1 TableEnvironment体系
14.3.2 TableEnvironment使用示例
14.4 Table API
14.5 SQL API
14.6 元数据
14.6.1 元数据管理
14.6.2 元数据分类
14.7 数据访问
14.7.1 Table Source
14.7.2 Table Slink
14.8 SQL函数
14.9 Planner关键抽象
14.9.1 Expression
14.9.2 ExpressionResolver
14.9.3 Operation
14.9.4 QueryOperation
14.9.5 物理计划节点
14.10 Blink Planner和Flink Planner对比
14.11 Blink与Calcite关系
14.12 Blink SQL执行过程
14.12.1 从SQL到Operation
14.12.2 Operation到Transformation
14.13 Blink Table API执行过程

see more please visit: https://homeofpdf.com


14.13.1 Table API到Operation
14.13.2 Operation到Transformation
14.14 Flink与Calcite的关系
14.15 Flink SQL执行过程
14.15.1 SQL到Operation
14.15.2 Operation到DataStream/DataSet
14.16 Flink Table API执行过程
14.17 SQL优化
14.18 Blink优化
14.18.1 优化器
14.18.2 代价计算
14.18.3 优化过程
14.18.4 优化规则
14.18.5 公共子图
14.19 Flink优化
14.19.1 优化器
14.19.2 优化过程
14.19.3 优化规则
14.20 代码生成
14.20.1 为什么进行代码生成
14.20.2 代码生成范围
14.20.3 代码生成示例
14.21 总结
第15章 运维监控
15.1 监控指标
15.2 指标组
15.3 监控集成
15.4 指标注册中心

see more please visit: https://homeofpdf.com


15.5 指标查询服务
15.6 延迟跟踪实现原理
15.7 总结
第16章 RPC框架
16.1 Akka简介
16.1.1 Akka是什么
16.1.2 使用Akka
16.1.3 Akka的通信
16.2 RPC消息的类型
16.3 RPC通信组件
16.3.1 RpcGateway
16.3.2 RpcEndpoint
16.3.3 RpcService
16.3.4 RpcServer
16.3.5 AkkaRpcActor
16.4 RPC交互过程
16.4.1 RPC请求发送
16.4.2 RPC请求响应
16.5 总结
专家寄语
参考文献

see more please visit: https://homeofpdf.com


第1章 Flink入门
决定Flink的架构和实现的核心概念: 。数据 数据是流,是动态的
不是在某一个时刻凭空产生的,而是随着时间的流逝不断地产生,实
时数据是当前的数据,历史数据就是截止到某个时刻产生的数据流集
合。
大数据处理中有两种经典模式:批处理、流处理。以流为核心,
必须解决如何在数据流上实现流批统一。Google在Streaming 101、
Streaming 102两篇文章中对此进行了阐述,提出了一套以流为核心的
计算引擎的关键概念,如有界数据(静态数据集)与无界数据(数据
流)、时间、窗口、Watermark等,对流批的概念进行了融合。
无界数据就是持续产生的数据流,有界数据是过去一个时间窗口
内不变的数据流。对无界数据的处理就是流处理,对有界数据的处理
就是批处理。流处理就是对未来持续产生的数据进行计算,批处理是
对过去不再变化的数据流的计算。基于这种抽象,Flink解决了流批统
一的问题。
流即未来,Flink以流为核心的实现与Google Cloud Dataflow平
台有异曲同工之妙。其中有两层含义:
● 一是实时处理的需求越来越大,数据的时效性非常重要。
● 二是以流为核心的计算引擎,是更加自然的大数据处理引擎的
实现形式。
以数据流为核心基础,Apache Flink构建出了高性能、高可用的
批流一体的分布式大数据计算引擎,在数据流上提供数据分发、通
信、具备容错能力的分布式计算功能。
过去,很多人认为Flink是一个类似Storm的实时计算引擎,而如
今Flink以流计算引擎为基础,同样支持批处理,并且提供了SQL、复
杂事件处理CEP、机器学习、图计算等更高阶的数据处理场景。
提到Flink,必然绕不开Spark,Spark的核心是批处理,采用微批
处理实现实时计算。但是随着Flink的成熟度越来越高,阿里巴巴在
2019年收购了Flink的商业公司,把Blink重大改进贡献给社区,使

see more please visit: https://homeofpdf.com


Flink 在 功 能 、 性 能 方 面 突 飞 猛 进 , 在 实 时 计 算 领 域 相 比 Spark
Streaming、Storm的优势越来越大,在批处理方面的性能也与Spark不
相上下。

1.1 核心特点
1.1.1 批流一体
所有的数据都天然带有时间的概念,必然发生在某一个时间点。
把事件按照时间顺序排列起来,就形成了一个事件流,也叫作数据
流。例如信用卡交易事务,传感器收集设备数据、机器日志数据以及
网站或移动应用程序上的用户交互行为数据等,所有这些数据都是数
据流。
数据时时刻刻都在产生,如同江河奔流不息。例如,每一个人每
天都在处理各种各样的事情(事件),解决问题(响应),一年结束
之时,人们往往会坐下来总结这一年的得失,并制订新一年的计划。
在总结过去的时候,其实就默认给了一个时间范围。再举一个例子,
企业进行年终总结时,会统计当年完成了多少业绩,而不是考虑每一
笔业务的业绩。由此可以引出两个概念:无界数据(流)、有界数据
(批)。
1.无界数据
无界数据是持续产生的数据,所以必须持续地处理无界数据流。
数据是无限的,也就无法等待所有输入数据到达后处理,因为输入是
无限的,没有终止的时间。处理无界数据通常要求以特定顺序(例如
事件发生的顺序)获取,以便判断事件是否完整、有无遗漏。
2.有界数据
有界数据,就是在一个确定的时间范围内的数据流,有开始有结
束,一旦确定了就不会再改变。
Flink的设计思想与谷歌Cloud Dataflow的编程模型较为接近,都
以流为核心,批是流的特例。Flink擅长处理无界和有界数据。Flink
提供的精确的时间控制能力和有状态计算的机制,让它可以轻松应对
任何类型的无界数据流,同时Flink还专门设计了算法和数据结构来高
效地处理有界数据流。

see more please visit: https://homeofpdf.com


1.1.2 可靠的容错能力
在分布式系统中,硬件故障、进程异常、应用异常、网络故障等
多种多样的异常无处不在。像Flink这样的分布式计算引擎必须能够从
故障中恢复到正常状态,以便实现全天候运行。这就要求引擎在故障
发生后不仅可以重新启动应用程序,还要确保其内部状态保持一致,
从最后一次正确的点重新执行,从用户的角度来说,最终的计算结果
与未发生故障是一样的。
1.集群级容错
(1)与集群管理器集成
Flink 与 集 群 管 理 器 紧 密 集 成 , 例 如 Hadoop YARN 、 Mesos 或
Kubernetes。当进程挂掉时,将自动启动一个新进程来接管它的工
作。
(2)高可用性设置
Flink具有高可用性模式特性,可消除所有单点故障。HA模式基于
Apache ZooKeeper,Zookeeper是一种经过验证的可靠的分布式协调服
务。
2.应用级容错
Flink使用轻量级分布式快照机制,设计了检查点(Checkpoint)
来实现可靠的容错。其特性如下。
(1)一致性
Flink的恢复机制基于应用程序状态的一致性检查点。如果发生故
障,将重新启动应用程序并从最新检查点加载其状态。结合可重放的
流数据源,此特性可以保证精确、一次的状态一致性。
Flink、Spark、Storm等都支持引擎内的Exactly-Once语义,即确
保数据仅处理一次,不会重复也不会丢失。但是在把结果写入外部存
储的时候,可能会发生存储故障、网络中断、Flink应用异常恢复等多
种情况,在这些情况下,部分数据可能已经写入外部存储,重复执行
可能导致数据的重复写出,此时需要开发者为写出到外部存储的行为
保证幂等性。
在 Spark 、 Storm 中 需 要 开 发 者 自 行 实 现 Sink , 实 现 端 到 端 的
Exactly-Once 行 为 。 而 Flink 利 用 检 查 点 特 性 , 在 框 架 层 面 提 供 了

see more please visit: https://homeofpdf.com


Exactly-Once的支持,内置了支持Exactly-Once语义的Sink,即使出
现故障,也能保证数据只写出一次。
(2)轻量级
对于长期运行的Flink应用程序,其检查点的状态可能高达TB级,
生成和保存检查应用程序的检查点成本非常高。所以Flink提供了检查
点的执行异步和增量检查点,以便尽量降低生成和保存检查点带来的
计算负荷,避免数据处理的延迟异常变大和吞吐量的短暂剧降。
1.1.3 高吞吐、低延迟
从Storm流计算引擎开始,大家似乎留下了这样一个印象,要实现
低延迟,就要牺牲吞吐量,高吞吐、低延迟是流处理引擎的核心矛
盾。以Storm为代表的第一代流计算引擎可以做到几十毫秒的处理延
迟,但是吞吐量确实不高。后来的Spark Streaming基于mini-batch的
流计算框架能够实现较高的吞吐量,但是数据处理的延迟不甚理想,
一般可达到秒级。
Flink借助轻量级分布式快照机制,能够定时生成分布式快照,并
将快照保存到外部存储中。检查点之间的数据处理被当作是原子的,
如果失败,直接回到上一个检查点重新执行即可。在整个数据处理过
程中不会产生阻塞,不必像mini-batch机制一样需要等待调度,可以
持续处理数据,容错开销非常低。Flink在数据的计算、传输、序列化
等方面也做了大量的优化,既能保持数据处理的低延迟,也能尽可能
地提高吞吐量。
1.1.4 大规模复杂计算
Flink在设计之初就非常在意性能相关的任务状态和流控等关键技
术的设计,这些都使得用Flink执行复杂的大规模任务时性能更胜一
筹。
对于大规模复杂计算,尤其是长期运行的流计算应用而言,有状
态计算是大数据计算引擎中一个比较大的需求点。所谓的有状态计算
就是要结合历史信息进行的计算,例如对于反欺诈行为的识别,要根
据用户在近几分钟之内的行为做出判断。一旦出现异常,就需要重新
执行流计算任务,但重新处理所有的原始数据是不现实的,而Flink的

see more please visit: https://homeofpdf.com


容错机制和State能够使Flink的流计算作业恢复到近期的一个时间
点,从这个时间点开始执行流计算任务,这无疑能够大大降低大规模
任务失败恢复的成本。
Flink为了提供有状态计算的性能,针对本地状态访问进行了优
化,任务状态始终驻留在 内存
中,如果状态大小超过可用内存,则保
存在 高效磁盘 上的数据结构中。因此,任务通过访问本地(通常是内
存中)状态来执行所有计算,从而达到特别低的处理延迟。Flink通过
定期和异步检查点将本地状态进行持久存储来保证在出现故障时实现
精确、一次的状态一致性。
Flink的轻量级容错机制也能够尽量降低大规模数据处理时的调
度、管理成本,计算规模的增大不会显著增加容错,数据吞吐不会剧
烈下降,数据延迟不会急剧增大。
1.1.5 多平台部署
Flink是一个分布式计算系统,需要计算资源才能执行应用程序。
Flink可以与所有常见的集群资源管理器(如Hadoop YARN、Apache
Mesos和Kubernetes)集成,也可以在物理服务器上作为独立集群运
行。
为了实现不同的部署模式,Flink设计了一套资源管理框架,针对
上 面 提 到 的 资 源 管 理 平 台 实 现 了 对 应 的 资 源 管 理 器
(ResourceManager),能够与上面提到的资源管理平台无缝对接。
部署Flink应用程序时,Flink会根据应用程序配置的并行度自动
识别所需资源,并向资源管理器申请资源。如果发生故障,Flink会通
过请求新的资源来替换发生故障的资源。Flink提供了提交或控制应用
程序的REST接口,方便与外部应用进行集成,管理Flink作业。

1.2 架构
从概念上来说,所有的计算都符合“数据输入—处理转换—数据
输出”的过程,这个过程有时候叫作数据处理流水线(Pipeline),
流水线的概念来自生产制造中的流水线。以WordCount为例,其处理过
程抽象如图1-1所示。

see more please visit: https://homeofpdf.com


图1-1 Flink WordCount处理过程抽象
图1-1中,Source表示数据输入;转换表示数据处理的过程,在处
理转换中会包含1个或者多个计算步骤;Sink表示数据输出。Source、
Sink合并起来就是IO。无论是Source、Sink还是中间的转换,在Flink
中都统一抽象为Transformation。
随着数据量越来越大,远远超过了单机的处理能力,作业需要在
一个几十上百台的集群上执行,这时候就涉及如何将作业横向和纵向
拆分。横向拆分是将作业中的步骤并行执行,用并行度(Parallism)
来表示一个步骤有多少个实例并行执行。纵向拆分是将作业的步骤进
行拆分,拆分出来的每一个实体叫作Task,每个Task最终会分配到一
台服务器上执行,最终形成一个由Task组成的有向无环图(DAG),
Task中执行1个或者多个算子的示例如图1-2所示。

图1-2 Flink作业的Task DAG示例


从DAG中可以看到,Source的并行度为1,所以其只有1个Task。
FlatMap的并行度为2,所以其有2个Task。KeyedAgg与Sink根据优化策
略合并成了1个执行单元,并行度都是2,所以有2个Task。KeyedAgg与

see more please visit: https://homeofpdf.com


Sink的合并使用了算子融合,将符合优化策略的计算步骤合并成为一
个OperatorChain,具体内容后边章节会阐述。
1.2.1 技术架构
Flink是一个批流一体的分布式计算引擎,作为一个分布式计算引
擎,必须提供面向开发人员的API,根据业务逻辑开发Flink作业,作
业除了包含业务逻辑外,还需要跟外部的数据存储进行交互。作业开
发、测试完毕后,交给Flink集群进行执行,同时还要让运维人员能够
管理与监控Flink。
Flink技术架构如图1-3所示。

图1-3 Flink技术架构
对于应用开发者而言,直接使用API层和应用框架层,两者的差别
在于API的层次不同,API层是Flink对外提供的核心API,应用框架层
是在核心API之上提供的面向特定计算场景、更加易用的API。
1.应用框架层

see more please visit: https://homeofpdf.com


该层也可以称为Flink应用框架层,是指根据API层的划分,在API
层之上构建的满足特定应用场景的计算框架,总体上分为流计算和批
处 理 两 类 应 用 框 架 。 面 向 流 计 算 的 应 用 框 架 有 流 上 SQL ( Flink
Table&SQL)、CEP(复杂事件处理),面向批处理的应用框架有批上
SQL ( Flink Table&SQL ) 、 Flink ML ( 机 器 学 习 ) 、 Gelly ( 图 处
理)。
(1)Table&SQL
Table&SQL 是 Flink 中 提 供 SQL 语 义 支 持 的 内 置 应 用 框 架 , 其 中
Table API提供Scala和Java语言的SQL语义支持,允许开发者使用编码
的方式实现SQL语义。SQL基于Apache Calcite,支持标准SQL,使用者
可以在应用中直接使用SQL语句,同时也支持Table API和SQL的混合编
码。
Table API和SQL在流计算和批处理上提供了一致的接口,批处理
和流式传输的Table API和SQL程序都遵循相同的模式。
两者在底层上都依赖于Apache Calcite提供的优化能力,借助
Apache Calcite内置的优化规则,加上Flink实现的分布式流、批优化
规则,在逻辑和物理两个层面上进行优化。
(2)CEP
CEP本质上是一种实时事件流上的模式匹配技术,是实时事件流上
常见的用例。CEP通过分析事件间的关系,利用过滤、关联、聚合等技
术,根据事件间的时序关系和聚合关系制定匹配规则,持续地从事件
流中匹配出符合要求的事件序列,通过模式组合能够识别更加复杂的
事件序列,主要用于反欺诈、风控、营销决策、网络安全分析等场
景。
常见的开源CEP引擎有Esper、Siddhi、Drools等,商业CEP引擎有
Esper企业版、StreamBase、StreamInsight等。其中,Esper是成熟且
资历比较老的CEP引擎,在金融行业的应用比较广泛,开源Esper支持
单机版,Esper企业版支持双机热备。
现有的复杂事件处理引擎除Siddhi支持分布式部署之外(依赖
Storm),其余的复杂事件处理引擎都存在分布式计算支持不够的问
题。Flink CEP应用开发框架借助Flink的分布式计算引擎,提供了复
杂事件处理API,能够实现完整的模式匹配语义,同时部分实现了SQL

see more please visit: https://homeofpdf.com


2016标准中的SQL MatchRecognize语义,支持通过SQL定义复杂事件处
理匹配规则。
(3)Gelly
Gelly是一个可扩展的图形处理和分析库。Gelly是在DataSet API
之上实现的,并与DataSet API集成在一起。因此,它受益于其可扩展
且强大的操作符。Gelly具有内置算法,如label propagation(标签
传播)、triangle enumeration和page rank, 但也提供了一个自定义
图算法实现的简化Graph API。
Gelly的应用不多,本书中不进行深入阐述,读者可参考官方文档
进行了解。
(4)ML
Flink ML是Flink的机器学习框架,定位类似于Spark MLLib,但
是在目前阶段其实现的算法和成熟度距离Spark MLLib有较大差距,不
具备生产环境的可用性,在Flink1.9之后的版本中会对其进行重构。
本书中不进行详细阐述,读者可参考官方文档进行了解。
2. API层
API 层 是 Flink 对 外 提 供 能 力 的 接 口 , 实 现 了 面 向 流 计 算 的
DataStream API和面向批处理的DataSet API。理论上来说,Flink的
API应该像Apache Beam、Spark那样实现API层流批统一,但是目前却
依然是两套系统,使用起来并不方便,所以社区也在以DataStream
API为核心,推进批流API的统一。DataSet API未来会被废弃,所以本
书不会对基于DataSet的批处理方面进行过多的阐述。
3.运行时层
运行时层提供了支持Flink集群计算的核心,将开发的Flink应用
分布式执行起来,包含如下内容。
1)DAG抽象:将分布式计算作业拆分成并行子任务,每个子任务
表示数据处理的一个步骤,并且在上下游之间建立数据流的流通关
系。
2)数据处理:包含了开发层面、运行层面的数据处理抽象,例如
包含数据处理行为的封装、通用数据运算的实现(如Join、Filter、
Map等)。

see more please visit: https://homeofpdf.com


3)作业调度:调度批流作业的执行。
4)容错:提供了集群级、应用级容错处理机制,保障集群、作业
的可靠运行。
5)内存管理、数据序列化:通过序列化,使用二进制方式在内存
中存储数据,避免JVM的垃圾回收带来的停顿问题。
6)数据交换:数据在计算任务之间的本地、跨网络传递。
Flink运行时层并不是给一般的Flink应用开发者使用的。
4.部署层
该层是Flink集群部署抽象层,Flink提供了灵活的部署模式,可
以本地运行、与常见的资源管理集群集成,也支持云上的部署。
Flink支持多种部署模式:
1)Standalone模式:Flink安装在普通的Linux机器上,或者安装
在K8s中,集群的资源由Flink自行管理。
2)Yarn、Mesos、K8s等资源管理集群模式:Flink向资源集群申
请资源,创建Flink集群。
3)云上模式:Flink可以在Google、亚马逊云计算平台上轻松部
署。
5.连接器(Connector)
Connector是Flink计算引擎与外部存储交互的IO抽象,是前面提
到过的Source和Sink的具体实现。在Connector的实现上,流和批的实
现是两套系统,这属于历史遗留问题,未来会逐渐统一。
1.2.2 运行架构
通过技术架构来了解的Flink是静态的,而实际上Flink集群在运
行的时候,存在不同的进程角色来完成集群的管理,作业的提交、执
行、管理等一系列的动作,如图1-4所示。
Flink集群采用Master-Slave架构,Master的角色是JobManager,
负责集群和作业管理,Slave的角色是TaskManager,负责执行计算任
务。除此之外,Flink还提供了客户端来管理集群和提交任务,其中

see more please visit: https://homeofpdf.com


JobManager和TaskManager是集群的进程,Flink客户端是在集群外部
执行的进程,不是集群的一部分。
1. Flink客户端
Flink客户端是Flink提供的CLI命令行工具,用来提交Flink作业
到 Flink 集 群 , 在 客 户 端 中 负 责 Stream Graph ( 流 图 ) 和 Job
Graph(作业图)的构建,后面有详细介绍。使用Table API和SQL编写
的Flink应用,还会在客户端中负责SQL解析和优化。
Flink 的 Flip 改 进 建 议 中 提 出 了 新 的 模 式 , SQL 解 析 、 优 化 ,
StreamGraph 、 JobGraph 、 ExecutionGraph 构 建 转 换 等 全 部 都 会 在
JobManager中完成,这将在Flink1.10后续版本中实现。
2. JobManager
JobManager根据并行度将Flink客户端提交的Flink应用分解为子
任务,从资源管理器申请所需的计算资源,资源具备之后,开始分发
任务到TaskManager执行Task,并负责应用容错,跟踪作业的执行状
态,发现异常则恢复作业等。

see more please visit: https://homeofpdf.com


一个并行度就是
一个task

图1-4 Flink运行时架构
3. TaskManager
TaskManager接收JobManager分发的子任务,根据自身的资源情
况,管理子任务的启动、停止、销毁、异常恢复等生命周期阶段。
作业启动后开始从数据源消费数据、处理数据,并写入外部存储
中。
无论使用哪种资源集群,以上所介绍的角色是必不可少的,其作
用一样。
从 图 1-4 中 可 以 看 到 , JobManager 是 一 个 单 点 的 部 署 模 式 , 在
Flink中支持JobManager的HA部署,在后续章节中会介绍Flink HA的部

see more please visit: https://homeofpdf.com


署。

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所示。

see more please visit: https://homeofpdf.com


图1-5 Flink1.10版本源码包下载

1.5 总结
本章介绍了Flink的核心特点,如批流一体、可靠的容错能力、高
吞吐低延迟、大规模复杂计算、多平台部署等,以及为了实现这些特
点,Flink的技术架构和运行架构所进行的有针对性的设计。Blink的
引入提升了批流一体化和性能,使得Flink的未来更值得期待。
对于Flink,应该从代码层面上进行深度了解,所以建议读者一边
阅读本书,一边了解Flink的源码,必定会有不同的收获。

see more please visit: https://homeofpdf.com


第2章 Flink应用
Flink作为计算引擎,需要给开发者提供API。为了简化开发,
Flink提供了不同层次的API。其本身不提供数据存储能力,计算时需
要从不同的第三方存储引擎中把数据读过来进行处理,然后再写入另
外的存储引擎中。为了连接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所示。

see more please visit: https://homeofpdf.com


图2-1 Flink DataStream API与Transformation的转换
执行时,Flink应用被映射成Dataflow,由数据流和转换操作组
成。每个Dataflow从一个或多个数据源开始,并以一个或多个Slink输
出结束。Dataflow本质上是一个有向无环图(DAG),但是允许通过迭
代构造允许特殊形式的有向有环图。为了简单起见,大部分任务都是
有向无环图。
Flink应用由相同的基本部分组成。
(1)获取参数(可选)
如果有配置参数,则读取配置参数,可以是命令行输入的参数,
也可以是配置文件(配置文件可能是命令行给的1个配置文件路径)。
(2)初始化Stream执行环境
这是必须要做的,读取数据的API依赖于该执行环境。
(3)配置参数

see more please visit: https://homeofpdf.com


读取到的参数可以是执行环境参数或者业务参数。执行环境参数
调用对应的API赋予即可,这些参数会覆盖flink.conf中默认的配置参
数,如最大并行度maxParallism等。如果是业务级的参数,可以放入
GlobalJobParameters中,在Job执行时从GlobalJobParameters读取参
数。
一般在生产或者实际的应用场景中,多多少少需要提供一些配置
信息,如果只是为了学习用途,则可以不用考虑。
GlobalJobParameters可以视作一个Map,执行环境参数等具体细
节可以参照官方文档的详细说明。
(4)读取外部数据
Flink作为分布式执行引擎,本身没有数据存储能力,所以定义了
一系列接口、连接器与外部存储进行交互,读写数据。
在Flink中,数据来源叫作Source,Flink支持从Kafka、HDFS、
HBase、数据库等外部存储读取数据。
(5)数据处理流程
调 用 DataStream 的 API 组 成 数 据 处 理 的 流 程 , 如 调 用
DataStream.map().filter()...组成一个数据处理流水线。
(6)将处理结果写入外部
在Flink中将数据写入外部的过程叫作Sink,Flink支持写出数据
到Kafka、HDFS、HBase、数据库等外部存储。
(7)触发执行
StreamExecutionEnvironment#execute是Flink应用执行的触发入
口,无论是一般的DataStream API开发还是Table &SQL开发都是如
此。
调 用 该 API 之 后 , 才 会 触 发 后 续 的 一 系 列 生 成 StreamGraph 、
JobGraph、ExecutionGraph和任务分发执行的过程。

2.2 API层次
API面向的是开发者,从纵向来看Flink中的API分为4个层次,从
下而上,API层次越高,抽象程度越高,使用起来越方便,灵活性则会

see more please visit: https://homeofpdf.com


降低。
API层次如图2-2所示。

图2-2 Flink API层次


1.核心底层API
核心底层API提供了Flink的最底层的分布式计算构建块的操作
API,包含了ProcessFunction、状态、时间和窗口等操作的API。
ProcessFunction 是 Flink 提 供 的 最 具 表 现 力 的 底 层 功 能 接 口 。
Flink 提 供 单 流 输 入 的 ProcessFunction 和 双 流 输 入 的
CoProcessFunction,能够对单个事件进行计算,也能够按照窗口对时
间进行计算。
ProcessFunction提供对时间和状态的细粒度控制能力,它可以处
理事件时间和处理时间两种时间概念,在时间上定义、修改触发回调
函数的触发器。因此,ProcessFunction可以实现许多有状态计算中的
复杂业务逻辑。
2.核心开发API (DataStream/DataSet API)
实际上,大多数应用程序不需要上述ProcessFunction的低级别抽
象,使用核心开发API(如DataStream API处理实时数据流和DataSet
API处理静态数据集)足以应对绝大部分场景。
DataStream/DataSet使用Fluent风格API,提供了常见数据处理的
API接口,如用户指定的各种转换形式,包括连接(Join)、聚合
(Aggregation)、窗口(Window)、状态(State)等。在这些API中
处理的数据类型以各自的编程语言定义为Class类(Java类或者Scala
类)。同时为了提供灵活性,DataStream/DataSet中也提供了直接使
用底层ProcessFunction的能力,使得一些特定的操作可以实现更低层

see more please visit: https://homeofpdf.com


次的抽象如DataSet API为有界数据集提供了额外的原函数(如循环/
迭代)。
3.声明式DSL API
Table API 是 以 表 为 中 心 的 声 明 式 领 域 专 用 语 言 ( Domain
Specified Language,DSL)。表是关系型数据库的概念,用在批处理
中。在流计算中,为了引入了动态表的概念(Dynamic Table),用来
表达数据流表,在批处理和流计算中统一了表的概念,后边会介绍。
Table API遵循(扩展)关系模型,使用Schema定义元数据(与关
系数据库中的表相似),提供Table API实现SQL操作,如select、
project、join、group-by、aggregate等。Table API表达的是“应该
做什么”的逻辑操作,而不是编写如何处理数据的底层代码。Table
API可以通过各种类型的用户定义的函数进行扩展,虽然不如核心API
表达能力强,但使用起来更加简洁(少写很多代码)。
此外,Table API程序还可以通过在执行之前使用SQL优化器进行
优化。可以在表和DataStream/DataSet之间无缝转换,允许程序中混
合使用Table API和DataStream/DataSet API。
4.结构化API
SQL是Flink的结构化API,是最高层次的计算API,与Table API基
本等价,区别在于使用的方式。SQL与Table API可以混合使用,SQL可
以操作Table API定义的表,Table API也能操作SQL定义的表和中间结
果。
SQL对复杂逻辑的语义表达不如DataStream API,但是SQL也带来
了不少好处。
(1)缩短上线周期
传统的实现流计算的方式是通过流计算平台提供的API进行编程
的,包括确定需求、实现设计、编写代码、进行本地单元测试、进行
集成测试,没有问题后部署上线等流程。整个开发过程中,开发人员
不光要满足业务需求,还需要关注技术实现的细节,而使用SQL的方式
后,开发人员只要关注业务需求即可,技术实现的细节可以交给SQL引
擎去解析、编译、优化。最终,相比传统的通过编码实现流计算的方
式,上线周期可以从数天缩短为数小时。

see more please visit: https://homeofpdf.com


(2)更好地支持流计算需求的演变
随着业务需求持续不断的变化,编码方式的开发、测试、部署上
线的周期不能很快的响应业务需求的变化,使用SQL则能够缩短开发、
测试、部署的周期。
(3)自动调优
查询优化器可以为用户的SQL生成最高效的执行计划。用户不需要
了解它就能自动享受优化器带来的性能提升。
(4)接口稳定
SQL拥有几十年的历史,是一个非常稳定的语言,很少有变动。所
以升级引擎的版本、甚至替换成另一个引擎时,都可以做到兼容并且
平滑地升级。
(5)易于理解
SQL的学习门槛很低,很多不同行业不同领域的人都懂SQL,用SQL
作为跨团队的开发语言可以大大提高效率。
在 Flink1.9 及 以 后 的 版 本 中 , Flink 会 在 API 层 面 上 统 一
DataStream流处理API和DataSet批处理API,DataSet API会逐渐被废
弃,未来会使用DataStream API统一表达流批两种处理,作为流批统
一的计算引擎,这种做法是合理的。Google Cloud Dataflow开源的
Apache Beam提供了一种流批统一API的实现范例。
本书重点阐述DataStream API和Table&SQL API。

2.3 数据流
对Flink这种以流为核心的分布式计算引擎而言,数据流是核心数
据 抽 象 , 表 示 一 个 持 续 产 生 的 数 据 流 , 与 Apache Beam 中 的
PCollection 的 概 念 类 似 。 在 Flink 中 使 用 DataStream 表 示 数 据 流 ,
DataStream是一种逻辑概念,并不是底层执行的概念。DataStream上
定义了常见的数据处理操作API(转换为Transformation),同时也具
备自定义数据处理函数的能力,当DataStream提供的常见操作不满足
需求的时候,可以自定义数据处理的逻辑。
DataStream体系如图2-3所示。

see more please visit: https://homeofpdf.com


图2-3 DataStream体系
DataStreamSource本身就是一个DataStream。DataStreamSink、
AsyncDataStream 、 BroadcastDataStream 、
BroadcastConnectedDataStream 、 QueryableDataStream 都 是 对 一 般
DataStream对象的封装,在DataStream实现特定的功能,接下来对这
些DataStream一一进行介绍。
1. DataStream
DataStream是Flink数据流的核心抽象,其上定义了对数据流的一
系列操作,同时也定义了与其他类型DataStream的相互转换关系。每

see more please visit: https://homeofpdf.com


个DataStream都有一个Transformation对象,表示该DataStream从上
游的DataStream使用该Transformation而来。
2. DataStreamSource
DataStreamSource 是 DataStream 的 起 点 , DataStreamSource 在
StreamExecutionEnvironment 中 创 建 , 由
StreamExecutionEnvironment.addSource ( SourceFunction ) 创 建 而
来,其中SourceFunction中包含了DataStreamSource从数据源读取数
据的具体逻辑。
3. DataStreamSink
数据从DataSourceStream中读取,经过中间的一系列处理操作,
最 终 需 要 写 出 到 外 部 存 储 , 通 过
DataStream.addSink(sinkFunction)创建而来,其中SinkFunction
定义了写出数据到外部存储的具体逻辑。
4. KeyedStream
KeyedStream用来表示根据指定的key进行分组的数据流。一个
KeyedStream 可 以 通 过 调 用 DataStream.keyBy ( ) 来 获 得 。 而 在
KeyedStream上进行任何Transformation都将转变回DataStream。在实
现中,KeyedStream把key的信息写入了Transformation中。每条记录
只能访问所属key的状态,其上的聚合函数可以方便地操作和保存对应
key的状态。
5. WindowedStream & AllWindowedStream
WindowedStream代表了根据key分组且基于WindowAssigner切分窗
口的数据流。所以WindowedStream都是从KeyedStream衍生而来的,在
WindowedStream 上 进 行 任 何 Transformation 也 都 将 转 变 回
DataStream。
6. JoinedStreams & CoGroupedStreams
Join 是 CoGroup 的 一 种 特 例 , JoinedStreams 底 层 使 用
CoGroupedStreams来实现。两者的区别如下。
CoGrouped侧重的是Group,对数据进行分组,是对同一个key上的
两组集合进行操作,可以编写灵活的代码来实现特定的业务功能。
Join侧重的是数据对,对同一个key的每一对元素进行操作。CoGroup

see more please visit: https://homeofpdf.com


更通用,但因为Join是数据库上常见的操作,所以在CoGroup基础上提
供Join的特性。
JoinGroup和CoGroup两者都是对持续不断地产生的数据做运算,
但是又不能无限地在内存中持有数据,对所有的数据进行Join的笛卡
儿积操作理论上不可行(理论上内存不足可以刷出到磁盘,反复的硬
盘读写会导致性能变得很差),所以在底层上,两者都基于Window实
现。
7. ConnectedStreams
ConnectedStreams表示两个数据流的组合,两个数据流可以类型
一样,也可以类型不一样。ConnectedStreams适用于两个有关系的数
据流的操作,共享State。一种典型的场景是动态规则数据处理。两个
流中一个是数据流,一个是随着时间更新的业务规则,业务规则流中
的规则保存在State中,规则会持续更新State。当数据流中的新数据
到来时,使用保存在State中的规则进行数据处理。
8. BroadcastStream & BroadcastConnectedStream
BroadcastStream实际上是对一个普通DataStream的封装,提供了
DataStream的广播行为。
BroadcastConnectedStream 一 般 由 DataStream/KeyedDataStream
与BroadcastStream连接而来,类似于ConnectedStream。
9. IterativeStream
IterativeDataStream是对一个DataStream的迭代操作,从逻辑上
来说,包含IterativeStream的Dataflow是一个有向有环图,在底层执
行层面上,Flink对其进行了特殊处理。
10. AsyncDataStream
AsyncDataStream是个工具,提供在DataStream上使用异步函数的
能力。

2.4 数据流API
DataStream API是Flink流计算应用中最常用的API,相比Table &
SQL API更加底层、更加灵活。

see more please visit: https://homeofpdf.com


2.4.1 数据读取
数据读取的API定义在StreamExecutionEnvironment,这是Flink
流计算应用的起点,第一个DataStream就是从数据读取API中构造出来
的。在Flink中,除了内置的数据读取API外,还针对不同类型的外部
存储系统提供了对应的Connector连接器,使用连接器也能够实现数据
读取的目的。
1.从内存读取数据
Flink提供了一系列的方法,直接在内存中生成数据,方便测试和
演示。API如图2-4所示。

图2-4 内存数据读取API
2.文件读取数据
内置的从文件中读取数据的API如图2-5所示。

图2-5 读取文件API

see more please visit: https://homeofpdf.com


从文件中读取分为读取文本文件和一般文件两类,文本文件无须
多说,一般文件指的是带有结构的文件,如Avro、Parquet等。
文件读取的模式有一次性读取FileProcessingMode.PROCESS_ONCE
和持续读取FileProcessingMode.PROCESS_CONTINUOUSLY。如果不指定
则默认为一次性读取。使用持续读取模式时,可以设定读取间隔,单
位为ms。间隔越小实时性越高,资源消耗相应变多,反之则实时性越
低,资源消耗降低。
3. Socket接入数据
Socket接入数据即从网络端口接收数据。内置的从Socket接入数
据的API如图2-6所示。

图2-6 Socket接入数据API
socketTextStream()的参数比较简单,需要提供hostname(主
机名)、port(端口号)、delimiter(分隔符)和maxRetry(最大重
试次数)。
4.自定义读取
自定义数据读取就是使用Flink连接器、自定义数据读取函数,与
外部存储交互,读取数据,如从Kafka、JDBC、HDFS等读取。自定义数
据读取的API如图2-7所示。

see more please visit: https://homeofpdf.com


图2-7 自定义数据读取API
addSource()方法本质上来说依赖于Flink的SourceFunction体
系 , 与 外 部 的 存 储 进 行 交 互 。 createInput ( ) 方 法 底 层 调 用 的 是
addSource()方法,封装为InputFormatSourceFunction,所以自定
义 读 取 方 式 的 本 质 就 是 实 现 自 定 义 的 SourceFunction 。 关 于
SourceFunction,将在第3章进行详细介绍。
2.4.2 处理数据
DataStream API使用Fluent风格处理数据,在开发的时候其实是
在编写一个DataStream转换过程,形成了DataStream处理链,在Flink
开发章节有过阐述。调用DataStream API生成新的DataStream的转换
关系如图2-8所示。

see more please visit: https://homeofpdf.com


图2-8 DataStream相互转换关系
从图中可以看到,并不是所有的DataStream都可以相互转换。
1. Map
接收1个元素,输出1个元素。Map应用在DataStream上,输出结果
为DataStream。
DataStream#map 运 算 对 应 的 是 MapFunction , 其 类 泛 型 为
MapFunction<T,O>,T代表输入数据类型(Map方法的参数类型),O代
表操作结果输出类型(Map方法返回的数据类型),如代码清单2-1所
示。
代码清单2-1 Map代码示例

see more please visit: https://homeofpdf.com


2. FlatMap
接收1个元素,输出0、1、…、N个元素。该类运算应用在
DataStream上,输出结果为DataStream。
DataStream#flatMap接口对应的是FlatMapFunction,其类泛型为
FlatMapFunction<T,O>,T代表输入数据类型(FlatMap方法的参数类
型),O代表操作结果输出类型,如代码清单2-2所示。
代码清单2-2 FlatMap接口示例

3. Filter
过滤数据,如果返回true则该元素继续向下传递,如果为false则
将 该 元 素 过 滤 掉 。 该 类 运 算 应 用 在 DataStream 上 , 输 出 结 果 为
DataStream。
DataStream#filter 接 口 对 应 的 是 FilterFunction , 其 类 泛 型 为
FilterFunction<T>,T代表输入和输出元素的数据类型,如代码清单
2-3所示。

see more please visit: https://homeofpdf.com


代码清单2-3 Filter代码示例

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代码示例

see more please visit: https://homeofpdf.com


6. Fold
Fold 与 Reduce 类 似 , 区 别 在 于 Fold 是 一 个 提 供 了 初 始 值 的
Reduce,用初始值进行合并运算。该类运算应用在KeyedStream上,输
出结果为DataStream。
Folder接口对应的是FoldFunction,其类泛型为FoldFunction<O,
T>,O为KeyStream中的数据类型,T为初始值类型和Fold方法返回值类
型,如代码清单2-6所示。
代码清单2-6 Fold代码示例

FoldFunction<O, T>已经被标记为Deprecated废弃,替代接口是
AggregateFunction<IN, ACC, OUT>。
7. Aggregation
渐进聚合具有相同Key的数据流元素,以min和minBy为例,min返
回的是整个KeyedStream的最小值,minBy按照Key进行分组,返回每个
分 组 的 最 小 值 。 在 KeyedStream 上 应 用 聚 合 运 算 输 出 结 果 为
DataStream,如代码清单2-7所示。
代码清单2-7 内置聚合运算代码示例

see more please visit: https://homeofpdf.com


8. Window
对KeyedStream的数据,按照Key进行时间窗口切分,如每5秒钟一
个滚动窗口,每个key都有自己的窗口。该类运算应用在KeyedStream
上,输出结果为WindowedStream。
输 出 结 果 的 类 泛 型 为 WindowedStream<T, K, W extends
Window>,T为KeyedStream中的元素数据类型,K为指定Key的数据类
型,W为窗口类型,如代码清单2-8所示。
代码清单2-8 Window代码示例

关于窗口,第4章会有详细讲解。
9. WindowAll
对一般的DataStream进行时间窗口切分,即全局1个窗口,如每5
秒 钟 一 个 滚 动 窗 口 。 应 用 在 DataStream 上 , 输 出 结 果 为
AllWindowedStream,如代码清单2-9所示。
代码清单2-9 WindowAll代码示例

注意:在一般的DataStream上进行窗口切分,往往会导致无法并
行计算,所有的数据会集中到WindowAll算子的一个Task上。

see more please visit: https://homeofpdf.com


关于窗口请参照Window原理和机制章节。
10. Window Apply
将Window函数应用到窗口上,Window函数将一个窗口的数据作为
整体进行处理。Window Stream有两种:分组后的WindowedStream和未
分组的AllWindowedStream。
(1)WindowedStream
在WindowedStream上应用的是WindowFunction,在WindowStream
应用此类运算,输出结果为DataStream。WindowFunction<IN, OUT,
KEY, W extends Window>中的IN表示输入值的类型,OUT表示输出值的
类型,KEY表示Key的类型,W表示窗口的类型,如代码清单2-10所示。
代码清单2-10 WindowFunction代码示例

(2)AllWindowedStream
在AllWindowedStream上应用的是AllWindowFunction,输出结果
为DataStream。该类运算对应的是AllWindowFunction,其类泛型定义
为AllWindowFunction<IN, OUT, W extends Window>,IN表示输入值
的类型,OUT表示输出值的类型,W表示窗口的类型,如代码清单2-11
所示。
代码清单2-11 AllWindowFunction代码示例

see more please visit: https://homeofpdf.com


11. Window Reduce
在 WindowedStream 上 应 用 ReduceFunction , 输 出 结 果 为
DataStream。参见前面的Reduce章节,如代码清单2-12所示。
代码清单2-12 Window Reduce代码示例

12. Window Fold


在 WindowedStream 上 应 用 FoldFunction , 输 出 结 果 为
DataStream,参见前面的Fold章节,如代码清单2-13所示。
代码清单2-13 Window Fold代码示例

see more please visit: https://homeofpdf.com


13. Window Aggregation
统 计 聚 合 运 算 , 在 WindowedStream 应 用 该 运 算 , 输 出 结 果 为
DataStream。
在 WindowedStream 上 应 用 AggregationFunction , 参 见 前 面 的
Aggregations章节,如代码清单2-14所示。
代码清单2-14 内置的Window聚合运算代码示例

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代码示例

see more please visit: https://homeofpdf.com


16. Interval Join
对两个KeyedStream进行Join,需要指定时间范围和Join时使用的
Key,输出结果为DataStream。
例如对于事件e1和e2,Key相同,时间判断条件为:

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代码示例

see more please visit: https://homeofpdf.com


18. Connect
连接(connect)两个DataStream输入流,并且保留其类型,输出
流为ConnectedStream。两个数据流之间可以共享状态。
输出数据流的类泛型为ConnectedStreams<IN1,IN2>,IN1代表第1
个数据流中的数据类型,IN2表示第2个数据流中的数据类型,如代码
清单2-19所示。
代码清单2-19 Connect代码示例

19. CoMap和CoFlatMap
在 ConnectedStream 上 应 用 Map 和 FlatMap 运 算 , 输 出 流 为
DataStream。其基本逻辑类似于在一般DataStream上的Map和FlatMap
运算,区别在于CoMap转换有2个输入,Map转换有1个输入,CoFlatMap
同理,如代码清单2-20所示。
代码清单2-20 CoMap和CoFlatMap代码示例

see more please visit: https://homeofpdf.com


20. Split
将 DataStream 按 照 条 件 切 分 为 多 个 DataStream , 输 出 流 为
SplitDataStream 。 该 方 法 已 经 标 记 为 Deprecated 废 弃 , 推 荐 使 用
SideOutput,如代码清单2-21所示。
代码清单2-21 Split代码示例

see more please visit: https://homeofpdf.com


21. Select
Select 与 Split 运 算 配 合 使 用 , 在 Split 运 算 中 切 分 的 多 个
DataStream中,Select用来选择其中某一个具体的DataStream,如代
码清单2-22所示。
代码清单2-22 Select代码示例

22. Iterate
在 API 层 面 上 , 对 DataStream 应 用 迭 代 会 生 成 1 个
IteractiveStream,然后在IteractiveStream上应用业务处理逻辑,
最终生成1个新的DataStream,IteractiveStream本质上来说是一种中
间数据流对象。
在数据流中创建一个迭代循环,即将下游的输出发送给上游重新
处理。如果一个算法会持续地更新模型,这种情况下反馈循环比较有
用,如代码清单2-23所示。

see more please visit: https://homeofpdf.com


代码清单2-23 Iterate代码示例

23. Extract Timestamps


从 记 录 中 提 取 时 间 戳 , 并 生 成 Watermark 。 该 类 运 算 不 会 改 变
DataStream,如代码清单2-24所示。
代码清单2-24 提取时间戳代码示例

see more please visit: https://homeofpdf.com


24. Project
该类运算只适用于Tuple类型的DataStream,使用Project选取子
Tuple,可以选择Tuple的部分元素,可以改变元素顺序,类似于SQL语
句中的Select子句,输出流仍然是DataStream,如代码清单2-25所
示。
代码清单2-25 Project代码示例

2.4.3 数据写出
数据读取的API绑定在StreamExecutionEnvironment上,数据写出
的API绑定在DataStream对象上。在现在的版本中,只有写到Console
控制台、Socket网络端口、自定义三类,写入文本文件、CSV文件等文
件接口都已被标记为废弃了。接口使用的详细介绍参照官方文档即
可。
自定义数据写出接口是DataStream.addSink,对于Sink的详细介
绍参见连接器和输出函数章节。
2.4.4 旁路输出
旁 路 输 出 在 Flink 中 叫 作 SideOutput , 用 途 类 似 于
DataStream#split,本质上是一个数据流的切分行为,按照条件将
DataStream切分为多个子数据流,子数据流叫作旁路输出数据流,每
个旁路输出数据流可以有自己的下游处理逻辑。如图2-9所示,通过旁
路输出将正常和异常的数据分别记录到不同的外部存储中。

see more please visit: https://homeofpdf.com


图2-9 旁路输出示意
旁路输出数据流的元素的数据类型可以与上游数据流不同,多个
旁路输出数据流的数据类型也不必相同。
当使用旁路输出的时候,首先需要定义OutputTag,OutputTag是
每一个下游分支的标识,其定义如代码清单2-26所示。
代码清单2-26 OutputTag定义
OutputTag<String>表示该旁路输出的数据类型为String。"
side-output-name"是给定该旁路输出的名称。
定义好OutputTag之后,只有在特定的函数中才能使用旁路输出,
具体如下。
1)ProcessFunction。
2)KeyedProcessFunction。
3)CoProcessFunction。
4)ProcessWindowFunction。
5)ProcessAllWindowFunction。
6)ProcessJoinFunction。
7)KeyedCoProcessFunction。
只有在上述函数中才可以通过Context上下文对象,向OutputTag
定义的旁路中输出emit数据。
旁路输出的使用如代码清单2-27所示。
代码清单2-27 旁路输出代码示例

see more please visit: https://homeofpdf.com


旁路输出的数据(DataStream)可以被下游获取,还可以将旁路
输出DataStream当作一般的DataStream进行处理。按照不同的分支进
行不同的业务处理,获取旁路数据的方法如代码清单2-28所示。
代码清单2-28 获取旁路输出

Table & SQL 的 语 义 中 多 条 Insert 语 句 一 起 执 行 , 使 用 不 同 的


Where条件输出到不同的目的地,这就是SideOutput旁路输出的适用场
景。

2.5 总结

see more please visit: https://homeofpdf.com


本章介绍了Flink应用的大体框架、Flink API的层次、常用数据
流操作的API等,此处并没有特别详细地介绍如何开发、如何使用
API,这不是本书的重点。

see more please visit: https://homeofpdf.com


第3章 核心抽象
Flink API提供了开发的接口,此外,为了实现业务逻辑,还必须
为开发者提供自定义业务逻辑的能力。Flink中设计了用户自定义函数
体系(User Defined Function,UDF),开发人员实现业务逻辑就是
开发UDF,为了更加精确清晰地表达概念,本书的后续章节使用UDF来
表示用户自定义函数。
Flink的应用采用流水线的形式,而用户编写的UDF表达的是业务
逻辑处理,并不关心数据的上下游关系,为了表达上下游的关系,
Flink引入了Transformation的概念,Transformation中记录了上游的
数据来源,将用户处理逻辑组织成流水线。Transformation是一个逻
辑概念,并不关心数据的物理来源、序列化、数据转发等一系列执行
时刻的问题,所以在运行时Flink引入了算子,从上游获取数据、交给
UDF执行并将UDF执行结果交给下游,同时还提供了容错方面的支持。
有了Flink开发接口、函数体系,Flink运行时的体系还有一个比
较重要的抽象:环境对象。环境对象提供了开发的入口、配置信息的
传递等,在作业提交、部署、运行时可以获取这些配置信息。

3.1 环境对象
StreamExecutionEnvironment是Flink应用开发时的概念,表示流
计算作业的执行环境,是作业开发的入口、数据源接口、生成和转换
DataStream的接口、数据Sink的接口、作业配置接口、作业启动执行
的入口。
Environment 是 运 行 时 作 业 级 别 的 概 念 , 从
StreamExecutionEnvironment中的配置信息衍生而来。进入到Flink作
业执行的时刻,作业需要的是相关的配置信息,如作业的名称、并行
度、作业编号Job ID、监控的Metric、容错的配置信息、IO等,用
StreamExecutionRuntime对象就不合适了,很多API是不需要的,所以
在Flink中抽象出了Environment作为运行时刻的上下文信息。

see more please visit: https://homeofpdf.com


RuntimeContext是运行时Task实例级别的概念。Environment本身
仍然是比较粗粒度作业级别的配置,对于每一个Task而言,其本身有
更细节的配置信息,所以Flink又抽象了RuntimeContext,每一个Task
实 例 有 自 己 的 RuntimeContext , RuntimeContext 的 信 息 实 际 上 是
StreamExecutionEnvironment中配置信息和算子级别信息的综合。
3种环境对象之间的关系如图3-1所示。

图3-1 3种环境对象的关系
对 于 开 发 者 而 言 , StreamExecutionEnvironment 在 作 业 开 发 的
Main函数中使用,RuntimeContext在UDF开发中使用,Environment则
起到衔接StreamExecutionEnvironment和RuntimeContext的作用。
3.1.1 执行环境
执行环境是Flink作业开发、执行的入口,当前版本Flink的批流
在API并没有统一,所以有流计算(StreamExecutionEnvironment)和
批处理(ExecutionEnvironment)两套执行环境。在本书中,主要介
绍流计算应用执行环境。
流计算执行环境体系如图3-2所示。

see more please visit: https://homeofpdf.com


图3-2 StreamExcecutionEnvironment类体系
StreamExecutionEnvironment是Flink流计算应用的执行环境,是
Flink 作 业 开 发 和 启 动 执 行 的 入 口 , 开 发 者 对
StreamExecutionEnvironment的实现是无感知的。
1. LocalStreamEnvironment
本地执行环境,在单个JVM中使用多线程模拟Flink集群。
一般用作本地开发、调试。使用Idea之类的IDE工具,可以比较方
便地在代码中设置断点调试和单元测试。如果测试没有问题,就可以
提交到真正的生产集群。
其基本的工作流程如下。
1 ) 执 行 Flink 作 业 的 Main 函 数 生 成 Streamgraph , 转 化 为
JobGraph。
2)设置任务运行的配置信息。
3)根据配置信息启动对应的LocalFlinkMiniCluster。
4)根据配置信息和miniCluster生成对应的MiniClusterClient。
5)通过MiniClusterClient提交JobGraph到MiniCluster。
2. RemoteStreamEnvironment
在大规模数据中心中部署的Flink生成集群的执行环境。
当 将 作 业 发 布 到 Flink 集 群 的 时 候 , 使 用
RemoteStreamEnvironment。
其基本的工作流程如下:

see more please visit: https://homeofpdf.com


1 ) 执 行 Flink 作 业 的 Main 函 数 生 成 Streamgraph , 转 化 为
JobGraph。
2)设置任务运行的配置信息。
3)提交JobGraph到远程的Flink集群。
3. StreamContextEnvironment
在Cli命令行或者单元测试时候会被使用,执行步骤同上。
4. StreamPlanEnvironment
在Flink Web UI管理界面中可视化展现Job的时候,专门用来生成
执行计划(实际上就是StreamGraph),如图3-3所示。

图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所示。

see more please visit: https://homeofpdf.com


图3-4 Environment类体系
其有两个实现类RuntimeEnvironment和SavepointEnvironment。
1. RuntimeEnvironment
在Task开始执行时进行初始化,把Task运行相关的信息都封装到
该对象中,其中不光包含了配置信息,运行时的各种服务也会被包装
到其中,如代码清单3-1所示。
代码清单3-1 Task初始化RuntimeEnvironment

see more please visit: https://homeofpdf.com


2. SavepointEnvironment
SavepointEnvironment是Environment的最小化实现,在状态处理
器 的 API 中 使 用 。 Flink1.9 版 本 引 入 的 状 态 处 理 器 ( State
Processor)API真正改变了这一现状,实现了对应用程序状态的操
作。该功能借助DataSet API扩展了输入和输出格式以读写保存点或检
查点数据。由于DataSet和Table API的互通性,用户甚至可以使用关
系表API或SQL查询来分析和处理状态数据。
3.1.3 运行时上下文
RuntimeContext是Function运行时的上下文,封装了Function运
行时可能需要的所有信息,让Function在运行时能够获取到作业级别

see more please visit: https://homeofpdf.com


的 信 息 , 如 并 行 度 相 关 信 息 、 Task 名 称 、 执 行 配 置 信 息
(ExecutionConfig)、State等。
Function 的 每 个 实 例 都 有 一 个 RuntimeContext 对 象 , 在
RichFunction中通过getRunctionContext()可以访问该对象。
RuntimeContext的类体系如图3-5所示。
不同的使用场景中有不同的RuntimeContext,具体如下。
1)StreamingRuntimeContext:在流计算UDF中使用的上下文,用
来访问作业信息、状态等。
2)DistributedRuntimeUDFContext:由运行时UDF所在的批处理
算子创建,在DataSet批处理中使用。
3)RuntimeUDFContext:在批处理应用的UDF中使用。
4)SavepointRuntimeContext:Flink1.9版本引入了一个很重要
的状态处理API,这个框架支持对检查点和保存点进行操作,包括读
取、变更、写入等。

see more please visit: https://homeofpdf.com


图3-5 RuntimeContext类体系
5)CepRuntimeContext:CEP复杂事件处理中使用的上下文。
另外,在一些场景中不需要将RuntimeContext中的信息完全暴
露,只需要其中某一部分信息,或者需要使用RuntimeContext之外的
一些其他信息,这两种情况下,需要对RuntimeContext再进行一次封
装。

3.2 数据流元素
数 据 流 元 素 在 Flink 中 叫 作 StreamElement , 有 数 据 记 录
StreamRecord、延迟标记Latency Marker、Watermark、流状态标记
StreamStatus这4种,分别有各自不同的用途。
在执行层面上,4种数据流元素都被序列化成二进制数据,形成混
合的数据流,在算子中将混合数据流中的数据流元素反序列化出来,

see more please visit: https://homeofpdf.com


根据其类型分别进行处理。
StreamElement类体系如图3-6所示。

图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

see more please visit: https://homeofpdf.com


用 来 通 知 Task 是 否 会 继 续 接 收 到 上 游 的 记 录 或 者 Watermark 。
StreamStatus在数据源算子中生成,向下游沿着Dataflow传播。
StreamStatus可以表示两种状态:
1)空闲状态(IDLE)。
2)活动状态(ACTIVE)。

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,可以持久保存状态。

see more please visit: https://homeofpdf.com


图3-7 DataStream流水线到Transformation流水线

图3-8 虚拟Transformation被优化后的算子树

see more please visit: https://homeofpdf.com


3)bufferTimeout:buffer超时时间。
4)parallelism:并行度。
5)id:跟属性uid无关,生成方式是基于一个静态累加器。
6)outputType: 输出类型,用来进行序列化数据。
7 ) slotSharingGroup: 给 当 前 的 Transformation 设 置 Slot 共 享
组。Slot共享参见第9章。
1.物理Transformation
物理Transformation一共有4种,具体如下。
(1)SourceTransformation
从 数 据 源 读 取 数 据 的 Transformation , 是 Flink 作 业 的 起 点 。
SourceTransformation 只 有 下 游 Transformation , 没 有 上 游 输 入
Transformation。

图3-9 Transformation类体系

see more please visit: https://homeofpdf.com


一个作业可以有多个SourceTransformation,从多个数据源读取
数据,如多流Join、维表Join、BroadcastState等场景。
(2)SinkTransformation
将数据写到外部存储的Transformation,是Flink作业的终点。
SinkTransformation 只 有 上 游 Transformation , 下 游 就 是 外 部 存 储
了。
一个作业内可以有多个SinkTransformation,将数据写入不同的
外部存储汇总,如计算结果的热数据写入Redis实时查询,历史数据写
入HDFS。
(3)OneInputTransformation
单流输入的Transformation(只接收一个输入流),跟上面的
SinkTransformation构造器类似,同样需要input和operator参数。
(4)TwoInputTransformation
双流输入的Transformation(接收两种流作为输入),分别叫作
第1输入和第2输入。其他的实现同OneInputTransformation,如图3-
10所示。
从上图示例中可以看到,TwoInputTransformation的两个上游输
出的类型不同。此类型的两个上游输入的数据类型可以相同也可以不
同。
2.虚拟Transformation
(1)SideOutputTransformation
SideOutputTransformation 在 旁 路 输 出 中 转 换 而 来 , 表 示 上 游
Transformation 的 一 个 分 流 , 上 游 Transformation 可 以 有 多 个 下 游
SideOutputTransformation,如图3-11所示。
上 游 的 OneInputTransformation 分 流 给 下 游 的 多 个
SideOutputTransformation,每一个SideOutput通过OutputTag进行标
识。

see more please visit: https://homeofpdf.com


图3-10 TowInputTransformation示例

图3-11 SideOutput示例
(2)SplitTransformation
用来按条件切分数据流,该转换用于将一个流拆分成多个流(通
过OutputSelector来达到这个目的),当然这个操作只是逻辑上的拆
分(它只影响上游的流如何跟下游的流连接)。
构造该转换器,同样也依赖于其输入转换器(input)以及一个输
出 选 择 器 ( outputSelector ) , 但 会 实 例 化 其 父 类
(StreamTransformation,没有提供自定义的名称,而是固定的常量
值Split)。
(3)SelectTransformation
与 SplitTransformation 配 合 使 用 , 用 来 在 下 游 选 择
SplitTransformation切分的数据流。
(4)PartitionTransformation
该转换器用于改变输入元素的分区,其名称为Partition。因此,
工作时除了提供一个StreamTransformation作为输入外,还需要提供
一个StreamPartitioner的实例来进行分区。

see more please visit: https://homeofpdf.com


PartitionTransformation 需 要 特 别 提 及 一 下 , 它 在 Flink
DataStream Runtime中和Blink的流处理、批处理的都被使用了。其有
一个ShuffleMode,用来统一表示流、批数据Shuffle的模式。对于流
而 言 , ShuffleMode 是 ShuffleMode.PIPELINED; 对 于 批 而 言 ,
ShuffleMode是ShuffleMode.BATCH。
(5)UnionTransformation
合并转换器,该转换器用于将多个输入StreamTransformation进
行合并,因此该转换器接收StreamTransformation的集合,其名称也
在内部被固定为Union,如图3-12所示。

图3-12 UnionTransformation示例
Union运算要求其直接上游输入的数据的结构必须是完全相同的。
(6)FeedbackTransformation
表示Flink DAG中的一个反馈点。简单来说,反馈点就是把符合条
件的数据重新发回上游Transformation处理,一个反馈点可以连接一
个或者多个上游的Transformation,这些连接关系叫作反馈边。处于
反馈点下游的Transformation将可以从反馈点和反馈边获得元素输
入。符合反馈条件并交给上游的Transformation的数据流叫作反馈流
(Feedback DataStream),如图3-13所示。

see more please visit: https://homeofpdf.com


图3-13 FeedbackTransformation示意图
FeedbackTransformation的固定名称为Feedback,有两个重要参
数:
1)input: 上游输入StreamTransformation。
2)waitTime: 默认为0,即永远等待,如果设置了等待时间,一
旦超过该等待时间,则计算结束并且不再接收数据。
实例化FeedbackTransformation时,会自动创建一个用于存储反
馈 边 的 集 合 feedbackEdges 。 那 么 反 馈 边 如 何 收 集 呢 ?
FeedbackTransformation通过定义一个实例方法——addFeedbackEdge
来 进 行 收 集 。 而 这 里 所 谓 的 “ 收 集 ” 就 是 将 下 游
StreamTransformation的实例加入feedbackEdges集合中(这里可以理
解为将两个点建立连接关系,也就形成了边)。不过,这里加入的
StreamTransformation 的 实 例 有 一 个 要 求 : 当 前
FeedbackTransformation的实例跟待加入StreamTransformation实例
的并行度应一致。
(7)CoFeedbackTransformation
CoFeedbackTransformation 与 FeedbackTransformation 类 似 , 也
是 Flink DAG 中 的 一 个 反 馈 点 。 两 者 的 不 同 之 处 在 于 ,
CoFeedBackTransformation 反 馈 给 上 游 的 数 据 流 与 上 游
Transformation的输入类型不同,所以要求上游的Transformation必
须 是 TwoInputTransformation 。 CoFeedbackTransformation 是 从
ConnectedIterativeStreams 创 建 而 来 的 , 而
ConnectedIterativeStreams 由 IterativeStream#withFeedbackType 设
置新的反馈流的数据类型而来,如图3-14所示。

图3-14 CoFeedbackTransformation示意图
在图3-14中,可以看到TwoInputTransformation有两个输入,第1
输 入 的 类 型 为 Tuple<String,Long> , 反 馈 流 的 输 入 类 型 为

see more please visit: https://homeofpdf.com


Tuple<String,Long,Integer>。
Transformation作为中介,负责将执行时刻初始化作业所需要的
StreamTask类和算子工程(StreamOperatorFactory)构建好,算子作
为UDF的执行时容器,这样就能将作业开发和作业运行联系起来了。
Transformation、算子、UDF的关系如图3-15所示。
在本例中,OneInputTransformation包装了算子StreamMap,算子
StreamMap 又 包 装 了 UDF 。 在 逻 辑 上 , 下 游 Transformation 连 接 上 游
Transformation , 逻 辑 上 数 据 是 从 上 游 Trans formation 流 向 下 游
Transformation,实际上是从算子流向算子,在算子内部交给UDF处
理。

图3-15 Transformation、算子、UDF的关系
图 3-15 是 OneInputTransformation 的 示 例 ,
TwoInputTransformation与此相同,接下来介绍算子、UDF。

3.4 算子
算子在Flink中叫作StreamOperator。StreamOperator是流计算的
算子。Flink作业运行时由Task组成一个Dataflow,每个Task中包含一
个或者多个算子,1个算子就是1个计算步骤,具体的计算由算子中包
装的Function来执行。除了业务逻辑的执行算子外,还提供了生命周
期的管理。

see more please visit: https://homeofpdf.com


Flink的DataStream和DataSet有两套不同的算子体系,未来发展
重点是以算子来取代DataSet的算子,实现流批算子的统一,所以在本
书中如果不特指,算子指的就是流算子,批处理的算子未来会被流算
子取代。
阿里巴巴自收购了Flink的商业公司以来,逐渐将Blink的改进优
化合并到Flink中。Blink合并进来之后对Table模块做了比较大的重
构,引入了flink-table-runtime-blink模块,该模块中实现了一批新
的算子。以流计算为基础,Blink SQL中实现了流批算子的统一。
本节介绍如下所示的两套算子。
(1)Flink DataStream和Flink SQL的算子体系
在flink-streaming-java中的算子实现的算子体系,用来支撑原
有的DataStream API编程和旧的Flink SQL。
(2)Blink SQL算子体系
在flink-table-runtime-blink中的流批通用的算子体系,用来支
撑Blink SQL,Blink SQL中同时也使用了Flink-streaming-java和CEP
模块的一些算子。
3.4.1 算子行为
所有的算子都包含了生命周期管理、状态与容错管理、数据处理3
个方面的关键行为。
1.生命周期管理
所有的算子都有共同的生命周期管理,其核心生命周期阶段如
下。
1)setup:初始化环境、时间服务、注册监控等。
2)open:该行为由各个具体的算子负责实现,包含了算子的初始
化逻辑,如状态初始化等。算子执行该方法之后,才会执行Function
进行数据的处理。
3)close:所有的数据处理完毕之后关闭算子,此时需要确保将
所有的缓存数据向下游发送。
4)dispose:该方法在算子生命周期的最后阶段执行,此时算子
已经关闭,停止处理数据,进行资源的释放。

see more please visit: https://homeofpdf.com


StreamTask作为算子的容器,负责管理算子的生命周期。
2.状态与容错管理
算子负责状态管理,提供状态存储,触发检查点的时候,保存状
态快照,并且将快照异步保存到外部的分布式存储。当作业失败的时
候算子负责从保存的快照中恢复状态。
3.数据处理
算子对数据的处理,不仅会进行数据记录的处理,同时也会提供
对Watermark和LatencyMarker的处理。算子按照单流输入和双流输
入,定义了不同的行为接口。
(1)OneInputStreamOperator
OneInputStreamOperator是单输入算子,对输入流提供了3个关键
处理接口,分别对数据、Watermark、LatencyMarker进行处理,如代
码清单3-2所示。
代码清单3-2 OneInputStreamOperator接口

(2)TowInputStreamOperator
TwoInputStreamOperator是双流输入算子,对于两个输入流提供
了6个关键处理接口:
1)数据处理processElement1、processElement2接口,分别对应
上游两个输入流的数据记录。
2)Watermark处理processWatermark1、processWatermark2接口
分别对应上游两个输入流的Watermark。
3 ) LatencyMark 处 理 processLatencyMarker1 、
processLatencyMarker2 接 口 分 别 对 应 上 游 两 个 输 入 流 的
LatencyMarker,如代码清单3-3所示。

see more please visit: https://homeofpdf.com


代码清单3-3 TwoInputStreamOperator接口

(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)单流输入算子

see more please visit: https://homeofpdf.com


该类型算子只接收上游1个数据流作为输入,一般的算子都属此类
型。
此 类 算 子 包 含 StreamProject 、 StreamFilter 、 StreamMap 、
StreamFlatMap 、 StreamSink 、 StreamGroupedReduce 、
StreamGroupedFold、KeyedProcessOperator和ProcessOperator。
从算子的名称就能看其与DataStream API中接口的对应关系。
(2)双流输入算子
该 算 子 与 TwoInputTransformation 对 应 , 接 收 上 游 2 个 不 同 的
DataStream作为输入,如CoGroup、Join类操作。
此 类 算 子 包 含 CoStreamMap 、 CoStreamFlatMap 、
CoProcessOperator 、 KeyedCoProcessOperator 、
IntervalJoinOperator 、 CoBroadcastWithKeyedOperator 和
CoBroadcastWithNonKeyedOperator。
从算子的名称就能看其与DataStream API中接口的对应关系。
注意:并不存在2个以上输入流的算子。
(3)数据源算子
无 输 入 流 的 算 子 只 有 一 个 , 就 是 StreamSource , Source 是
DataStream的起点,其从数据源读取数据,向下游发送数据,只有输
出流。
此类算子之所以只有StreamSource 1个实现,是因为该类算子直
接从外部存储读取数据,是Dataflow的起点,并没有上游算子。
(4)异步算子AsyncWaitOperator
将在后边章节中详细介绍。

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
图3-16 Flink算子体系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等。

see more please visit: https://homeofpdf.com


3)通过动态代码生成的算子。
Blink Runtime中的算子并不是完全的流批通用,流批算子最大的
差异就是数据是否有界,所有支持批的算子都实现了BoundedOneInput
接 口 或 者 BoundedMultiInput 接 口 。 这 两 个 接 口 中 有 一 个 关 键 方 法
endInput用来标识数据是否是有界,如代码清单3-4所示。
代码清单3-4 endInput方法

see more please visit: https://homeofpdf.com


图3-18 Blink算子体系1
1. Blink内置算子
(1)Join算子
1)HashJoin算子。Blink中普通的Join目前实现了Hash Join,有
7 个 实 现 类 , 分 别 为 InnerHashJoinOperator 、
BuildOuterHashJoinOperator 、
BuildLeftSemiOrAntiHashJoinOperator 、
ProbeOuterHashJoinOperator 、 FullOuterHashJoinOperator 、
AntiHashJoinOperator、SemiHashJoinOperator。

see more please visit: https://homeofpdf.com


2)维表Join(Lookup Join)。是通过代码生成的方式实现的。
(2)Temporal Join
在 ANSI-SQL 2011 中 提 出 了 Temporal 的 概 念 , Oracle 、
SQLServer、DB2等大型数据库厂商也先后实现了这个标准。Temporal
Table记录了历史上任何时间点的所有数据改动,Temporal Join就是
按照时间找到该事件点对应的数据进行维表JOIN的。
Flink实现了两种算子:TemporalProcessTimeJoinOperator基于
处理时间的Temporal Join、TemporalRowTimeJoinOperator基于事件
时间的Temporal Join。
(3)Sort算子
排序算子,分为流批两种,排序是一种全局操作,所以对于流批
而言,无法共用相同的算子实现。
流上算子包括RowTimeSortOperator和ProcTimeSortOperator,分
别是在事件时间和处理时间上进行排序的算子,支持时间+其他排序字
段。

see more please visit: https://homeofpdf.com


图3-19 Blink算子体系2
(4)OverWindow算子
OverWindow运算的算子,包含两种实现:

see more please visit: https://homeofpdf.com


1)BufferDataOverWindowOperator:Over开窗运算经常需要用当
前数据跟之前N条数据一起计算,所以需要采用将之前的数据缓存起来
的方式,在内存不足的情况下会自动溢出到磁盘。
2)NonBufferOverWindowOperator:该算子应用于rank等不需要
跟之前N条数据一起计算的开窗运算,无须缓存数据,可以提高计算效
率。
(5)Window算子
流上窗口算子有两个:
1)AggregateWindowOperator:使用普通聚合函数(UDAF)的窗
口算子。
2)TableAggregateWindowOperator:使用表聚合函数(UDTAF)
的窗口算子。
SQL上的UDF介绍参见后边的Flink SQL章节。
(6)Watermark算子
Watermark算子用在流上负责生成Watermark,有3个实现:
1)WatermarkAssignerOperator:从数据元素中提取时间戳,周
期性地生成Watermark。
2 ) RowTimeMiniBatchAssignerOperator : 用 在 mini-batch 模 式
下,依赖上游的Watermark,基于事件时间周期性地生成Watermark。
3 ) ProcTimeMiniBatchAssignerOperator : 用 在 mini-batch 模 式
下,基于处理时间周期性地生成Watermark,不依赖上游。
(7)Mini-batch算子
Mini-batch算子用微批来提升计算效率,提高吞吐量。使用Java
的Map来缓存数据,Map的Key与State的Key保持一致,在进行聚合运算
的时候可以批量操作,避免每一条数据都访问State。有2个实现:
1)MapBundleOperator:应用于未按照Key分组的数据流。
2)KeyedMapBundleOperator:应用于按照Key分组后的数据流,
即KeyedStream。
2.批上算子
1)SortOperator:实现批上的全局数据排序。

see more please visit: https://homeofpdf.com


2)SortLimitOperator:实现批上的带有Limit的排序。
3)LimitOperator:实现批上的limit语义。
4)RankOperator:实现批上的Top N语义。
3.其他模块的算子
(1)通用算子
对于StreamMap、StreamFlatMap等比较通用的算子,Blink直接使
用了Flink DataStream体系中定义的算子,并没有重复实现。
(2)ProcessOperator & KeyedProcessOperator
ProcessOperator 和 KeyedProcessOperator 执 行 比 较 底 层 的
ProcessFunction,Join、LookupJoin、Window Join、去重、聚合、
Limit、Rank、Sort等运算中都使用到了该算子。
(3)CEPOperator
SQL2016标准中加入了MatchRecognize语义,即SQL CEP复杂事件
处理,其底层对应的就是CEPOperator。在Blink Runtime中,受限于
Calcite,对MatchRecognize语义提供了基本的支持,没有支持完整的
MatchRecognize语义。
4.代码生成算子
对于像SQL中的Filter、Project、Correlate等这一类比较简单的
运算,可以通过代码动态生成算子。关于代码生成的内容,请参考
Table & SQL中Blink Planner的代码生成章节。
3.4.4 异步算子
异步算子的目的是解决与外部系统交互时网络延迟所导致的系统
瓶颈问题。
流计算系统中经常需要与外部系统进行交互,如需要查询外部数
据库以关联上用户的额外信息。通常的实现方式是向数据库发送用户a
的查询请求(如在MapFunction中),然后等待结果返回,在这之前无
法发送用户b的查询请求。这是一种同步访问的模式,如图3-20左边所
示。

see more please visit: https://homeofpdf.com


图3-20中没有字母的空白长条表示等待时间,可以发现网络等待
时间极大地阻碍了吞吐和延迟。为了解决同步访问的问题,异步模式
可以并发地处理多个请求和回复。也就是说,可以连续地向数据库发
送用户a、b、c等的请求,与此同时,哪个请求的回复先返回,就处理
哪个回复,从而使连续的请求之间不需要阻塞等待,如图3-20右边所
示。这也正是Async I/O的实现原理。

图3-20 同步IO与异步IO
既然是异步请求,那么就存在后调用的请求先返回的情况,所以
为了更好地适应实际场景,Flink在异步算子中提供了两种输出模式。
(1)顺序输出模式
先收到的数据元素先输出,后续数据元素的异步函数调用无论是
否先完成,都需要等待。顺序输出模式可以保证消息不乱序,但是可
能增加延迟、降低算子的吞吐量,其原理如图3-21所示。

see more please visit: https://homeofpdf.com


图3-21 异步算子顺序输出
数据进入算子,对每一条数据元素调用异步函数
(AsyncFunction,后边会介绍),并封装为ResultFuture放入到队列
中 , 如 R1 表 示 第 1 条 数 据 元 素 异 步 调 用 的 ResultFuture , 如 果 是
Watermark,也会放入到队列中(图中是W1)。输出时则严格按照R1、
R2、R3、W1、R4的顺序输出给下游,如R3比R1先完成了,也需要等待
R1返回结果,R1先输出。注意,W1不允许提前输出,即必须等待R1、
R2、R3首先输出。
(2)无序输出模式
简单来讲,就是先处理完的数据元素先输出,不保证消息顺序,
相比顺序模式,无序输出模式算子延迟低、吞吐量更高,其原理如图
3-22所示。

see more please visit: https://homeofpdf.com


图3-22 异步算子无序输出
数据进入算子,对每一条数据元素调用异步函数
(AsyncFunction),并封装为ResultFuture放入到队列中,如R1表示
第1条数据元素异步调用的ResultFuture,如果是Watermark也会放入
到队列中(图中是W1),当异步函数返回结果时,放入已完成队列,
按照顺序输出给下游。
无序输出模式并不是完全的无序,仍然要保持Watermark不能超越
其前面数据元素的原则。等待完成队列中将按照Watermark切分成组,
组内可以无序输出,组之间必须严格保证顺序。
如图3-22所示,R1、R2、R3属于一个组,R4、R5属于一组。在异
步顺序输出时严格按照[R1、R2、R3]、W1、[R4、R5]的顺序输出
给下游,其中R1、R2、R3可以无序输出。图中R2先于R1完成,被放入
已完成队列,W1则需要等待[R1、R2、R3]都进入完成队列之后,才
能进入已完成队列,同样[R4、R5]必须等待W1进入已完成队列,才
能进入已完成队列。
在实际场景中,可以根据业务需要选择输出模式。
异步算子同时也支持对异步函数调用的超时处理,支持完整的容
错特性。

see more please visit: https://homeofpdf.com


3.5 函数体系
函数在Flink中叫作Function,开发者编写的函数叫作UDF(User
Defined Function),当然Flink对于通用场景也内置了大量的预定义
的通用UDF来简化开发,如Join、GroupBy、Sum等SQL语义等价的UDF。
UDF在Flink的DataStream开发和SQL开发中被广泛使用。开发者使用
UDF主要是实现非通用的计算逻辑,一般是业务逻辑。在本书语境中,
UDF、Function、用户自定义函数的含义是相同的。
按照输入和输入的不同特点分类,Flink中的UDF大概分为3类(见
图3-23)。
1. SourceFunction
无 上 游 Function , SourceFunction 直 接 从 外 部 数 据 存 储 读 取 数
据,所以SourceFunction所在的算子是起始,没有上游算子。

图3-23 Function分类与关系
2. SinkFunction
无下游Function,SinkFunction直接将数据写入外部存储,所以
Sink函数所在的算子是作业的重点,没有下游算子。
3. 一般Function
一 般 的 UDF 函 数 用 在 作 业 的 中 间 处 理 步 骤 中 , 其 接 口 定 义 与
SourceFunction和SinkFunction不同。一般UDF所在的算子有上游算
子,也有下游算子。
Flink的一般UDF有单流输入和双流输入两种,从UDF输入、输出的
模型来说,多流输入可以通过多个双流输入串联而成,这种设计比较
简单实用,如图3-24所示。

see more please visit: https://homeofpdf.com


图3-24 多流输入转换为多层双流输入
SourceFunction和SinkFunction主要在Flink中的连接器使用,也
会在自定义读取、写出数据的时候使用。其余的大量实现逻辑的函数
都属于一般UDF。
3.5.1 函数层次
UDF在DataStream API层使用,Flink提供的函数体系从接口的层
级来看,从高阶Function到低阶Function如图3-25所示。

图3-25 Function层次
Flink 内 置 的 DataStream 上 的 API 接 口 , 如 DataStream#map 、
DataStream#flatMap、DataStreamFilter#filter等,使用的都是高阶
函数,开发者使用高阶函数的时候,无须关心定时器之类的底层概
念,只需要关注业务逻辑即可。低阶函数即ProcessFunction。
无 状 态 Function 用 来 做 无 状 态 计 算 , 使 用 比 较 简 单 , 如
MapFunction 。 无 状 态 Function 和 RichFunction 是 一 一 对 应 的 , 如
MapFunction对应RichMapFunction,如代码清单3-5所示。
代码清单3-5 MapFunction代码示例

see more please visit: https://homeofpdf.com


从上边的代码可以看出来,使用MapFunction只需实现Map方法即
可,所以无状态Function一般都是直接继承接口,如Map接口,或者通
过匿名类实现接口。
RichFunction相比无状态Function,有两方面的增强:
1)增加了open和close方法来管理Function的生命周期,在作业
启动时,Function在open方法中执行初始化,在Function停止时,在
close方法中执行清理,释放占用的资源等。无状态Function不具备此
能力。
2 ) 增 加 了 getRuntimeContext 和 setRuntimeContext 。 通 过
RuntimeContext,RichFunction能够获取到执行时作业级别的参数信
息,而无状态Function不具备此能力。
无状态Function天然是容错的,作业失败之后,重新执行即可,
但是有状态的Function(RichFunction)需要处理中间结果的保存和
恢复,待有了状态的访问能力,也就意味着Function是可以容错的,
执行过程中,状态会进行快照然后备份,在作业失败,Function能够
从快照中恢复回来。
3.5.2 处理函数
处理函数(ProcessFunction)可以访问流应用程序所有(非循
环)基本构建块:
1)事件(数据流元素)。
2)状态(容错和一致性)。
3)定时器(事件时间和处理时间)。
ProcessFunction根据场景不同有几种实现,如图3-26所示。

see more please visit: https://homeofpdf.com


图3-26 ProcessFunction类体系
1)ProcessFunction:单流输入函数。
2)CoProcessFunction:双流输入函数。
3)KeyedProcessFunction:单流输入函数。
4)KeyedCoProcessFunction:双流输入函数。
Kyed ProcessFunction与Non-Keyed ProcessFunction的区别是,
Keyed ProcessFunction只能用在KeyedStream上。
ProcessFunction 和 CoProcessFunction 的 区 别 是 ,
CoProcessFunction是双流输入,而ProcessFunction是单流输入。
1.双流Join
下面是使用CoProcessFunction实现双流Join的例子。
(1)即时双流Join
其逻辑如下(见图3-27)。
1)创建1个State对象。
2)接收到输入流1事件后更新State。
3)接收到输入流2的事件后遍历State,根据Join条件进行匹配,
将匹配后的结果发送到下游。

see more please visit: https://homeofpdf.com


图3-27 双流即时Join
(2)延迟双流Join
在流式数据里,数据可能是乱序的,数据会延迟到达,并且为了
提供处理效率,使用小批量计算模式,而不是每个事件触发一次Join
计算,如图3-28所示。

图3-28 双流延迟Join
其逻辑如下。
1)创建2个State对象,分别缓存输入流1和输入流2的事件。
2)创建1个定时器,等待数据的到达,定时延迟触发Join计算。
3)接收到输入流1事件后更新State。

see more please visit: https://homeofpdf.com


4)接收到输入流2事件后更新State。
5)定时器遍历State1和State2,根据Join条件进行匹配,将匹配
后的结果发送到下游。
2.延迟计算
在上面的延迟Join示例中,使用了计时器来暂存一批数据之后再
触发计算,在流计算中这是非常常见的场景。在前面提到的批流合一
的关键概念中,关键是Watermark和Window,在Flink中的窗口计算
(WindowOperator)就是典型的延迟计算,使用Window暂存数据,使
用 Watermark 触 发 Window 的 计 算 , 如 图 3-29 所 示 。 在 Blink Table &
SQL中也大量使用了定时器。

图3-29 延迟计算过程
触发器在算子层面上提供支持,所有支持延迟计算的算子都继承
了Triggerable接口。Triggerable接口主要定义了基于事件时间和基
于处理时间的两种触发行为,如代码清单3-6所示。
代码清单3-6 Triggerable接口

see more please visit: https://homeofpdf.com


3.5.3 广播函数
前边介绍了单输入Function和双输入Function。在Flink 1.5.0版
本中引入了广播状态模式,将一个数据流的内容广播到另一个流中,
同时也引入了新的函数类型:广播函数。
广播函数的体系如图3-30所示。
在图3-30中可以看到,广播函数有BroadcastProcessFunction和
KeyedBroadcastProcess Function,广播函数跟双流输入的处理函数
类似,也有两个数据元素处理的接口,processElement()负责处理
一般的数据流,processBroadcastElement()负责处理广播数据流。
完整定义如代码清单3-7和代码清单3-8所示。

图3-30 广播函数体系
代码清单3-7 BroadcastProcessFunction抽象类

see more please visit: https://homeofpdf.com


代码清单3-8 BroadcastProcessFunction类

上 面 的 两 个 广 播 函 数 BroadcastProcessFunction 和
KeyedBroadcastProcessFunction都是抽象类,所以在实际使用中,开
发者需要实现其定义的抽象方法。
processElement()方法和processBroadcastElement()方法的
区别在于:processElement只能使用只读的上下文ReadOnlyContext,
而processBroadcastElement()方法则可以使用支持读写的上下文
Context。这么设计看起来很奇怪,但是合理的。广播状态模式下,要
求所有算子上的广播状态应完全一致,如果也允许processElement方
法更新、删除广播状态中的数据,那么会使得算子之间的广播状态变
得不一致,导致系统行为不可预测。在后边会介绍数据分区,数据分
区会将数据流进行分流,交给下游的不同算子,那么不同算子接收的

see more please visit: https://homeofpdf.com


数据流就是不同的,如果开发者在processElement方法中更新了广播
状态,必然会导致广播状态变得不一致。也许会有人说,在算子更新
广播状态的时候,通知其他算子不就可以了吗?但是Flink中的平行算
子之间没有通信接口,所以此处的设计强制要求processElement()
不能更新广播状态。
注 意 , 只 有 设 计 的 强 制 要 求 还 不 够 ,
processBroadcastElement()必须确保行为的不可变性,即无论什么
时间、在哪个物理机器、广播数据是否乱序,都必须保证执行结果完
全相同。比较典型的破坏不可变性的例子包括处理逻辑依赖于当前时
间,不同的节点当前时间并不完全一致,而且还要考虑到作业恢复执
行的情况,因此跟恢复之前的当前时间更是不可能相同。
3.5.4 异步函数
在介绍异步算子的时候提到了异步函数(AsyncFunction),异步
函数就是对Java异步编程框架的封装。
异步函数的类体系如图3-31所示。

图3-31 异步函数类体系

see more please visit: https://homeofpdf.com


如 图 3-31 所 示 , 异 步 函 数 的 抽 象 类 RichAsyncFunction 实 现
AsyncFunction接口,继承AbstractRichFunction获得了生命周期管理
和FunctionContext的访问能力。
异步函数的接口中定义了两种行为,异步调用行为将调用结果封
装到ResultFutrue中,同时提供了调用超时的处理,防止不释放资
源,如代码清单3-9所示。
代码清单3-9 AsyncFunction接口

3.5.5 数据源函数
数据源函数在Flink中叫作SourceFunction,Flink是一个计算引
擎,其需要从外部读取数据,所以在Flink中设计了SourceFunction体
系,专门用来从外部存储读取数据。SourceFunction是Flink作业的起
点,一个作业中可以有多个起点,即读取多个数据源的数据。
SourceFunction体系如图3-32所示。

see more please visit: https://homeofpdf.com


图3-32 SourceFunction体系
SourceFunction接口本身只定义了接口的业务逻辑相关行为,在
实 际 使 用 中 , 一 般 会 继 承 抽 象 类 RichSourceFunction 或
RichParallelSourceFunction 。 这 两 个 抽 象 类 通 过 继 承
AbstractRichFunction 获 得 了 Function 的 生 命 周 期 管 理 、 访 问
RuntimeContext的能力。
从 类 的 定 义 上 来 说 , RichSourceFunction 和
RichParallelSourceFunction的代码完全相同,甚至代码中的注释都
基本相同,但是为什么要设计这两个类呢?
其实这两个类的差异在运行层面上,RichSourceFunction是不可
并 行 的 , 并 行 度 限 定 为 1 , 超 过 1 则 会 报 错 。 而
RichParallelSourceFunction 是 可 并 行 的 , 并 行 度 可 以 根 据 需 要 设
定 , 并 没 有 限 制 。 差 异 体 现 在
StreamExecutionEnvironment#addSource方法中,其对Function的类
型 进 行 了 判 断 , 如 果 是 ParallelSourceFunction 类 型 , 则 是 可 并 行
的。如代码清单3-10所示。
代码清单3-10 构造DataStreamSource

see more please visit: https://homeofpdf.com


SourceFunction有几个比较关键的行为。
1)生命周期管理:在实际中,一般SourceFunction的实现类会同
时继承AbstractRichFunction,所以其生命周期包含open、close、
cancle三种方法,在生命周期方法中可以包含相应的初始化、清理
等。
2)读取数据: 持续地从外部存储读取数据,不同的外部存储有
不同的实现,如从Kafka读取数据依赖于Kafka Producer等。
3)向下游发送数据。
4)发送Watermark:生成Watermark并向下游发送,Watermark的
生成参见“时间与窗口”章节。
5)空闲标记:如果读取不到数据,则将该Task标记为空闲,向下
游发送Status#Idle,阻止Watermark向下游传递。
SourceFunction接口定义如代码清单3-11所示,SourceFunction
中内嵌了SourceContext接口。
代码清单3-11 SourceFunction接口

see more please visit: https://homeofpdf.com


上边SourceFunction定义的数据发送、Watermark发送、空闲标记
实际上都定义在SourceContext中。
StreamSourceContexts中提供了生成不同类型SourceContext的实
例的方法,从总体上按照带不带时间分为两类SourceContext如图3-33
所示。
1. NonTimestampContext
NonTimestampContext为所有的元素赋予-1作为时间戳,也就意味
着永远不会向下游发送Watermark。
使用Processing Time时使用此Context,使用Processing Time的
时候向下游发送Watermark没有意义,在实际处理中,各个计算节点会
根 据 本 地 时 间 定 义 触 发 器 , 触 发 执 行 Window 类 计 算 , 而 不 是 根 据
Watermark来触发。

see more please visit: https://homeofpdf.com


图3-33 SourceContext类体系
2. WatermarkContext
WatermarkContext定义了与Watermark相关的行为:
1)负责管理当前的StreamStatus,确保StreamStatus向下游传
递。
2)负责空闲检测的逻辑,当超过设定的事件间隔而没有收到数据
或者Watermark时,认为Task处于空闲状态。
WatermarkContext有两个实现类。
(1)AutomaticWatermarkContext
使 用 摄 取 时 间 ( Ingestion Time ) 的 时 候 ,
AutomaticWatermarkContext自动生成Watermark。在该Context中,启
动 WatermarkEmittingTask 向 下 游 发 送 Watermark , 使 用 了 一 个 定 时
器,其触发时间=(作业启动的时刻+Watermark周期×n),一旦启动
之后,WatermarkEmittingTask会持续地自动注册定时器,向下游发送
Watermark。
(2)ManualWatermarkContext
使用事件时间(Event Time)的时候,ManualWatermarkContext
不会产生Watermark,而是向下游发送透传上游的Watermark。
3.5.6 输出函数

see more please visit: https://homeofpdf.com


输出函数在Flink中叫作SinkFunction,负责将计算结果写入外部
存储中,是作业终点,一个作业可以有多个Sink,即将数据写入不同
的外部存储中。
SinkFunction类体系如图3-34所示。
SinkFunction只是单纯地定义了数据写出到外部存储的行为,并
没 有 Function 的 生 命 周 期 管 理 行 为 , 函 数 的 生 命 周 期 定 义 在
AbstractRichFunction中。在Connector中实际实现Sink的时候,基本
都是从RickSinkFunction和TwoPhaseCommitSinkFunction继承。
TowPhaseCommitSinkFunction是Flink中实现端到端Exactly-Once
的关键函数,提供框架级别的端到端Exactly-Once的支持,其在实现
过程中与Flink检查点机制结合,在第13章有详细介绍。

图3-34 SinkFunction类体系

3.5.7 检查点函数

see more please visit: https://homeofpdf.com


检查点函数就是在Flink中支持函数级别状态的保存和恢复的函
数 。 为 了 实 现 函 数 级 别 的 State 管 理 , Flink 中 设 计 了
CheckpointedFunction和ListCheckpointed接口。在检查点函数接口
中主要设计了状态快照的备份和恢复两种行为。
CheckpointedFunction虽然已经标记为废弃,但仍然是现在用得
最多的接口。当保存状态之后,其snapshotStat()会被调用,用于
备 份 保 存 状 态 到 外 部 存 储 。 当 恢 复 状 态 的 时 候 , 其
initializeState()方法负责初始化State,执行从上一个检查点恢
复状态的逻辑。
CheckpointedFunction接口定义如代码清单3-12所示。
代码清单3-12 CheckpointedFunction接口

ListCheckpointed接口的行为跟Checkpointed行为类似,除了提
供状态管理能力之外,修改作业并行度的时候,还提供了状态重分布
的支持。ListCheckpointed接口定义如代码清单3-13所示。
代码清单3-13 ListCheckpointed接口

3.6 数据分区
数据分区在Flink中叫作Partition。本质上来说,分布式计算就
是把一个作业切分成子任务Task,将不同的数据交给不同的Task计
算。在分布式存储中,Partition分区的概念就是把数据集切分成块,

see more please visit: https://homeofpdf.com


每一块数据存储在不同的机器上。同样,对于分布式计算引擎,也需
要将数据切分,交给位于不同物理节点上的Task计算。
StreamPartitioner是Flink中的数据流分区抽象接口,决定了在
实际运行中的数据流分发模式,将数据切分交给Task计算,每个Task
负责计算一部分数据流。所有的数据分区器都实现了ChannelSelector
接口,该接口中定义了负载均衡选择行为。
代码清单3-14 ChannelSelector接口定义

在该接口中可以看到,每一个分区器都知道下游通道数量,通道
数量在一次作业运行中是固定的,除非修改作业的并行度,否则该值
是不会改变的(此处跟后边容错章节模型中提到的Flink DAG有关系,
Flink DAG的拓扑关系是静态的)。
数据分区类体系如图3-35所示。

see more please visit: https://homeofpdf.com


图3-35 数据分区类体系
1.自定义分区
在API层面上,自定义分区应用在DataStream上,生成一个新的
DataStream。
使用用户自定义分区函数,为每一个元素选择目标分区,其使用如
代码清单3-15所示。
代码清单3-15 DataStream中使用自定义分区

2. ForwardPartitioner
在API层面上,ForwardPartitioner应用在DataStream上,生成一
个新的DataStream。
该Partitioner比较特殊,用于在同一个OperatorChain中上下游
算子之间的数据转发,实际上数据是直接传递给下游的。
3. ShufflePartitioner
在API层面上,ShufflePartitioner应用在DataStream上,生成一
个新的DataStream。

see more please visit: https://homeofpdf.com


随机将元素进行分区,可以确保下游的Task能够均匀地获得数
据,其使用如代码清单3-16所示。
代码清单3-16 DataStream中使用ShufflePartitioner
4. ReblancePartitioner
在API层面上,ReblancePartitioner应用在DataStream上,生成
一个新的DataStream。
以Round-robin的方式为每个元素分配分区,确保下游的Task可以
均匀地获得数据,避免数据倾斜,其使用如代码清单3-17所示。
代码清单3-17 DataStream中使用ReblancePartitioner
5. RescalingPartitioner
在API层面上,RescalingPartitioner应用在DataStream上,生成
一个新的DataStream。
根据上下游Task的数量进行分区。使用Round-robin选择下游的一
个Task进行数据分区,如上游有2个Source,下游有6个Map,那么每个
Source会分配3个固定的下游Map,不会向未分配给自己的分区写入数
据。这一点与ShufflePartitioner和ReblancePartitioner不同,后两
者会写入下游所有的分区,如图3-36所示。

图3-36 Rescaling分区效果示意
其使用如代码清单3-18所示。
代码清单3-18 DataStream中使用RescalingPartitioner

see more please visit: https://homeofpdf.com


6. BroadcastPartitioner
在API层面上,BroadcastPartitioner应用在DataStream上,生成
一个新的DataStream。
将该记录广播给所有分区,即有N个分区,就把数据复制N份,每
个分区1份,其使用如代码清单3-19所示。
代码清单3-19 DataStream中使用BroadcastPartitioner
7. KeyGroupStreamPartitioner
在API层面上,KeyGroupStreamPartitioner应用在KeyedStream,
生成一个新的KeyedStream。
KeyedStream根据KeyGroup索引编号进行分区,该分区器不是提供
给用户来用的。KeyedStream在构造Transformation的时候默认使用
KeyedGroup分区形式,从而在底层上支持作业Rescale功能。

3.7 连接器
连接器在Flink中叫作Connector。Flink本身是计算引擎,并不提
供数据存储能力,所以需要访问外部数据,外部数据源类型繁多,连
接器因此应运而生,它提供了从数据源读取数据和写入数据的能力。
基于SourceFunction和SinkFunction构建出了种类繁多的连接器。
Flink在Flink-Connectors模块中提供了内置的Connector,包含
常 见 的 数 据 源 , 如 HDFS 、 Kafka 、 HBase 等 , 同 时 结 合 Source &
SinkFunction体系也能够自定义连接器。也有一部分第三方实现的连
接器,如GitHub的Bahir项目。
1.流内置连接器(见表3-1)
表3-1 Flink流内置连接器

see more please visit: https://homeofpdf.com


2. Bahir连接器(见表3-2)
表3-2 Bahir提供的连接器

连接器中有两个关键行为,即读取和写入,分别对应Flink中的
SourceFunction和SinkFunction。根据外部存储类型的不同,实现逻
辑各不相同。
下面以KafkaConnector为例说明连接器是如何构建和运转的。
Kafka是一个分布式的高性能消息队列,是流计算中最常用的数据
存储。Kafka在概念上与传统的消息中间件类似,有Topic、消费者、

see more please visit: https://homeofpdf.com


生产者。Kafka使用了专有的通信协议,所以Kafka提供了Consumer类
库用来从Kafka集群中消费数据,提供了Producer类库用来向Kafka集
群写入数据,如图3-37所示。

图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等都有各自的身份标识实现。

see more please visit: https://homeofpdf.com


图3-38 分布式ID类体系

3.9 总结
Flink作业的API只是面向开发者层面的抽象,在底层Flink做了重
要的核心运行时抽象,首先是数据流和在数据流上的操作,这是API的
核心实现,然后从API层逐渐向下,抽象了数据转换作为API层到执行
层转换的中间层,抽象了算子、函数、数据分区体系作为运行时业务
逻辑的载体,抽象了数据IO屏蔽外部数据存储的差异性。

see more please visit: https://homeofpdf.com


第4章 时间与窗口
流式处理系统长期以来一直应用在提供低延迟、不准确/近似结果
的场景里,通常结合批处理系统来提供最终正确的结果,两者组合即
Lambda架构。
Lambda架构的基本思想是,在批处理系统旁边运行一个流处理系
统,它们都执行基本相同的计算。流式处理系统提供低延迟、不准确
的结果(由于使用近似算法,或者因为流系统本身不提供严格正确
性),所以每过一段时间,批处理系统持续滚动处理并计算出正确的
结果,修正流处理系统的计算结果。Lambda最初是由Twitter的Nathan
Marz(Apache Storm创始人)提出的,相当成功。然而维护Lambda系
统却是一件麻烦事:需要构建、配置和维护两套不同类型的集群,然
后再将两者的计算结果合并。
设计良好的流处理系统实际上是批处理的功能超集,因此建立一
个全面的流处理系统,把批处理视作流处理的特例即可,Flink的设计
就是基于此。
流处理取代Lambada架构的历史时刻就要来了,在此之前,需要解
决两个问题。
1. 正确性——流计算需要与批处理一样计算准确
强一致性是正确处理的前提,对于流处理系统来说,想超越批处
理系统,这是基本要求。除非真的不关心结果准确与否,否则应避免
使用不能提供强一致性的流处理系统。
如果想了解流处理系统中如何实现强一致性,可以参考
MillWheel Fault-Tolerant Stream Processing at Internet Scale
:
和 Discretized Streams Fault-Tolerant Streaming Computation at
:
Scale
两篇论文,在此不再详细阐述。
2. 时间推理工具——批流统一的关键
对于乱序无界的数据流,数据产生的时间和数据真正被处理的时
间之间的偏差很大,用于推理时间的工具至关重要。越来越多的现代

see more please visit: https://homeofpdf.com


数据集体现了这个特点,现有的批处理系统(以及大多数流处理系
统)缺乏必要的工具来应对这个问题。
在 谷 歌 的 Dataflow 编 程 模 型 的 论 文 The Dataflow Model:A
Practical Approach to Balancing Correctness,Latency,and Cost
in Massive-Scale,Unbounded,Out-of-Order Data Processing 中,介
绍了其流批一体计算中的核心设计,其中重点是Window窗口。在论文
中,批处理本质是处理有限不变的数据集,流处理本质是处理无限持
续产生的数据集,所以批本质上来说是流的一种特例,那么窗口就是
流和批统一的桥梁,对流上的数据进行窗口切分,每一个窗口一旦到
了计算的时刻,就可以被看成一个不可变的数据集,在触发计算之
前,窗口的数据可能会持续地改变,因此对窗口的数据进行计算就是
批处理。

4.1 时间类型
在Flink中定义了3种时间类型:事件时间(Event Time)、处理
时间(Processing Time)和摄取时间(Ingestion Time)。
3种时间类型如图4-1所示。

图4-1 3种时间类型
(1)事件时间

see more please visit: https://homeofpdf.com


事件时间指事件发生时的时间,一旦确定之后再也不会改变。例
如,事件被记录在日志文件中,日志中记录的时间戳就是事件时间。
通过事件时间能够还原出来事件发生的顺序。
使用事件时间的好处是不依赖操作系统的时钟,无论执行多少
次,可以保证计算结果是一样的,但计算逻辑稍微复杂,需要从每一
条记录中提取时间戳。
(2)处理时间
处理时间指消息被计算引擎处理的时间,以各个计算节点的本地
时间为准。例如,在物理节点1处理时,处理时间(即当前系统时间)
为2019-02-05 12∶00∶00,然后交给下游的计算节点进程处理,此时
的处理时间(即当前系统时间)为2019-02-05 12∶00∶01。可以看到
处理时间是在不停变化的。使用处理时间依赖于操作系统的时钟,重
复执行基于窗口的统计作业,结果可能是不同的。处理时间的计算逻
辑非常简单,性能好于事件时间,延迟低于事件时间,只需要获取当
前系统的时间戳即可。
(3)摄取时间
摄取时间指事件进入流处理系统的时间,对于与一个事件来说,
使用其被读取的那一刻的时间戳作作为摄取时间。
摄取时间一般使用得较少,从处理机制上来说,其类似于事件时
间,在作业异常重启执行的时候,也无法避免使用处理时间的结果不
准确的问题。一般来说,若在数据记录中没有记录时间,又想使用事
件时间机制来处理记录,会选择使用摄取时间。
在Flink应用中可以使用这3种时间类型,其中最常用的是事件时
间和处理时间,如代码清单4-1所示。
代码清单4-1 使用TimeCharacteristic设置Flink使用的时间类型

see more please visit: https://homeofpdf.com


4.2 窗口类型
前面提到过对数据集进行切分的概念,为此Flink中提供了3类默
认窗口:计数窗口(Count Window)、时间窗口(Time Window)和会
话窗口(Session Window)。
不同类型的窗口及其示例如图4-2所示。

see more please visit: https://homeofpdf.com


图4-2 窗口类型
1. Count Window
1)Tumble Count Window:累积固定个数的元素就视为一个窗
口,该类型的窗口无法像时间窗口一样事先切分好。
2)Sliding Count Window:累积固定个数的元素视为一个窗口,
每超过一定个数的原则个数,则产生一个新的窗口。
2. Time Window
1)Tumble Time Window:表示在时间上按照事先约定的窗口大小
切分的窗口,窗口之间不会相互重叠。

see more please visit: https://homeofpdf.com


2)Sliding Time Window:表示在时间上按照事先约定的窗口大
小、滑动步长切分的窗口,滑动窗口之间可能会存在相互重叠的情
况。
3. Session Window
Session Window是一种特殊的窗口,当超过一段时间,该窗口没
有收到新的数据元素,则视为该窗口结束,所以无法事先确定窗口的
长度、元数个数,窗口之间也不会相互重叠。

4.3 窗口原理与机制
窗口算子负责处理窗口,数据流源源不断地进入算子,每一个数
据元素进入算子时,首先会被交给WindowAssigner。WindowAssigner
决定元素被放到哪个或哪些窗口,在这个过程中可能会创建新窗口或
者合并旧的窗口。在Window Operator中可能同时存在多个窗口,一个
元素可以被放入多个窗口中。
数据进入窗口时,分配窗口和计算的逻辑如图4-3所示。

see more please visit: https://homeofpdf.com


图4-3 Window原理
此处需要注意,Window本身只是一个ID标识符,其内部可能存储
了一些元数据,如TimeWindow中有开始和结束时间,但是并不会存储
窗口中的元素。窗口中的元素实际存储在Key/Value State中,Key为
Window,Value为数据集合(或聚合值)。为了保证窗口的容错性,
Window实现依赖Flink的State机制,State的介绍参见本书相关章节。
每一个窗口都拥有一个属于自己的Trigger,Trigger上有定时
器,用来决定一个窗口何时能够被计算或清除。每当有元素被分配到
该窗口,或者之前注册的定时器超时时,Trigger都会被调用。
Trigger被触发后,窗口中的元素集合就会交给Evictor(如果指
定了的话)。Evictor主要用来遍历窗口中的元素列表,并决定最先进
入窗口的多少个元素需要被移除。剩余的元素会交给用户指定的函数
进行窗口的计算。如果没有Evictor的话,窗口中的所有元素会一起交
给函数进行计算。

see more please visit: https://homeofpdf.com


计算函数收到窗口的元素(可能经过了Evictor的过滤),计算出
窗口的结果值,并发送给下游。窗口的结果值可以是一个也可以是多
个。DataStream API上可以接收不同类型的计算函数,包括预定义的
sum()、min()、max(),以及ReduceFunction、FoldFunction和
WindowFunction。WindowFunction是最通用的计算函数,其他的预定
义函数基本上都是基于该函数实现的。
Flink对一些聚合类的窗口计算(如sum和min)做了优化,因为聚
合类的计算不需要将窗口中的所有数据都保存下来,只需要保存一个
中间结果值就可以了。每个进入窗口的元素都会执行一次聚合函数并
修改中间结果值,这样可以大大降低内存的消耗并提升性能。但是如
果用户定义了Evictor,则不会启用对聚合窗口的优化,因为Evictor
需要遍历窗口中的所有元素,必须将窗口中的所有元素都存下来。
4.3.1 WindowAssigner
WindowAssigner用来决定某个元素被分配到哪个/哪些窗口中去。
SessionWindowAssigner比较特殊,因为Session Window无法事先确定
窗口的范围,是动态改变的。
Flink 中 有 两 套 WindowAssigner 体 系 , 分 别 位 于 Streaming 中 和
Blink 中 。 Streaming 中 的 WindowAssigner 为 DataStream API 服 务 ,
Blink中的WindowAssigner为基于Blink Planner的Flink SQL服务。
Streaming中的WindowAssigner类体系如图4-4所示。

see more please visit: https://homeofpdf.com


图4-4 Streaming中的WindowAssigner类体系
Blink中的WindowAssigner体系如图4-5所示。

see more please visit: https://homeofpdf.com


图4-5 Blink中的WindowAssigner体系

4.3.2 WindowTrigger
Trigger触发器决定了一个窗口何时能够被计算或清除,每一个窗
口都拥有一个属于自己的Trigger,Trigger上会有定时器,用来决定
一个窗口何时能够被计算或清除。每当有元素加入该窗口,或者之前
注册的定时器超时时,Trigger都会被调用。
Trigger触发的结果如下。
1)Continue:继续,不做任何操作。
2)Fire:触发计算,处理窗口数据。

see more please visit: https://homeofpdf.com


3)Purge:触发清理,移除窗口和窗口中的数据。
4)Fire + Purge:触发计算+清理,处理数据并移除窗口和窗口
中的数据。
当数据到来的时候,调用Trigger判断是否需要触发计算,如果调
用结果只是Fire,则计算窗口并保留窗口原样,窗口中的数据不清
理,数据保持不变,等待下次触发计算的时候再次执行计算。窗口中
的数据会被反复计算,直到触发结果清理。在清理之前,窗口和数据
不会释放,所以窗口会一直占用内存。
Trigger触发流程如下。
1)当Trigger Fire时,窗口中的元素集合会交给Evictor(如果
已经指定了)。Evictor主要用来遍历窗口中的元素列表,并决定最先
进入窗口的多少个元素需要被移除。剩余的元素会交给用户指定的函
数进行窗口的计算。如果没有Evictor,窗口中的所有元素会一起交给
函数进行计算。
2)计算函数收到窗口的元素(可能经过了Evictor的过滤),计
算出窗口的结果值,并发送给下游。窗口的结果值可以是一个也可以
是多个。DataStream API上可以接收不同类型的计算函数,包括预定
义 的 sum ( ) 、 min ( ) 、 max ( ) , 以 及 ReduceFunction 、
FoldFunction和WindowFunction。WindowFunction是最通用的计算函
数,其他预定义的函数基本都是基于该函数实现的。
3)Flink对于一些聚合类的窗口计算(如sum和min)做了优化,
因为聚合类的计算不需要将窗口中的所有数据都保存下来,只需要保
存一个result值就可以了。每个进入窗口的元素都会执行一次聚合函
数并修改result值,这样可以大大降低内存的消耗并提升性能。但是
如 果 用 户 定 义 了 Evictor , 则 不 会 启 用 对 聚 合 窗 口 的 优 化 , 因 为
Evictor需要遍历窗口中的所有元素,必须将窗口中所有元素都存下
来。
Streaming模块Trigger类体系如图4-6所示。

see more please visit: https://homeofpdf.com


图4-6 Streaming模块Trigger类体系
Blink SQL模块Trigger体系如图4-7所示:

图4-7 Blink SQL模块Trigger体系

see more please visit: https://homeofpdf.com


在抽象类Trigger中定义了Trigger的行为,Trigger的关键行为分
为两类:触发逻辑的判断和合并。
在大数据中有3种典型延迟计算:
1)基于数据记录个数的触发:即等待Window中的数据达到一定个
数,则触发窗口的计算,在类体系中对应的是CountTrigger。
2)基于处理时间的触发:在处理时间维度判断哪些窗口需要触
发,对应的是ProcessingTimeTrigger。
3)基于事件时间的触发:使用Watermark机制触发。
Trigger接口的定义如代码清单4-2所示。
代码清单4-2 Trigger接口

see more please visit: https://homeofpdf.com


4.3.3 WindowEvictor
Evictor 可 以 理 解 为 窗 口 数 据 的 过 滤 器 , Evictor 可 在 Window
Function执行前或后,从Window中过滤元素。Flink内置了3种窗口数
据过滤器,如图4-8所示。

see more please visit: https://homeofpdf.com


图4-8 Evictor类体系
1)CountEvictor: 计数过滤器。在Window中保留指定数量的元
素,并从窗口头部开始丢弃其余元素。
2)DeltaEvictor: 阈值过滤器。本质上来说就是一个自定义规
则,计算窗口中每个数据记录,然后与一个事先定义好的阈值做比
较,丢弃超过阈值的数据记录。
3)TimeEvictor: 时间过滤器。保留Window中最近一段时间内的
元素,并丢弃其余元素。
4.3.4 Window函数
数据经过WindowAssigner之后,已经被分配到不同的Window中,
接下来,要通过窗口函数对窗口内的数据进行处理。窗口函数主要分
为两种。
1. 增量计算函数
增量计算指的是窗口保存一份中间数据,每流入一个新元素,新
元素都会与中间数据两两合一,生成新的中间数据,再保存到窗口
中,如ReduceFunction、AggregateFunction、FoldFunction。
增量计算的优点是数据到达后立即计算,窗口只保存中间结果,
计算效率高,但是增量计算函数计算模式是事先确定的,能够满足大
部分的计算需求,对于特殊业务需求可能无法满足。
2. 全量计算函数
全量计算指的是先缓存该窗口的所有元素,等到触发条件后对窗
口内的所有元素执行计算。Flink内置的ProcessWindowFunction就是
全量计算函数,通过全量缓存,实现灵活计算,计算效率比增量聚合
稍低,毕竟要占用更多的内存。

4.4 水印
水印(Watermark)用于处理乱序事件,而正确地处理乱序事件,
通常用Watermark机制结合窗口来实现。

see more please visit: https://homeofpdf.com


从流处理原始设备产生事件,到Flink读取到数据,再到Flink多
个算子处理数据,在这个过程中,会受到网络延迟、数据乱序、背
压、Failover等多种情况的影响,导致数据是乱序的。虽然大部分情
况下没有问题,但是不得不在设计上考虑此类异常情况,为了保证计
算结果的正确性,需要等待数据,这带来了计算的延迟。对于延迟太
久的数据,不能无限期地等下去,所以必须有一个机制,来保证特定
的时间后一定会触发窗口进行计算,这个触发机制就是Watermark。
在 DataStream 和 Flink Table & SQL 模 块 中 , 使 用 了 各 自 的
Watermark生成体系。
4.4.1 DataStream Watermark生成
通常Watermark在Source Function中生成,如果是并行计算的任
务 , 在 多 个 并 行 执 行 的 Source Function 中 , 相 互 独 立 产 生 各 自 的
Watermark。而Flink提供了额外的机制,允许在调用DataStream API
操作(如map、filter等)之后,根据业务逻辑的需要,使用时间戳和
Watermark生成器修改数据记录的时间戳和Watermark。
1. Source Function中生成Watermark
Source Function可以直接为数据元素分配时间戳,同时也会向下
游 发 送 Watermark 。 在 Source Function 中 为 数 据 分 配 了 时 间 戳 和
Watermark就不必在DataStream API中使用了。需要注意的是:如果一
个 timestamp 分 配 器 被 使 用 的 话 , 由 源 提 供 的 任 何 Timestamp 和
Watermark都会被重写。
为 了 通 过 SourceFunction 直 接 为 一 个 元 素 分 配 一 个 时 间 戳 ,
SourceFunction 需 要 调 用 SourceContext 中 的
collectWithTimestamp(...)方法。为了生成Watermark,源需要调
用emitWatermark(Watermark)方法,如代码清单4-3所示。
代码清单4-3 SourceFunction中为数据元素分配时间戳和生成
Watermark示例

see more please visit: https://homeofpdf.com


2. DataStream API中生成Watermark
DataStream API中使用的TimestampAssigner接口定义了时间戳的
提 取 行 为 , 其 有 两 个 不 同 接 口 AssignerWithPeriodicWatermarks 和
AssignerWithPunctuatedWatermarks,分别代表了不同的Watermark生
成策略。TimestampAssigner接口体系如图4-9所示。

图4-9 DataStream中TimestampAssigner接口体系
AssignerWithPeriodicWatermarks是周期性生成Watermark策略的
顶层抽象接口,该接口的实现类周期性地生成Watermark,而不会针对
每一个事件都生成。
AssignerWithPunctuatedWatermarks对每一个事件都会尝试进行
Watermark的生成,但是如果生成的Watermark是null或者Watermark小
于之前的Watermark,则该Watermark不会发往下游,因为发往下游也
不会有任何效果,不会触发任何窗口的执行。
4.4.2 Flink SQL Watermark生成

see more please visit: https://homeofpdf.com


Flink SQL没有DataStram API开发那么灵活,其Watermark的生成
主要是在TableSource中完成的,其定义了3类Watermark生成策略。其
Watermark生成策略体系如图4-10所示。

图4-10 Flink SQL的Watermark生成策略体系


Watermark的生成机制分为如下3类。
(1)周期性Watermark策略
周 期 性 Watermark 策 略 在 Flink 中 叫 作
PeriodicWatermarkAssigner,周期性(一定时间间隔或者达到一定的
记 录 条 数 ) 地 产 生 一 个 Watermark 。 在 实 际 的 生 产 中 使 用 周 期 性
Watermark策略的时候,必须注意时间和数据量,结合时间和积累条数
两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延
时。
1) AscendingTimestamps:递增Watermark,作用在Flink SQL中
的Rowtime属性上,Watermark=当前收到的数据元素的最大时间戳-1,
此处减1的目的是确保有最大时间戳的事件不会被当做迟到数据丢弃。
2)BoundedOutOfOrderTimestamps:固定延迟Watermark,作用在
Flink SQL的Rowtime属性上,Watermark=当前收到的数据元素的最大
时间戳-固定延迟。
(2)每事件Watermark策略
每 事 件 Watermark 策 略 在 Flink 中 叫 作
PuntuatedWatamarkAssigner,数据流中每一个递增的EventTime都会
产生一个Watermark。在实际的生产中Punctuated方式在TPS很高的场
景下会产生大量的Watermark,在一定程度上会对下游算子造成压力,

see more please visit: https://homeofpdf.com


所以只有在实时性要求非常高的场景下才会选择Punctuated的方式进
行Watermark的生成。
(3)无为策略
无为策略在Flink中叫作PreserveWatermark。在Flink中可以使用
DataStream API 和 Table & SQL 混 合 编 程 , 所 以 Flink SQL 中 不 设 定
Watermark 策 略 , 使 用 底 层 DataStream 中 的 Watermark 策 略 也 是 可 以
的,这时Flink SQL的Table Source中不做处理。
4.4.3 多流的Watermark
在实际的流计算中一个作业中往往会处理多个Source的数据,对
Source的数据进行GroupBy分组,那么来自不同Source的相同key值会
shuffle到同一个处理节点,并携带各自的Watermark,Apache Flink
内部要保证Watermark保持单调递增,多个Source的Watermark汇聚到
一起时可能不是单调自增的,对于这样的情况,ApacheFlink的内部处
理如图4-11所示。

图4-11 Watermark处理逻辑
Apache Flink内部实现每一个边上只能有一个递增的Watermark,
当出现多流携带EventTime汇聚到一起(GroupBy或Union)时,Apache

see more please visit: https://homeofpdf.com


Flink会选择所有流入的EventTime中最小的一个向下游流出,从而保
证Watermark的单调递增和数据的完整性。
Watermark是在Source Function中生成或者在后续的DataStream
API中生成的。Flink作业一般是并行执行的,作业包含多个Task,每
个 Task 运 行 一 个 或 一 组 算 子 ( OperatorChain ) 实 例 , Task 在 生 成
Watermark的时候是相互独立的,也就是说在作业中存在多个并行的
Watermark。
Watermark 在 作 业 的 DAG 从 上 游 向 下 游 传 递 , 算 子 收 到 上 游
Watermark后会更新其Watermark。如果新的Watermark大于算子的当前
Watermark,则更新算子的Watermark为新Watermark,并发送给下游算
子。
某些算子会有多个上游输入,如Union或keyBy、partition之后的
算子。在Flink的底层执行模型上,多流输入会被分解为多个双流输
入,所以对于多流Watermark的处理也就是双流Watermark的处理,无
论是哪一个流的Watermark进入算子,都需要跟另一个流的当前算子进
行 比 较 , 选 择 较 小 的 Watermark , 即
Min ( input1Watermark,intput2Watermark ) , 与 算 子 当 前 的
Watermark 比 较 , 如 果 大 于 算 子 当 前 的 Watermark , 则 更 新 算 子 的
Watermark为新的Watermark,并发送给下游,如代码清单4-4所示。
代码清单4-4 双流输入的StreamOperator Watermark处理

see more please visit: https://homeofpdf.com


如图4-12所示,多流Watermark中使用了事件时间。

图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中使用到时间概念。一般情况下,

see more please visit: https://homeofpdf.com


在KeyedProcessFunction#processElement()方法中会用到Timer,
注册Timer然后重写其onTimer()方法,在Watermark超过Timer的时
间点之后,触发回调onTimer()。根据时间类型的不同,可以注册事
件时间和处理时间两种Timer。
说到此处,不得不提到Timer注册过程中使用到的定时器服务
(TimerService)。TimeService是在算子中提供定时器的管理行为,
包 含 定 时 器 的 注 册 和 删 除 。 TimerService 在 DataStream 、 State 、
Blink中都有应用。在DataStream和State模块中,一般会在Keyed算子
中 使 用 , 在 引 入 Blink 之 后 , 在 Blink 的 一 般 算 子 中 也 会 使 用 , 如
BaseTemporalSortOperator算子。
那么在执行层面上,时间服务TimerService具体是怎么发挥其作
用的呢?
简单来讲,在算子中使用时间服务来创建定时器(Timer),并且
在Timer触发的时候进行回调,从而进行业务逻辑处理。前边章节中延
迟Join的示例中使用过Timer。
4.5.1 定时器服务
定 时 器 服 务 在 Flink 中 叫 作 TimerService , 窗 口 算 子
( WindowOperator ) 中 使 用 了 InternalTimerService 来 管 理 定 时 器
(Timer),其初始化是在WindowOperator#open()中实现的。
对于InternalTimerService而言,有几个元素比较重要:名称、
命名空间类型N(及其序列化器)、键类型K(及其序列化器)和
Triggerable对象(支持延时计算的算子,继承了Triggerable接口来
实现回调)。
如代码清单4-5所示。
代码清单4-5 注册Timer入口

see more please visit: https://homeofpdf.com


一个算子中可以有多个InternalTimeService,通过名称进行区
分 , 如 在 WindowOperator 中 , InternalTimeService 的 名 称 是
“window-timers” , 在 KeyedProcessOperator 中 名 称 是 “user-
timers”,在CepOperator中名称是“watermark-callbacks”。
InternalTimerService 接 口 的 实 现 类 是
InternalTimerServiceImpl , Timer 的 实 现 类 是 InternalTimer 。
InternalTimerServiceImpl 使 用 了 两 个 TimerHeapInternalTimer 的 优
先队列(HeapPriorityQueueSet,该优先队列是Flink自己实现的),
分别用于维护事件时间和处理时间的Timer。
InternalTimeServiceManager 是 Task 级 别 提 供 的
InternalTimeService 集 中 管 理 器 , 其 使 用 Map 保 存 了 当 前 所 有 的
InternalTimeService,Map的Key是InternalTimerService的名字。
4.5.2 定时器
定时器在Flink中叫作Timer。窗口的触发器与定时器是紧密联系
的。
Flink的定时器使用InternalTimer接口定义行为,如代码清单4-6
所示。
代码清单4-6 InternalTimer接口

see more please visit: https://homeofpdf.com


前面提到了Timer的注册和保存,那么Timer到底是如何触发然后
回调用户逻辑的呢?答案在InternalTimerServiceImpl中。
对于事件时间,会根据Watermark的时间,从事件时间的定时器队
列中找到比给定时间小的所有定时器,触发该Timer所在的算子,然后
由算子去调用UDF中的onTime()方法,如代码清单4-7所示。
代码清单4-7 事件时间触发与回调

处理时间也是类似的逻辑,区别在于,处理时间是从处理时间
Timer优先级队列中找到Timer。处理时间因为依赖于当前系统,所以
其使用的是周期性调度。
4.5.3 优先级队列
直接使用Java的PriorityQueue看起来也能实现InternalTimer的
需求,但是Flink在优先级队列中使用了KeyGroup,是按照KeGroup去
重的,并不是按照全局的Key去重,如图4-13所示。

see more please visit: https://homeofpdf.com


图4-13 Flink实现的优先级队列体系
Flink自己实现了优先级队列来管理Timer,共有2种实现。
1)基于堆内存的优先级队列HeapPriorityQueueSet:基于Java堆
内存的优先级队列,其实现思路与Java的PriorityQueue类似,使用了
二叉树。
2)基于RocksDB的优先级队列:分为Cache+RocksDB量级,Cache
中 保 存 了 前 N 个 元 素 , 其 余 的 保 存 在 RocksDB 中 。 写 入 的 时 候 采 用
Write-through策略,即写入Cache的同时要更新RocksDB中的数据,可
能需要访问磁盘。
基于堆内存的优先级队列比基于RocksDB的优先级队列性能好,但
是受限于内存大小,无法容纳太多的数据;基于RocksDB的优先级队列
牺牲了部分性能,可以容纳大量的数据。

4.6 窗口实现
在Flink中窗口有两套实现,分别位于flink-streaming-java和
flink-table-runtime-blink模块中,其会话窗口、时间窗口实现基本
是一样的,计数窗口的实现不同。Flink-streaming-java中计数窗口
依赖于GlobalWindow来实现,在flink-table-runtime-blink中,计数
窗口与时间窗口一样有类定义和窗口分配器。

see more please visit: https://homeofpdf.com


在 Flink 中 有 3 类 窗 口 : CountWindow 、 TimeWindow 、
SessionWindow,其执行时的算子是WindowOperator。
WindowOperator中数据处理的基本过程如图4-14所示。

图4-14 WindowOperator中数据处理过程

4.6.1 时间窗口
按照时间类型,窗口分为两类:处理时间窗口和事件时间窗口。
按照窗口行为,时间窗口分为两类:滚动窗口和滑动窗口。
滚动窗口(TumbleWindow)的关键属性有两个:
1)Offset:窗口的起始时间。
2)Size:窗口的长度。
滑动窗口(SlidingWindow)的关键属性有3个:
1)Offset:窗口的起始时间。
2)Size:窗口的长度。
3)Slide:滑动距离。
滚动窗口、滑动窗口其本质上是类似的,滚动窗口可以看作是滑
动距离与窗口长度相同的滑动窗口。
在数据元素分配窗口的时候,对于滚动窗口,一个数据元素只属
于一个窗口,但可能属于多个滑动窗口。

see more please visit: https://homeofpdf.com


以最复杂的滑动窗口为例,如代码清单4-8所示。
代码清单4-8 基于事件时间的滑动窗口数据元素窗口分配示例

从上边的代码中可以看到,Flink会为每个数据元素分配一个或者
多个TimeWindow对象,然后使用TimeWindow对象作为Key来操作窗口对
应的State。
注:TimeWindow的equals方法,使用类型和窗口的起止时间进行
相等比较,所以使用TimeWindow作为Key没有问题。
4.6.2 会话窗口

see more please visit: https://homeofpdf.com


在需要登录的网站上,如果用户过一段时间没有操作,为了安
全,账户就会自动退出。一般会通过会话超时设定一个时间,如30分
钟。30分钟的超时间隔可以理解为会话窗口的Gap,如果从用户登录账
户到超时退出的所有动作被看作事件的话,这些事件都处于同一个会
话窗口中。
会话窗口的效果如图4-15所示。
在Flink中提供了4种Session Window的默认实现。
1)ProcessingTimeSessionWindows:处理时间会话窗口,使用固
定会话间隔时长。
2 ) DynamicProcessingTimeSessionWindows : 处 理 时 间 会 话 窗
口,使用自定义会话间隔时长。

图4-15 会话窗口分割示例
3)EventTimeSessionWindows:事件时间会话窗口,使用固定会
话间隔时长。
4)DynamicEventTimeSessionWindows:事件时间会话窗口,使用
自定义会话间隔时长。
不同类型的会话窗口使用示例如代码清单4-9所示。
代码清单4-9 会话窗口使用示例

see more please visit: https://homeofpdf.com


会话窗口不同于事件窗口,它的切分依赖于事件的行为,而不是
时间序列,所以在很多情况下会因为事件乱序使得原本相互独立的窗
口因为新事件的到来导致窗口重叠,而必须要进行窗口的合并,如图
4-16所示。

see more please visit: https://homeofpdf.com


图4-16 Session Window会话窗口合并示例
在图4-16中,元素8和元素7的时间间隔超过了会话窗口的超时间
隔,所以生成了两个会话窗口。
元素4在会话窗口触发计算之前进入了Flink,此时因为元素4的存
在,4与8的间隔、4与7的间隔都小于超时间隔,所以此时元素8、4、7
应该位于一个会话窗口,那么此时就需要对窗口进行合并,窗口的合
并涉及3个要素:
1)窗口对象合并和清理。
2)窗口State的合并和清理。
3)窗口触发器的合并和清理。
下面通过一个会话窗口的合并示例来理解其过程,如图4-17所
示。

see more please visit: https://homeofpdf.com


图4-17 会话窗口合并
(1)窗口合并
对于会话窗口,因为无法事先确定窗口的长度,也不知道该将数
据元素放到哪个窗口,所以对于每一个事件分配一个SessionWindow。
然后判断窗口是否需要与已有的窗口进行合并。窗口合并时按照
窗口的起始时间进行排序,然后判断窗口之间是否存在时间重叠,重
叠的窗口进行合并,将后序窗口合并到前序窗口中,如图4-17所示,
延长窗口W1的长度,将W3窗口的结束时间作为W1的结束时间,清理掉
W2、W3窗口。
(2)State合并
窗口合并的同时,窗口对应的State也需要进行合并,默认复用最
早的窗口的状态,本例中是W1窗口的状态,将其他待合并窗口的状态
(W2、W3)合并到W1状态中。
创建状态需要跟StateBackend进行交互,成本比较高,对于会话
窗口来说合并行为比较频繁,所以尽量复用已有的状态。
(3)触发器合并
Trigger#onMerge方法中用于对触发器进行合并,触发器的常见成
本比较低,所以触发器的合并实际上是删除合并的窗口的触发器,本
例中会删除W1、W2、W3的触发器,然后为新的W1窗口创建新的触发
器,触发时间为W3触发器的触发时间。
4.6.3 计数窗口
在 DataStream API 中 没 有 定 义 计 数 窗 口 的 实 体 类 , 使 用
GlobalWindow 来 实 现 CoutntWindow 。 在 DataStream API 中 使 用
CountWindow如代码清单4-10所示。
代码清单4-10 DataStream API中使用CountWindow

see more please visit: https://homeofpdf.com


滚动计数窗口和滑动计数窗口依托于GlobalWindow实现,从实现
上来说,对于一个Key,滚动计数窗口全局只有一个窗口对象,使用
CountTrigger来实现窗口的触发,使用Evictor来实现窗口滑动和窗口
数据的清理。
计数窗口与会话窗口类似,依赖于数据元素的行为,无法像时间
窗口一样事先划分好窗口,其在处理过程中也会涉及窗口的合并。
使用Evictor的窗口,其最终运行在EvictorWindowOperator中,
与普通的WindowOperator相比,EvictorWindowOperator多了一个对窗
口State进行清理的动作。

4.7 总结
本章中介绍了Flink中的时间类型,包括事件时间、处理时间、摄
取时间,不同的时间类型有其各自的适用场景。窗口是流上的重要概
念,在Flink有计数窗口、时间窗口、会话窗口3大类,其原理与机制
类似。
基于时间,使用窗口进行数据流切分,按照窗口进行计算,触发
窗口统计的是Watermark机制,Watermark在DataStream和SQL中有各自
的生成机制。在Flink内部,算子的Watermark可能来自上游的多个算

see more please visit: https://homeofpdf.com


子,Flink会选取其中最小的Watermark作为其当前的Watermark,并向
下游广播。时间服务是实现窗口的重要基石,其与窗口、Watermark机
制协同配合,共同实现了流上的窗口运算。窗口和时间服务同时依赖
于Flink的状态机制,支持可靠的容错。

see more please visit: https://homeofpdf.com


第5章 类型与序列化
Flink内部自主进行内存管理,将数据以二进制结构保存在内存
中,目前的实现中大量使用了堆外内存。如果让开发人员直接操作二
进制结构,代码会变得复杂臃肿,所以大数据平台在设计API的时候,
允许用户直接像编写普通Java应用程序一样使用其API开发Function,
直接使用JDK提供的类型和自定义类型。
普通的Java对象类型与内部的二进制结构之间存在不一致,所以
需要设计一套相互转换的机制,让Flink能够识别对象的类型,并且知
道如何进行序列化/反序列化,Flink中的类型与序列化系统的目的就
是解决此问题。
在Flink中有物理类型和逻辑类型两种类型系统,其中物理类型系
统是面向开发者的,让普通开发者在开发Flink应用的时候就像编写普
通的Java代码一样,逻辑类型系统是描述物理类型的类型系统,让
Flink能够对物理类型进行序列化/反序列化,其作用如图5-1所示。

图5-1 逻辑类型/序列化的承上启下作用
Flink 目 前 有 两 套 逻 辑 类 型 系 统 : TypeInfomation 类 型 系 统 和
Flink SQL中的LogicalTypes类型系统。
TypeInformation类型系统是为DataStream/DataSet API设计的,
用来描述对象的类型信息,在运行时根据TypeInfomation的类型描述
来序列化对象。在1.9版本引入Blink Planner之前,Flink SQL依赖于
TypeInfomation 类 型 系 统 定 义 表 的 元 信 息 ( Schema ) 。 在

see more please visit: https://homeofpdf.com


DataStream/DataSet API中,TypeInfomation类型系统没有问题,但
是应用在Flink SQL时存在以下一些问题。
1)该类型系统与SQL的兼容性不好。
2)无法控制Decimal类型的精度。
3)无法区分char和varchar类型。
4)物理类型和逻辑类型紧耦合。
5)物理类型是类型描述,而不是类型的序列化/反序列化器。
所以Flink在1.9版本中为Flink SQL引入了新的LogicalTypes类型
系统。使用DataType描述SQL的表、字段、函数参数等类型,DataType
有两个职责:
1)声明逻辑类型LogicalType。
2)运行时逻辑转换类,允许为空。
在运行时将LogicalType转换为对应的TypeInfomation类型进行序
列化。
下面将一一介绍DataStream的类型系统、SQL的类型系统、数据的
序列化过程。

5.1 DataStream类型系统
DataStream是面向开发者的比较偏底层的API,在Flink SQL推出
之前是Flink主要的开发接口。开发者在开发DataStream应用的时候,
就像在编写Java或者Scala的程序一样,在定义数据类型的时候,使用
的是Java或者Scala的类型,如代码清单5-1所示。
代码清单5-1 WindowWordCount代码示例

see more please visit: https://homeofpdf.com


上 述 代 码 中 , Splitter#flatMap 的 输 入 是 String , 输 出 是
Tuple2<String, Integer>,其中涉及了String、Integer、Tuple2这
样的Java类型,也使用了泛型。对于Flink而言,在运行时需要将这些
不同类型的数据序列化为二进制数据,在内存中保存或者在网络上传
输。
5.1.1 物理类型
Flink中支持的物理类型如图5-2所示。为了方便开发者,对于大
部分的常用类型,Flink都提供了内置的类型描述和序列化/反序列化

see more please visit: https://homeofpdf.com


方法,但是对于用户自定义类型,需要用户注册该类型,并自行实现
序列化、反序列化的方法。

图5-2 Flink物理类型分类
如果开发者在编写Flink应用过程中使用了自定义类型,并且又没
有提供类型的注册和序列化/反序列化方法,Flink就无法对该类型进
行该自定义序列化/反序列化。此时为了Flink的正常运行,对于这一
类的数据类型,无法识别的类型就会交给Kryo进行序列化。Kryo可以
对任意类型的Java对象进行序列化,是一种Java中的通用序列化方
式,缺点是序列化/反序列化效率相对较低。
5.1.2 逻辑类型

see more please visit: https://homeofpdf.com


Flink应用开发者使用的是Java/Scala的原生类型,对于Flink而
言,UDF的输入和返回类型在开发时都是物理类型。逻辑类型是物理类
型的描述,Flink在运行时会根据逻辑类型进行数据的序列化和反序列
化。
TypeInformation是Flink类型系统的核心类,所有的逻辑类型都
继承自该类,Flink的逻辑类型分类如图5-3所示。
TypeInformation 作 为 核 心 抽 象 类 , 是 Java/Scala 对 象 类 型 和
Flink的二进制数据之间的桥梁,在其中定义了关键的序列化器的方法
createSerializer (ExecutionConfig config),所有的逻辑类型都
必须实现该方法,为该类型提供定制的序列化器Serializer,在作业
执行时,序列化器将Java/Scala对象序列化成二进制数据,从二进制
数据中心反序列化为Java/Scala对象。
为了降低逻辑类型系统给开发者带来的负担,Flink内置了大量类
型的逻辑类型,对Java基本类型(如String、Integer等)、数组类
型、对象类型(如Tuple、Crow、Pojo对象)、集合类型等都提供了默
认实现,这些类型可以直接使用,Flink会自动将其识别为对应的逻辑
类型。

see more please visit: https://homeofpdf.com


图5-3 Flink逻辑类型分类
Flink作业在执行时被分发到不同的机器上执行,逻辑类型作为重
要的类型信息,也需要分发到各个计算节点上,类型信息本身的序列
化使用的是Java自带的序列化机制,实现了Serializable接口。
5.1.3 类型推断
在上文提到过,开发者使用的是物理类型,而Flink运行时需要的
是逻辑类型,所以需要从物理类型包装为逻辑类型。那么如何从开发
者编写的代码中提取Function的输入输出类型呢?
Flink使用了两种开发语言Java和Scala,两者的类型提取的实现
方式不同。Java的Flink应用使用反射机制获取Function的输入和输出
类型。Scala使用Scala Macro类提取类型。
1. 类型提取的时机

see more please visit: https://homeofpdf.com


类型信息很重要,那么类型提取是在什么时候发生的呢?如代码清
单5-2所示。
代码清单5-2 DataStream#map方法类型提取示例

从上述代码中可以看到,在使用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函数比较特殊,其类
型提取是匿名的,也没有与之相关的类,所以其类型信息较难获取。

see more please visit: https://homeofpdf.com


Eclipse的JDT编译器会把Lambda函数的泛型签名等信息写入编译后的
字节码中,而对于javac等常见的其他编译器,则不会这样做,因而
Flink就无法获取具体类型信息了。
所 以 在 Flink 中 借 鉴 了 Google GSON 的 TypeToken 的 实 现 , 使 用
TypeHint的匿名类来保存类型信息。
(1)Java类型擦除的原因
1)避免JVM的重构。如果JVM将泛型类型延续到运行期,那么到运
行期时JV就需要进行大量的重构工作,提高了运行期的效率。
2 ) 版 本 兼 容 。 在 编 译 期 擦 除 可 以 更 好 地 支 持 原 生 类 型 ( Raw
Type)。
(2)Java泛型类型擦除规则
1 ) 如 果 是 继 承 基 类 而 来 的 泛 型 , 就 用
getGenericSuperclass(), 转型为ParameterizedType来获得实际类
型。
2 ) 如 果 是 实 现 接 口 而 来 的 泛 型 , 就 用
getGenericInterfaces ( ) , 针 对 其 中 的 元 素 转 型 为
ParameterizedType来获得实际类型。
3)Java泛型在字节码中会被擦除,并不总是擦除为Object类型,
而是擦除到上限类型。
5.1.4 显式类型
一般情况下,Flink除了自动类型推断之外,还提供了显式的类型
声明,可以手动创建TypeInfomation,为了简化使用使用,Flink提供
了两层简化的类型使用方式。
1. 按照数据类型的快捷方式
例如,BasicTypeInfo这个类定义了基本类型的TypeInfomation的
快 捷 声 明 , 如 String 、 Boolean 、 Byte 、 Short\Integer 、 Long 、
Float、Double、Char等。
2. 通用的类型快捷方式
使用上述类型声明方式已经比使用TypeInfomation方便快捷了,
但是使用不同的类型还是要引入不同的类型声明类,所以Flink还提供

see more please visit: https://homeofpdf.com


了 等 价 的 Types 类
(org.apache.flink.api.common.typeinfo.Types),Types作为类型
声明的统一入口,基本涵盖了常用类型。
注:Flink Table & SQL的Types类,在旧版本中是提供给SQL的类
型声明,现在已经被标记为废弃。
5.1.5 类型系统存在的问题
Flink内置的类型系统虽然强大而灵活,但仍然有一些需要注意的
点。
1. Lambda函数的类型提取
因为类型擦除导致Lambda函数的类型提取并不能总是有效的,有
时候需要手动指定类型。
2. Kryo 的 JavaSerializer 在 Flink 下 存 在 Bug , 可 能 导 致
ClassNotFound异常
推 荐 使 用
org.apache.flink.api.java.typeutils.runtime.kryo.JavaSerializ
er而非com.esot-ericsoftware.kryo.serializers.JavaSerializer,
以防止与Flink不兼容。

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所示。

see more please visit: https://homeofpdf.com


LogicalType是SQL类型系统的核心抽象,所有SQL类型都继承自
LogicalType。LogicalType子类定义了每种类型的特殊参数,如对于
数值类型,定义了其长度和精度。
注意 :LogicalType类型只是用来描述数据类型的,当前
planner和runtime实现尚未支持所有的LogicalType类型。
SQL类型系统只是逻辑类型,但是在Flink SQL执行时,最终转换
为了Flink DataStream/DataSet应用,此时就需要TypeInfomation类
型信息来实现序列化/反序列化,所以SQL逻辑类型LogicalType需要转
换为TypeInfomation。
Row是SQL类型系统中顶层的数据容器,表示表中的一行数据或者
数据流上的一个数据记录,在目前版本的Flink中存在两套Row结构:

图5-4 Flink SQL支持的SQL逻辑类型


1)org.apache.flink.types.Row:在Flink Planner中使用,是
1.9版本之前Flink SQL使用的Row结构,在SQL相关的算子、UDF函数、

see more please visit: https://homeofpdf.com


代码生成中都是使用该套Row结构。
2 ) org.apache.flink.table.dataformat.BaseRow 及 其 子 类 : 是
在Blink Runtime和Blink Planner中使用的新的Row类型数据结构,在
Blink算子、UDF函数和代码生成中使用此结构。
5.2.1 Flink Row
在1.9之前版本的Flink实现中,使用Row作为Flink SQL中一行记
录的表达和存储形式,Row使用了对象数组(Object[])来记录数
据,本质上来说是基于Java对象的方式。在数据计算过程中,需要消
耗大量的CPU来序列化/反序列化Row对象。
假设对于一个(Integer,String,String)结构的表,其中一行
记录为(321,“awesome”,“flink”),使用Fink Row存储,其结
构如图5-5所示。

图5-5 Flink Row存储结构


Flink Row本身不是强类型的,所以需要为Row提供RowTypeInfo来
描述Row中的数据类型,在序列化/反序列化的时候使用。
5.2.2 Blink Row
在1.9版本中Blink引入二进制的内存行式存储和列式存储两种数
据组织形式,两种存储结构都比较紧凑。
内存中的行、列式存储和磁盘上的行、列式存储不同。在磁盘上
的列式存储一般会采用压缩存储,比行式存储压缩效率高,占用存储
空间小、IO效率高,所以非常适用于分析型的计算。在内存中的列式
存储一般没有必要采用压缩存储,毕竟解压缩也需要消耗计算成本。
所以在内存中行、列式存储各有优势,考虑到计算类型的不同,并没
有绝对的优劣之分。Blink Row总览如图5-6所示。

see more please visit: https://homeofpdf.com


图5-6 Blink Row总览
1. Blink中的行式存储结构
1)BinaryRow:表数据的二进制行式存储,分为定长部分和不定
长部分,定长部分只能在一个MemorySegment内。
2 ) NestedRow : 与 BinaryRow 的 内 存 存 储 结 构 一 样 , 区 别 在 于
NestedRow的定长部分可以跨MemorySegment。
3)UpdatableRow:该类型的Row比较特别,其保存了该行所有字
段的数据,更新字段数据的时候不修改原始数据,而是使用一个数组
记录被修改字段的最新值。读取数据的时候,首先判断数据是否被更
新过,如果更新过则读取最新值,如果没有则读取原始值。
4)ObjectArrayRow:使用对象数据保存数据,比二进制结构存储
形式多了对象的序列化/反序列化,理论上来说成本更高。其有两个实
现类GenericRow和BoxedWrapperRow。GenericRow中存储的数据类型是
原始类型(如int等),BoxedWrapperRow中存储的数据类型是可序列
化和可比较大小的对象类型。
5)JoinedRow:表示Join或者关联运算中的两行数据的逻辑结
构,如Row1、Row2,两行数据并没有进行物理上的合并,物理合并成
本高。但是从使用者的角度来说,看起来就是一行数据,无须关注底
层。

see more please visit: https://homeofpdf.com


为 了 提 升 Flink SQL 的 性 能 , 在 1.9 版 本 中 实 现 了 BinaryRow ,
BinaryRow直接使用MemorySegment来存储和计算,计算过程中直接对
二进制数据结构进行操作,避免了序列化/反序列化的开销。
在 此 重 点 介 绍 一 下 BinaryRow , 假 设 对 于 一 个 ( Integer ,
String,String)结构的表,其中一行记录为(321,“awesome”,
“flink”),使用BinaryRow存储,其结构如图5-7所示。

图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 数据类型是否为定长判断

see more please visit: https://homeofpdf.com


在目前的设计中,定长部分全部保存在1个MemorySegment中,以
提升读写BinaryRow中字段的速度。在写入阶段,如果BinaryRow中定
长部分超过单个MemorySegment的存储容量,确实有非常多的字段,则
建议增加MemorySegment的大小。
(2)变长部分
变长部分用来保存超过8个字节长度的字段的值,可能会保存跨越
多个MemorySegment的字段。

see more please visit: https://homeofpdf.com


注意 : BinaryRow实 际 上是 参照 Spark 的 UnsafeRow 来 设计
的,两者的区别在于Flink的BinaryRow不是保存在连续内存中,如果
不定长部分足够小,可以保存在一个固定长度的内存中。
按照阿里巴巴官方的测试,使用了BinaryRow之后,Blink在流计
算方面的性能提升了2倍。
2. Blink中的内存列式存储
内存中的列式存储形式,目前在Flink中用来读取ORC类型的列式
存储的数据。
ColumnarRow:表数据的二进制列式存储,每一列是一个Vector向
量。
5.2.3 ColumnarRow
ColumnarRow 是 一 种 内 存 列 式 存 储 结 构 , 每 一 列 的 抽 象 结 构 为
ColumnVector。在当前的实现中,只支持堆上ColumnVector,堆外的
ColumnVector尚不被支持。堆上ColumnVector本质上是使用Java原始
类型数据保存一列的数据。Orc类型的列式存储使用了ColumnarRow。
对于查询类的请求,使用列式存储能够提高CPU缓存命中率。CPU
的数据预读取策略总是尝试将相邻的数据预读取到缓存中,因为列式
存储形式中一列数据总是紧邻的,与行式数据相比,访问同一个字段
的时候,CPU缓存命中率更高,因此CPU就无须浪费宝贵的事件周期去
等待数据从内存加载,从而提高计算效率,如图5-8所示。

图5-8 ColumnarRow组织数据的结构示意图
默认情况下,一个ColumnarRow示例中保存2048行的数据,2048是
个经验数字。

see more please visit: https://homeofpdf.com


5.3 数据序列化
数据序列化就是Java对象和二进制数据流之间的相互转换,前文
提到的TypeInfomation#createSerializer接口负责创建每种类型的序
列化器,进行数据的序列化。
5.3.1 数据序列化/反序列化
前文提到过TypeInfomation是数据物理类型的描述。在需要将数
据进行序列化的时候,使用TypeInfomation的生成序列化器接口创建
出TypeSerializer,TypeSerializer提供了序列化和反序列化能力。
TypeSerializer是抽象类,所有的常见数据类型都实现了自己的序列
化器TypeSerializer实现。
数据序列化、反序列化的概要过程如图5-9所示。

图5-9 序列化和反序列化过程
TypeSerializer的实现类比较多,感兴趣的读者可以查看官方的
Java API文档或者源代码了解更多细节。
对于嵌套类型的数据结构,从最内层的原子字段开始进行序列
化,外层的TypeSerializer负责将内层的序列化结果组装到一起。

see more please visit: https://homeofpdf.com


下边通过一个实例来了解嵌套复杂结构的序列化和反序列化,如
图5-10所示。
一个Tuple3<Integer,Double,Person>对象,其中前两个字段为
基本类型,Person是一个自定义的Pojo类,Person类中包含了两个基
本类型的字段,Tuple3<Interger,Double,Person>的类型信息描述使
用TupleTypeInfo进行保存。
序列化的时候,从TupleTypeInfo中获取对应的序列化器,注意对
于Tuple这种嵌套类型,Tuple的序列化器会遍历Tuple中所有字段,并
根据字段的数据类型生成子序列化器,如果Tuple嵌套了其他复杂类
型,则会递归地生成子序列化器。按照顺序将Tuple中的字段序列化为
二进制数据,依次写入连续的内存片段中。
反序列化的时候,则会依次调用Tuple中字段对应的序列化器,从
内存中顺序读取二进制数据,反序列化为Java对象,重新组装成Tuple
对象。

图5-10 嵌套类型序列化示例

注意:反序列化的时候,Tuple中的每个子序列化器能够自
动识别应该读取多少字节的数据,如对于int类型,读取32字节,对于

see more please visit: https://homeofpdf.com


String类型,则会首先读取长度部分,根据长度的数值计算出字符串
的起始内存地址和应该读取的字节长度。
5.3.2 String序列化过程示例
StringSerializer中实现了serialize和deserialize方法,调用
StringValue.class实现了数据的序列化和反序列化,如代码清单5-5
所示。
代码清单5-5 StringSerializer序列化字符串

最 终 的 实 际 序 列 化 的 动 作 交 给 StringValue.class 执 行 , 写 入
String的长度和String的值到java.io.DataOutput,实际上就是写入
MemorySegment中,如代码清单5-6所示。
代码清单5-6 String类型数据实际序列化过程

see more please visit: https://homeofpdf.com


Flink 中 的 序 列 化 的 类 实 际 上 都 调 用 了 DataOutputView 接 口 ,
DataOutputView接口继承自DataOutput接口。
反序列化的逻辑是相反的,将二进制数据流转换为UTF8编码的字
符串,如代码清单5-7所示。
代码清单5-7 String类型数据反序列化过程

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
5.3.3 作业序列化
Flink作业在执行时,需要进行数据序列化,执行前还要先将作业
的UDF代码等进行序列化,所有的UDF都需要实现Serializable接口。
这里需要特别提一下内部类序列化,在Flink中编写UDF的时候,
经常会使用匿名内部类的实现方式,如代码清单5-8所示。
代码清单5-8 使用匿名内部类实现MapFunction

see more please visit: https://homeofpdf.com


上边代码中的MapFunction使用了匿名内部类的方式实现,默认内
部类会持有一个外部对象的引用this$0,如果外部对象不实现序列化
接口,内部类的序列化会失败,所在Flink中使用ASM操作字节码将匿
名 内 部 类 中 的 this$0 设 置 为 null 。 在 Flink DataStreamp 的 map 、
filter、keyBy等接口中都使用了ClosureCleaner#clean方法来设置
this$0。
5.3.4 Kryo序列化
Kryo 的 JavaSerializer 在 Flink 下 存 在 Bug , 可 能 导 致
ClassNotFound 异 常 。 所 以 Flink 自 行 维 护 了
org.apache.flink.api.java.typeutils.runtime.kryo.JavaSerializ
er替代Kryo的JavaSerializer。

5.4 总结
DataStream类型系统分为物理类型和逻辑类型,在Flink UDF中需
要使用类型推断将用户代码中的类型转换为DataStream的逻辑类型,
在运行时刻需要使用逻辑类型信息来实现数据的序列化和反序列化。
DataStream的类型系统对SQL的类型系统的支持不够完善,所以Blink
SQL引入了新的类型系统,执行层面使用DataStream类型系统。
对于Flink类型系统没有覆盖的类型,使用Kryo来实现序列化。对
于Flink类型系统支持的类型,则会使用类型描述信息,将数据序列化
为二进制数据和从二进制反序列化成对象。

see more please visit: https://homeofpdf.com


第6章 内存管理
在现阶段,大部分开源的大数据计算引擎都是用Java或者是基于
JVM 的 编 程 语 言 实 现 的 , 如 Apache Hadoop 、 Apache Spark 、 Apache
Drill、Apache Flink等。Java语言的好处是不用考虑底层,降低了程
序员的门槛,JVM可以对代码进行深度优化,对内存资源进行管理,自
动回收内存。但是自动内存管理的问题在于不可控,基于JVM的大数据
引擎常常会面临一个问题,即在处理海量数据的时候,如何在内存中
存储大量的数据(包括缓存和高效处理)。

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会达到秒级甚至分钟级,直接影响执行效率。

see more please visit: https://homeofpdf.com


GC带来的中断会使集群中的心跳信息超时,导致节点被踢出集
群,整个集群进入不稳定的状态。虽然通过JVM参数的调优可以提升回
收效率,尽量减少Full GC,但是仍然不能避免这个问题,精确的调优
也非常困难。
(3)OOM问题影响稳定性
OutOfMemoryError是分布式计算框架经常会遇到的问题,当JVM中
所 有 对 象 大 小 超 过 分 配 给 JVM 的 内 存 大 小 时 , 就 会 发 生
OutOfMemoryError错误,导致JVM崩溃,分布式框架的健壮性和性能都
会受到影响。
(4)缓存未命中问题
CPU进行计算的时候,是从CPU缓存中获取数据,而不是直接从内
存获取数据。CPU有分L1和L2/3级缓存。L1小,一般为32KB,L3大,能
达到32MB。缓存的理论基础是程序局部性原理,包括时间局部性和空
间局部性:最近被CPU访问的数据,短期内CPU还要访问(时间);被
CPU访问的数据附近的数据,CPU短期内还要访问(空间)。Java对象
在堆上存储的时候并不是连续的,所以从内存中读取Java对象时,缓
存的邻近的内存区域的数据往往不是CPU下一步计算所需要的,这就是
缓存未命中。此时CPU需要空转等待从内存中重新读取数据,CPU的速
度和内存的速度之间差好几个数量级,导致CPU没有充分利用起来。如
果数据没有在内存中,而是需要从磁盘上加载,那么执行效率就会变
得惨不忍睹。
不同硬件的访问延迟如图6-1所示。

see more please visit: https://homeofpdf.com


图6-1 不同硬件的访问延迟
从图中可以看到,L1级缓存的访问效率最高可以到普通磁盘的108
倍(1亿倍)。
2. 自主内存管理
因为JVM存在诸多问题,所以越来越多的大数据计算引擎选择自行
管理JVM内存,如Spark、Flink、HBase,尽量达到C/C++ 一样的性
能,同时避免OOM的发生。本章主要介绍Flink是如何解决上面的问题
的,主要内容包括内存管理、定制的序列化工具、缓存友好的数据结
构和算法、堆外内存等。
在Flink中,Java对象的有效信息被序列化为二进制数据流,在内
存中连续存储,保存在预分配的内存块上,内存块叫作
MemorySegment。MemorySegment是内存分配的最小单元,是一段固定
长度的内存(默认大小为32KB),并且提供了非常高效的读写方法,
很多运算可以直接操作二进制数据,不需要反序列化即可执行。
MemorySegment可以保存在堆上,其内部存储为一个Java byte数
组,也可以保存在堆外的ByteBuffer中。每条记录都会以序列化的形
式存储在一个或多个MemorySegment中。

see more please visit: https://homeofpdf.com


Flink早期版本使用的是堆上内存,在堆内存上管理序列化之后的
数据。如果需要处理的数据超出了内存限制,则会将部分数据存储到
硬盘上。操作多块MemorySegment就像操作一块大的连续内存一样,
Flink会使用逻辑视图(AbstractPagedInputView)以方便操作。
但使用堆上内存,仍然不是完全自主的内存管理,还存在以下问
题。
1)超大内存(上百GB)JVM的启动需要很长时间,Full GC可以达
到分钟级。使用堆外内存,可以将大量的数据保存在堆外,极大地减
小堆内存,避免GC和内存溢出的问题。
2 ) 高 效 的 IO 操 作 。 堆 外 内 存 在 写 磁 盘 或 网 络 传 输 时 是 zero-
copy,而堆上内存则至少需要1次内存复制。
3)堆外内存是进程间共享的。也就是说,即使JVM进程崩溃也不
会丢失数据。这可以用来做故障恢复(Flink暂时没有利用这项功能,
不过未来很可能会去做)。
3. 堆外内存的不足之处
堆外内存提供了更好的性能和更可控的内存管理,但是也存在几
个问题:
1)堆上内存的使用、监控、调试简单,堆外内存出现问题后的诊
断则较为复杂。
2)Flink有时需要分配短生命周期的MemorySegment,在堆外内存
上分配比在堆上内存开销更高。
3)在Flink的测试中,部分操作在堆外内存上会比堆上内存慢。
同时为了提高效率,Flink在计算中采用了DBMS的Sort和Join算
法,直接操作二进制数据,避免数据反复序列化带来的开销。Flink的
内部实现更像C/C++ 而非Java。

6.2 内存模型
Flink 1.10以前的版本中内存模型存在一些缺陷,导致优化资源
充分利用率比较困难,例如:
● 流和批处理内存占用的配置模型不同,配置参数多、关系乱。

see more please visit: https://homeofpdf.com


● 流处理中的RocksDBStateBackend需要依赖用户进行复杂的配
置。
为了让内存配置对于用户更加清晰、直观,Flink 1.10版本对
TaskExecutor的内存模型和配置逻辑进行了较大的改动(FLIP-49)。
这些改动使得Flink能够更好地适配所有部署环境(如Kubernetes、
Yarn、Mesos),让用户能够更加严格地控制其内存开销。
6.2.1 内存布局
TaskManager是Flink中执行计算的核心组件,是用来运行用户代
码的Java进程。其中大量使用了堆外内存。
Flink TaskManager的简化和详细内存结构如图6-2所示。

see more please visit: https://homeofpdf.com


图6-2 Flink TaskManager简化和详细内存模型
从大的方面来说,TaskManager进程的内存模型分为JVM本身所使
用的内存和Flink使用的内存,Flink使用了堆上内存和堆外内存。
1. Flink使用的内存
(1)JVM堆上内存
1)框架堆上内存Framework Heap Memory。Flink框架本身所使用
的内存,即TaskManager本身所占用的堆上内存,不记入Slot的资源
中。
配 置 参 数 : taskmanager.memory.framework.heap.size =
128MB,默认128MB。
2)Task堆上内存Task Heap Memory。Task执行用户代码时所使用
的堆上内存。
配置参数:taskmanager.memory.task.heap.size。
(2)JVM堆外内存
1)框架堆外内存Framework Off-Heap Memory。Flink框架本身所
使用的内存,即TaskManager本身所占用的堆外内存,不记入Slot资
源。
配 置 参 数 : taskmanager.memory.framework.off-heap.size =
128MB,默认128MB。
2)Task堆外内存Task Off-Heap Memory。Task执行用户代码时所
使用的堆外内存。
配置参数:taskmanager.memory.task.off-heap.size = 0,默认
为0。
3)网络缓冲内存Network Memory。网络数据交换所使用的堆外内
存大小,如网络数据交换缓冲区(Network Buffer,后文会介绍)。
配 置 参 数 :taskmanager.memory.network. [ 64/1024/0.1 ] )
(min/max/fraction),默认min=64MB, max=1gb, fraction=0.1。
4)堆外托管内存Managed Memory。Flink管理的堆外内存。
配 置 参 数 : taskmanager.memory.managed.
[size|fraction]),默认fraction=0.4。

see more please visit: https://homeofpdf.com


2. JVM本身使用的内存
JVM本身直接使用了操作系统的内存。
(1)JVM元空间
JVM元空间所使用的内存。
配 置 参 数 : taskmanager.memory.jvm-metaspace =96m , 默 认
96MB。
(2)JVM执行开销
JVM在执行时自身所需要的内容,包括线程堆栈、IO、编译缓存等
所使用的内存。
配 置 参 数 : taskmanager.memory.jvm-overhead =
[min/max/fraction])。默认min=192MB,max=1GB, fraction=0.1。
3. 总体内存
(1)Flink使用内存
综上而言,Flink使用的内存包括Flink使用的堆上、堆外内存。
使用参数taskmanager. memory.flink.size进行控制。
(2)进程使用内存
整个进程所使用的内存,包括Flink使用的内存和JVM使用的内
存。使用参数taskmanager.memory.process.size进行控制。
JVM内存控制参数如下所示。
1)JVM堆上内存,使用-Xmx和-Xms参数进行控制。
2)JVM直接内存,使用参数-XX:MaxDirectMemorySize进行控制。
对于托管内存,使用Unsafe.allocateMemory()申请,不受该参数控
制。
3)JVM Metaspace使用-XX:MaxMetaspaceSize进行控制。
6.2.2 内存计算
目前的实现中,在JVM启动之前就需要确定各个内存区块的大小。
一旦JVM启动了,在TaskManager进程内部就不再重新计算。Flink中有
两个地方进行内存大小计算:
● 在Standalone部署模式下,内存的计算在启动脚本中实现。

see more please visit: https://homeofpdf.com


● 在 容 器 环 境 下 ( Yarn 、 K8s 、 Mesos ) , 计 算 在
ResourceManager中进行。
在启动脚本与容器环境下的内存大小计算都调用了Fink的Java代
码时间,保证了所有部署模式下的统一,计算好的参数使用-D参数交
给Java进程。
计算时,需要配置如下3个参数组合中的至少1个。
(1)Task的堆上内存和托管内存
如果手动配置了网络缓冲区内存大小,则使用该参数。如果没有
明确配置,则使用分配系数fraction×总体Flink使用内存计算网络缓
冲区内存大小。
(2)总体Flink使用内存
如果配置了该选项,而没有配置(1),则从总体Flink内存中划
分网络缓冲区内存和托管内存,剩余的内存作为Task堆上内存。
如果手动设置了网络缓冲内存,则使用其值,否则使用默认的分
配系数fraction×总体Flink内存。
如果手动设置了托管内存,则使用其值,否则使用默认的分配系
数fraction×总体Flink内存。
(3)总体进程使用内存
如果只配置了总体进程使用内存,则从总体进程中扣除JVM元空间
和JVM执行开销内存,剩余的内存作为总体Flink使用内存。

6.3 内存数据结构
Flink的内存管理像操作系统管理内存一样,将内存划分为内存
段、内存页等结构。
6.3.1 内存段
内存段在Flink内部叫作MemorySegment,是Flink的内存抽象的最
小分配单元。默认情况下,一个MemorySegment对应着一个32KB大小的
内存块。这块内存既可以是堆上内存(Java的byte数组),也可以是
堆外内存(基于Netty的DirectByteBuffer)。

see more please visit: https://homeofpdf.com


MemorySegment同时也提供了对二进制数据进行读取和写入的方
法。对于Java基本数据类型,如short、int、long等,MemorySegment
内置了方法,可以直接返回或者写入数据,对于其他类型,读取二进
制数组byte[]后进行反序列化,序列化为二进制数组byte[]后写
入。
1. MemorySegment结构
为了更加清晰地理解MemorySegment,下面看一下MemorySegment
的关键属性。
1)BYTE_ARRAY_BASE_OFFSET:二进制字节数组的起始索引,相对
于字节数组对象而言。
2)LITTLE_ENDIAN:判断是否为Little Endian模式的字节存储顺
序,若不是,就是Big Endian模式。
3)HeapMemory:如果MemeorySegment使用堆上内存,则表示一个
堆上的字节数组(byte[]),如果MemorySegment使用堆外内存,则
为null。
4)address:字节数组对应的相对地址(若HeapMemory为null,即
可能为堆外内存的绝对地址,后续会详解)。
5)addressLimit:标识地址结束位置(address+size)。
6)size:内存段的字节数。
2. 字节顺序Big Endian与Little Endian
字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺
序,不同的CPU架构体系使用不同的存储顺序。PowerPC系列采用Big
Endian方式存储数据,低地址存放最高有效字节(MSB),而x86系列
则 采 用 Little Endian 方 式 存 储 数 据 , 低 地 址 存 放 最 低 有 效 字 节
(LSB),如图6-3所示。

图6-3 Big Endian与Little Endian的内存存储差异

see more please visit: https://homeofpdf.com


3. MemorySegment实现
Flink的MemorySegment有堆上和堆外两种实现,其类体系如图6-4
所示。

图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保存的数据,如果需要上层的使用者,需要考虑所有的

see more please visit: https://homeofpdf.com


细节,非常烦琐,所以Flink又抽象了一层,叫作内存页。内存页是
MemorySegment 之 上 的 数 据 访 问 视 图 , 数 据 读 取 抽 象 为
DataInputView,数据写入抽象为DataOutputView。有了这一层,上层
使 用 者 无 须 关 心 MemorySegment 的 细 节 , 该 层 会 自 动 处 理 跨
MemorySegment的读取和写入。
1. DataInputView
DataInputView 是 从 MemorySegment 数 据 读 取 抽 象 视 图 , 继 承 自
java.io.DataInput,提供了从二进制流中读取不同数据类型的方法,
如图6-5所示。
InputView 中 持 有 多 个 MemorySegment 的 引 用
( MemorySegment [ ] ) , 这 一 组 MemorySegment 被 视 为 一 个 内 存 页
(Page),可以顺序读取MemorySegment中的数据。
基本上所有的InputView实现类都继承了AbstractPageInputView
抽象类,也就是说所有的InputView实现类都支持Page。
2. DataOutputView
DataOutputView是数据写入MemorySegment的抽象视图,继承自
java.io.DataOutput,提供了将不同类型的数据写入二进制流的一系
列方法。同样,DataOutputView中持有一个或者多个MemorySegment的
引用(MemorySegment[]),这一组MemorySegment被视为一个内存
页(Page),可以顺序地向MemorySegment中写入数据。
DataOutputView的接口继承关系如图6-6所示。

图6-5 DataInputView接口继承关系

see more please visit: https://homeofpdf.com


图6-6 DataOutputView接口继承关系
基 本 上 所 有 的 OutputView 实 现 类 都 继 承 了
AbstractPageOutputView抽象类,也就是说所有的OuputView实现类都
支持跨MemorySegment写入。
3. 内存页的使用
对内存的读取写入操作是非常底层的行为,对于上层应用
(DataStream作业)而言,涉及向MemorySegment写入、读取二进制的
地方都使用到了DataOutputView和DataInputView,而不是直接使用
MemorySegment。
例 如 , 在 flink-table-runtime-blink 中 , BinaryRowSerializer
中使用AbstractPagedInputView从MemorySegment中读取二进制数据并
转 换 成 BinaryRow , 使 用 AbstractPagedOutputView 将 BinaryRow 写 入
MemorySegment中。
6.3.3 Buffer
Task算子处理数据完毕,将结果交给下游的时候,使用的抽象或
者说内存对象是Buffer。Buffer接口是网络层面上传输数据和事件的
统一抽象,其实现类是NetworkBuffer。Flink在各个TaskManager之间
传递数据时,使用的是这一层的抽象。1个NetworkBuffer中包装了1个
MemorySegment。
Buffer接口的类体系如图6-7所示。

see more please visit: https://homeofpdf.com


图6-7 NetworkBuffer接口的类体系
Buffer的底层是MemorySegment,Buffer申请和释放由Flink自行
管理,Flink引入了引用数的概念,当有新的Buffer消费者时,引用数
加1,当消费者消费完Buffer时,引用数减1,最终当引用数变为0时,
就可以将Buffer释放重用了。
NetworkBuffer 同 时 继 承 了 AbstractReferenceCountedByteBuf 。
AbstractReferenceCountedByteBuf是Netty中的抽象类,通过继承该
类 , Flink 中 Buffer 具 备 了 引 用 计 数 的 能 力 , 并 且 实 现 了 对
MemorySegment的读写。感兴趣的读者可以去了解一下Netty。
6.3.4 Buffer资源池
Buffer资源池在Flink中叫作BufferPool。BufferPool用来管理
Buffer,包含Buffer的申请、释放、销毁、可用Buffer通知等,其实
现类是LocalBufferPool,每个Task拥有自己的LocalBufferPool。
BufferPool的类体系如图6-8所示。

see more please visit: https://homeofpdf.com


图6-8 BufferPool类体系
为 了 方 便 对 BufferPool 的 管 理 , Flink 设 计 了
BufferPoolFactory,提供BufferPool的创建和销毁,其唯一的实现类
是NetworkBufferPool。
每 个 TaskManager 只 有 一 个 NetworkBufferPool , 同 一 个
TaskManager上的Task共享NetworkBufferPool,在TaskManager启动的
时候,就会创建NetworkBufferPool,为其分配内存。
NetworkBufferPool持有该TaskManager在进行数据传递时所能够
使用的所有内存,所以其除了作为BufferPool的工厂外,还作为Task
所 需 内 存 段 ( MemorySegment ) 的 提 供 者 , 每 个 Task 的
LocalBufferPool 所 需 要 的 内 存 都 是 从 NetworkBufferPool 申 请 而 来
的。

6.4 内存管理器
MemoryManager是Flink中管理托管内存的组件,其管理的托管内
存只使用堆外内存。在批处理中用在排序、Hash表和中间结果的缓存
中,在流计算中作为RocksDBStateBackend的内存。

see more please visit: https://homeofpdf.com


在1.10之前的Flink版本中,MemoryManager负责TaskManager的所
有内存,1.10版本中,MemoryManager的管理范围缩小为Slot级别,即
为Task管理内容,TaskManager为每个Slot分配相同的内容,Task不能
使用超过其Slot分配的资源。这样的实现并不完美,但是相比1.10之
前的版本,能够更好地隔离任务,系统更加稳定。未来版本可能会采
取更好的Slot资源使用策略,在资源空闲的情况下,允许Task中的
StreamOperator申请超过预先分配的资源。
MemoryManager 主 要 通 过 内 部 接 口 MemoryPool 来 管 理 所 有 的
MemorySegment。托管内存的管理相比于Network Buffers的管理更为
简单,因为不需要Buffer的那一层封装。
6.4.1 内存申请
批处理计算任务中,MemoryManager负责为算子申请堆外内存。最
终实际申请的是堆外的ByteBuffer,如代码清单6-1所示。
代码清单6-1 MemoryManager申请MemorySegment

see more please visit: https://homeofpdf.com


流计算任务中,MemoryManager更多的作用是管理,控制RocksDB
的内存使用量,通过RocksDB的Block Cache和WriterBufferManager参
数来限制,参数的具体值从TaskManager的内存配置参数中计算而来。
RocksDB自己来负责运行过程中的内存申请和内存释放,如代码清单6-
2所示。
代码清单6-2 RocksDB申请内存资源

see more please visit: https://homeofpdf.com


6.4.2 内存释放
Flink自行管理内存,也就意味着内存的申请和释放都由Flink来
负责。触发Java堆外内存释放的行为一般有如下两种。
● 内存使用完毕。
● Task停止(正常或者异常)执行。
在Flink中实现了一个JavaGcCleanerWrapper来进行堆外内存的释
放,提供了两个Java Cleaner。
(1)LegacyCleanerProvider

see more please visit: https://homeofpdf.com


该CleanerProvider提供1.8及以下版本JDK的Flink管理的内存的
垃圾回收,使用sun.misc.Cleaner来释放内存。
(2)Java9CleanerProvider
该CleanerProvider提供1.9及以上版本JDK的Flink管理的内存的
垃圾回收,使用java.lang.ref.Cleaner来释放内存。
JavaGcCleanerWrapper 会 为 每 个 Owner 创 建 一 个 包 含 Cleaner 的
Runnable 对 象 , 在 每 个 MemorySegment 释 放 内 存 的 时 候 , 调 用 此
Cleaner进行内存的释放。
当MemoryManager关闭的时候会对所有申请的MemorySegment进行
释放,交还给操作系统,如代码清单6-3所示。
代码清单6-3 HybridMemorySegment内存回收

流计算中,当Task停止执行的时候RocksDBStateBackend负责释放
物 理 内 存 , 并 将 资 源 归 还 给 MemoryManager 。 资 源 归 还 给
MemoryManager只是更新其可用资源的大小数值,并不是对内存的物理
操作。

6.5 网络缓冲器
网络缓冲器(NetworkBuffer)是网络交换数据的包装,其对应于
MemorySegment内存段,当结果分区(ResultParition)开始写出数据
的 时 候 , 需 要 向 LocalBufferPool 申 请 Buffer 资 源 , 使 用
BufferBuilder将数据写入MemorySegment。当MemorySegment都分配完
后,则会持续等待Buffer的释放。
Network的使用如图6-9所示。

see more please visit: https://homeofpdf.com


图6-9 NetworkBuffer的使用
BufferBuilder在上游Task中,用来向申请到的MemorySegment写
入数据。与BufferBuilder相对的是BufferConsumer,BufferConsumer
位 于 下 游 Task 中 , 负 责 从 MemorySegment 中 读 取 数 据 。 1 个
BufferBuilder对应1个BufferConsumer。
6.5.1 内存申请
LocalBufferPool的大小是动态的,在最小内存段数量与最大内存
段数量之间浮动。使用NetworkBufferPool创建LocalBufferPool时,
如 果 该 TaskManager 的 内 存 无 法 满 足 所 有 Task 所 需 的 最 小
MemorySegment的数量总和,则会发生错误。
1. Buffer申请
结果分区(ResultParition)申请Buffer进行数据写入,如代码
清单6-4所示。
代码清单6-4 ResultPartition申请Buffer

LocalBufferPool 首 先 从 自 身 持 有 的 MemorySegment 中 分 配 可 用
的 , 如 果 没 有 可 用 的 , 则 从 TaskManager 的 NetworkBufferPool 中 申
请,如果没有,则阻塞等待可用的MemorySegment,如代码清单6-5所
示。
代码清单6-5 LocalBuffer分配MemorySegment

see more please visit: https://homeofpdf.com


2. MemorySegment申请
申 请 Buffer 本 质 上 来 说 就 是 申 请 MemorySegment , 如 果 在
LocalBufferPool中,则申请新的堆外内存MemorySegment,如代码清
单6-6所示。
代码清单6-6 LocalBufferPool分配、申请MemorySegment

6.5.2 内存回收

see more please visit: https://homeofpdf.com


Buffer使用了引用计数机制来判断什么时候可以释放Buffer到可
用资源池。每创建一个BufferConsumer,就会对Buffer的引用计数
+1,每个Buffer被消费完,就会对Buffer的引用计数-1,当Buffer引
用计数为0的时候就可以回收了。
1. Buffer回收
前边介绍过Buffer的主要实现类是NetworkBuffer,同时继承了
AbstractReferenceCountedByteBuf。当Buffer被消费一次后,就会对
Buffer的引用计数-1,如代码清单6-7所示。
代码清单6-7 NetworkBuffer更新引用计数

Buffer 回 收 之 后 , 并 不 会 释 放 MemorySegment , 此 时
MemorySegment仍然在LocalBufferPool的资源池中,除非TaskManager
级别内存不足,才会释放回TaskManager持有的全局资源池。
释放MemorySegment的时候,同样要根据MemorySegment的类型来
进行,并且要在不低于保留内存的情况下,将内存释放回内存段中,
变为可用内存,后续申请MemorySegment的时候,可以重复利用该内存
片段。
2. MemorySegment释放
当NetworkBufferPool关闭的时候进行内存的释放,交还给操作系
统,相关介绍参见MemoryManager内存章节的内存释放。

6.6 总结
大数据场景下,使用Java的内存管理会带来一系列的问题,所以
Flink从一开始就选择自主管理内存。为了实现内存管理,Flink对内
存进行了一系列的抽象,内存段MemorySegment是最小的内存分配单

see more please visit: https://homeofpdf.com


位 , 对 于 跨 段 的 内 存 访 问 , Flink 抽 象 了 DataInputView 和
DataOutputView,可以看作是内存页。
Flink在1.10版本重构了其TaskManger的内存管理模型,主要分为
堆上内存和堆外内存,并简化了内存参数。在计算层面上,Flink的内
存管理器提供了对内存的申请和释放,在数据传输层面上,Flink抽象
了网络内存缓冲Buffer(1个Buffer对应一个MemorySegment)的申请
和释放。

see more please visit: https://homeofpdf.com


第7章 状态原理
状态在Flink中叫作State,用来保存中间计算结果或者缓存数
据。根据是否需要保存中间结果,分为无状态计算和有状态计算。对
于流计算而言,事件持续不断地产生,如果每次计算都是相互独立
的,不依赖于上下游的事件,则是无状态计算。如果计算需要依赖于
之前或者后续的事件,则是有状态计算。State是实现有状态计算下的
Exactly-Once的基础。
使用State的典型示例如下。
1. Sum求和
Sum求和的一般思路是把所有的事件缓存起来,触发计算的时候进
行求和运算,但是这种方式效率偏低,需要占用大量的内存,而使用
增量计算,只记录当前所有值的总和(称之为中间结果),当新的事
件到来时,只需要在中间结果的基础上进行求和即可。
2. 重
数据流是持续产生的,如果要去掉数据流中的重复数据,那么要
知道之前收到过哪些数据,就需要将之前的数据保存下来,每收到一
条新的数据,都要与已经收到的数据比较是否重复。
3. 模式检测
Flink实现了CEP复杂事件处理框架,用来在数据流中检测符合模
式的事件,假设做一个密码慢速破解的模式检查,反复用不同的用户
密码组合尝试通过SSH登录到关键的服务器,每一次的尝试行为和结果
就是一个事件E(用户名密码不正确拒绝登录),如果在一个月内反复
尝试失败10次以上则认为符合慢速破解模式。在这个过程中,必然要
记录之前发生的事件,才能够做出判断。
Flink 的 State 提 供 了 对 State 的 操 作 接 口 , 向 上 对 接 Flink 的
DataStream API,让用户在开发Flink应用的时候,可以将临时数据保
存在State中,从State中读取数据,在运行的时候,在运行层面上与
算子、Function体系融合,自动对State进行备份(Checkpoint),一
旦出现异常能够从保存的State中恢复状态,实现Exactly-Once。

see more please visit: https://homeofpdf.com


要做到比较好的State管理,需要考虑以下几点内容。
(1)状态数据的存储和访问
在Task内部,如何高效地保存状态数据和使用状态数据。
(2)状态数据的备份和恢复
作业失败是无法避免的,那么就要思考如何高效地将状态数据保
存下来,避免状态备份降低集群的吞吐量,并且在Failover的时候恢
复作业到失败前的状态。
(3)状态数据的划分和动态扩容
作业在集群内并行执行,那么就要思考对于作业的Task而言如何
使用同一的方式对状态数据进行切分,在作业修改并行度导致Task数
量改变的时候,如何确保正确地恢复到Task。
(4)状态数据的清理
状态的保存和使用都是有成本的,而且状态并不是永久有效的,
所以对于过期的状态进行清理就非常有必要。

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>

see more please visit: https://homeofpdf.com


聚合State,和(3)不同的是,这里聚合的类型可以是不同的元
素类型,使用add(IN)来加入元素,并使用AggregateFunction函数
计算聚合结果。
(5)MapState<UK,UV>
使 用 Map 存 储 Key-Value 对 , 通 过 put ( UK,UV ) 或 者
putAll(Map<UK,UV>)来添加,使用get(UK)来获取。
(6)FoldingState<T,ACC>
跟ReducingState有点类似,不过它的状态值类型可以与add方法
中传入的元素类型不同。已被标记为废弃,不建议使用。
7.1.1 KeyedState与OperatorState
State按照是否有Key划分为KeyedState和OperatorState两种,见
表7-1。
表格7-1 State划分

1. KeyedState
在 KeyedStream 中 使 用 。 状 态 是 跟 特 定 的 Key 绑 定 的 , 即
KeyedStream流上的每一个Key对应一个State对象。KeyedState可以使
用所有的State。KeyedState保存在StateBackend中。
可以使用ValueState进行求和运算,其使用如代码清单7-1所示。
代码清单7-1 ValueState求和运算示例

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
2. 算子状态OperatorState
与KeyedState不同,OperatorState跟一个特定算子的一个实例绑
定,整个算子只对应一个State。相比较而言,在一个算子上,可能会
有很多个Key,从而对应多个KeyedState。
OperatorState目前只支持使用ListState,其使用如代码清单7-2
所示。
代码清单7-2 OpreatorState示例

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
7.1.2 原始和托管状态
按照由Flink管理还是用户自行管理,状态可以分为原始状态
(Raw State)和托管状态(Managed State)。
原始状态,即用户自定义的State,Flink在做快照的时候,把整
个State当作一个整体,需要开发者自己管理,使用byte数组来读写状
态内容。
托 管 状 态 是 由 Flink 框 架 管 理 的 State , 如 ValueState 、
ListState、MapState等,其序列化与反序列化由Flink框架提供支
持,无须用户感知、干预。
KeyedState和OperatorState可以是原始状态,也可以是托管状
态。
通常在DataStream上的状态推荐使用托管状态,一般情况下,在
实现自定义算子时,才会使用到原始状态。

7.2 状态描述
State既然是暴露给用户的,那么就有一些属性需要指定,如
State名称、State中类型信息和序列化/反序列化器、State的过期时
间等。在对应的状态后端(StateBackend)中,会调用对应的create
方 法 获 取 到 StateDescriptor 中 的 值 。 在 Flink 中 状 态 描 述 叫 作
StateDescriptor,其体系如图7-1所示。

see more please visit: https://homeofpdf.com


图7-1 StateDescriptor体系
对 应 于 每 一 类 State , Flink 内 部 都 设 计 了 对 应 的
StateDescriptor , 在 任 何 使 用 State 的 地 方 , 都 需 要 通 过
StateDescriptor描述状态的信息。
运 行 时 , 在 RichFunction 和 ProcessFunction 中 , 通 过
RuntimeContext 上 下 文 对 象 , 使 用 StateDescriptor 从 状 态 后 端
(StateBackend)中获取实际的State实例,然后在开发者编写的UDF
中 就 可 以 使 用 这 个 State 了 。 StateBackend 中 有 对 应 则 返 回 现 有 的
State,没有则创建新的State。
以ValueState为例展示StateDescriptor,如代码清单7-3所示。
代码清单7-3 使用StateDescriptor获取ValueState

see more please visit: https://homeofpdf.com


7.3 广播状态
广播状态在Flink中叫作BroadcastState,在广播状态模式中使
用。所谓广播状态模式,就是来自一个流的数据需要被广播到所有下
游任务,在算子本地存储,在处理另一个流的时候依赖于广播的数
据。下面以一个示例来说明广播状态模式,如图7-2所示。

see more please visit: https://homeofpdf.com


图7-2 广播状态模式示例
假设在一个基于规则的异常行为识别系统中,规则算子根据规则
判断业务数据是否触发异常告警。另外,系统需要能够支持规则的更
新。
最容易想到的方式是将规则保存在数据库中,规则算子启动之
后,启动一个线程周期性(假设10分钟)从数据库中读取最新规则,
但是这样面临一个问题,即规则更新不是实时的,也就是说规则更新
之后,最坏的情况下要等待10分钟才会被应用。虽然可以将更新周期
设定得很短,也许能够满足业务规则,但是这不是重点,重点是这种
方式无论如何都是实时的,所以Flink基于流体系做了一个扩展,将规
则数据流广播到下游所有的算子。
在图7-2中可以看到,业务数据流是一个普通数据流,规则数据流
是广播数据流,这样就可以满足实时性、规则更新的要求。规则算子
将规则缓存在本地内存中,在业务数据流记录到来时,能够使用规则
处理数据。
广播State必须是MapState类型,广播状态模式需要使用广播函数
进行处理,广播函数提供了处理广播数据流和普通数据流的接口。

7.4 状态接口
在Flink中使用状态,有两种典型场景:
● 使用状态对象本身存储、写入、更新数据。
● 从StateBackend获取状态对象本身。
前者叫作状态操作接口,后者叫作状态访问接口。
7.4.1 状态操作接口
Flink中的State面向两类用户,即应用开发者和Flink框架本身。
两者对State的操作是不同的,所以在Flink中设计了两套接口:面向
应用开发者的State接口和内部State接口。面向开发者的接口要保持
稳定,考虑Flink升级的兼容性。内部的State接口则是Flink内部引擎
使用的,提供了更多的State操作方法,可以根据需要灵活地扩展改
进。

see more please visit: https://homeofpdf.com


1. 面向应用开发者的State接口
面向开发的State接口只提供了对State中数据的添加、更新、删
除等基本的操作接口,用户无法访问状态的其他运行时所需要的信
息。
面向用户的State接口体系如图7-3所示。

图7-3 面向开发者的State接口体系
以MapState为例,其提供了添加、获取、删除、遍历的API接口。
其接口定义如代码清单7-4所示。
代码清单7-4 MapState接口

see more please visit: https://homeofpdf.com


2. 内部State接口
内部State接口是给Flink框架使用的,除了对State中数据的访问
之外,还提供了内部的运行时信息接口,如State中数据的序列化器、
命名空间(namespace)、命名空间的序列化器、命名空间合并的接
口。
内部State接口的命名方式为InternalxxxState,内部State接口
的 体 系 非 常 复 杂 。 下 面 以 InternalMapState 为 例 介 绍 其 体 系 ,
InternalMapState的体系如图7-4所示。

see more please visit: https://homeofpdf.com


图7-4 MapSate内部接口
从图7-4中可以看出InternalMapState继承了面向应用开发者的
State接口,也继承了InternalKvState接口,既能访问MapState中保
存的数据,也能访问MapState运行时的信息,如代码清单7-5所示。
代码清单7-5 InternalKvState接口

see more please visit: https://homeofpdf.com


7.4.2 状态访问接口
有了状态之后,在开发者自定义的UDF中如何访问状态?
状态保存在StateBackend中,StateBackend又有3种不同的类型,
状态又分为OperatorState和KeyedState,如果直接使用就会比较麻
烦,所以在Flink中抽象了两个状态访问接口:OperatorStateStore和
KeyedStateStore。有了这两个接口,用户在UDF中就无须考虑到底是
哪种StateBackend了。
OperatorStateStore接口原理如图7-5所示。

see more please visit: https://homeofpdf.com


图7-5 OperatorStateStore接口原理
从图7-5中可以看到,OperatorState数据以Map形式保存在内存
中,并没有使用RocksDBStateBackend和HeapKeyedStateBackend。
KeyedStateStore接口原理如图7-6所示。

图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所示。

see more please visit: https://homeofpdf.com


图7-7 面向用户的StateBackend类体系
1)纯内存:MemoryStateBackend,适用于验证、测试,不推荐生
产环境。
2)内存+文件:FsStateBackend,适用于长周期大规模的数据。
3)RocksDB:RocksDBStateBackend,适用于长周期大规模的数
据。
上面提到的StateBackend是面向用户的,那么在Flink内部3种
State的关系如图7-8所示。

图7-8 3种StateBackend的关系

see more please visit: https://homeofpdf.com


在运行时,MemoryStateBackend和FsStateBackend本地的State都
保 存 在 TaskMananger 的 内 存 中 , 所 以 其 底 层 都 依 赖 于
HeapKeyedStateBackend 。 HeapKeyedStateBackend 面 向 Flink 引 擎 内
部,使用者无须感知。
7.5.1 内存型和文件型状态存储
内存型状态存储和文件型状态存储都依赖于内存保存运行时所需
要的State,区别在于状态保存的位置。
1. 内存型StateBackend
内存型StateBackend在Flink中叫作MemoryStateBackend,运行时
所需要的State数据保存在TaskManager JVM堆上内存中,KV类型的
State、窗口算子的State使用HashTable来保存数据、触发器等。执行
检查点的时候,会把State的快照数据保存到JobManager进程的内存
中。MemoryStateBackend可以使用异步的方式进行快照,(也可以使
用同步的方式。)推荐使用异步的方式,以避免阻塞算子处理数据。
基于内存的StateBackend在生产环境下不建议使用,可以在本地
开发调试测试。
注意点如下。
1)State存储在JobManager的内存中,受限于JobManager的内存
大小。
2)每个State默认5MB,可通过MemoryStateBackend构造函数调
整。
3)每个State不能超过Akka Frame大小。
2. 文件型StateBackend
文件型StateBackend在Flink中叫作FsStateBackend,运行时所需
要的State数据保存在TaskManger的内存中,执行检查点的时候,会把
State的快照数据保存到配置的文件系统中,可以使用分布式文件系统
或 本 地 文 件 系 统 , 如 使 用 HDFS 的 路 径 为
“hdfs://namenode:40010/flink/checkpoints”,使用本地文件系统
的路径为“file:///data/flink/checkpoints”。
(1)适用场景

see more please visit: https://homeofpdf.com


1)适用于处理大状态、长窗口,或大键值状态的有状态处理任
务。
2)FsStateBackend非常适合用于高可用方案。
(2)注意点
1)State数据首先会被存在TaskManager的内存中。
2)State大小不能超过TM内存。
3)TM异步将State数据写入外部存储。
3. 内存型和文件型StateBackend存储结构
内 存 型 和 文 件 型 StateBackend 依 赖 于 HeapKeydStateBacked ,
HeapKeydStateBackend使用StateTable存储数据。
StateTable体系如图7-9所示。
NestedMapsStateTable使用两层嵌套的HashMap保存状态数据,支
持同步快照。CopyOnWriteStateTable使用CopyOnWriteStateMap来保
存状态数据,支持异步快照,可以避免在保存快照的过程中持续写入
导致的状态不一致的问题。

图7-9 StateTable类体系

7.5.2 基于RocksDB的StateBackend
RocksDBStateBackend跟内存型和文件型StateBackend不同,其使
用嵌入式的本地数据库RocksDB将流计算数据状态存储在本地磁盘中,
不会受限于TaskManager的内存大小,在执行检查点的时候,再将整个
RocksDB中保存的State数据全量或者增量持久化到配置的文件系统

see more please visit: https://homeofpdf.com


中,在JobManager内存中会存储少量的检查点元数据。RocksDB克服了
State受内存限制的问题,同时又能够持久化到远端文件系统中,比较
适合在生产中使用。
但 是 RocksDBStateBackend 相 比 基 于 内 存 的 StateBackend , 访 问
State的成本高很多,可能导致数据流的吞吐量剧烈下降,甚至可能降
低为原来的1/10。
1. 适用场景
1)最适合用于处理大状态、长窗口,或大键值状态的有状态处理
任务。
2)RocksDBStateBackend非常适合用于高可用方案。
3)RocksDBStateBackend是目前唯一支持增量检查点的后端。增
量检查点非常适用于超大状态的场景。
2. 注意点
1)总State大小仅限于磁盘大小,不受内存限制。
2 ) RocksDBStateBackend 也 需 要 配 置 外 部 文 件 系 统 , 集 中 保 存
State。
3)RocksDB的JNI API基于byte数组,单key和单Value的大小不能
超过23 1字节。
4)对于使用具有合并操作状态的应用程序,如ListState,随着
时间可能会累积到超过231 字节大小,这将会导致在接下来的查询中失
败。

7.6 状态持久化
StateBackend中的数据最终需要持久化到第三方存储中,确保集
群故障或者作业故障能够恢复,针对不同类型的快照策略如图7-10所
示。
HeapSnapshotStrategy 策 略 对 应 于 HeapKeyedStateBackend ,
RocksDBStateBackend 的 持 久 化 策 略 有 两 种 : 全 量 持 久 化 策 略
( RocksFullSnapshotStratey ) 和 增 量 持 久 化 策 略
(RocksIncementalSnapshotStrategy)。

see more please visit: https://homeofpdf.com


(1)全量持久化策略
全量持久化,也就是说每次把全量的State写入到状态存储中(如
HDFS)。内存型、文件型、RocksDB类型的StateBackend都支持全量持
久化策略。

图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 文 件 就 可 以 计 算 出 状 态 有 哪 些 改 变 。 为 了 确 保

see more please visit: https://homeofpdf.com


sstable 是 不 可 变 的 , Flink 会 在 RocksDB 上 触 发 刷 新 操 作 , 强 制 将
memtable刷新到磁盘上。在Flink执行检查点时,会将新的sstable持
久化到存储中(如HDFS等),同时保留引用。这个过程中Flink并不会
持久化本地所有的sstable,因为本地的一部分历史sstable在之前的
检查点中已经持久化到存储中了,只需要增加对sstable文件的引用次
数就可以。
RocksDB会在后台合并sstable并删除其中重复的数据。然后在
RocksDB删除原来的sstable,替换成新合成的ssttable,新的sstable
包含了被删除的sstable中的信息。通过合并,历史的sstable会合并
成一个新的sstable,并删除这些历史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拼接起来,然后不做划分,直接交给用户。

see more please visit: https://homeofpdf.com


如图7-12所示,算子的并行度为3,在恢复的过程中修改并行度为
2,Union模式下,当重新分区后会将之前所有的OperatorState的数据
合并,分配给新的算子。

图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。

see more please visit: https://homeofpdf.com


Key-Group 数 量 取 决 于 最 大 并 行 度 ( MaxParallism ) 。
KeyedStream并发的上限是Key-Group的数量,等于最大并行度。
KeyGroup分配算法如代码清单7-6所示。
代码清单7-6 KeyGroup分配算法

7.8 状态过期
流计算应用肯定会用到State。流上的数据处理是永无止境的,假
如状态不自动清除,并且随着作业运行的时间越来越久,就会累积越来
越多的状态,进而影响任务的性能,甚至可能会因为状态太大,导致
整个作业的崩溃。
另一方面,数据是有时效性的,一般情况下,历史数据在业务上
的价值会随着时间流逝不断下降,在State持有大量低价值的数据是否
真的有必要?
7.8.1 DataStream中状态过期
在DataStream作业中,对于复杂的逻辑而言,有时需要精细的控
制,此时可以通过API来进行,如代码清单7-7所示,对每一个State可
以设置清理的策略StateTtlConfig,可以设置的内容如下。
● 过期时间:超过多长时间未访问,视为State过期,类似于缓
存。
● 过期时间更新策略:创建和写时更新、读取和写时更新。
● State的可见性: 未清理可用,超期则不可用。
代码清单7-7 API控制设置State过期策略

see more please visit: https://homeofpdf.com


7.8.2 Flink SQL中状态过期
Flink SQL是数据分析的高层抽象,在SQL的世界里并无State的概
念,而在流Join、聚合类的场景中,使用了State,如果State不定时
清理,则可能会导致State过多,内存溢出,为了稳妥起见,最好为每
个Flink SQL作业提供State清理的策略。如果定时清理State,则存在
可能因为State被清理而导致计算结果不完全准确的风险,Flink的
Table API和SQL接口中提供了参数设置选项,能够让使用者在精确和
资源消耗做折中,如代码清单7-8所示。
代码清单7-8 设置SQL中State的过期时间

根据阿里巴巴的实践经验,过期时间一般为1.5天左右。
7.8.3 状态过期清理
默认情况下,只有在明确读出过期值时才会删除过期值,如通过
调用ValueState#value(),具体如代码清单7-9所示。
代码清单7-9 启用过期清理

see more please visit: https://homeofpdf.com


做完整快照时清理后,在获取完整状态快照时激活清理,减小其
大小。在当前实现下不清除本地状态,但在从上一个快照恢复的情况
下,不会包括已删除的过期状态。可以在StateTtlConfig中配置,如
代码清单7-10所示。
代码清单7-10 完整快照清理

注意:此选项不适用于RocksDBStateBackend中的增量检查
点。
通过增量触发器渐进清理State。一种设计是当进行状态访问或者
处理数据时,在回调函数中进行处理。当每次增量清理触发时,遍历
StateBackend中的状态,清理掉过期的,如代码清单7-11所示。
代码清单7-11 增量清理

7.9 总结

see more please visit: https://homeofpdf.com


计算分为无状态计算和有状态计算两类。无状态计算不需要容
错,有状态计算则必须有容错机制,这就是State的作用。不同的状态
类型面向开发者提供了状态操作接口,可以访问状态中保存的数据,
面向Flink引擎的状态访问接口则提供了丰富的有关状态本身的管理接
口。
状态最终依赖于不同的StateBackend实现状态的存储,来实现状
态的持久化。最重要的状态分类是KeyedState和OperatorState,在修
改并行度的情况下,两者状态重分布行为不同。因为流上的数据是永
无止境的,随着时间的流逝状态越来越大,甚至可能超出资源承受能
力,所以不能永远保存所有状态,必须通过有状态的清理来保证性能
和降低资源消耗。

see more please visit: https://homeofpdf.com


第8章 作业提交
开发人员在IDE中开发Flink作业,可以使用Flink单机模式进行测
试,测试完毕没有问题后,将代码打包成Jar文件交给运维人员,运维
人员使用Flink CLI或者Web UI提交作业到Flink生成集群。
在真正执行Flink作业之前,有几个问题需要解决:
● Flink作业是如何提交到集群的?
● Flink集群是如何在资源管理集群上启动起来的?
● Flink的计算资源是如何分配给作业的?
● Flink作业提交之后是如何启动的?
Flink的Jar文件并不是Flink集群的可执行文件,需要经过转换之
后提交给集群。其转换过程分为两个大的步骤:
1)在Flink Client中通过反射启动Jar中的main函数,生成Flink
StreamGraph、JobGraph,将JobGraph提交给Flink集群。
2 ) Flink 集 群 收 到 JobGraph 之 后 , 将 JobGraph 翻 译 成
ExecutionGraph,然后开始调度执行,启动成功之后开始消费数据。
总结来说,Flink的核心执行流程,对用户的API调用,可以转换
为 StreamGraph→JobGraph→ExecutionGraph→ 物 理 执 行 拓 扑 ( Task
DAG)。
Graph直译过来叫作图,图有节点,节点之间有边相连,节点用来
表示数据的处理逻辑,边用来表示数据处理的流转。从数据源读取数
据开始,上游的数据处理完毕之后,交给下游继续处理,直到数据输
出到外部存储中,整个过程用图来表示,在不同的语境下也会有其他
的叫法,如Dataflow等。
在本章中,主要介绍从Flink DataStream开发的应用程序如何一
步步变成Flink的Graph结构,然后交给Flink集群执行。

8.1 提交流程

see more please visit: https://homeofpdf.com


Flink 作 业 在 开 发 完 毕 之 后 , 需 要 提 交 到 Flink 集 群 执 行 。
ClientFrontend是入口,触发用户开发的Flink应用Jar文件中的main
方法,然后交给PipelineExecutor#execue方法,最终会选择一个触发
一个具体的PipelineExecutor执行,过程如图8-1所示。

图8-1 Flink Client提交作业的核心过程示意


作业执行可以选择Session和Per-Job模式两种集群:
1)Session模式的集群,一个集群中运行多个作业。
2)Per-Job模式的集群,一个集群只运行一个作业,作业执行完
毕则集群销毁。
每种模式适合于不同的场景。不同的运行模式和其适用的场景见
表8-1。
表8-1 不同模式的适用场景

根据Flink Client提交作业之后是否可以退出Client进程,提交
模式又可分为Detached模式和Attached模式。Detached模式下,Flink
Client 创 建 完 集 群 之 后 , 可 以 退 出 命 令 行 窗 口 , 集 群 独 立 运 行 。
Attached模式下,Flink Client创建完集群后,不能关闭命令行窗
口,需要与集群之间维持连接,好处是能够感知集群的退出,集群退
出之后有机会做一些资源清理等动作,此处的清理是Flink作业可能占

see more please visit: https://homeofpdf.com


用外部的资源,如在金融行业里,作业占用的加密机连接需要在作业
退出时释放等。
8.1.1 流水线执行器PipelineExecutor
流水线执行器在Flink中叫作PipelineExecutor,是Flink Client
生成JobGraph之后,将作业提交给集群的重要环节。集群有Session和
Per-Job两种模式。在这两种模式下,集群的启动时机、提交作业的方
式不同,所以在生产环境中有两种PipelineExecutor。Session模式对
应 于 AbstractSessionClusterExecutor , Per-Job 模 式 对 应 于
AbstractJobClusterExecutor,如图8-2所示。

图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接口

see more please visit: https://homeofpdf.com


提交JobGraph,Dispatcher为每个作业启动一个JobMaster,进入作业
执行阶段。
2. Per-Job模式
该模式下,一个作业一个集群,作业之间相互隔离。
在Flink 1.10版本中,只有Yarn上实现了Per-Job模式,K8s的
Per-Job模式在后续版本中会实现。
Per-Job模式下,因为不需要共享集群,所以在PipelineExecutor
中执行作业提交的时候,可以创建集群并将JobGraph以及所需要的文
件等一同提交给Yarn集群,Yarn集群在容器中启动Flink Master进程
(即JobManager进程),进行一系列的初始化动作,初始化完毕之
后,从文件系统中获取JobGraph,交给Dispatcher。之后的执行流程
与Session模式下的执行流程相同。
8.1.2 Yarn Session提交流程
从总体上来说,在Yarn集群上使用Session模式提交Flink作业的
过程分为3个阶段:首先在Yarn上启动Flink Session模式的集群;其次
通过Flink Client提交作业;再次进行作业调度执行。
完整过程如图8-3所示。

see more please visit: https://homeofpdf.com


图8-3 Yarn Session模式提交任务流程
1.启动集群
(1)使用bin/yarn-session.sh提交会话模式的作业
如果提交到已经存在的集群,则获取Yarn集群信息、应用ID,并
准备提交作业。
如果是启动新的Yarn Session集群,则进入到步骤(2)。
(2)Yarn启动新Flink集群
1)如果没有集群,则创建一个新的Session模式的集群。首先将
应用配置(flink-conf.yaml、logback.xml、log4j.properties)和
相关文件(Flink Jar、配置类文件、用户Jar文件、JobGraph对象
等)上传至分布式存储(如HDFS)的应用暂存目录。
2)通过Yarn Client向Yarn提交Flink创建集群的申请,Yarn分配
资源,在申请的Yarn Container中初始化并启动Flink JobManager进
程,在JobManager进程中运行YarnSessionClusterEntrypoint作为集

see more please visit: https://homeofpdf.com


群启动的入口(不同的集群部署模式有不同的ClusterEntrypoint实
现),初始化Dispatcher、ResourceManager,启动相关的RPC服务,
等待Client通过Rest接口提交作业。
2.作业提交
Yarn集群准备好后,开始作业提交。
1)Flink Client通过Rest向Dispatcher提交JobGraph。
2)Dispatcher是Rest接口,不负责实际的调度、执行方面的工
作 , 当 收 到 JobGraph 后 , 为 作 业 创 建 一 个 JobMaster , 将 工 作 交 给
JobMaster ( 负 责 作 业 调 度 、 管 理 作 业 和 Task 的 生 命 周 期 ) , 构 建
ExecutionGraph(JobGraph的并行化版本,调度层最核心的数据结
构)。
这两个步骤结束后,作业进入调度执行阶段。
3.作业调度执行
1 ) JobMaster 向 YarnResourceManager 申 请 资 源 , 开 始 调 度
ExecutionGraph执行,向YarnResourceManager申请资源;初次提交作
业集群中尚没有TaskManager,此时资源不足,开始申请资源。
2)YarnResourceManager收到JobMaster的资源请求,如果当前有
空闲Slot则将Slot分配给JobMaster,否则YarnResourceManager将向
Yarn Master请求创建TaskManager。
3)YarnResourceManager将资源请求加入等待请求队列,并通过
心跳向YARN RM申请新的Container资源来启动TaskManager进程;Yarn
分配新的Container给TaskManager。
4)YarnResourceManager启动,然后从HDFS加载Jar文件等所需的
相关资源,在容器中启动TaskManager。
5)TaskManager启动之后,向ResourceManager注册,并把自己的
Slot资源情况汇报给ResourceManager。
6)ResourceManager从等待队列中取出Slot请求,向TaskManager
确 认 资 源 可 用 情 况 , 并 告 知 TaskManager 将 Slot 分 配 给 了 哪 个
JobMaster。
7 ) TaskManager 向 JobMaster 提 供 Slot , JobMaster 调 度 Task 到
TaskManager的此Slot上执行。

see more please visit: https://homeofpdf.com


至此作业进入执行阶段,关于作业的调度执行细节,请参考资源
管理、作业调度章节。
8.1.3 Yarn Per-Job提交流程
如图8-4所示,Yarn Per-Job模式提交作业与Yarn-Session模式提
交作业只在步骤1~3有差异,步骤4~10是一样的。Per-Job模式下,
JobGraph和集群的资源需求一起提交给Yarn。

图8-4 Yarn Per-Job提交流程


1.启动集群
1)使用./flink run -m yarn-cluster提交Per-Job模式的作业。
2 ) Yarn 启 动 Flink 集 群 。 该 模 式 下 Flink 集 群 的 启 动 入 口 是
YarnJobClusterEntryPoint,其他与Yarn Session模式下集群的启动
类似。
2.作业提交

see more please visit: https://homeofpdf.com


该 步 骤 与 Session 模 式 下 的 不 同 , Client 并 不 会 通 过 Rest 向
Dispatcher 提 交 JobGraph , 由 Dispatcher 从 本 地 文 件 系 统 获 取
JobGraph,其后的步骤与Session模式一样。
3.作业调度执行
与Yarn Session模式下一致。
8.1.4 K8s Session提交流程
从总体上来说,在K8s集群上使用Session模式提交Flink作业的过
程分为3个阶段:首先在K8s上启动Flink Session模式的集群;其次通
过Flink Client提交作业;再次进行作业调度执行。
整体过程如图8-5所示。
1.启动集群
1)Flink客户端首先连接Kubernetes API Server,提交Flink集
群 的 资 源 描 述 文 件 , 包 括 flink-configuration-configmap.yaml 、
jobmanager-service.yaml 、 jobmanager-deployment.yaml 和
taskmanager-deployment.yaml等。

see more please visit: https://homeofpdf.com


图8-5 K8s Session模式提交流程
2)Kubernetes Master会根据这些资源描述文件去创建对应的
Kubernetes实体。以JobManager部署为例,Kubernetes集群中的某个
节点收到请求后,Kubelet进程会从中央仓库下载Flink镜像,准备和
挂载卷,然后执行启动命令。Pod启动后Flink Master(JobManager)
进程随之启动,初始化Dispacher和KubernetesResourceManager。并
通过K8s服务对外暴露Flink Master的端口,K8s服务类似于路由服
务。
两个步骤完成之后,Session模式的集群就创建成功,集群可以接
收作业提交请求,但是此时还没有JobMaster、TaskManager,当提交
作业需要执行时,才会按需创建。
2.作业提交
1)Client用户可以通过Flink命令行(即Flink Client)向这个
会话模式的集群提交任务。此时JobGraph会在Flink Client端生成,
然后和用户Jar包一起通过RestClinet上传。
2 ) 作 业 提 交 成 功 , Dispatcher 会 为 每 个 作 业 启 动 一 个
JobMaster,将JobGraph交给JobMaster调度执行。
两个步骤完成之后,作业进入调度执行阶段。
3.作业调度执行
K8s Session模式集群下,ResourceManager向K8s Master申请和
释放TaskManager,除此之外,作业的调度与执行和Yarn模式是一样
的。
1)JobMaster向KubernetesResourceManager请求Slot。
2 ) KubernetesResourceManager 从 Kubernetes 集 群 分 配
TaskManager 。 每 个 TaskManager 都 是 具 有 唯 一 标 识 的 Pod 。
KubernetesResourceManager 会 为 TaskManager 生 成 一 份 新 的 配 置 文
件 , 里 面 有 Flink Master 的 service name 作 为 地 址 。 这 样 在 Flink
Master failover之后,TaskManager仍然可以重新连上。
3 ) Kubernetes 集 群 分 配 一 个 新 的 Pod 后 , 在 上 面 启 动
TaskManager。
4)TaskManager启动后注册到SlotManager。

see more please visit: https://homeofpdf.com


5)SlotManager向TaskManager请求Slot。
6)TaskManager提供Slot给JobMaster,然后任务就会被分配到这
个Slot上运行。

8.2 Graph总览
在Flink中可以使用高阶的Table & SQL API进行开发,也可以使
用底层的DataStream和DataSet API进行开发,从应用开发完毕、打包
执行到具体的调度执行,需要经过多层抽象转换。早期,Batch和
Stream 的 图 结 构 和 优 化 方 法 有 很 大 的 区 别 , 所 以 批 处 理 使 用
OptimizedPlan来做Batch相关的优化,使用StreamGraph表达流计算的
逻辑,最终都转换为JobGraph,实现了流批的统一。
综上而言,在当前版本中,不同的Flink API、不同层次Graph的
对应关系略微复杂,如图8-6所示。

图8-6 Flink Graph层次与API对应关系


1. 流计算应用的Graph转换
对于流计算应用来说,首先要将DataStream API的调用转换为
Transformation,然后经过StreamGraph→JobGraph→ExecutionGraph
3层转换(Flink内置的数据结构),最后经过Flink的调度执行,在
Flink集群中启动计算任务,形成一个物理执行图,该图是物理执行的

see more please visit: https://homeofpdf.com


Task之间的拓扑关系,但是在Flink中没有对应的Graph数据结构,是
运行时的概念。
2. 批处理应用的Graph转换
对 于 批 处 理 应 用 而 言 , 首 先 将 DataSet API 的 调 用 转 换 为
OptimizedPlan,然后转换为JobGraph。批处理和流计算在JobGraph上
完成了统一。
3. Table & SQL API的Graph转换
Table & SQL API是高阶API,在开发的时候并不区分到底是批处
理还是流计算,语法上二者基本上没有区别。在未来的Flink中会统一
DataSet API和DataStream API,废弃DataSet API,使用DataStream
API来统一编写批处理计算任务和流处理计算任务。目前正处于过渡阶
段,所以在Table & SQL模块使用了新的Blink Table Planner和旧的
Flink Table Planner。Flink Table Planner未来会逐渐废弃,最终
从Flink移除。
在Blink Table Planner中,批处理和流计算都依赖于流计算体
系,所以无论是流计算还是批处理与流计算应用的Graph转换过程都是
一样的。在旧的Flink Table Planner中,流计算依赖于DataStream
API,其Graph转换过程就是流计算应用的Graph转换过程,批处理依赖
于DataSet API,所以其转换过程就是批处理应用的Graph转换过程。
未来DataSet API会被废弃,最终从Flink中移除,所以其Graph转
换过程将不复存在,下面以流计算的转换过程为例,介绍其内部原
理,批处理的转换过程不再赘述。

8.3 流图
使 用 DataStream API 开 发 的 应 用 程 序 , 首 先 被 转 换 为
Transformation,然后被映射为StreamGraph,该图与具体的执行无
关,核心是表达计算过程的逻辑。WordCount的StreamGraph如图8-7所
示。

see more please visit: https://homeofpdf.com


图8-7 StreamGraph示意图
从图8-7中可以看到,StreamGraph由StreamNode和StreamEdge构
成,接下来分别介绍这两个核心对象。
8.3.1 StreamGraph核心对象
1. StreamNode
StreamNode 是 StreamGraph 中 的 节 点 , 从 Transformation 转 换 而
来,可以简单理解为一个StreamNode表示一个算子,从逻辑上来说,
StreamNode 在 StreamGraph 中 存 在 实 体 和 虚 拟 的 StreamNode 。
StreamNode可以有多个输入,也可以有多个输出。
实体的StreamNode会最终变成物理的算子。虚拟的StreamNode会
附着在StreamEdge上。
2. StreamEdge
StreamEdge是StreamGraph中的边,用来连接两个StreamNode,一
个StreamNode可以有多个出边、入边,StreamEdge中包含了旁路输
出、分区器、字段筛选输出(与SQL Select中选择字段的逻辑一样)
等的信息。
8.3.2 StreamGraph生成过程
StreamGraph在Flink Client中生成,由Flink Client在提交的时
候 触 发 Flink 应 用 的 main 方 法 , 用 户 编 写 的 业 务 逻 辑 组 装 成
Transformation 流 水 线 , 在 最 后 调 用
StreamExecutionEnvironment.execute ( ) 的 时 候 开 始 触 发
StreamGraph的构建。

see more please visit: https://homeofpdf.com


StreamGraph在Flink的作业提交前生成,生成StreamGraph的入口
在StreamExecutionEnvironment中,如代码清单8-1所示。
代码清单8-1 StreamGraph生成入口

StreamGraph 实 际 上 是 在 StreamGraphGenerator 中 生 成 的 , 从
SinkTransformatiom(输出)向前追溯到SourceTransformation。在
遍历过程中一边遍历一边构建StreamGraph,如代码清单8-2所示。
代码清单8-2 StreamGraphGenerator生成StreamGraph

see more please visit: https://homeofpdf.com


在 遍 历 Transformation 的 过 程 中 , 会 对 不 同 类 型 的
Transformation 分 别 进 行 转 换 。 对 于 物 理 Transformation 则 转 换 为
StreamNode实体,对于虚拟Transformation则作为虚拟StreamNode,
如代码清单8-3所示。
代码清单8-3 StreamGraphGenerator转换Transformation

see more please visit: https://homeofpdf.com


从上述代码可以看出,针对具体某一种类型的Tranformation,会
调用其相应的transformXXX()函数进行转换。transformXXX()首
先转换上游Transformation进行递归转换,确保上游的都已经完成了
转 换 。 然 后 通 过 addOperator ( ) 方 法 构 造 出 StreamNode , 通 过
addEdge()方法与上游的transform进行连接,构造出StreamEdge。
在构造StreamNode的过程中,运行时所需要的关键信息,即执行
算 子 的 容 器 类 ( StreamTask 及 其 子 类 ) 和 实 例 化 算 子 的 工 厂
(StreamOperatorFactory),也会确定下来,封装到StreamNode中。
注意,在添加StreamEdge的过程中,如果ShuffleMode为null,则
使用ShuffleMode. PIPELINED模式,在流计算中,只有PIPELINED模式
才 会 在 批 处 理 中 涉 及 其 他 模 式 。 构 建 StreamEdge 的 时 候 , 在 转 换
Transformation过程中生成的虚拟StreamNode会将虚拟StreamNode的
信息附着在StreamEdge上。

see more please visit: https://homeofpdf.com


8.3.3 单输入物理Transformation的转换示例
物理的Transformation会转换为一个StreamNode,下面是单个输
入的Transformation的转换代码过程。
1 ) 存 储 这 个 OneInputTransformation 的 上 游 Transformation 的
id,方便构造边,在这里递归,确保所有的上游Transformation都已经
转化。
2)确定共享的Slot组。
3)添加算子到StreamGraph中。
4)设置StateKeySelector。
5)设置并行度、最大并行度。
6)构造StreamEdge的边,关联上下游StreamNode。
如代码清单8-4所示。
代码清单8-4 单输入Transformation的转换代码示例

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
8.3.4 虚拟Transformation的转换示例
虚拟的Transformation生成的时候不会转换为StreamNode,而是
添加为虚拟节点,如代码清单8-5所示。
代码清单8-5 虚拟Transformation的转换

从上边的代码可以看出,对PartitionTransformation的转换没有
生 成 具 体 的 StreamNode 和 StreamEdge , 而 是 通 过
streamGraph.addVirtualPartitionNode ( ) 方 法 添 加 了 一 个 虚 拟 节
点 。 当 数 据 分 区 的 下 游 Transformation 添 加 StreamEdge 时 ( 调 用
streamGraph.addEdge ( ) ) , 会 把 Partitioner 分 区 器 封 装 进 到
StreamEdge中,如代码清单8-6所示。
代码清单8-6 在StreamGraph添加边

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
8.4 作业图
JobGraph可以由流计算的StreamGraph和批处理的OptimizedPlan
转换而来。流计算中,在StreamGraph的基础上进行了一些优化,如通
过OperatorChain机制将算子合并起来,在执行时,调度在同一个Task
线程上,避免数据的跨线程、跨网络的传递。
WordCount示例的JobGraph如图8-8所示。

see more please visit: https://homeofpdf.com


图8-8 StreamGraph到JobGraph示意图
在JobGraph中实现了流和批的统一表达。从JobGraph的图里可以
看到,数据从上一个算子流到下一个算子的过程中,上游作为生产者
提供了中间数据集(IntermediateDataSet),而下游作为消费者需要
JobEdge。JobEdge是一个通信管道,连接了上游生产的中间数据集和
下游的JobVertex节点。
8.4.1 JobGraph核心对象
JobGraph 的 核 心 对 象 是 JobVertex 、 JobEdge 和
IntermediateDataSet。
1. JobVertex
经过算子融合优化后符合条件的多个StreamNode可能会融合在一
起 生 成 一 个 JobVertex , 即 一 个 JobVertex 包 含 一 个 或 多 个 算 子 ,
JobVertex的输入是JobEdge,输出是IntermediateDataSet。
2. JobEdge
JobEdeg 是 JobGraph 中 连 接 IntermediateDatSet 和 JobVertex 的
边 , 表 示 JobGraph 中 的 一 个 数 据 流 转 通 道 , 其 上 游 数 据 源 是

see more please visit: https://homeofpdf.com


IntermediateDataSet,下游消费者是JobVertex,即数据通过JobEdge
由IntermediateDataSet传递给目标JobVertex。
JobEdge中的数据分发模式会直接影响执行时Task之间的数据连接
关系,是点对点连接还是全连接。
3. IntermediateDataSet
中 间 数 据 集 IntermediateDataSet 是 一 种 逻 辑 结 构 , 用 来 表 示
JobVertex的输出,即该JobVertex中包含的算子会产生的数据集。不
同的执行模式下,其对应的结果分区类型不同,决定了在执行时刻数
据交换的模式(参见10.3数据交换模式)。
IntermediateDataSet的个数与该JobVertext对应的StreamNode的
出边数量相同,可以是一个或者多个。
8.4.2 JobGraph生成过程
JobGraph的生成入口在StreamGraph中,流计算的JobGraph和批处
理 的 JobGraph 的 生 成 逻 辑 不 同 , 对 于 流 而 言 , 使 用 的 是
StreamingJobGraphGenerator , 对 于 批 而 言 , 使 用 的 是
JobGraphGenerator。转换入口如代码清单8-7所示。
代码清单8-7 StreamGraph中触发生成JobGraph

see more please visit: https://homeofpdf.com


StreamingJobGraphGenerator负责流计算JobGraph的生成,在转
换前需要进行一系列的预处理,如代码清单8-8所示。
代码清单8-8 StreamingJobGraphGenerator生成JobGraph预处理

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
预处理完毕之后,开始构建JobGraph中的点和边,从Source向下
遍历StreamGraph,逐步创建JobGraph,在创建的过程中同时完成算子
融合(OperatorChain)优化。
如代码清单8-9所示。
代码清单8-9 构建JobGraph的点和边

执 行 具 体 的 Chain 和 JobVertex 生 成 、 JobEdge 的 关 联 、


IntermediateDataSet。从StreamGraph读取数据的StreamNode开始,
递归遍历同时将StreamOperator连接在一起。在前边的图中,可以看
到KeyedAgg算子和Sink算子被链接在一起。
整个构建的逻辑如下。
1)从Source开始,Source与下游的FlatMap不可连接,Source是
起始节点,自己成为一个JobVertex。
2)此时开始一个新的连接分析,FlatMap是起始节点,与下游的
KeyedAgg也不可以连接,那么FlatMap自己成为一个JobVertext。
3)此时开始一个新的连接分析,KeyedAgg是起始节点,并且与下
游的Sink可以连接,那么递归地分析Sink节点,构造Sink与其下游是
否可以连接,因为Sink没有下游,所以KeyedAgg和Sink节点连接在一
起,共同构成了一个JobVertex。在这个JobVertex中,KeyedAgg是起
始节点,index编号为0,Sink节点index编号为1。
构建JobVertex的时候需要将StreamNode中的重要配置信息复制到
JobVertex中。构建好JobVertex之后,需要构建JobEdge将JobVertex

see more please visit: https://homeofpdf.com


连接起来。KeyedAgg和Sink之间构成了一个算子连接,连接内部的算
子之间无须构建JobEdge进行连接。复制的配置信息如图8-9所示。

图8-9 StreamNode向JobVertex复制的StreamConfig信息
在构建JobEdge的时候,很重要的一点是确定上游JobVertex和下
游 JobVertext 的 数 据 交 换 方 式 。 此 时 根 据 ShuffleMode 来 确 定
ResultPartition的类型(在执行算子写出数据和数据交换中使用),
用前边介绍的Flink Partition来确定JobVertext的连接方式(在生成
ExecutionGraph中使用)。
ShuflleMode 确 定 了 ResultParition , 那 么 就 可 以 确 定 上 游
JobVertext 输 出 的 IntermediateDataSet 的 类 型 了 , 也 就 知 道 该

see more please visit: https://homeofpdf.com


JobEdge的输入IntermediateDataSet了。
ForwardPartitioner 和 RescalePartitioner 两 种 类 型 的
Partitioner转换为DistributionPattern.POINTWISE的分发模式。其
他类型的Partitioner统一转换为DistributionPattern.ALL_TO_ALL模
式。JobGraph构建与OperatorChain优化如代码清单8-10所示。
以上就是在JobGraph的重要环节,其他细节不一一展开。
代码清单8-10 JobGraph构建与OperatorChain优化

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
see more please visit: https://homeofpdf.com
8.4.3 算子融合
为了更高效地实现分布式执行,Flink会尽可能地将多个算子融合
在一起,形成一个OperatorChain。一个Operatorchain在同一个Task
线程内执行。OperatorChain内的算子之间,在同一个线程内通过方法
调用的方式传递数据,能减少线程之间的切换,减少消息的序列化/反
序列化,无须借助内存缓冲区,也无须通过网络在算子间传递数据,
可在减少延迟的同时提高整体的吞吐量。
Flink的算子融合优化类似于Spark的窄依赖,形成相互无关的数
据 处 理 微 流 水 线 。 如 图 8-8 所 示 , KeyedAgg 和 Sink 算 子 融 合 成 一 个
OperatorChain,在执行的时候OperatorChain所在的Task相互之间没
有关系,数据在OperatorChain内部的算子间串行处理。
1. 算子融合的条件

see more please visit: https://homeofpdf.com


要想算子都能融合在一起,形成OperatorChain必须具备严格的条
件才行。一共有如下9个条件。
1)下游节点的入边为1。
2)StreamEdge的下游节点对应的算子不为null。
3)StreamEdge的上游节点对应的算子不为null。
4)StreamEdge的上下游节点拥有相同的slotSharingGroup,默认
都是default。
5)下游算子的连接策略为ALWAYS。
6)上游算子的连接策略为ALWAYS或者HEAD。
7)StreamEdge的分区类型为ForwardPartitioner。
8)上下游节点的并行度一致。
9)当前StreamGraph允许chain。
2.调用链示例
WordCount示例如代码清单8-11所示。
代码清单8-11 WordCount代码示例

如上代码清单中所示的WordCount代码,其生成的调用链如代码清
单8-12所示。

see more please visit: https://homeofpdf.com


代码清单8-12 WordCount代码生成的调用链

后文在数据传递的部分会详细解释这个调用链。

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。

see more please visit: https://homeofpdf.com


图8-10 ExecutionGraph示意图

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

see more please visit: https://homeofpdf.com


ExecutionJobVertex中会对作业进行并行化处理,构造可以并行
执行的实例,每一个并行执行的实例就是ExecutionVertex。
构造ExecutionVertex的同时,也会构建ExecutionVertex的输出
IntermediateResult 。 并 且 将 ExecutionEdge 输 出 为
IntermediateResultPartition。
ExecutionVertex 的 构 造 函 数 中 , 首 先 会 创 建
IntermediateResultPartition , 并 通 过
IntermediateResult.setPartition ( ) 建 立 IntermediateResult 和
IntermediateResultPartition之间的关系;然后生成Execution,并
配置资源相关。
3. IntermediateResult
IntermediateResult又叫作中间结果集,该对象是个逻辑概念,
表 示 ExecutionJobVertex 的 输 出 , 和 JobGraph 中 的
IntermediateDataSet一一对应,同样,一个ExecutionJobVertex可以
有多个中间结果,取决于当前JobVertex有几个出边(JobEdge)。
一 个 中 间 结 果 集 包 含 多 个 中 间 结 果 分 区
IntermediateResultPartition , 其 个 数 等 于 该 Job Vertext 的 并 发
度,或者叫作算子的并行度。
4. IntermediateResultPartition
IntermediateResultPartition 又 叫 作 中 间 结 果 分 区 , 表 示 1 个
ExecutionVertex输出结果,与ExecutionEdge相关联。
5. ExecutionEdge
表 示 ExecutionVertex 的 输 入 , 连 接 到 上 游 产 生 的
IntermediateResultPartition 。 一 个 Execution 对 应 于 唯 一 的 1 个
IntermediateResultParition 和 1 个 ExecutionVertex 。 一 个
ExecutionVertex可以有多个ExecutionEdge。
6. Execution
ExecutionVertex相当于每个Task的模板,在真正执行的时候,会
将 ExecutionVerterx 中 的 信 息 包 装 为 1 个 Execution , 执 行 一 个
ExecutionVertex 的 一 次 尝 试 。 JobManager 和 TaskManager 之 间 关 于
Task的部署和Task执行状态的更新都是通过ExecutionAttemptID来标

see more please visit: https://homeofpdf.com


识实例的。在发生故障或者数据需要重算的情况下,ExecutionVertex
可 能 会 有 多 个 ExecutionAttemptID 。 一 个 Execution 通 过
ExecutionAttemptID来唯一标识。
8.5.2 ExecutionGraph生成过程
初始化作业调度器的时候,根据JobGraph生成ExecutionGraph。
在 SchedulerBase 的 构 造 方 法 中 触 发 构 建 , 最 终 调 用
SchedulerBase#createExecutionGraph 触 发 实 际 的 构 建 动 作 , 使 用
ExecutionGraphBuilder构建ExecutionGraph。
核心的逻辑入口在ExecutionGraphBuilder中,如代码清单8-13所
示。
代码清单8-13 ExecutionGraph构建入口

在从JobGraph向ExecutionGraph转换的核心逻辑中,主要完成的
两件事情:
1 ) 构 造 ExecutionGraph 的 节 点 , 将 JobVertex 封 装 成
ExecutionJobVertex。
2)构造ExecutionEdge,建立ExecutionGraph的节点之间的相互
联系,把节点通过ExecutionEdge连接。
核心过程如代码清单8-14所示。
代码清单8-14 构建ExecutionGraph的核心逻辑

see more please visit: https://homeofpdf.com


1. 构建ExecutionGraph节点
前 边 提 到 过 , ExecutionGraph 相 比 JobGraph 增 加 了 并 行 度 的 概
念,到目前为止还没有看到Flink的作业变成并行的Task,其实其逻辑
隐含在了ExecutionJobVertex的构造函数中,在该构造函数中生成了
一 组 ExecutionVertex , 数 量 与 并 行 度 一 样 , 简 单 理 解 就 是
ExecutionVertex对应于Task。核心的逻辑如下。
1)设置并行度。
2)设置Slot共享和CoLocationGroup。
3 ) 构 建 当 前 ExecutionJobVertex 的 输 出 中 间 结 果
( IntermediateResult ) 及 其 中 间 结 果 分 区
(IntermediateResultPartition)。中间结果可以有1个或者多个,
每对应一个下游JobEdge,创建一个中间结果,如当前JobVertext只对
应一个下游JobVertext,则构建1个中间结果。
4 ) 构 建 ExecutionVertex , 根 据 该 ExecutionJobVertex 的 并 行
度,创建对应数量的ExecutionVertex。如当前JobVertex的并行度为
5,则会创建5个ExecutionVertex,在运行时刻,也就会部署5个Task
到TaskManager。

see more please visit: https://homeofpdf.com


5 ) 检 查 中 间 结 果 分 区 ( IntermediateResultParition ) 和
ExecutionVertex之间有没有重复的引用。
6)对可切分的数据源进行输入切分(InputSplit)。
至此,ExecutionGraph的节点构造完成,节点的输出也确定了。
接下来将构建ExecutionGraph的数据交换关系,即ExecutionEdge。
2. 构造ExecutionEdge
在 创 建 ExecutionJobVertex 的 过 程 中 , 调 用
ejv.connectToPredecessor ( ) 方 法 , 创 建 ExecutionEdge 将
ExecutionVertex和IntermediateResult关联起来,为运行时建立Task
之间的数据交换就是以此为基础建立数据的物理传输通道的。
连 接 过 程 中 , 根 据 JobEdge 的 DistributionPattern 属 性 创 建
ExecutionEdge , 将 ExecutionVertex 和 上 游 的
IntermediateResultPartition连接起来。
连 接 策 略 有 两 种 : DistributionPattern.POINTWISE ( 点 对 点 连
接)和DistributionPattern.ALL_TO_ALL(全连接)。
(1)点对点连接
即 DistributionPattern.POINTWISE , 该 策 略 用 来 连 接 当 前
ExecutionVertex与上游的IntermediateResultPartition。
上 游 IntermediateResult 的 分 区 数 记 做 numSources , 该
ExecutionJobVertex的并发度记做parallelism。
连接一共分3种情况。
1)一对一连接。numSources == parallism,即并发的Task数量
与分区数相等,则一对一进行连接,如图8-11所示。

see more please visit: https://homeofpdf.com


图8-11 一对一连接
2)多对一连接。parallelism<numSources,即下游的Task数量小
于上游的分区数,此时分为两种情况:
a)numSources % parallelism == 0,即下游Task可以分配等同
数量的结果分区IntermediateResultPartition,如上游有4个结果分
区,下游有两个Task,那么每个Task会分配两个结果分区进行消费,
如图8-12所示。

图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

see more please visit: https://homeofpdf.com


个结果分区分配两个Task消费,另一个结果分区分配1个Task消费,如
图8-15所示。

图8-13 多对一不等量点对点连接

图8-14 一对多等量点对点连接

see more please visit: https://homeofpdf.com


图8-15 一对多不等量点对点连接
(2)全连接
即 DistributionPattern.ALL_TO_ALL , 在 该 策 略 下 游 的
ExecutionVertex与上游的所有IntermediateResultPartition建立连
接,消费其产生的数据。一般全连接的情况意味着数据在Shuffle,如
图8-16所示。

图8-16 全连接

see more please visit: https://homeofpdf.com


8.6 总结
Flink作业执行前需要提交Flink集群,Flink集群可以与不同的资
源 框 架 ( Yarn 、 K8s 、 Mesos 等 ) 进 行 集 成 , 可 以 按 照 不 同 的 模 式
(Session模式和Per-Job模式)运行,所以在Flink作业提交过程中,
可能在资源框架上启动Flink集群。Flink就绪之后,进入作业提交过
程中,在Flink客户端中进行StreamGraph、JobGraph的转换,提交
JobGraph 到 Flink 集 群 , 然 后 Flink 集 群 负 责 将 JobGraph 转 换 为
ExecutionGraph,之后进入调度执行阶段。

see more please visit: https://homeofpdf.com


第9章 资源管理
在实际的环境中,大数据集群规模从几十台到几千台,往往是多
个团队共享的计算资源,执行不同类型的计算任务,如离线计算、流
计算、实时计算、AI模型训练等不同的计算任务。按对计算资源的需
求不同,计算任务可以分为IO密集型、CPU密集型、内存密集型等。
不同类型的计算任务使用不同的计算引擎,不同用户使用不同的
资源集。各种不同的计算引擎版本不断推陈出新,旧的业务依赖于旧
版本的计算引擎,迁移过程不是一蹴而就的,所以往往在集群中存在
相同计算引擎的不同版本,如集群中同时存在Flink 1.5版本和Flink
1.10版本,这两个版本的差异很大。所以需要对计算集群和占用的资
源进行隔离,确保一个计算集群宕机不会影响其他的计算集群、用
户。
随着数据规模的爆炸性提升,新类型的计算硬件不断推陈出新,
如AI模型训练除了使用CPU,还会利用GPU进行加速(CPU/GPU、内
存/Optane、SSD/DISK等),不同类型的计算资源也需要抽象。
以上的这些需求催生了大数据资源管理框架的发展,诞生了
Yarn、K8s、Mesos等资源管理框架,它们可以提供硬件资源的抽象、
计算任务的隔离、资源的管理等特性。

9.1 资源抽象
Flink涉及的资源分为两级:集群资源和Flink自身资源。
集群资源管理的是硬件资源,包括CPU、内存、GPU等,由资源管
理框架(yarn、K8s、Mesos)来管理,Flink从资源管理框架申请和释
放资源。
Flink从资源管理框架申请资源容器(Yarn的Container或者K8s的
Pod),1个容器中运行1个TaskManager进程。容器的资源对于Flink来
说也是比较粗粒度的,如单个容器可以使用1个CPU核心,8GB内存,因
为计算类型的不同,一个任务占用一个容器可能无法充分利用资源,
所以单个容器会被多个Flink的任务共享。

see more please visit: https://homeofpdf.com


Flink对申请到的资源进行切分,每一份叫作Task Slot,如图9-1
所示。
在1.10及之前的版本中,1个TaskManager的资源被等量切分成n份
(n来自flink-conf.yaml配置文件),也就是说TaskManager上最多可
以运行n个Task,图9-1中1个TaskManager包含3个TaskSlot。
从 总 体 上 来 说 , 在 资 源 管 理 中 涉 及 了 JobMaster 、
ResourceManager、TaskManager三种角色。JobMaster是Slot资源的使
用者,向ResourceManager申请资源,ResourceManager负责分配资源
和资源不足时申请资源,资源空闲时释放资源。TaskManager是Slot资
源的持有者,在其Slot清单中记录了Slot分配给了哪个作业的哪个
Task,如图9-2所示。

图9-1 Flink资源抽象

see more please visit: https://homeofpdf.com


图9-2 资源管理涉及的组件及其概要关系

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

see more please visit: https://homeofpdf.com


Yarn资源管理器,用来对接Yarn,在Yarn集群上启动和运行Flink
集群,能够实现动态的资源申请和释放,是使用最多的资源管理器。

图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有多

see more please visit: https://homeofpdf.com


少空闲的Slot和Slot等资源的使用情况。当Flink作业调度执行时,根
据Slot分配策略为Task分配执行的位置。
SlotManager虽然是ResourceManager的组件,但是其逻辑是通用
的,并不关心到底使用了哪种资源集群。面向不同的对象,
SlotManager提供不同的功能:
1)对TaskManager提供注册、取消注册、空闲退出等管理动作,
注册则集群可用的Slot变多,取消注册、空闲推出则释放资源,还给
资源管理集群。
2)对Flink作业,接收Slot的请求和释放、资源汇报等。当资源
不足的时候,SlotManger将资源请求暂存在等待队列中,SlotManager
通 知 ResourceManager 去 申 请 更 多 的 资 源 , 启 动 新 的 TaskManager ,
TaskManager注册到SlotManager之后,SlotManager就有可用的新资源
了,从等待队列中依次分配资源。

9.4 SlotProvider
SlotProvider接口定义了Slot的请求行为,支持两种请求模式。
● 立即响应模式:Slot请求会立即执行。
● 排队模式:排队等待可用Slot,当资源可用时分配资源。
最 终 的 实 现 在 SchedulerImpl 中 , 其 中 Scheduler 接 口 增 加 了
SlotSelectionStrategy。

9.5 Slot选择策略
Flink在决定Task运行在哪个TaskManager上时,会根据策略进行
选择,选择Slot的时候有不同的选择策略,SlotSelectionStrategy就
是策略定义的接口,其类体系如图9-4所示。

see more please visit: https://homeofpdf.com


图9-4 SlotSelectionStrategy类体系
选择策略从总体上分为两大类。
( 1 ) 位 置 优 先 的 选 择 策 略
LocationPreferenceSlotSelectionStrategy
位置优先的策略分为两类:
1 ) 默 认 策 略 。
DefaultLocationPreferenceSlotSelectionStrategy , 该 策 略 不 考 虑
资源的均衡分配,会从满足条件的可用Slot集合选择第1个,以此类
推。
2 ) 均 衡 策 略 。
EvenlySpreadOutLocationPreferenceSlotSelectionStrategy , 该 策
略考虑资源的均衡分配,会从满足条件的可用Slot集合中选择剩余资
源最多的Slot,尽量让各个TaskManager均衡地承担计算压力。
( 2 ) 已 分 配 Slot 优 先 的 选 择 策 略
PreviousAllocationSlotSelectionStrategy
如果当前没有空闲的已分配Slot,则仍然会使用位置优先的策略
来分配和申请Slot。

9.6 Slot资源池
Slot资源池在Flink中叫作SlotPool,是JobMaster中记录当前作
业 从 TaskManager 获 取 的 Slot 的 集 合 。 JobMaster 的 调 度 器 首 先 从
SlotPool中获取Slot来调度任务,SlotPool在没有足够的Slot资源执
行 作 业 的 时 候 , 首 先 会 尝 试 从 ResourceManager 中 获 取 资 源 , 如 果

see more please visit: https://homeofpdf.com


ResourceManager当前不可用、ResourceManager拒绝资源请求或者请
求超时,资源申请失败,则作业启动失败。
JobMaster 申 请 到 资 源 之 后 , 会 在 本 地 持 有 Slot , 避 免
ResourceManager异常导致作业运行失败。对于批处理而言,持有资源
JobMaster 首 先 可 以 避 免 多 次 向 ResourceManager 申 请 资 源 , 同 时
ResourceManager不可用也不会影响作业的继续执行,只有资源不足时
才会导致作业执行失败。
当作业已经执行完毕或者作业完全启动且资源有剩余时,
JobMaster会将剩余资源交还给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分配关系

see more please visit: https://homeofpdf.com


通过调整Slot的数量,用户可以定义Task之间如何互相隔离。如
果一个TaskManager只有一个Slot,意味着每个Task独立地运行在JVM
中。而一个TaskManager有多个Slot,则意味着更多的Task可以共享一
个JVM。在同一个JVM进程中的Task将共享TCP连接和心跳消息。Task之
间也可能共享数据集和数据结构,这样可以减少每个Task的负载。
虽然通过Slot对TaskManager的资源进行划分,在一定程度上能够
提高集群的计算资源利用率,但是这种做法并没有考虑到不同Task的
计算任务对资源需求的差异,计算任务有IO密集型、内存密集型、CPU
密集型、GPU密集型等不同的资源消耗类型,有时候还会是多种资源混
合类型。
所 以 在 Slot 的 基 础 上 , Flink 设 计 了 Slot 共 享 机 制 。 其 中 ,
SlotSharingManager用在Flink作业的执行调度中,负责Slot的共享,
不同的Task可以共享Slot。
1. Slot共享的优点
默认情况下,Flink作业共享同一个SlotSharingGroup,同一个作
业中来自不同JobVertex的Task可以共享作业。使用Slot共享,可以在
一个Slot中运行Task组成的流水线。共享Slot带来了如下优点。
(1)资源分配简单
Flink集群需要的Slot的数量和作业中的最高并行度一致,不需要
计算一个程序总共包含多少个Task。
(2)资源利用率高
如果没有Slot共享,资源密集型的Task(如长周期的窗口计算)
跟 非 密 集 型 的 作 业 ( 如 Source/Map ) 占 用 相 同 的 资 源 , 在 整 个
TaskManager层面上,资源没有充分利用。如果共享Slot,如图9-6所
示,将并行度从2提高到6,可以充分利用Slot资源,同时确保资源保
密集型的Task在Taskmanager中公平分配。

see more please visit: https://homeofpdf.com


图9-6 并行度6的Task与Slot分配关系
在实际运行的时候,每个Task会被赋予一个编号,如在图9-6中,
Task[Source-Map]在集群中一共有6个Task实例,分别被赋予1~6的
编号,同样Task[keyBy()/window()/apply()]也被赋予了1~6
的编号,在Slot共享的时候,同一个计算步骤的不同编号的Task不能
分配到相同的Slot中。所以在图中可以看到,所有编号为1的都在同一
个Task Slot中,其他编号的也是类似。
2. Slot共享组与Slot共享管理器
Slot共享管理器在Flink中叫作SlotSharingManager,Slot共享组
在Flink中叫作SlotSharingGroup。SlotSharingManager对象管理资源
共享与分配,1个Slot共享组对应1个Slot共享管理器。两者在作业调
度执行的时候发挥作用,部署Task之前,选择Slot确定Task发布到哪
个TaskManager。
Flink有两种共享组。
(1)SlotSharingGroup

see more please visit: https://homeofpdf.com


非强制性共享约束,Slot共享根据组内的JobVertices ID查找是
否已有可以共享的Slot,只要确保相同JobVertext ID不能出现在一个
共享的Slot内即可。
在符合资源要求的Slot中,找到没有相同JobVertext ID的Slot,
根据Slot选择策略选择一个Slot即可,如果没有符合条件的,则申请
新的Slot。
(2)CoLocationGroup
CoLocationGroup又叫作本地约束共享组,具有强制性的Slot共享
限制,CoLocationGroup用在迭代运算中,即在IterativeStream的API
中 调 用 。 迭 代 运 算 中 的 Task 必 须 共 享 同 一 个 TaskManager 的 Slot 。
CoLocationGroup可以看成是SlotSharingGroup的特例。
此处需要注意,JobGraph向ExecutionGraph的转换过程中,为每
一个ExecutionVertex赋予了按照并行度编写的编号,相同编号的迭代
计 算 ExecutionVertex 会 被 放 入 本 地 共 享 约 束 组 中 , 共 享 相 同 的
CoLocationConstraint对象,在调度的时候,根据编号就能找到本组
其他Task的Slot信息。
CoLocation 共 享 根 据 组 内 每 个 ExecutionVertex 关 联 的
CoLocationConstraint查找是否有相同CoLocationConstraint约束已
分配Slot可用,在调度作业执行的时候,首先要找到本约束中其他
Task部署的TaskManager,如果没有则申请一个新的Slot,如果有则共
享该TaskManager上的Slot。
3. Slot资源申请
(1)单独Slot资源申请
该类型的Slot申请首先会从JobMaster的当前SlotPool中尝试获取
资源,如果资源不足,则从SlotPool中申请新的Slot,然后SlotPool
向ResourceManager请求新的Slot。
(2)共享Slot资源申请
共享Slot在申请的时候,需要向SlotSharingManager请求资源,
如果有CoLocation限制,则申请CoLocation MultiTaskSlot,否则申
请一般的MultiTaskSlot。

see more please visit: https://homeofpdf.com


在Flink中使用TaskSlot来定义Slot共享的结构,其类体系如图9-
7所示。

图9-7 TaskSlot类体系
SingleTaskSlot表示运行单个Task的Slot,每个SingleTaskSlot
对应于一个LogicalSlot。MultiTaskSlot中包含了一组TaskSlot。
借助SingleTaskSlot和MultiTaskSlot,Flink实现了一般Slot共
享和CoLocationGroup共享,两者的数据结构如图9-8所示。

图9-8 一般Slot共享和CoLocation Slot共享的数据结构的差异

9.8 总结
本章主要介绍了Flink中的资源抽象。资源抽象分为两个层面:集
群 资 源 抽 象 和 Flink 自 身 资 源 抽 象 。 集 群 级 使 用 资 源 管 理 器
( ResourceManager ) 来 解 耦 Flink 和 资 源 管 理 集 群 ( Yarn 、 K8s 、
Mesos等),Flink自身的资源使用Slot精细地划分其计算资源,作业

see more please visit: https://homeofpdf.com


能够充分利用计算资源,同时使用Slot共享进一步提高资源利用效
率。

see more please visit: https://homeofpdf.com


第10章 作业调度
作业提交给JobManager生成ExecutionGraph之后,就进入了作业
调度执行的阶段。在作业调度阶段中,调度器根据调度模式选择对应
的调度策略,申请所需要的资源,将作业发布到TaskManager上,启动
作业执行,作业开始消费数据,执行业务逻辑。在作业的整个执行过
程中,涉及计算任务的提交、分发、管理和Failover等。

10.1 调度
调度器是Flink作业执行的核心组件,管理作业执行的所有相关过
程,包括JobGraph到ExecutionGraph的转换、作业生命周期管理(作
业的发布、取消、停止)、作业的Task生命周期管理(Task的发布、
取消、停止)、资源申请与释放、作业和Task的Failover等。
调度器的类体系如图10-1所示。

图10-1 调度器类体系
在调度相关的体系中有几个非常重要的组件。

see more please visit: https://homeofpdf.com


●调度器:SchedulerNG及其子类、实现类。
● 调度策略:SchedulingStrategy及其实现类。
● 调度模式:ScheduleMode包含流和批的调度,有各自不同的调
度模式。
1. 调度器
作业调度器是作业的执行、异常处理的核心,具备如下基本能
力。
1)作业的生命周期管理,如作业开始调度、挂起、取消。
2)作业执行资源的申请、分配、释放。
3)作业的状态管理,作业发布过程中的状态变化和作业异常时的
FailOver等。
4)作业的信息提供,对外提供作业的详细信息。
SchedulerNG接口中定义了调度器的行为模型,如代码清单10-1所
示。
代码清单10-1 SchedulerNG接口

see more please visit: https://homeofpdf.com


在Flink中有两个调度器的实现:
(1)DefaultScheduler
该调度器是当前版本的默认调度器,是Flink新的调度设计,使用
SchedulerStrategy来实现调度。
(2)LegacyScheduler
该调度器是遗留的调度器,实际上使用了原来的ExecutionGraph
的调度逻辑,在后文中不再阐述该调度器的调度过程。
细心的读者可能会发现,在Flink中还有一个Scheduler接口,该
接口虽然跟SchedulerNG的名称类似,但是作用截然不同,Scheduler
只负责Slot资源的分配,在调度器中会使用Scheduler来申请和归还
Slot。
Scheduler接口体系如图10-2所示。

see more please visit: https://homeofpdf.com


图10-2 Scheduler接口体系
从图中可以看出,Scheduler接口的所有行为都是跟Slot相关的,
在调度器(SchedulerNG)中使用Scheduler作为Slot的提供者。
2.调度行为
SchedulingStrategy接口定义了调度行为,其中定义了4种行为。
1)startScheduling:调度入口,触发调度器的调度行为。
2)restartTasks:重启执行失败的Task,一般是Task执行异常导
致的。
3)onExecutionStateChange:当Execution的状态发生改变时。
4)onPartitionConsumable:当IntermediateResultPartition中
的数据可以消费时。
3. 调度模式
Flink 一 共 提 供 了 3 种 调 度 模 式 : Eager 调 度 、 分 阶 段 调 度
( Lazy_From_Source ) 、 分 阶 段 Slot 重 用 调 度
(Lazy_From_Sources_With_Batch_Slot_Request)。不同调度模式分
别适合于不同的场景。
(1)Eager调度
该模式适用于流计算。一次性申请需要所有的资源,如果资源不
足,则作业启动失败。
(2)分阶段调度

see more please visit: https://homeofpdf.com


分 阶 段 调 度 ( Lazy_From_Sources ) 适 用 于 批 处 理 。 从 Source
Task开始分阶段调度,申请资源的时候,一次性申请本阶段所需要的
所有资源。上游Task执行完毕后开始调度执行下游的Task,读取上游
的数据,执行本阶段的计算任务,执行完毕之后,调度后一个阶段的
Task,依次进行调度,直到作业执行完成。
(3)分阶段Slot重用调度
分 阶 段 Slot 重 用 调 度
(Lazy_From_Sources_With_Batch_Slot_Request)适用于批处理。与
分阶段调度基本一样,区别在于该模式下使用批处理资源申请模式,
可以在资源不足的情况下执行作业,但是需要确保在本阶段的作业执
行中没有Shuffle行为。
注意:目前的实现中Eager模式和Lazy_From_Source模式的资源
申请逻辑一样,Lazy_From_Sources_With_Batch_Slot_Request是单独
的资源申请逻辑。
4. 调度策略
流和批的调度策略是不同的,如流计算作业在调度的时候需要一
次性获取所有需要的Slot,部署Task并开始执行,而对于批处理作
业,则可以分阶段调度执行,上一阶段执行完毕,数据可消费的时
候,开始调度下游的执行。
调度策略目前有两种实现。
● EagerSchelingStrategy:该调度策略用来执行流计算作业的
调度,详细内容参见流作业。
● LazyFromSourceSchedulingStrategy:该调度策略用来执行批
处理作业的调度。

10.2 执行模式
流计算作业的数据执行模式毫无疑问是推送的模式,但是批处理
作业的情况则比较复杂,Flink在底层统一批流作业的执行,执行模式
指定批处理程序在数据交换方面的执行方式在Flink中有两类执行模

see more please visit: https://homeofpdf.com


式:Pipelined模式(流水线)或Batch模式,它们又可细分为5种具体
的执行模式。模式定义如代码清单10-2所示。
代码清单10-2 执行模式定义

1. 流水线模式
即Pipelined,此模式以流水线方式(包括Shuffle和广播数据)
执行作业,但流水线可能会出现死锁的数据交换除外。如果可能会出
现数据交换死锁,则数据交换以Batch方式执行。
当数据流被多个下游分支消费处理,处理后的结果再进行Join
时,如果以Pipelined模式运行,则可能出现数据交换死锁。
代码清单10-3 数据交换死锁代码示例

2. 强制流水线模式
即 Pipelined_Forced , 此 模 式 以 流 水 线 方 式 ( 包 括 shuffle 和
broadcast数据)执行作业,即便流水线可能会出现死锁的数据交换时
仍然执行。
一般情况下,Pipelined模式是优先选择,确保不会出现数据死锁
的情况下才会使用Pipelined_Forced模式。
3.流水线优先模式

see more please visit: https://homeofpdf.com


即Pipelined_With_Batch_Fallback,此模式首先使用Pipelined
启动作业,如果可能死锁则使用Pipelined_Forced启动作业。当作业
异常退出时,则使用Batch模式重新执行作业(注意:此模式当前未实
现)。
4. 批处理模式
即Batch,此模式对于所有的shuffle和broadcast都使用Batch模
式执行,仅本地的数据交换使用Pipelined模式。
5. 强制批处理模式
即Batch_Forced,此模式对于所有的数据交换都使用Batch模式,
对于本地交换也不例外。

10.3 数据交换模式
执行模式的不同决定了数据交换行为的不同,为了能够实现不同
的数据交换行为,Flink在ResultPartitionType中定义了4种类型的数
据分区模式,与执行模式一起完成批流在数据交换层面的统一,如代
码清单10-4所示。
代码清单10-4 ResultPartitionType的数据交换模式定义

1. BLOCKING
BLOCKING类型的数据分区会等待数据完全处理完毕,然后才会交
给下游进行处理,在上游处理完毕之前,不会与下游进行数据交换。
该类型的数据分区可以被多次消费,也可以并发消费。被消费完毕之
后不会自动释放,而是等待调度器来判断该数据分区无人再消费之
后,由调度器发出销毁指令。

see more please visit: https://homeofpdf.com


该模式适用于批处理,不提供反压流控能力。
2. BLOCKING_PERSISTENT
BLOCKING_PERSISTENT类型的数据分区类似于BLOCKING,但是其生
命周期由用户指定。调用JobManager或者ResourceManager API进行销
毁,而不是由调度器控制。
3. PIPELINED
PIPELINED(流水线)式数据交换适用于流计算和批处理。
数据处理结果只能被1个消费者(下游的算子)消费1次,当数据
被消费之后即自动销毁。PIPELINED分区可能会保存一定数据的数据,
与PIPELINED_BOUNDED相反。此结果分区类型可以在运行中保留任意数
量的数据。当数据量太大内存无法容纳时,可以写入磁盘中。
4. PIPELINED_BOUNDED
PIPELINED_BOUNDED 是 PIPELINED 带 有 一 个 有 限 大 小 的 本 地 缓 冲
池。
对于流计算作业来说,固定大小的缓冲池可以避免缓冲太多的数
据和检查点延迟太久。不同于限制整体网络缓冲池的大小,该模式下
允许根据分区的总数弹性地选择网络缓冲池的大小。
对于批处理作业来说,最好使用无限制的PIPELINED数据交换模
式,因为在批处理模式下没有CheckpointBarrier,其实现Exactly-
Once与流计算不同。

10.4 作业生命周期
Flink作业的生命周期状态非常多,状态之间的变化关系非常复
杂,所以Flink作业生命周期的管理参考了有限状态机。
有限状态机又叫作Finite State Machine(FSM),是表示有限个
状态及在这些状态之间的转移和动作等行为的数学模型,在计算机领
域有着广泛的应用。
状态机可归纳为4个要素,即现态、条件、动作、次态。这样归纳
主要是出于对状态机的内在因果关系的考虑。“现态”和“条件”是
因,“动作”和“次态”是果,详解如下。

see more please visit: https://homeofpdf.com


现态:是指当前所处的状态。

● 条件:又称为“事件”。当一个条件被满足时,将会触发一个
动作,或者执行一次状态的迁移。
● 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到
新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足
后,也可以不执行任何动作,直接迁移到新状态。
● 次态
:条件满足后要迁往的新状态。“次态”是相对于“现
态”而言的,“次态”一旦被激活,就转变成了新的“现态”。
Flink在JobMaster中有作业级别的状态管理,ExecutionGraph有
单个Task的状态管理,接下来分别介绍。
10.4.1 作业生命周期状态
JobMaster负责作业的生命周期管理,具体的管理行为在调度器和
ExecutionGraph中实现。
作业的完整生命周期状态变化如图10-3所示。

see more please visit: https://homeofpdf.com


图10-3 作业生命周期状态迁移
在Flink中使用状态机管理作业的状态,作业状态在JobStatus中
定义,总共有9种状态。
(1)Created状态
作业刚被创建,还没有Task开始执行。
(2)Running状态
作业创建完之后,就开始申请资源,申请必要的资源成功,并且
向TaskManager调度Task执行成功,就进入Running状态。在Running状

see more please visit: https://homeofpdf.com


态下,对于流作业而言,作业的所有Task都在执行,对于批作业而
言,可能部分Task被调度执行,其他的Task在等待调度。
(3)Restarting状态
当作业执行出错,需要重启作业的时候,首先进入Failing状态,
如果可以重启则进入Restarting状态,作业进行重置,释放所申请的
所有资源,包含内存、Slot等,开始重新调度作业的执行。
(4)Cancelling状态
调用Flink的接口或者在WebUI上对作业进行取消,首先会进入此
状态,此状态下,首先会清理资源,等待作业的Task停止。
(5)Canceled状态
当所有的资源清理完毕,作业完全停止执行后,进入Canceled状
态,此状态一般是用户主动停止作业。
(6)Suspended状态
挂起作业之前,取消Running状态的Task,Task进入Canceled状
态,销毁掉通信组件等其他组件,但是仍然保留ExecutionGraph,等
待恢复。Suspended状态一般在HA下主JobManager宕机、备JobManager
接管继续执行时,恢复ExecutionGraph。
(7)Finished状态
作业的所有Task都成功地执行完毕后,作业退出,进入Finished
状态。
(8)Failing状态
作业执行失败,可能是因为作业代码中抛出的异常没有处理,或
者是资源不够,作业进入Failing状态,等待资源清理。
(9)Failed状态
如果作业进入Failing状态的异常达到了作业自动重启次数的限
制,或者其他更严重的异常导致作业无法自动恢复,此时作业就会进
入Failed的状态。
10.4.2 Task的生命周期

see more please visit: https://homeofpdf.com


TaskManager负责Task的生命周期管理,并将状态的变化通知到
JobMaster , 在 ExecutionGraph 中 跟 踪 Execution 的 状 态 变 化 , 一 个
Execution对应于一个Task。作业生命周期的状态本质上来说就是各个
Task状态的汇总。Task的生命周期变化如图10-4所示。
作业被调度执行发布到各个TaskManager上开始执行,Task在其整
个生命周期中有8种状态,定义在ExecutionState中。其中Created是
起始状态,Failed、Finished、Canceled状态是Final状态。
(1)Created状态
ExecutionGraph 创 建 出 来 之 后 , Execution 默 认 的 状 态 就 是
Created。
(2)Scheduled状态
在ExecutionGraph中有Scheduled状态,在TaskManager上的Task
不会有该状态,Scheduled状态表示被调度执行的Task进入Scheduled
状态,但是并不是所有的Task都会变成Scheduled状态,然后开始申请
所需的计算资源。
(3)Deploying状态
Deploying状态表示资源申请完毕,向TaskManager部署Task。

see more please visit: https://homeofpdf.com


图10-4 Task生命周期变化
(4)Running状态
TaskManager启动Task,并通知JobManager该Task进入Running状
态,JobManager将该Task所在的ExecutionGraph中对应的Execution设
置为Running状态。
(5)Finished状态
当Task执行完毕,没有异常,则进入Finished状态,JobManager
将该Task所在的ExecutionGraph中的Execution设置为Finished状态。
(6)Cancelling状态
Cancelling 状 态 与 Scheduled 、 Deploying 状 态 类 似 , 是
ExecutionGraph中维护的一种状态,表示正在取消Task执行,等待
TaskManager取消Task的执行,并返回结果。
(7)Canceled状态

see more please visit: https://homeofpdf.com


TaskManager取消Task执行成功,并通知JobManager,JobManager
将该Task所在的ExecutionGraph中对应的Execution设置为Canceled状
态。
(8)Failed状态
若TaskManger执行Task时出现异常导致Task无法继续执行,Task
会进入Failed状态,并通知JobManager,JobManager将该Task所在的
ExecutionGraph中对应的Execution设置为Failed状态。整个作业也将
会进入Failed状态。

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。

see more please visit: https://homeofpdf.com


(1)InputSplit分配
在批处理中使用,为批处理计算任务分配待计算的数据分片。
(2)结果分区跟踪
结果分区跟踪器(PartitionTracker)跟踪非Pipelined模式的分
区,其实就是跟踪批处理中的结果分区,当结果分区消费完之后,具
备结果分区释放条件时,向TaskExecutor和ShuffleMaster发出释放请
求。
(3)作业执行异常
根据作业的执行异常,选择重启作业或者停止作业。
2.作业Slot资源管理
Slot资源的申请、持有和释放。JobMaster将具体的管理动作交给
SlotPool 来 执 行 , SlotPool 持 有 资 源 , 资 源 不 足 时 负 责 与
ResourceManager交互申请资源。
释放TaskManager的情况:作业停止、闲置TM、TM心跳超时。
3.检查点与保存点
CheckpointCoordinator负责进行检查点的发起、完成确认,检查
点异常或者重复时取消本次检查点的执行。保存点由运维管理人员手
动触发或者通过接口调用触发。
4.监控运维相关
反压跟踪、作业状态、作业各算子的吞吐量等监控指标。
5.心跳管理
JobMaster、ResourceManager、TaskManager是3个分布式组件,
相互之间通过网络进行通信,那么不可避免地会遇到各种导致无法通
信的情况。所以三者之间通过两两心跳相互感知对方。一旦出现心跳
超时,则进入异常处理阶段,或是进行切换,或是进行资源清理。
10.5.2 TaskManager
TaskManager是Flink集群中负责执行计算任务的角色,其实现类
是TaskExecutor。

see more please visit: https://homeofpdf.com


TaskExecutor 是 Flink 中 非 常 重 要 的 角 色 , 绝 大 部 分 组 件 都 与
TaskExecutor 有 关 系 , 其 在 生 命 周 期 中 需 要 对 外 与 JobManager 、
ResourceManager通信,对内需要管理Task及其相关的资源、结果分区
等。
TaskManager是Task的载体,负责启动、执行、取消Task,并在
Task异常时向JobManager汇报。TaskManager作为Task执行者,为Task
之间的数据交换提供基础框架。
从集群资源管理的角度,TaskManager是计算资源的载体,一个
TaskManger 通 过 Slot 切 分 其 CPU 、 内 存 等 计 算 资 源 , 未 来 还 会 包 含
GPU。
一个Flink集群的TaskManager的个数从几十到几千上万都有可
能。
为 了 实 现 Exactly-Once 和 容 错 , 从 整 个 集 群 的 视 角 来 看 ,
JobManager是检查点的协调管理者,TaskManager是检查点的执行者。
从集群管理的角度,TaskManager与JobManager之间通过心跳保持
相互感知。与ResourceManager保持心跳,汇报资源的使用情况,以便
ResourceManager能够掌握全局资源的分布和剩余情况。集群内部的信
息交换基于Flink的RPC通信框架。
TaskManager提供的数据交换基础框架,最重要的是跨网络的数据
交换、内存资源的申请和分配以及其他需要在计算过程中Task共享的
组件,如ShuffleEnvironment等。
10.5.3 Task
Task是Flink作业的子任务,由TaskManager直接负责管理调度,
为StreamTask执行业务逻辑的时候提供基础的组件,如内存管理器、
IO管理器、输入网关、文件缓存等。
TaskManager、Task、StreamTask和算子的关系如图10-5所示。

see more please visit: https://homeofpdf.com


图10-5 TaskManager、Task、StreamTask和算子的关系
Flink中流计算执行层面使用StreamTask体系,批处理执行层面使
用 了 BatchTask 体 系 , 两 套 体 系 互 不 相 通 , 所 以 通 过 Task 可 以 解 耦
TaskManager,使得TaskManager本身无须关心计算任务是流计算作业
还是批处理作业。未来版本中批流在底层统一为流执行模型之后,此
处的抽象其实是可以简化的。
Task执行所需要的核心组件如下。
1 ) TaskStateManager : 负 责 State 的 整 体 协 调 。 其 中 封 装 了
CheckpointResponder,在StreamTask中用来跟JobMaster交互,汇报
检查点的状态。
2)MemoryManager:Task通过该组件申请和释放内存。
3)LibraryCacheManager:开发者开发的Flink作业打包成jar提
交给Flink集群,在Task启动的时候,需要从此组件远程下载所需要的
jar文件等,在Task的类加载器中加载,然后才能够执行业务逻辑。
4)InputSplitProvider:在数据源算子中,用来向JobMaster请
求分配数据集的分片,然后读取该分片的数据。
5)ResultPartitionConsumableNotifier:结果分区可消费通知
器,用于通知消费者生产者生产的结果分区可消费。
6)PartitionProducerStateChecker:分区状态检查器,用于检
查生产端分区状态。
7 ) TaskLocalStateStore : 在 TaskManager 本 地 提 供 State 的 存
储,恢复作业的时候,优先从本地恢复,提高恢复速度。但是本地
State存储的方式可能因为硬件问题丢失,所以如果不能从本地恢复,
需要再从可靠分布式存储中恢复。

see more please visit: https://homeofpdf.com


8)IOManager:IO管理器,在批处理计算中(如排序、Join等场
景)经常会遇到内存中无法放下所有数据的情况,IOManager就负责将
数据溢出到磁盘,并在需要的时候将其读取回来。
9)ShuffleEnvironment:数据交换的管理环境,其中包含了数据
写出、数据分区的管理等组件。
10)BroadcastVariableManager:广播变量管理器,Task可以共
享该管理器,通过引用计数跟踪广播变量的使用,没有使用的时候则
清除。
11)TaskEventDispatcher:任务事件分发器,从消费者任务分发
事件给生产者任务。
10.5.4 StreamTask
StreamTask是所有流计算作业子任务的执行逻辑的抽象基类,是
算子的执行容器。
StreamTask的类型与算子的类型一一对应,其体系如图10-6所
示。

图10-6 StreamTask体系
StreamTask的实现分为几类:
(1)TwoInputStreamTask
两个输入的StreamTask,对应于TowInputStreamOperator。
(2)OneInputStreamTask

see more please visit: https://homeofpdf.com


单个输入的StreamTask,对应于OneInputStreamOperator。其两
个子类StreamIterationHead和StreamIterationTail用来执行迭代运
算。
(3)SourceStreamTask
SourceStreamTask是用在流模式的执行数据读取的StreamTask。
(4)BoundedStreamTask
该StreamTask是用在是模拟批处理的数据读取行为。
(5)SourceReaderStreamTask
SourceReaderStreamTask 用 来 执 行
SourceReaderStreamOperator , 前 边 提 到 过 Flip-27 中 提 出 的 重 构
Source接口,但是目前暂未实现,所以在当前版本中该类实际上还没
有应用。
StreamTask的生命周期有3个阶段:初始化、运行、关闭与清理,
如图10-7所示。

图10-7 StreamTask生命周期
1.初始化阶段

see more please visit: https://homeofpdf.com


1)StateBackend初始化,这是实现有状态计算和Exactly-Once的
关键组件。
2)时间服务初始化,在第4章介绍过Timer定时器,此处的时间服
务即最终管理定时器的服务。
3)构建OperatorChain,实例化各个算子。
4)算子构建完毕,然后开始Task的初始化。根据Task类型的不
同,其初始化略有不同。对于SourceStreamTask而言,主要是启动
SourceFunction开始读取数据,如果支持检查点,则开启检查点。
对 于 OneInputStreamTask 和 TwoInputStreamTask , 构 建
InputGate , 包 装 到 StreamTask 的 输 入 组 件 StreamTaskNetworkInput
中 , 从 上 游 StreamTask 读 取 数 据 , 构 建 Task 的 输 出 组 件
StreamTaskNetworkOutput。此处需要注意,StreamTask之间的数据传
递关系由下游StreamTask负责建立数据传递通道,上游StreamTask只
负责写入内存。详见第11章和第12章内容。
然 后 初 始 化 StreamInputProcessor , 将 输 入
( StreamTaskNetworkInput ) 、 算 子 处 理 数 据 、 输 出
(StreamTaskNetworkOutput)关联起来,形成StreamTask的数据处理
的完整通道。
之后设置监控指标,使之在运行时能够将各种监控数据与监控模
块打通。
5)对OperatorChain中的所有算子恢复状态,如果作业是从快照
恢复的,就把算子恢复到上一次保存的快照状态。如果是无状态算子
或者作业第一次执行,则无须恢复。
6)算子状态恢复之后,开启算子,将UDF函数加载、初始化进入
执行状态。不同的算子也有一些特殊的初始化行为,此处不再赘述。
2.运行阶段
初始化StreamTask进入运行状态,StreamInputProcessor持续读
取数据,交给算子执行业务逻辑,然后输出。
3.关闭与清理阶段
当 作 业 取 消 、 异 常 的 时 候 , 中 止 当 前 的 StreamTask 的 执 行 ,
StreamTask进入关闭与清理阶段。

see more please visit: https://homeofpdf.com


1)管理OperatorChain中的所有算子,同时不再接收新的Timer定
时器,处理完剩余的数据,将算子的数据强制清理。
2)销毁算子,销毁算子的时候,关闭StateBackend和UDF。
3)通用清理,停止相关的执行线程。
4 ) Task 清 理 , 关 闭 StreamInputProcessor , 本 质 上 是 关 闭 了
StreamTaskInput,清理InputGate、释放序列化器。

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所示。

see more please visit: https://homeofpdf.com


图10-8 JobMaster管理作业
作 业 调 度 的 入 口 在 JobMaster 中 , 由 JobMaster 发 起 调 度 。
DefaultScheduler是未来的默认调度器,所以以其为例说明其调度过
程。
JobMaster作业调度的启动入口如代码清单10-5所示。
代码清单10-5 JobMaster启动调度

根据调度器,启动不同调度器的调度。批流调度选择在
DefaultScheduler中,通过多态的方式交给调度策略执行具体的调
度,如代码清单10-6所示。
代码清单10-6 多态调度策略

see more please visit: https://homeofpdf.com


10.6.2 流作业启动调度
流计算调度策略计算所有需要调度的ExecutionVertex,然后把需
要 调 度 的 ExecutionVertex 交 给
DefaultScheduler#allocateSlotsAndDeploy , 最 终 调 用
Execution#deploy()开始部署作业,当作业所有的Task启动之后,
则作业启动成功。
流作业的调度策略中,申请该作业所需要的Slot来部署Task,在
申请之前将为所有的Task构建部署信息,如代码清单10-7所示。
代码清单10-7 流作业申请Slot部署Task

DefaultScheduler中根据调度策略,选择不同的调度方法,对于
流作业启动而言,最终调用方法如代码清单10-8所示。

see more please visit: https://homeofpdf.com


代码清单10-8 流作业调度

流 计 算 作 业 中 , 需 要 一 次 性 部 署 所 有 Task , 所 以 会 对 所 有 的
Execution异步获取Slot,申请到所有需要的Slot之后,经过一系列的
过程,最终调用Execution#deploy进行实际的部署,实际上就是将
Task部署相关的信息通过TaskMangerGateway交给TaskManager,如代
码清单10-9所示。
代码清单10-9 部署所有的Task

see more please visit: https://homeofpdf.com


在将Task发往TaskManager的过程中,需要将部署Task需要的信息
进行包装,通过TaskManagerGateway部署Task到TaskManger,如代码
清单10-10所示。
代码清单10-10 部署Task

see more please visit: https://homeofpdf.com


现在版本中的TaskManager为了兼容以前的代码,实际的逻辑在
TaskExecution中,所以TaskManagerGateway#submitTask的调用最后
是对TaskExecutor#submitTask的调用。

see more please visit: https://homeofpdf.com


至此,将Task发送到TaskManager进程,TaskManager中接收Task
部署信息,接下来开始启动Task的执行,并向JobMaster汇报状态变
换。
10.6.3 批作业调度
批处理的本质是分阶段调度,上一个阶段执行完毕,且
ResultParition准备完毕之后,通知JobManager,JobManager调度下
游消费ResultPartition的Execution(即Task)启动执行。
批处理调度中因为需要分阶段调度,所以为了能够确定什么时候
该 调 度 下 一 个 阶 段 可 以 执 行 , 增 加 了 一 个
InputDependencyConstraintChecker,用来决定哪些下游Task具备执
行条件,即上游的数据准备好了,可以开始消费,然后调用
DefaultScheduler进行资源申请,进入部署阶段,如代码清单10-11所
示。
代码清单10-11 申请Slot部署Task

see more please visit: https://homeofpdf.com


对于本阶段需要调度的Task,异步申请资源,申请资源完毕,最
终调用Execution#deploy进行实际的部署,如代码清单10-12所示。
代码清单10-12 部署Task入口

see more please visit: https://homeofpdf.com


批处理作业是分阶段、分批执行的,所以JobMaster需要知道何时
能够启动下游的Task执行,在InputDependencyConstraint中调度时有
两种限制规则。
● ANY规则:Task的所有上游输入有任意一个可以消费即可调度
执行。
● ALL规则:Task的所有上游输入全部准备完毕后才可以进行调
度执行。
当接收到结果分区可消费的消息时,会再次触发调度执行的行
为,遍历作业所有ExecutionVertex,选择符合调度条件的进行调度,
如代码清单10-13所示。
代码清单10-13 调度结果分区下游消费数据

see more please visit: https://homeofpdf.com


对于PIPELINED类型的结果分组,当中间结果分区开始接收第一个
Buffer数据时,触发调度下游消费Task的部署与执行。
批 处 理 和 流 计 算 作 业 从 JobMaster 向 TaskManager 的 部 署 过 程 一
样,都是通过TaskManagerGateway接口进行,此处不再赘述。
10.6.4 TaskManger启动Task
JobMaster通过TaskManagerGateway#submit()RPC接口将Task发
送到TaskManager上,TaskManager接收到Task的部署消息后,分为两
个阶段执行:第一个阶段从部署消息中获取Task执行所需要的信息,
初始化Task,然后触发Task的执行,Task完成一系列的初始化动作
后,进入Task执行阶段。在部署和执行的过程中,TaskExecutor与
JobMaster 保 持 交 互 , 将 Task 的 状 态 汇 报 给 JobMaster , 并 接 受
JobMaster的Task管理操作,如图10-9所示。

see more please visit: https://homeofpdf.com


图10-9 Task部署、启动
1.Task启动
(1)Task部署
TaskManager的实现类是TaskExecutor,JobMaster将Task的部署
信息封装为TaskDeploymentDescriptor对象,通过SubmitTask消息发
送给TaskExecutor。而处理该消息的入口方法是submitTask方法。
该方法的核心逻辑是初始化Task,在初始化Task的过程中,需要
为Task生成核心组件,准备好Task的可执行文件。
上边所有的核心组件的准备工作,目的都是实例化Task,如代码
清单10-14所示。
代码清单10-14 TaskExecutor实例化Task

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
Task 在 实 例 化 的 过 程 中 , 还 进 行 了 非 常 重 要 的 准 备 工 作 。 在
ExecutionGraph中每一个Execution对应一个Task,ExecutionEdge代
表 Task 之 间 的 数 据 交 换 关 系 , 所 以 在 Task 的 初 始 化 中 , 需 要
ExecutionEdge的数据交换关系落实到运行层面上。在这个过程中,最
重要的是建立上下游之间的交换通道,以及Task如何从上游读取,计
算结果如何输出给下游。读取上游数据只用InputGate,结果写出使用
ResultPartitionWriter,详见第11章内容。
ResultPartitionWriter 和 InputGate 的 创 建 、 销 毁 等 管 理 由
ShuffleEnvironment来负责。ShuffleEnvironment底层数据存储对应
的是Buffer,一个TaskManager只有一个ShuffleEnvironment,所有的
Task共享。
(2)启动Task
在上一个阶段中,TaskExecutor准备好了Task,然后进入Task的
执行阶段。在这个阶段中,Task被分配到单独的线程中,循环执行。
Task是容器,其中StreamTask才是用户逻辑执行的起点。在Task
中 通 过 反 射 机 制 实 例 化 StreamTask 的 子 类 , 触 发
StreamTask#invoke()启动真正的业务逻辑的执行。
Task本身是一个Runnable对象,由TaskManager管理和调度,最终
启动算子的逻辑封装在StreamTask和BatchTask中,随着新版本的演
化,流批统一后BatchTask将不再使用,统一使用StreamTask。线程启
动后进入run()方法,如代码清单10-15所示。
代码清单10-15 Task执行

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
see more please visit: https://homeofpdf.com
在Task的初始化中,构建了InputGate组件,用来从上游Task读取
数据,构建了ResultParitionWriter组件,用来将计算结果写出。但
ResultPartition 此 时 还 没 有 注 册 到 ResultParitionManager ,
InputGate也并未与上游Task之间建立物理上的数据传输通道,在Task
开始执行的过程中完成了实际的关联,如代码清单10-16所示。
代码清单10-16 初始化ResultParition和InputGate

至此Task启动完毕。下节介绍Task启动过程中,启动StreamTask
的相关细节。
2. StreamTask启动
StreamTask是算子的执行容器。在JobGraph中将算子连接在一起
进行了优化,在执行层面上对应的是OperatorChain。在Task启动前,
无论是单个算子还是连接在一起的一组算子,都会首先被构造成
OperatorChain , 构 造 OperatorChain 的 过 程 中 , 包 含 了 算 子 的 实 例
化,同时也构造了算子的输出(Output)。
因为不符合链接条件,所以Source算子和FlatMap算子都是单个的
算子,单个算子构成的OperatorChain如图10-10所示。

see more please visit: https://homeofpdf.com


图10-10 一个算子的OperatorChain
算 子 输 出 计 算 结 果 的 时 候 , 包 含 了 两 层 Output , 其 中
CountingOutput 用 来 统 计 算 子 的 输 出 数 据 元 素 个 数 ,
RecordWriterOutput用来序列化数据,写入NetworkBuffer,交给下游
算子,同时计算两个监控指标:向下游发送的字节总数、向下游发送
的Buffer总数。
KeyedAgg算子和Sink算子符合算子链接的条件,构成两个算子的
OperatorChain,如图10-11所示。

图10-11 KeyedAgg和Sink组成的多算子OperatorChain示意
两 个 或 以 上 算 子 构 成 的 OperatorChain , 算 子 之 间 包 含 了 两 层
Output,其中CountingOutput用来统计上游算子(KeyedAgg)输出的
数 据 元 素 个 数 。 ChaingOutput 提 供 了 Watermark 的 统 计 和 下 游 算 子
(Sink)的输入数据元素个数。
Task启动完毕之后,就进入了作业执行的阶段。作业执行的详细
内容见第11章内容讲解。

10.7 作业停止
当作业执行完毕(批处理作业)、执行失败无法恢复时就会进入
停止状态。同时在一些场景中,也需要将作业手动停止,如集群升
级、作业升级、作业迁移等,此时作业都会进入停止状态。与作业的
启动相比,作业的停止要简单许多,主要是资源的清理和释放。

see more please visit: https://homeofpdf.com


JobMaster 向 所 有 的 TaskManager 发 出 取 消 作 业 的 指 令 ,
TaskManager执行Task的取消指令,进行相关的内存资源的清理,当所
有的清理作业完成之后,向JobMaster发出通知,最终JobMaster停
止,向ResourceManager归还所有的Slot资源,然后彻底退出作业的执
行。

10.8 作业失败调度
一个分布式系统涉及上千台服务器,服务上运行着成千上万个进
程,同时还要依赖网络设备进行通信。角色越多,整个系统出错的概
率就越高,在如此大规模的分布式系统中,每天都在发生着硬件故
障、软件故障,网络波动等异常、故障,一个可靠的分布式系统必须
能够应对各种异常对系统稳定性带来的冲击。
Flink作为低延迟的分布式计算引擎,在流计算中引入了分布式快
照容错机制,以满足低延迟和高吞吐的要求。Flink所使用的容错机制
使用分布式快照保存作业状态,与Flink的作业恢复机制相结合,确保
数据不丢失、不重复处理。发生错误时,Flink作业能够根据重启策略
自动从最近一次成功的快照中恢复状态。
Flink对作业失败的原因做了归纳定义,有如下4种类型。
● NonRecoverableError: 不可恢复的错误。此类错误意味着即
便是重启也无法恢复作业到正常状态,一旦发生此类错误,则作业执
行失败,直接退出作业执行。
● PartitionDataMissingError:分区数据不可访问错误。下游
Task无法读取上游Task的产生的数据,需要重启上游的Task。
● EnvironmentError: 环境的错误,问题本身来自机器硬件、外
部服务等。这种错误需要在调度策略上进行改进,如使用黑名单机
制,排除有问题的机器、服务,避免将失败的Task重新调度到这些机
器上。
● RecoverableError: 可恢复的错误。
目前Flink有两套调度策略:默认的作业失败调度和遗留的作业失
败调度。默认的作业失败调度是Flip-1中提出Failover的改进调度策

see more please visit: https://homeofpdf.com


略,是Flink 1.9.0及以后版本的调度策略;遗留的作业调度是Flink
1.9.0版本之前的调度策略。
10.8.1 默认作业失败调度
在错误发生时,首先会尝试对作业进行局部恢复,如果无法恢复
或者局部恢复失败,则会将整个作业进行重启,从保存快照中恢复。
默认失败调度集成在默认调度器DefaultScheduler,具体的调度
行为代理给ExecutionGraph来实现。
在运行时,Task是Flink作业的最小执行单位,一个作业有不定数
量的Task,取决于计算逻辑的复杂度和并行度。可能使Task出问题的
错误非常多,如机器故障、用户代码逻辑中未处理的异常、网络故障
等。
在Flink的DefaultScheduler和细粒度的恢复策略(Flip1)中,
Task错误恢复策略定义如图10-12所示。

图10-12 Flip1失败调度策略体系
● RestartAllStarttegy : 若 Task 发 生 异 常 , 则 重 启 所 有 的
Task,恢复成本高,但其是恢复作业一致性的最安全策略。
● RestartPipelinedRegionStrategy:分区恢复策略,若Task发
生异常,则重启该分区的所有Task,恢复成本低,实现逻辑复杂。
Flip1引入的作业Failover机制,将整个作业的物理执行拓扑Task
DAG切分为不同的FailoverRegion。FailoverRegion本质上是一组有相
互关系的Task,失败恢复的时候按照FailoverRegion回溯,重新启动
需要启动的Task,其过程如图10-13所示。
1. FailoverRegion切分
实现细粒度的Failover,首先需要对作业进行FailoverRegion的
切分,切分策略如下。

see more please visit: https://homeofpdf.com


(1)带有CoLocation限制的作业
在目前实现中,带有CoLocation限制的作业不切分,所有的Task
都位于同一个FailoverRegion,也就是说,若一个Task发生错误,全
部Task都要恢复。
(2)按照ResultPartitionType纵向切分
纵 向 是 指 数 据 流 转 方 向 , 以 Task 间 的 数 据 传 递 方 式 来 确 定
FailoverRegion的边界,简单理解,就是以Shuffle作为边界。
如图10-13a所示,算子A1和B1间以Pipelined方式进行数据传递,
则认为属于同一个FailoverRegion,算子B1和C1之间使用非Pipelined
方式传递数据,则认为不属于同一个FailoverRegion。
注意:ResultPartitonType.PIPELINED和
ResultPartitonType.PIPELINED_BOUNDED都是Pipelined。
(3)按照上下游的数据依赖关系横向切分
横向相互没有依赖关系的Task隶属于不同的分区。类似于Spark的
窄依赖,没有相互依赖关系的Task可以相互独立恢复。
按照这3条切分策略,图10-13a中的作业物理执行拓扑可切分成4
个分区,如图10-14所示。
作 业 恢 复 的 时 候 , 并 不 能 完 全 做 到 按 照 FailoverRegion 恢 复
Task,如FailOverRegion 4因为PartitionDataMissingError无法读取
上游的数据,在批处理作业中虽然按照Shuffle作为边界进行了恢复,
但是对于PartitionDataMissingError,只恢复FailoverRegion4显然
是不够的,此时需要恢复FailoverRegion 2重新产生数据。

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
图10-13 作业的FailoverRegion失败恢复
a)Task异常 b)追溯上游Task c)停止受影响Task d)重启受影响Task

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中

see more please visit: https://homeofpdf.com


的Task都需要恢复,重新调度执行。C1执行失败,则C2同样也需要进
行恢复。
(2)判断是否需要重新调度上游
对于C1的上游B而言,如果B的ResultPartition是重复消费的,并
且目前仍然是可用的,那么B就无须重新调度,如果B的结果分区
ResultPartition已经被销毁了,那么B就需要重新调度。此时对于B而
言,同样需要与步骤1中一样,递归地向上分析A是否重新调度执行,
以此类推。
(3)下游FailoverRegion分析
D1、D2所在的FailoverRegion与C1所在的FailoverRegion共同消
费B的结果分区,如果要重新调度B,需要分析B的下游是否需要重新调
度。
此时需要特别注意,D1可能存在3种情况:
● Task D1正在执行,B的结果分区ResultPartition可访问。此
种情况下D1无须重新调度,B的结果分区可以继续访问。
● Task D正在执行,B的结果分区ResultPartition不可访问。此
种情况下,D1需要重新调度执行,D1所在的整个FailoverRegion也需
要重新调度。
● Task D1 执 行 完 毕 。 D1 已 经 执 行 完 毕 , 无 论 B 的
ResultPartition是否可以访问,都不影响Task D1以及之后的Task。
综 上 而 言 , 对 于 兄 弟 FailoverRegion ( D1 所 在
FailoverRegion),情况2需要重新调度,情况1、3无须重新调度。
但是情况1、3恢复策略保证结果正确有一个前提条件:Task B产
生的结果集分区ResultPartition是确定的,即无论执行多少次,结果
集一样。Task C1、Task D1接收到的数据集和恢复前接收到的数据集
完 全 相 同 , 此 时 D1 无 须 重 新 调 度 。 但 是 如 果 Task B 产 生 的 结 果 集
ResultPartition是不确定的,如使用了GlobalPartitioner进行随机
分区,那么如果不重新调度Task D1,恢复前应该由Task C1处理的数
据(3,7,8)可能会被路由到Task D1所在的结果分区,但是Task D1读
取的是旧版本的Task B的结果分区ResultPartition,(3,7,8)这几
条数据既不会被Task C1处理,也不会被Task D1处理,此时发生了数

see more please visit: https://homeofpdf.com


据的丢失,也就无法保证Exactly-Once,最终的计算结果将变得不可
预料。
因为事先无法确定结果B的结果分区ResultPartition是确定的还
是非确定的,所以默认情况下,只要B被重新调度了,Task D1所在
FailoverRegion 也 需 要 被 重 新 调 度 , 依 次 类 推 , Task D1 的 下 游
FailoverRegion分区也要根据步骤3中的策略分析是否需要重新调度。
RestartPipelinedRegionStrategy未来还有改进优化空间。
当 Task 发 生 错 误 , TaskManager 会 通 过 RPC 通 知 JobManager ,
JobManager将对应Execution的状态转为FAILED并触发Failover策略。
如果错误是可恢复的,JobManager会调度相关的Task重启,否则升级
为整个作业的失败。
3. Failover过程
在运行过程中,一旦发现Task出现异常无法继续执行,就会启动
触发Failover,其入口在DefaultScheduler默认调度器中,如代码清
单10-17所示。
代码清单10-17 Task错误处理入口

根据Flink抽象的错误类型判断Task是否可以恢复。如果Task可恢
复则恢复(可恢复的异常类型在前文提到了);如果不可恢复则整个
作业失败,尝试重启整个作业。如代码清单10-18所示。
代码清单10-18 重启或者失败选择

see more please visit: https://homeofpdf.com


对于可恢复的作业,根据FailoverRegion分析哪些Task需要重
启,调度重启Task,如代码清单10-19所示。
代码清单10-19 分析待重启Task

重启Task的过程中需要在ExecutionGraph中重置ExecutionVertex
状态,然后调度Task的重新执行,执行过程与作业发布类似,区别在

see more please visit: https://homeofpdf.com


于本地启动的Task是作业的Task子集(重启所有Task的情况也是存在
的,如流作业)。Task重启过程如代码清单10-20所示。
代码清单10-20 Task重启

如果Task是不可重启的,那么就需要走重启作业的流程。
10.8.2 遗留的作业失败调度
LegacyScheduler是遗留的调度器,未来以NG调度器默认调度器。
该调度器分为Task Failover和作业Failover。
在Task执行错误时,首先会进行Task Failover,如果Task错误无
法恢复到正常状态,最终触发了Full Restart,此时作业Restart策略
将会控制是否需要恢复作业。Flink提供了3种作业具体的重启策略:

see more please visit: https://homeofpdf.com


● FixedDelayRestartStrategy: 允许指定次数内的Execution
失败,如果超过该次数则导致作业失败。FixedDelayRestartStrategy
重启可以设置一定的延迟,以减少频繁重试对外部系统带来的负载和
不 必 要 的 错 误 日 志 。 目 前 FixedDelayRestartStrategy 是 默 认 的
RestartStrategy。
● FailureRateRestartStrategy: 允许在指定时间窗口中指定
次数内的Execution失败,如果超过这个频率则导致作业失败。同样
地,FailureRateRestartStrategy也可以设置一定的重启延迟。
● NoRestartStrategy: 在Execution失败时直接让作业失败。

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所示。

see more please visit: https://homeofpdf.com


图10-16 JobManager HA过程

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所示。

see more please visit: https://homeofpdf.com


图10-17 Leader选举服务类体系
● ZooKeeperLeaderElectionService:基于ZooKeeper的选举服
务,ZooKeeper天生比较擅长此类场景。
● StandaloneLeaderElectionService:没有ZooKeeper情况下的
选举服务。
● EmbeddedLeaderElectionService:内嵌的轻量级选举服务,
用在Flink Local模式中,主要用来做本地的代码调试和单元测试。
(2)Leader变更服务
HA模式下若出现组件故障,会进行新的Leader选举,即选择主节
点。选举完成之后,要通知其他的组件。LeaderRetrievalService,
顾名思义,就是查找新的Leader节点的服务。
同样,对应于3种选举服务,也有3种Leader查询服务,其类体系
如图10-18所示。

图10-18 Leader变更通知类体系
LeaderRetrievalListener用来通知选举了新的Leader,在选举过
程中可能顺利选举出新的Leader,也可能因为内部或者外部异常,导
致无法选举出新的Leader,此时也需要通知各相关组件。对于无法处
理 的 故 障 , 无 法 进 行 恢 复 , 作 业 进 入 停 止 状 态 。
LeaderRetrievalListener通知行为的定义如代码清单10-21所示。
代码清单10-21 Leader选举通知

see more please visit: https://homeofpdf.com


2. 心跳服务
心跳机制是分布式集群中组件监控的常用手段,Flink各个组件的
监控统一使用心跳机制来实现。心跳服务作为基础性的服务在Flink被
广泛使用。
一个完整的心跳机制需要有心跳的发送者和接受者两个实体,心
跳行为需要进行管理,心跳超时需要有相应实体进行异常处理。为了
更充分地利用心跳机制,Flink的心跳数据包中还包含了一些技术信
息,如TaskManager通过心跳向ResourceManager汇报当前Slot的使用
情况。实现心跳机制涉及如下几个重要的对象。
(1)心跳目标(HeartbeatTarget)
心跳目标,用来表示心跳发送者和心跳接收者,同一个组件既是
发送者也是接收者。其接口定义如代码清单10-22所示。
代码清单10-22 HeartbeatTarget接口

(2)心跳管理器(HeartbeatManager)
通用的心跳管理器用来启动或停止监视HeartbeatTarget,并报告
该 目 标 心 跳 超 时 事 件 。 通 过 monitorTarget 来 传 递 并 监 控
HeartbeatTarget,这个方法可以看成整个服务的输入,告诉心跳服务
去管理哪些目标。心跳管理器的行为定义如代码清单10-23所示。

see more please visit: https://homeofpdf.com


代码清单10-23 HeartbeatManager接口

在 JobMaster 、 ResourceManager 、 TaskManager 中 使 用


HeartbeatManager进行两两心跳监控。
(3)心跳监听器(HeartbeatListener)
心跳监听器是和HeartbeatManager密切相关的接口,可以看成服
务的输出。主要有以下作用。
1)心跳超时通知。
2)接收心跳信息中的Payload。
3)检索作为心跳响应输出的Payload。
心跳行为定义如代码清单10-24所示。
代码清单10-24 HeartbeatListener接口

see more please visit: https://homeofpdf.com


JobMaster、ResourceManager、TaskManager都实现了该接口,心
跳超时时触发异常处理。
3. HA服务
HA服务是高可用服务的实现,提供了Leader选举、Leader获取等
服务,也封装了对Flink内部状态的保存和获取服务,以便在异常恢复
的时候使用。对应于Leader选举服务,HA服务也有3种实现,其中
ZooKeeperHAServices是生产级的高可用服务,其余两种在生产环境中
不建议使用。
HA服务的类体系如图10-19所示。

图10-19 HA服务类体系
(1)ZooKeeperHAServices
基于ZooKeeper高可用服务实现,使用ZooKeeper作为集群信息的
存储,能够实现真正的高可用。集群信息在Zookeeper的信息存储结构
如图10-20所示。

see more please visit: https://homeofpdf.com


图10-20 基于ZooKeeper的信息存储结构
图10-20是基于ZooKeeper的高可用服务,ZooKeeper文件存储目录
结构示例。可以看到以lock结尾的目录是Leader持有的锁,在集群级
别有ResourceManager的Leader锁(/resource_manager_lock) ,在
作业级别有JobMaster的Leader锁 (/job_manager_lock) ,在作业
级别还保存了作业的最新检查点的信息,包含元信息、时间、路径
等,作业恢复的时候会检查点信息,从最新的检查点恢复。
(2)StandaloneHAServices
基于内存和本地文件系统实现高可用服务,能在一定程度上实现
高可用,在物理硬件故障的情况下,可能无法达到高可用的预期,所
以多用在测试场景中。
(3)EmbeddedHAServices
在Flink的Local模式中实现高可用服务,本质上来说是为了实现
Flink的完整作业流程而实现的模拟服务。
10.9.3 JobMaster的容错

see more please visit: https://homeofpdf.com


JobMaster作为Job的管理节点,负责Job的调度,该组件故障会影
响Job的运行。在ResourceManager和TaskManger中都与JobMaster的保
持心跳。
1. TaskManager应对JobMaster故障
TaskManger 通 过 心 跳 超 时 检 测 到 JobMaster 故 障 , 或 者 收 到
ZooKeeper的JobMaster节点失去Leader角色的通知时,就会触发超时
的异常处理。
TaskManager根据该JobMaster管理的Job的ID,将该TaskManager
上所有隶属于该Job的Task取消执行,Task进入Failed状态,但是仍然
保 留 为 Job 分 配 的 Slot 一 段 时 间 。 然 后 尝 试 连 接 新 的 JobMaster
Leader , 如 果 新 的 JobMaster 超 过 了 等 待 时 间 仍 然 没 有 连 接 上 ,
TaskManger 不 再 等 待 , 标 记 Slot 为 空 闲 并 通 知 ResourceManager ,
ResourceManager就可以在下一次的Job调度执行中分配这些Slot资
源。
2. ResourceManager应对JobMaster故障
ResourceManager通过心跳超时检测到JobMaster故障,或者收到
Zookeeper的关于JobMaster失去Leader的通知时,ResourceManager会
通知JobMaster重新尝试连接,其他不作处理。
3. JobMaster切换
JobMaster保存了对作业执行至关重要的状态和数据,JobGraph、
用户的Jar包、配置文件、检查点数据等保存在配置的分布式可靠存储
中,一般使用HDFS。检查点访问信息保存在ZooKeeper中。
JobMaster 出 现 故 障 之 后 要 选 举 新 的 JobMaster Leader , 新 的
Leader选举出来之后,会通知ResourceManager和TaskManager。
JobMaster 的 首 要 任 务 是 重 新 调 度 Job , 如 果 Slot 还 没 有 被
TaskManager释放掉,TaskManager向ResourceManager发送的心跳信息
中告知资源的使用情况和当前的Slot属于哪个Job ID。JobMaster直接
向ResourceManager申请,ResourceManager无须申请新的Slot。
10.9.4 ResourceManager容错

see more please visit: https://homeofpdf.com


ResourceManager负责Job资源的管理,ResourceManager上维护着
很多重要信息,如所有可用的TaskManager的清单等。这些信息是在内
存中进行维护的,并不会持久化到存储上,JobMaster、TaskManager
向ResourceManager发送心跳的过程中,心跳数据中带有这些信息,很
快就会将状态同步到ResourceManager中。
在设计上,ResourceManager的故障不会中断Task的执行,但是无
法启动新的Task。
1. JobMaster应对ResourceManager故障
JobMaster通过心跳超时检测到ResourceManager故障,此时导致
故障的原因有很多,可能是简单的网络延迟问题。在非HA部署模式
下,不会有新的ResourceManager Leader出现。所以JobMaster会首先
尝试重新连接ResourceManager。
如果一直没有连接上,则在HA模式下,JobMaster通过Leader选举
通 知 得 到 新 的 ResourceManager 地 址 , 通 过 该 地 址 重 新 与
ResourceManager连接。
在最后实在无法与ResourceManager取得连接的情况下,则整个集
群就会停止。
2. TaskManager应对ResourceManager故障
TaskManager通过心跳检测到ResourceManager的故障,同样也会
首先尝试重新连接。如果一直没有连接成功,在HA模式下,选举出了
新 的 ResourceManager Leader , TaskManager 也 会 得 到 通 知 , 然 后
TaskManager 会 与 新 的 ResourceManager 取 得 连 接 , 将 自 己 注 册 到
ResourceManager,并将其自身的状态同步到ResourceManager。
在最后实在无法与ResourceManager取得连接的情况下,集群无法
恢复到正常运行的状态,集群同样会停止。
10.9.5 TaskManager的容错
TaskManager是集群的计算执行者,其上执行了一个或者多个Job
的Task。
1. ResourceManager应对TaskManager故障

see more please visit: https://homeofpdf.com


ResouceManager通过心跳超时检测到TaskManager故障,它会通知
对 应 的 JobMaster 并 启 动 一 个 新 的 TaskManager 作 为 代 替 。 注 意 ,
ResouceManager并不关心Flink作业的情况,管理Flink作业要做何种
反应是JobMaster的职责。
2. JobMaster应对TaskManager故障
JobMaster通过心跳超时检测到TaskManager故障,它首先会从自
己的Slot Pool中移除该TaskManager,并释放该TaskManager的Slot,
最终会触发Execution的异常处理,然后触发Job级别的恢复,从而重
新 申 请 资 源 , 由 ResourceManager 启 动 新 的 TaskManager 来 重 新 执 行
Job。
结合作业的失败调度过程,可以知道TaskManger的故障其实就是
作业Task异常的Failover的过程。
基于Standalone资源管理,TaskManger的数量是确定的,必须有
足 够 的 空 闲 Slot 资 源 才 能 够 将 Job 恢 复 执 行 。 对 于 基 于 资 源 框 架
(Yarn、K8s、Mesos)的资源管理,则可以向资源管理集群申请新的
容器,在容器中启动TaskManger,然后将TaskManger的Slot分配给作
业。
3. JobMaster和ResourceManager同时故障
在 YARN 部 署 模 式 下 , 因 为 JobMaster 和 ResourceManager 都 在
JobManager进程内,如果JobManager进程出现问题,通常是JobMaster
和ResourceManager同时故障,因为ResourceManager故障不会影响已
经运行的Task,相比JobMaster故障影响小一些,所以TaskManager会
优先恢复与新的JobMaster之间的连接,保留Slot一段时间,再同时尝
试着与ResourceManager建立连接。

10.10 总结
本章主要介绍了Flink作业的调度模式以及调度执行的过程,在调
度执行过程中作业和Task有各自的生命周期转换。在执行过程中,作
业可能因为某个Task执行异常或者Flink集群组件故障导致执行失败,
需要对作业进行Failover。同时,各个集群组件也会出现故障,为了
应对此类问题,Flink实现了HA服务。

see more please visit: https://homeofpdf.com


第11章 作业执行
Flink JobMaster调度作业的Task到TaskManager,所有的Task启
动成功,进入执行状态,则整个作业进入执行状态。从外部数据源开
始读取数据,数据在Flink Task DAG中流转,处理完毕后,写出到外
部存储。

11.1 作业执行图
经过Flink的多层Graph转换之后,作业进入调度阶段,开始分发
任务执行,最终会在集群中形成如图11-1所示的物理拓扑结构,借用
前边的Graph的概念,此处称为物理执行图。
当一个作业执行起来之后,其拓扑关系如图11-1所示,但是在作
业真正执行之前需要经历作业调度和启动的过程。

图11-1 Flink Job物理执行图


物 理 执 行 图 并 非 Flink 的 数 据 结 构 , 而 是 JobMaster 根 据
ExecutionGraph对作业进行调度后,在各个TaskManager上部署Task后
形成的“图”,是物理上各个Task对象的关系拓扑,包含上下游的连
接关系、内存中数据的存储、数据的交换等。

see more please visit: https://homeofpdf.com


其核心对象有Task、ResultPartition & ResultSubPartition、
InputGate & InputChannel。

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所示。

see more please visit: https://homeofpdf.com


图11-3 StreamOneInputProcessor数据处理过程

11.2.2 Task输入
Task输入在Flink中叫作StreamTaskInput,是StreamTask的数据
输入的抽象,其类体系如图11-4所示。
对于Flink中的StreamTask而言,数据读取的行为有两种:
1 ) StreamTaskNetworkInput 负 责 从 上 游 Task 获 取 数 据 , 使 用
InputGate作为底层读取数据。
2)StreamTaskSourceInput负责从外部数据源获取数据,本质上
是使用SourceFunction读取数据,交给下游的Task。

see more please visit: https://homeofpdf.com


图11-4 StreamTaskInput类体系

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取决于直接下游子任务的并行度和数据分
发模式。

see more please visit: https://homeofpdf.com


下游子任务消费上游子任务产生的ResultPartition,在实际请求
的 时 候 , 是 向 上 游 请 求 ResultSubPartition , 并 不 是 请 求 整 个
ResultPartition,请求的方式有远程请求和本地请求两种。
ResultParition的类体系如图11-6所示。

图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 在 输 出 端 将

see more please visit: https://homeofpdf.com


ResultPartition进行了切分。例如,上游Task对接下游4个Task,就
为ResultPartition生成了4个ResultSubPartition。
结果子分区类体系如图11-7所示。

图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是
单线程的。

see more please visit: https://homeofpdf.com


该ResultSubPartition是持有数据的容器,其中的BoundedData属
性是实际的数据操作接口,BoundedData提供了几种不同的实现。
11.2.6 有限数据集
有限数据集在Flink中叫作BoundedData,它定义了存储和读取批
处 理 中 间 计 算 结 果 数 据 集 的 阻 塞 式 接 口 ,
BoundedBlockingSubPartition使用BoundedData接口来实现中间结果
集的访问。BoundedData有3个不同的实现,分别对应于不同Buffer的
存储方式。这3种实现方式都是基于java.nio包的内存映射文件和
FileChannel。
BoundedData类体系如图11-8所示。

图11-8 BoundedData类体系
(1)FileChannelBoundedData
使用Java NIO的FileChannel写入数据和读取文件。
(2)FileChannelMemoryMappedBoundedData
使用FileChannel写入数据到文件,使用内存映射文件读取数据。
(3)MemoryMappedBoundedData
使用内存映射文件写入、读取,全部是内存操作。
内存映射文件(Memory-mapped File)是将一段虚拟内存逐字节
映射于一个文件,使得应用程序处理文件如同访问主内存(但在真正
使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操
作),内存映射文件与磁盘的真正交互由操作系统负责。

see more please visit: https://homeofpdf.com


java.io基于流的文件读写,读文件会产生2次数据复制,首先是
从硬盘复制到操作系统内核,然后从操作系统内核复制到用户态的应
用程序,写文件也类似。而使用java.nio包内存映射文件,一般情况
下,只有一次复制,且内存分配在操作系统内核,应用程序访问的就
是操作系统的内核内存空间,比基于流进行文件读写快几个数量级。
FileChannel 是 直 接 读 写 文 件 , 与 内 存 映 射 文 件 相 比 ,
FileChannel对小文件的读写效率高,内存映射文件则是针对大文件效
率高。
11.2.7 输入网关
输入网关在Flink中叫作InputGate,是Task的输入数据的封装,
和JobGraph中的JobEdge一一对应,对应于上游的ResultParition。
InputGate 中 负 责 实 际 数 据 消 费 的 是 InputChannel , 是
InputChannel的容器,用于读取中间结果(IntermediateResult)在
并 行 执 行 时 由 上 游 Task 产 生 的 一 个 或 多 个 结 果 分 区
(ResultPartition)。
Flink当前提供了3种InputGate的实现,如图11-9所示。

图11-9 InputGate类体系

see more please visit: https://homeofpdf.com


SingleInputGate 是 消 费 ResultPartition 的 实 体 , 对 应 于 一 个
IntermediateResult。而UnionInputGate主要充当InputGate容器的角
色,将多个InputGate联合起来,当作一个InputGate,一般是对应于
上游的多个输出类型相同的IntermediateResult,对应于多个上游的
IntermediateResult。
InputGateWithMetrics 本 质 上 来 说 就 是 一 个 InputGate+ 监 控 统
计,统计InputGate读取的数据量,单位为byte。
如图11-10所示,一个作业包含了两个算子,map算子产生数据,
reduce算子消费数据。

图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,负责

see more please visit: https://homeofpdf.com


从 上 游 的 所 有 ResultPartition 中 获 取 该 子 任 务 所 需 要 的
ResultSubPartition。
11.2.8 输入通道
输入通道在Flink中叫作InputChannel,每个InputGate会包含一
个以上的InputChannel,和ExecutionEdge一一对应,也和结果子分区
一对一相连,即一个InputChannel接收一个结果子分区的输出。
Flink中提供了3种InputChannel实现,如图11-12所示。

图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。

see more please visit: https://homeofpdf.com


11.3 Task执行
Flink之前的Task执行模型依赖于锁机制,可能导致多个潜在的线
程并发访问其内部状态,比如事件处理以及检查点的触发线程。当
前,它们都通过一个全局锁(检查点锁)来保证彼此互斥。这种机制
有一些劣势:
1)锁对象必须在类的各种互斥访问的代码段中进行传递,代码可
读性很差,使用不当或者漏用则容易造成难以定位的问题。
2 ) 设 计 不 够 优 雅 , 锁 对 象 暴 露 给 了 面 向 用 户 的
API(SourceContext)。
所以在Flip-27中提出了新的类似于基于Mailbox单线程的执行模
型,取代现有的多线程模型。所有的并发操作都通过队列进行排队
(Mailbox),单线程 (Mailbox线程)依次处理,这样就避免了并发
操作。
11.3.1 Task处理数据
启动Task进入执行状态,开始读取数据,当有可消费的数据时,
则持续读取数据,如代码清单11-1所示。
代码清单11-1 StreamTask数据处理入口

see more please visit: https://homeofpdf.com


StreamInputProcessor是数据读取、处理、输出的高层逻辑的载
体,由其负责触发数据的读取,并交给算子处理,然后输出,如代码
清单11-2所示。
代码清单11-2 StreamOneInputProcessor处理

see more please visit: https://homeofpdf.com


StreamInputProcessor 实 际 上 将 具 体 的 数 据 读 取 工 作 交 给 了
StreamTaskInput,当读取了完整的记录之后就开始向下游发送数据,
在发送数据的过程中,调用算子进行数据的处理,如代码清单11-3所
示。
代码清单11-3 从NetworkBuffer中读取完整的StreamRecord并触发处

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
前文中提到过数据元素的4种类型,读取到完整数据记录之后,根
据其类型进行不同的逻辑处理,如代码清单11-4所示。
代码清单11-4 按照StreamRecord的类型进行分别处理

至此,读取到数据,对于数据记录(StreamRecord),会在算子
中包装用户的业务逻辑,即使用DataStream编写的UDF,如代码清单

see more please visit: https://homeofpdf.com


11-5所示。
代码清单11-5 触发算子执行用户逻辑

至此,进入到算子内部,由算子去执行用户编写的业务逻辑,以
WordCount示例中的flatMap处理,调用flatMap方法执行用户编写的具
体业务逻辑,如代码清单11-6所示。
代码清单11-6 StreamFlatMap算子触发用户UDF

算子真正执行的是WordCount示例的Tokenizer分词器进行分词,
然后通过Output将数据输出给下游的算子(如果算子在OperatorChain
中且不是最后一个)或者下游Task,如代码清单11-7所示。
代码清单11-7 WordCount示例中Tokenizer

see more please visit: https://homeofpdf.com


在算子中处理完毕,数据要交给下一个算子或者Task进行计算,
此时就会涉及3种算子之间数据传递的情形。
1)OperatorChain内部的数据传递,发生OperatorChain所在本地
线程内。
2)同一个TaskManager的不同Task之间传递数据,发生在同一个
JVM的不同线程之间。
3)不同TaskManager的Task之间传递数据,即跨JVM的数据传递,
需要使用跨网络的通信,即便TaskManager位于同一个物理机上,也会
使用网络协议进行数据传递。
数据传递的详细过程将在第12章阐述。
11.3.2 Task处理Watermark
Task 处 理 Watermark 的 时 候 分 为 两 种 : 一 种 是 在
OneInputStreamOperator ( 单 流 输 入 算 子 ) 中 ; 一 种 是 在
TowInputStreamOperator(双流输入算子)中。

see more please visit: https://homeofpdf.com


单流输入逻辑比较简单,如果有定时器服务,则判断是否触发计
算,并将Watermark发往下游,如代码清单11-8所示。
代码清单11-8 单流输入Watermark处理

双 流 输 入 从 上 游 两 个 算 子 中 接 收 到 两 个 Watermark ,
inputWatermark1表示第一个输入流的Watermark,inputWatermark2表
示 第 2 个 输 入 流 的 Watermark , 选 择 其 中 较 小 的 那 一 个
Min(inputWatermrk1,inputWatermark2)作为当前的Watermark。之
后的处理逻辑与单流输入一致,如代码清单11-9所示。
代码清单11-9 双流输入Watermark处理

see more please visit: https://homeofpdf.com


11.3.3 Task处理StreamStatus
StreamStatus是StreamElement的一种,用来标识Task是活动状态
还是空闲状态。当SourceStreamTask或一般的StreamTask处于闲置状
态 ( IDLE ) , 不 会 向 下 游 发 送 数 据 或 Watermark 时 , 就 向 下 游 发 送
StreamStatus#IDLE状态告知下游,依次向下传递。当恢复向下游发送
数 据 或 者 Watermark 前 , 首 先 发 送 StreamStatus#ACTIVE 状 态 告 知 下
游。
1. 如何判断是Idle还是Active状态
StreamStatus状态变化在SourceFunction中产生。Source Task如
果读取不到输入数据,则认为是Idle状态,如Kafka Consumer未分配
到数据分区(Partition),则其不会读取数据。如果重新读取到数
据,则认为是Active状态,如代码清单11-10所示。
代码清单11-10 Kafka Connector中标记为Idle

see more please visit: https://homeofpdf.com


当StreamTask的所有上游Task全部处于Idle状态的时候,认为这
个StreamTask处于Idle状态,只要有一个上游的Source Task是Active
状态,StreamTask就是Active状态。
此处需要注意,在Flink中StreamTask中最多只有两个上游输入
Task,所以在实现中只判断了两个上游输入的状态。
2. Stream Status对Watermark的影响
由于SourceTask保证在Idle状态和Active状态之间不会发生数据
元素,所以StreamTask可以在不需要检查当前状态的情况下安全地处
理和传播收到数据元素。但是由于在Dataflow的任何中间节点都可能
产生Watermark,所以当前StreamTask在发送Watermark之前必须检查
当前算子的状态,如果当前的状态是Idle,则Watermark会被阻塞,不会
向下游发送。
如果StreamTask有多个上游输入,有两种情况:
1)上游输入的Watermark状态为Idle。
2)恢复到Active状态,但是其Watermark落后于当前算子的最小
Watermark,此时需要忽略这个特殊的Watermark。在判断是否需要向
前推进Watermark和向下游发送的时候,这个特殊Watermark不起作
用。在11.3.2小节Task处理Watermark的过程中中可以看到其逻辑。
注意:当Source通知下游SourceTask永久关闭,并且再也不会向
下游发送数据的时候,会发送一个值为Watermark. MAX_WATERMARK的
Watermark,而不是发送一个StreamStatus#I-DLE状态。StreamStatus
只用于临时性地停止数据发送和恢复发送的情况。
11.3.4 Task处理LatencyMarker

see more please visit: https://homeofpdf.com


LatencyMarker用来近似评估数据从读取到写出之间的延迟,但是
并不包含计算的延迟。在算子中只能将数据记录交给UDF执行,所以收
到LatencyMarker就直接交给下游了,如代码清单11-11所示。
代码清单11-11 算子处理LatencyMarker

11.4 总结
Flink作业真正执行起来之后,会在物理上构成Task相互连接的
DAG,在执行过程中上游Task结果写入结果分区,结果分区又分成结果
子 分 区 , 下 游 的 Task 通 过 InputGate 与 上 游 建 立 数 据 传 输 通 道 ,
InputGate中的InputChannel对应于结果子分区,将数据交给Task执
行。
Task 执 行 的 时 候 , 根 据 数 据 的 不 同 类 型 ( StreamRecord 、
Watermark、LatencyMarker)进行不同的处理逻辑,处理完后再交给
下游的Task。

see more please visit: https://homeofpdf.com


第12章 数据交换
Flink作为分布式计算引擎,作业最终执行的时候形成了一个分布
式Dataflow,子计算任务Task分布在不同的物理服务器上,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模式的特性对比

see more please visit: https://homeofpdf.com


12.2 关键组件
12.2.1 RecordWriter
RecordWriter负责将Task处理的数据输出,然后下游Task就可以
继续处理了。RecordWriter面向的是StreamRecord,直接处理算子的
输出结果。ResultPatitionWriter面向的是Buffer,起到承上启下的
作用。RecordWriter比ResultPartitionWriter的层级要高,底层依赖
于ResultPar titionWriter。
在 DataStream API 中 介 绍 过 数 据 分 区 , 其 中 最 核 心 的 抽 象 是
ChannelSelector,在RecordWriter中实现了数据分区语义,将开发时
对 数 据 分 区 API 的 调 用 转 换 成 了 实 际 的 物 理 操 作 , 如
DataStream#shuffle()等。
最底层内存抽象是MemorySegment,用于数据传输的是Buffer,那
么,承上启下对接、从Java对象转为Buffer的中间对象是什么呢?是
StreamRecord。
RecordWriter 类 负 责 将 StreamRecord 进 行 序 列 化 , 调 用
SpaningRecordSerializer,再调用BufferBuilder写入MemorySegment
中(每个Task都有自己的LocalBufferPool,LocalBufferPool中包含
了多个MemorySegment)。
哪些数据会被会序列化?
● 数据元素StreamElement:前面介绍过,StreamElement共有4
种。
● 事件Event:Flink内部的系统事件,如CheckpointBarrier事
件等。
Flink RecordWriter提供两种写入方式:单播和广播,其类体系
如图12-1所示。

see more please visit: https://homeofpdf.com


图12-1 RecordWriter类体系
1. 单播
根据ChannelSelector,对数据流中的每一条数据记录进行选路,
有 选 择 地 写 入 一 个 输 出 通 道 的 ResultSubPartition 中 , 适 用 于 非
BroadcastPartition。如果在开发的时候没有使用Partition,默认会
使用RoundRobinChannelSelector,使用RoundRobin算法选择输出通道
循环写入本地输出通道对应的ResultPartition,发送到下游Task,如
图12-2所示。单播数据写入如代码清单12-1所示。

图12-2 ChannelSelectorWriter选路和数据输出
2. 广播

see more please visit: https://homeofpdf.com


概念上来说,广播就是向下游所有的Task发送相同的数据,在所
有的ResultSubPartition中写入N份相同数据。但是在实际实现时,同
时写入N份重复的数据是资源浪费,所以对于广播类型的输出,只会写
入编号为0的ResultSubPartition中,下游Task对于广播类型的数据,
都会从编号为0的ResultSubPartition中获取数据,如图12-3所示。

图12-3 BroadcastRecordWriter广播数据输出
代码清单12-1 单播数据写入

see more please visit: https://homeofpdf.com


从代码清单12-1中可以看到,如果记录的数据无法被单个Buffer
所容纳,将会被拆分成多个Buffer存储,直到数据写完。广播记录或
者广播事件的整个过程也是类似的,只不过变成了遍历写入每个
ResultSubpartition,而不是像上面这样通过通道选择器来选择。

see more please visit: https://homeofpdf.com


12.2.2 数据记录序列化器
数据记录序列化器在Flink中叫作RecordSerializer,负责数据的
序 列 化 , SpanningRecordSerializer 是 其 唯 一 的 实 现 , 如 图 12-4 所
示。

图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 数据序列化过程

see more please visit: https://homeofpdf.com


12.2.3 数据记录反序列化器
数据记录反序列化器在Flink中叫作RecordDeserializer,负责数
据的反序列化,SpillingAdaptiveSpanningRecordDeserializer是其
唯一的实现,如图12-5所示。

图12-5 RecordDeserializer类体系
跟RecordSerializer类似,考虑到记录的数据大小以及Buffer对
应的内存段的容量大小,在反序列化时也存在不同的反序列化结果,
以枚举DeserializationResult表示。
● PARTIAL_RECORD: 表示记录并未完全被读取,但缓冲中的数
据已被消费完成。

see more please visit: https://homeofpdf.com


●INTERMEDIATERECORDFROM_BUFFER: 表示记录的数据已被完全
读取,但缓冲中的数据并未被完全消费。
● LASTRECORDFROM_BUFFER: 记录被完全读取,且缓冲中的数据
也正好被完全消费。
SpillingAdaptiveSpanningRecordDeserializer 适 用 于 数 据 相 对
较大且跨多个内存段的数据元素的反序列化,支持将溢出的数据写入
临时文件中。序列化与反序列化的过程相反,具体实现过程不再赘
述。
12.2.4 结果子分区视图
结果子分区视图在Flink中叫作ResultSubPartitionView,其定义
了 ResultSubPartition 中 读 取 数 据 、 释 放 资 源 等 抽 象 行 为 , 其 中
getNextBuffer是最重要的方法,用来获取Buffer。
Flink中设计了两种不同类型的结果子分区,其存储机制不同,对
应于结果子分区的不同类型,定义了两个结果子分区视图,其类体系
如图12-6所示。

图12-6 结果子分区视图类体系
● PipelinedSubPartitionView : 用 来 读 取
PipelinedSubPartition中的数据。
● BoundedBlockingSubPartitionReader : 用 来 读 取
BoundedBlockingSubPartition中的数据。
12.2.5 数据输出
数据输出(Output)是算子向下游传递的数据抽象,定义了向下
游 发 送 StreamRecord 、 Watermark 、 LetencyMark 的 行 为 , 对 于
StreamRecord,多了一个SideOutput的行为定义。

see more please visit: https://homeofpdf.com


Output类体系如图12-7所示。

图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

see more please visit: https://homeofpdf.com


包装类,基于一组OutputSelector选择发送给下游哪些Task。
DirectedOutput为共享对象模式,CopyingDirectedOutput为非共享对
象模式。
5. BroadcastingOutputCollector &
CopyingBroadcastingOutputCollector
包装类,内部包含了一组Output。向所有的下游Task广播数据。
Copying和非Copying的区别在于是否重用对象。
6. CountingOutput
CountingOutput其他Output实现类的包装类,该类没有任何业务
逻辑属性,只是用来记录其他Output实现类向下游发送的数据元素个
数,并作为监控指标反馈给Flink集群。

12.3 数据传递
在Flink中,数据处理的业务逻辑位于UDF的processElement()
方法中,算子调用UDF处理数据完毕之后,需要将数据交给下一个算
子。Flink的算子使用Collector接口进行数据传递。
Flink中有3种数据传递的方式:
● 本地线程内的数据交换。
● 本地线程之间的数据传递。
● 跨网络的数据交换。
下边分别对这3种数据交换方式进行详细介绍。
12.3.1 本地线程内的数据传递
本地线程内的数据交换是最简单、效率最高的传递形式,其本质
是属于同一个OperatorChain的算子之间的数据传递,如图12-8所示。

see more please visit: https://homeofpdf.com


图12-8 Task内算子间数据传递
图12-8中,3个算子属于同一个OperatorChain,在执行的时候,
会被调度到同一个Task。上游的算子处理数据,然后通过Collector接
口直接调用下游算子的processElement()方法,在同一个线程内执
行普通的Java方法,没有将数据序列化写入共享内存、下游读取数据
再反序列化的过程,线程切换的开销也省掉了。
12.3.2 本地线程间的数据传递
位于同一个TaskManager的不同Task的算子之间,不会通过算子间
的直接调用方法传输数据,而是通过本地内存进行数据传递。以
Source算子所在线程与下游的FlatMap算子所在线程间的通信为例,这
两个Task线程共享同一个BufferPool,通过wait()/notifyAll()来
同步。Buffer和Netty中的ByteBuf功能类似,可以看作是一块共享的
内存。InputGate负责读取Buffer或Event。
Source算子和FlatMap算子的数据交换如图12-9所示。
本地线程间的数据交换经历5个步骤:
1)FlatMap所在线程首先从InputGate的LocalInputChannel中消
费 数 据 , 如 果 没 有 数 据 则 通 过 InputGate 中 的
inputChannelWithData.wait()方法阻塞等待数据。
2)Source算子持续地从外部数据源(如Kafka读取数据)写入
ResultSubPartition中。

see more please visit: https://homeofpdf.com


3)ResultSubPartition将数据刷新写入LocalBufferPool中,然
后 通 过 inputChannelWithDa ta.notifyAll ( ) 方 法 唤 醒 FlatMap 线
程。

图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所
示。

see more please visit: https://homeofpdf.com


图12-10 跨网络Task数据传递
跨网络的数据交换比本地线程间的数据传递要复杂一些,需要9个
步骤:
1 ) Keyed/Agg 所 在 线 程 从 InputGate 的 RemoteChannel 中 消 费 数
据,如果没有数据则阻塞在RemoteInputChannel中的receivedBuffers
上,等待数据。
2 ) FlatMap 持 续 处 理 数 据 , 并 将 数 据 写 入 ResultSubPartition
中。
3 ) ResultSubPartition 通 知 PartitionRequestQueue 有 新 的 数
据。
4)PartitionRequestQueue从ResultSub读取数据。
5 ) ResultSubPartition 将 数 据 通 过
PartitionRequestServerHandler写入Netty Channel,准备写入下游
Netty。
6)Netty将数据封装到Response消息中,推送给下游。此处需要
下游对上游的request请求,用来建立数据从上游到下游的通道,此请

see more please visit: https://homeofpdf.com


求 是 对 ResultSubPartition 的 数 据 请 求 , 创 建 了
PartitionRequestQueue。
7)下游Netty收到Response消息,进行解码。
8)CreditBasedPartitionRequestClientHandler将解码后的数据
写入RemoteInputChannel的Buffer缓冲队列中,然后唤醒Keyed/Agg所
在线程消费数据。
9)从Keyed/Agg所在线程RemoteInputChannel中读取Buffer缓冲
队列中的数据,然后进行数据的反序列化,交给算子中的用户代码进
行业务处理。

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或者跨网

see more please visit: https://homeofpdf.com


络的数据传递。
注意:本质上,本机跨JVM这种数据传递的处理逻辑与跨网络的
数据是一样的,都是通过Netty进行通信的。
无论是本地线程间数据交换还是跨网络的数据交换,对于数据消
费端而言,其数据消费的线程会一直阻塞在InputGate上,等待可用的
数据,并将可用的数据转换成StreamRecord交给算子进行处理。

图12-11 数据可用通知类体系
其 基 本 过 程 为 :
StreamTask#processInput→StreamOneInputStreamOperator#process
Input→StreamTaskNetworkInput#emitNext→SpillingAdataptiveSpa
nningRecordDeserializer。
从NetworkInput中读取数据反序列化为StreamRecord,然后交给
DataOutput向下游发送。如代码清单12-3所示。
代码清单12-3 StreamTaskNetworkInput读取数据元素

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
数据反序列化时,使用DataInputView从内存中读取二进制数据,
根据数据的类型进行不同的反序列化。对于数据记录,则使用其类型
对 应 的 序 列 化 器 , 对 于 其 他 类 型 的 数 据 元 素 , 如 Watermark 、
LatencyMaker、StreamStatus等,直接读取其二进制数据转换为数值
类型如代码清单12-4所示。
代码清单12-4 从DataInputView反序列化数据

see more please visit: https://homeofpdf.com


12.4.2 数据写出
Task调用算子执行UDF之后,需要将数据交给下游进行处理。
RecordWriter 类 负 责 将 StreamRecord 进 行 序 列 化 , 调 用
SpaningRecordSerializer,再调用BufferBuilder写入MemorySegment
中(每个Task都有自己的LocalBufferPool,LocalBufferPool中包含
了多个MemorySegment),如代码清单12-5所示。
代码清单12-5 RecordWriter写出数据

see more please visit: https://homeofpdf.com


在数据序列化之后,通过Channel编号选择结果子分区,将数据复
制到结果子分区中,在写入过程中,序列化器将数据复制到
BufferBuilder中,如果数据太大,则需要写入多个Buffer,即跨内存
段写入数据,如代码清单12-6所示。
代码清单12-6 复制序列化后的数据到目标Channel

see more please visit: https://homeofpdf.com


12.4.3 数据清理

see more please visit: https://homeofpdf.com


RecordWriter 将 StreamRecord 序 列 化 完 成 之 后 , 会 根 据
flushAlways参数决定是否立即将数据进行推送,相当于每条记录发送
一次,这样做延迟最低,但是吞吐量会下降,Flink默认的做法是单独
启 动 一 个 线 程 , 每 隔 一 个 固 定 时 间 刷 新 ( flush ) 一 次 所 有 的
Channel,本质上是一种批量处理(与spark的mini-batch不同)。
当所有数据都写入完成后需要调用Flush方法将可能残留在序列化
器 Buffer 中 的 数 据 都 强 制 输 出 。 Flush 方 法 会 遍 历 每 个
ResultSubPartition,然后依次取出该ResultSubPartition对应的序
列化器,如果其中还有残留的数据,则将数据全部输出。这也是每个
ResultSubPartition都对应一个序列化器的原因。
数据写入之后,无论是即时Flush还是定时Flush,根据结果子分
区的类型的不同,行为都会有所不同。
1. PipelinedSubPartition
如果是立即刷新,则相当于一条记录向下游推送一次,延迟最
低,但是吞吐量会下降,Flink默认的做法是单独启动一个线程,默认
100ms刷新一次,本质上是一种mini-batch,这种mini-batch只是为了
增大吞吐量,与Spark的mini-batch处理不是一个概念。
ResultPartition遍历自身的PipelinedSubPartition,逐一进行
Flush。Flush之后,通知ResultSubPartitionView有可用数据,可以
进行数据的读取了,如代码清单12-7所示。
代码清单12-7 PipelinedSubPartition写入Buffer并通知

2. BoundedBlockingSubPartition
如果是立即刷新,则相当于1条记录向文件中写入1次,否则认为
是延迟刷新,每隔一定时间周期将该时间周期内的数据进行批量处
理,默认是100ms刷新一次数据刷新的时候根据ResultPartition中

see more please visit: https://homeofpdf.com


ResultSubPartition 的 类 型 的 不 同 有 不 同 的 刷 新 行 为 。
ResultPartition遍历自身的BoundedBlockingSubPartition,逐一进
行Flush。写入之后回收Buffer。该类型的SubPartition并不会触发数
据可用通知,如代码清单12-8所示。
代码清单12-8 BoundedBlockingSubparition写入Buffer无通知

see more please visit: https://homeofpdf.com


12.5 网络通信
一般的流计算系统的消息传递,都是基于推送机制的,即数据从
上游处理节点推送给下游处理节点。在这个过程中,如果下游的处理
能力无法应对上游节点的数据发送速度,那么就会导致数据在下游处
理节点累积,一旦超过了处理限度,就可能会发生数据丢失、进程错
误、内存空间不足、CPU使用率过高等各种难以预期的情况,最终导致
资源耗尽甚至系统崩溃。为了保持整个流计算系统的稳定性,需要对

see more please visit: https://homeofpdf.com


上游节点发送数据的速度进行流量控制,这种控制机制,一般叫作反
压(BackPressure)。
很多种情况都会导致反压,如JVM垃圾回收的停顿导致数据的堆
积;电商的秒杀或者大促活动导致的瞬时流量爆发;Task中业务逻辑
复杂导致数据记录处理比较慢等。
12.5.1 网络连接
从逻辑上来说,Flink作业的Task之间的连接关系一般是多对多
的,上游的Task生成多个结果子分区,每个子分区对应下游Task的一
个InputChannel,下游Task从上游多个Task获取数据,如图12-12所
示。

图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之间的逻辑连接关系

see more please visit: https://homeofpdf.com


在本例中选择Task A.1、A.2、B.3、B.4展示其物理连接关系,
Task A.1、A.2调度到TaskManager 1上执行,Task B.3、B.4调度到
TaskManager 2上执行,其连接关系为A.1→B.3、A.1→B.4、A.2→B.3
和A.2→B.4,如图12-13所示。

图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),与下游的

see more please visit: https://homeofpdf.com


InputChannel一一对应。在网络传输这一层次上,Flink不再区分单个
记录,而是将一组序列化记录写入网络缓冲区中。每个Task可用的本
地 最 大 缓 冲 池 大 小 为 : Task 的 InputChannel 数 量 ×buffers-per-
channel+floating-buffers-per-gate。
12.5.2 无流控
当Task的发送缓冲池耗尽时,也就是结果子分区的Buffer队列中
或更底层的基于Netty的网络栈的网络缓冲区满时,生产者Task就被阻
塞,无法继续处理数据,开始产生背压。当Task的接收缓冲区耗尽
时,也是类似的效果,较底层网络栈中传入的Netty Buffer需要通过
网 络 缓 冲 区 提 供 给 Flink 。 如 果 相 应 Task 的 缓 冲 池 中 没 有 可 用 的
Buffer,Flink停止从该InputChannel读取,直到在缓冲区中有可用
Buffer时才会开始读取数据并交给算子进行处理。这将对使用该TCP连
接的所有Task造成背压,影响范围是整个TaskManager上运行的Task。
如图12-14所示,Task B.4接收缓冲区满了,在整个TCP连接上的
链路产生背压,导致子任务B.3也无法接收和处理新的Buffer,影响了
TaskManager 2上的所有Task。

图12-14 基于连接的流控

see more please visit: https://homeofpdf.com


为了防止这种情况发生,Flink 1.5版本引入了基于信用的流控机
制。
12.5.3 基于信用的流控
在Flink原有流控机制的基础上,拓展出了新的流控机制——基于
信用的流量控制,用来解决同一个TaskManager上Task之间相互影响的
问题。基于信用的流控机制可以确保下游总是有足够的内存接收上游
的数据,不会出现下游无法接收上游数据的情况。该流控机制作用于
Flink的数据传递层,在结果子分区(ResultSubPartition)和输入通
道(InputChannel)引入了信用机制,每个远端的InputChannel现在
都有自己的一组独占缓冲区,不再使用共享的本地缓冲池。
LocalBufferPool 中 的 Buffer ( 缓 存 ) 称 为 浮 动 缓 存 ( Float
Buffer ) , 因 为 LocalBufferPool 的 大 小 是 浮 动 的 , 并 可 用 于 所 有
InputChannel。
下游接收端将当前可用的Buffer数量作为信用值(1 Buffer = 1
信用)通知给上游。每个结果子分区(ResultSubPartition)将跟踪
其对应的InputChannel的信用值。如果信用可用,则缓存仅转发到较
底层的网络栈,每发送一个Buffer都会对InputChannel的信用值减1。
在 发 送 Buffer 的 同 时 , 还 会 发 送 前 结 果 子 分 区
(ResultSubPartition)队列中的积压数据量(Backlog Size)。下
游的接收端会根据积压数据量从浮动缓冲区申请适当数量的Buffer,
以便更快地处理上游积压等待发送的数据。下游接收端首先会尝试获
取与Backlog大小一样多的Buffer,但浮动缓冲区的可用Buffer数量可
能不够,只能获取一部分甚至获取不到Buffer。下游接收端会充分利
用获取到的Buffer,并且会持续等待新的可用Buffer。其机制如图12-
15所示。

see more please visit: https://homeofpdf.com


图12-15 基于信用的流控机制
例如,上游Task A.2发送完数据后,还有5个Buffer被积压,那么
会把发送数据和积压数据量= 5 一块发送给下游Task B.4,下游接收
到 数 据 后 , 知 道 上 游 积 压 了 5 个 Buffer , 于 是 向 Buffer Pool 申 请
Buffer。由于容量有限,下游InputChannel目前仅有2个Buffer空间,
所 以 , Task B.4 会 向 上 游 Task A.2 反 馈 通 道 信 用 值 ( Channel
Credit)=2。然后上游下一次最多只给下游发送2个Buffer的数据,这
样每次上游发送的数据都是下游InputChannel的Buffer可以承受的数
据量。这种反馈策略保证了不会在公用的Netty和TCP这一层堆积数据
而影响其他Task通信。
基于信用的流量控制使用配置参数buffer-per-channel参数来设
置独占的缓冲池大小,使用配置参数floating-buffers-per-gate设置
InputGate输入网关的缓冲池大小,输入网关的缓冲池由属于该输入网
关的InputChannel共享,其缓冲区上限与基于连接的流控机制相同。
一般情况下,使用这两个参数的默认值,理论上可以达到与不采用流
控机制的吞吐量一样高。流量控制的最大(理论)吞吐量至少与没有
流量控制时一样高,前提是网络的延迟比较低。在实际生产环境中,
可以根据实际的网络延迟和带宽来调整参数。
相比没有流量控制的接收器的背压机制,信用机制提供了更直接
的控制逻辑:如果接收端缓存不足,其可用信用值会降到0,发送方会

see more please visit: https://homeofpdf.com


停止发送。这样只在这个InputChannel上存在背压,而不会影响其他
Task,从而避免一个Task的处理能力不足导致其所在的TaskManager上
所有的Task都无法接收数据的问题。

12.6 总结
Flink 进 行 数 据 交 换 有 3 种 模 式 : 本 地 线 程 内 的 数 据 交 换 , 即
OperatorChain内算子之间的数据交换;本地线程间的数据交换;跨网
络的数据交换。不同交换模式的工作流程不同。
Flink是基于推送的数据传输模型,在网络层面的数据交换必须使
用流控机制确保集群的稳定并实现高效的数据处理,所以在现在的版
本中使用了基于信用的流控机制。

see more please visit: https://homeofpdf.com


第13章 应用容错
对于7×24小时不间断运行的流程序来说,容错比较复杂。对于离
线任务,如果失败了,只需要清空已有结果,重新执行一次作业即
可。对于流任务,如果要保证能够重新处理已处理过的数据,就要把
数据保存下来,而这就面临着如下几个问题。
1)假设流计算任务执行了3个月,从3个月之前的数据起始位置开
始重新执行,成本和时间上是否允许?
2)要满足第一个问题,就要求完全保留3个月乃至1年的数据,这
是否可行?
3)从3个月前起始位置开始重复,那么计算的数据应如何处理,
才能保证结果不会重复?
可以看出来,流计算任务必须使用针对性设计的容错方案。基本
要求如下。
● 做到exactly-once。
● 处理延迟越低越好。
● 吞吐量越高越好。
● 计算模型应当足够简单易用,又具有足够的表达力。
● 从错误恢复的开销越低越好。
● 足够的流控制能力(背压能力)。

13.1 容错保证语义
按照数据处理保证的可靠程度,从低到高包含4个不同的层次。
1. 最多一次
最低级别的数据处理保证,即At-Most-Once,数据不重复处理,
但可能会丢失,在Flink中,不开启检查点就是最多一次的处理保证。
2. 最少一次

see more please visit: https://homeofpdf.com


即At-Least-Once,数据可能重复处理,但保证不丢失,在Flink
中,开启检查点不进行Barrier对齐就是最少一次的处理保证。
3. 引擎内严格一次
即Exactly-Once,在计算引擎内部,数据不丢失、不重复,在
Flink中开启检查点,且对Barrier进行对齐,就能达到引擎内严格一
次的处理保证。如果数据源支持断点读取,则能支持从数据源到引擎
处理完毕,再写出到外部存储之前的过程中的严格一次。
4. 端到端严格一次
即End-to-End Exactly-Once,从数据读取、引擎处理到写入外部
存储的整个过程中,数据不重复、不丢失。
端到端严格一次语义需要数据源支持可重放,外部存储支持事务
机制,能够进行回滚。在Flink中,设计了两阶段提交协议,提供了框
架级别的支持,即TwoPhaseCommitSinkFunction,在前面的函数体系
中提到过,本章中的后续内容会详细介绍。

13.2 检查点与保存点
检查点在Flink中叫作Checkpoint,是Flink实现应用容错的核心
机制,根据配置周期性通知Stream中各个算子的状态来生成检查点快
照,从而将这些状态数据定期持久化存储下来,Flink程序一旦意外崩
溃,重新运行程序时可以有选择地从这些快照进行恢复,将应用恢复
到最后一次快照的状态,从此刻开始重新执行,避免数据的丢失、重
复。
默认情况下,如果设置了检查点选项,则Flink只保留最近成功生
成的一个检查点,而当Flink程序失败时,可以从最近的这个检查点来
进行恢复。但是,如果希望保留多个检查点,并能够根据实际需要选
择其中一个进行恢复,会更加灵活。
默认情况下,检查点不会被保留,取消程序时即会删除它们,但
是可以通过配置保留定期检查点,根据配置,当作业失败或者取消的
时候,不会自动清除这些保留的检查点。
如果想保留检查点,那么Flink也设计了相关实现,可选项如下。

see more please visit: https://homeofpdf.com


● ExternalizedCheckpointCleanup.
RETAIN_ON_CANCELLATION:取消作业时保留检查点。在这种情况下,
必须在取消后手动清理检查点状态。
● ExternalizedCheckpointCleanup. DELETE_ON_CANCELLATION :
取消作业时删除检查点。只有在作业失败时检查点状态才可用。
保存点在Flink中叫作Savepoint,是基于Flink检查点机制的应用
完整快照备份机制,用来保存状态,可以在另一个集群或者另一个时
间点,从保存的状态中将作业恢复回来,适用于应用升级、集群迁
移、Flink集群版本更新、A/B测试以及假定场景、暂停和重启、归档
等场景。保存点可以视为一个 (算子ID→State)的Map,对于每一个
有状态的算子,Key是算子ID,Value是算子的State。

13.3 作业恢复
Flink提供了应用自动容错机制,可以减少人为干预,降低运维复
杂度。同时为了提高灵活度,也提供了手动恢复。Flink提供了如下两
种手动作业恢复方式。
(1)外部检查点
检查点完成时,在用户给定的外部持久化存储保存。当作业
Failed(或者Cancled)时,外部存储的检查点会保留下来。用户在恢
复时需要提供用于恢复的作业状态的检查点路径。
(2)保存点
用户通过命令触发,由用户手动创建、清理。使用了标准化格式
存储,允许作业升级或者配置变更。用户在恢复时需要提供用于恢复
作业状态的保存点路径。
13.3.1 检查点恢复
1. 自动检查点恢复
自动恢复可以在配置文件中提供全局配置,也可以在代码中为Job
特别设定。自动恢复的内容在第10章已有讲解,其中作业失败调度就
是自动恢复的实现,见表13-1。
表13-1 作业调拨重启策略及说明

see more please visit: https://homeofpdf.com


2. 手动检查点恢复
因为Flink检查点目录分别对应的是jobId ,每通过flink run方
式/页面提交方式恢复都会重新生成jobId,那么如何通过检查点恢复
失败任务或者重新执行保留时间点的任务?
Flink提供了在启动之时通过设置-s参数指定检查点目录的功能,
让新的jobId读取该检查点元文件信息和状态信息,从而达到指定时间
节点启动作业的目的。
启动方式如下。

13.3.2 保存点恢复
从保存点恢复作业并不简单,尤其是在作业变更(如修改逻辑、
修复bug)的情况下,需要考虑如下几点。
(1)算子的顺序改变
如果对应的UID没变,则可以恢复,如果对应的UID变了则恢复失
败。
(2)作业中添加了新的算子
如果是无状态算子,没有影响,可以正常恢复,如果是有状态的
算子,跟无状态的算子一样处理。
(3)从作业中删除了一个有状态的算子

see more please visit: https://homeofpdf.com


默认需要恢复保存点中所记录的所有算子的状态,如果删除了一
个有状态的算子,从保存点恢复的时候被删除的OperatorID找不到,
所 以 会 报 错 , 可 以 通 过 在 命 令 中 添 加 -allowNonRestoredState
(short: -n)跳过无法恢复的算子。
(4)添加和删除无状态的算子
如果手动设置了UID,则可以恢复,保存点中不记录无状态的算
子,如果是自动分配的UID,那么有状态算子的UID可能会变(Flink使
用 一 个 单 调 递 增 的 计 数 器 生 成 UID , DAG 改 版 , 计 数 器 极 有 可 能 会
变),很有可能恢复失败。
(5)恢复的时候调整并行度
Flink1.2.0及以上版本,如果没有使用作废的API,则没问题;
1.2.0以下版本需要首先升级到1.2.0才可以。
13.3.3 恢复时的时间问题
例如,从kafka消费数据中每1分钟统计一次结果,进行作业或者
Flink集群版本升级的时候,停了3个小时,使用保存点进行恢复的时
候,可能kafka中已经累积了3个小时的数据,使用事件时间可以保证
最终的处理结果是正确的一致的。如果使用的是处理时间,在这3个小
时内积压的数据,可能会在10分钟之内处理完毕,这3个小时内的统计
结果是0,10分钟之内的统计结果暴涨了几十乃至几百倍。所以如果需
要进行恢复、升级,最好使用事件时间,而不是处理时间。

13.4 关键组件
在介绍检查点的执行过程之前,首先介绍一下跟检查点生成相关
的几个关键内容。
13.4.1 检查点协调器
在Flink中检查点协调器叫作CheckpointCoordinator,负责协调
Flink 算 子 的 State 的 分 布 式 快 照 。 当 触 发 快 照 的 时 候 ,
CheckpointCoordinator向Source算子中注入Barrier消息,然后等待

see more please visit: https://homeofpdf.com


所有的Task通知检查点确认完成,同时持有所有Task在确认完成消息
中上报的State句柄。
13.4.2 检查点消息
在执行检查点的过程中,TaskManager和JobManager之间通过消息
确认检查点执行成功还是取消,Flink中设计了检查点消息类体系,如
图13-1所示。

图13-1 检查点消息类体系
检查点消息中有3个重要信息:该检查点所属的作业标识
(JobID)、检查点编号、Task标识(ExecutionAttemptID)。
(1)AcknowledgeCheckpoint消息
该消息从TaskExecutor发往JobMaster,告知算子的快照备份完
成。
(2)DeclineCheckpoint消息
该消息从TaskExecutor发往JobMaster,告知算子无法执行快照备
份,如Task了Running状态但是内部还没有准备好执行快照备份。

13.5 轻量级异步分布式快照
Flink采用轻量级分布式快照实现应用容错,采用此种实现方式基
于如下基本假设条件。
1)作业异常和失败极少发生,因为一旦发生异常,作业回滚到上
一个状态的成本很高。
2)为了低延迟,快照需要很快就能完成。
3)Task与TaskManager之间的关系是静态的,即分配完成之后,
在作业运行过程中不会改变(至少是一个快照周期内),除非手动改

see more please visit: https://homeofpdf.com


变并行度。恢复的时候也会恢复到原先的拓扑结构。
13.5.1 基本概念
分 布 式 快 照 最 关 键 的 是 能 够 将 数 据 流 切 分 , Flink 中 使 用
Barrier(屏障)来切分数据流。Barrier会周期性地注入数据流中,
作为数据流的一部分,从上游到下游被算子处理。Barriers会严格保
证顺序,不会超过其前边的数据。Barrier将记录分割成记录集,两个
Barrier之间的数据流中的数据隶属于同一个检查点。每一个Barrier
都携带一个其所属快照的ID编号。Barrier随着数据向下流动,不会打
断数据流,因此非常轻量。在一个数据流中,可能会存在多个隶属于
不同快照的Barrier,并发异步地执行分布式快照,如图13-2所示。

图13-2 Barrier切分数据流
Barrier会在数据流源头被注入并行数据流中。Barrier n所在的
位置就是恢复时数据重新处理的起始位置。例如,在kafka中,这个位
置就是最后一个记录在分区内的偏移量(offset),作业恢复时,会
根据这个位置从这个偏移量之后向kafka请求数据。这个偏移量就是
State中保存的内容之一。
Barrier接着向下游传递。当一个非数据源算子从所有的输入流中
收到了快照n的Barrier时,该算子就会对自己的State保存快照,并向
自己的下游广播送快照n的Barrier。一旦Sink算子接收到Barrier,有
两种情况:
1)如果是引擎内严格一次处理保证,当Sink算子已经收到了所有
上游的Barrier n时,Sink算子对自己的State进行快照,然后通知检

see more please visit: https://homeofpdf.com


查点协调器(CheckpointCoordinator),当所有的算子都向检查点协
调器汇报成功之后,检查点协调器向所有的算子确认本次快照完成。
2)如果是端到端严格一次处理保证,当Sink算子已经收到了所有
上游的Barrier n时,Sink算子对自己的State进行快照,并预提交事
务,再通知检查点协调器(CheckpointCoordinator),检查点协调器
向所有的算子确认本次快照完成,Sink算子提交事务,本次事务完
成。
13.5.2 Barrier对齐
当一个算子有多个上游输入的时候,为了达到引擎内严格一次、
端到端严格一次两种保证语义,此时必须要Barrier对齐。
假设算子有个上游输入通道,上边的是输入通道1,下边的是输入
通道2,算子的检查点Barrier对齐过程示例如图13-3所示。

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
图13-3 检查点Barrier对齐
a)开始对齐 b)对齐 c)执行检查点 d)继续处理数据

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 检查点过程示意

see more please visit: https://homeofpdf.com


13.6.1 JobMaster触发检查点
在 JobMaster 开 始 调 度 作 业 的 时 候 , 会 为 作 业 提 供 一 个
CheckpointCoordinator,周期性地触发检查点的执行。
在CheckpointCoordinator触发检查点的时候,只需通知执行数据
读 取 的 Task ( SourceTask ) , 从 SourceTask 开 始 会 产 生
CheckPointBarrier事件,注入数据流中,数据流向下游流动时被算子
读取,在算子上触发检查点行为。
1. 前置检查
在触发真正的执行之前要进行一系列检查,确保具备执行检查点
的条件。检查逻辑如下。
1)前置检查,确保作业关闭过程中不允许执行。如果未启用,或
尚未达到触发检查点的最小间隔等,同样不允许执行。
2)检查是否所有需要执行检查点的Task都处于执行状态,能够执
行检查点和向JobMaster汇报,若不是则整个作业的检查点无法完成。
3 ) 执 行
checkpointID=CheckpointIdCounter.getAndIncrement ( ) , 生 成 一
个新的id,然后生成一个PendingCheckpoint。PendingCheckpoint是
一个启动了的检查点,但是还没有被确认。等到所有的Task都确认了
本 次 检 查 点 , 那 么 这 个 检 查 点 对 象 将 转 化 为 一 个
CompletedCheckpoint。
4)JobMaster不能无限期等待检查点的执行,所以需要进行超时
监视,如果超时尚未完成检查点,则取消本次检查点。
5)触发MasterHooks,用户可以定义一些额外的操作,用以增强
检查点的功能(如准备和清理外部资源)。
6)再次执行步骤1)和步骤2)中的检查,如果一切正常,则向各
个SourceStreamTask发送通知,触发检查点执行。
检查完毕,没有问题,CheckpointCoordinator开始触发本次检查
点,通知各个Source类型的Task开始执行快照,如代码清单13-1所
示。
代码清单13-1 CheckpointCoordinator触发检查点

see more please visit: https://homeofpdf.com


从上边代码中可以看出保存点和检查点的触发入口是一样的,区
别在于保存是同步的,检查点是异步的。
2. JobMaster向Task发送触发检查点消息

see more please visit: https://homeofpdf.com


ExecutionVertex 与 Task 是 一 一 对 应 的 , Execution 表 示 一 次
ExecutionVertex 的 执 行 , 对 应 于 Task 的 实 例 , 在 JobMaster 端 通 过
Execution的Slot可以找到对应的TaskManagerGateway,远程触发Task
的检查点,如代码清单13-2所示。
代码清单13-2 通知Task执行检查点

13.6.2 TaskExecutor执行检查点
JobMaster通过TaskManagerGateway触发TaskManager的检查点执
行,TaskManager则转交给Task执行。
1. Task层面的检查点执行准备
Task类中的部分,该类创建了一个CheckpointMetaData的对象,
确保Task处于Running状态,把工作转交给StreamTask,如代码清单
13-3所示。
代码清单13-3 Task处理检查点

see more please visit: https://homeofpdf.com


代码里的invokable就是StreamTask。Task类实际上是将检查点委
托给了更具体的类去执行,而StreamTask也将委托给更具体的类,直
到最终执行用户编写的业务代码(即函数)。
2. StreamTask执行检查点

see more please visit: https://homeofpdf.com


从StreamTask开始,执行检查点就开始区分StreamTask类型了,
其中SourceStreamTask是检查点的触发点,产生CheckpointBarrier并
向下游广播,下游的StreamTask根据CheckpointBarrier触发检查点,
如代码清单13-4和代码清单13-5所示。
代码清单13-4 SourceStreamTask触发Checkpoint

代码清单13-5 Barrier触发检查点

see more please visit: https://homeofpdf.com


上边两个是触发检查点入口的不同方式,实际上执行检查点的时
候殊途同归,其核心逻辑如下。
如 果 Task 是 Running 状 态 , 那 就 可 以 执 行 检 查 点 , 首 先 在
OperatorChain上执行准备CheckpointBarrier的工作,然后向下游所
有Task广播CheckpointBarrier,最后触发自己的检查点。这样做可以
尽 快 将 CheckpointBarrier 广 播 到 下 游 , 避 免 影 响 下 游
CheckpointBarrier对齐,降低整个检查点执行过程的耗时。
如 果 Task 是 非 Running , 那 就 要 向 下 游 发 送
CancelCheckpointMarker,通知下游取消本次检查点,方法是发送一
个CacelCheckpointMarker,与CheckpointBarrier相反的操作,如代
码清单13-6所示。
代码清单13-6 StreamTask执行检查点

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
3. 算子生成快照
在 StreamTask 中 经 过 一 系 列 简 单 调 用 之 后 , 异 步 触 发
OperatorChain中所有算子的检查点。算子开始从StateBackend中深度
复制State数据,并持久化到外部存储中。注册回调,执行完检查点后
向JobMaster发出CompletedCheckPoint消息,这也是端到端Exactly-
Once中两阶段提交的一部分,如代码清单13-7所示。
代码清单13-7 StreamTask中异步触发算子检查点

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
4. 算子保存快照与State持久化
算 子 需 要 保 存 原 始 State 和 托 管 State ( OperatorState 、
KeyedState ) 触 发 保 存 快 照 的 动 作 之 后 , 首 先 对 OperatorState 和
KeyeState分别进行处理,如果是异步的,则将状态写入外部存储,如
代码清单13-8所示。
代码清单13-8 算子持久化State

see more please visit: https://homeofpdf.com


不同类型的StateBackend使用不同类型的持久化策略,持久化策
略负责将State写入目标存储。StateBackend持久化在第7章已介绍
过,此处不再赘述。
5. Task报告检查点完成
当一个算子完成其State的持久化之后,就会向JobMaster发送检
查点完成消息,其具体逻辑在reportCompletedSnapshotStates中。这
个方法把任务又最终委托给了RpcCheckpointResponder这个类,如代
码清单13-9所示。
代码清单13-9 汇报检查点完成

在向JobMaster汇报的消息中,TaskStateSnapshot中保存了本次
检查点的State数据,如果是内存型的StateBackend,那么其中保存的
是真实的State数据,如果是文件型的StateBackend,其中保存的则是
状态的句柄(StateHandle)。在分布式文件系统中的保存路径也是通
过TaskStateSnapshot中保存的信息恢复回来的。
状态的句柄分为OperatorStateHandle和KeyedStateHandle,分别
对应于OperatorState和KyedState,同时也区分了原始状态和托管状

see more please visit: https://homeofpdf.com


态。
RpcCheckpointResponder底层依赖Flink的RPC框架的方式远程调
用JobMaster的相关方法来完成报告事件。
13.6.3 JobMaster确认检查点
JobMaster 通 过 调 度 器 ScheduerNG 任 务 把 信 息 交 给
CheckpointCoordinator.receiveAcknowledgeMessage,来响应算子检
查点完成事件。
CheckpointCoordinator 在 触 发 检 查 点 时 , 会 生 成 一 个
PendingCheckpoint,保存所有算子的ID。当PendingCheckpoint收到
一个算子的完成检查点的消息时,就把这个算子从未完成检查点的节
点集合移动到已完成的集合。当所有的算子都报告完成了检查点时,
CheckpointCoordinator 会 触 发 completePendingCheckpoint ( ) 方
法,该方法做了以下事情。
1)把pendinCgCheckpoint转换为CompletedCheckpoint。
2)把CompletedCheckpoint加入已完成的检查点集合,并从未完
成检查点集合删除该检查点,CompletedCheckpoint中保存了状态的句
柄、状态的存储路径、元信息的句柄等信息。
3)向各个算子发出RPC请求,通知该检查点已完成。
确认过程如代码清单13-10所示。
代码清单13-10 确认检查点完成

see more please visit: https://homeofpdf.com


至此,检查点的简要过程就结束了。检查点分为两阶段:第一阶
段由JobMaster触发检查点,各算子执行检查点并汇报;第二阶段是
JobMaster确认检查点完成,并通知各个算子,在这个过程中,出现任
何异常都会导致检查点失败,放弃本次检查点。

13.7 检查点恢复过程
在作业发生异常自动恢复、从保存点恢复作业时,都会涉及从快
照中恢复作业状态。本书第10章介绍了作业的容错,但是只介绍了作
业调度层面的恢复,并没有深入到恢复的细节。
JobMaster会将恢复状态包装到Task的任务描述信息中,在上节执
行检查过程中提到,Task使用TaskStateSnapshot向JobMaster汇报自
身的状态信息,恢复的时候也是使用TaskStateSnapshot对象。

see more please visit: https://homeofpdf.com


作 业 状 态 以 算 子 为 粒 度 进 行 恢 复 , 包 括 OperatorState 恢 复 、
KeyedState恢复、函数级别的状态恢复。恢复OperatorState是各算子
的通用行为,对于UDF算子(继承自AbstractUdfStreamOperator的算
子),则需要额外函数状态。另外对于特殊的算子,如
WindowOperator,还需要恢复窗口状态,窗口状态为KeyedSate类型。
下面以UDF算子为例说明其过程,如代码清单13-11所示。
代码清单13-11 UDF算子状态恢复

从上面的代码中可以看出UDF算子在初始化的时候,复用了非UDF
算子的状态恢复,然后又做了函数状态的恢复。
1. OperatorState恢复
在 初 始 化 算 子 状 态 的 时 候 , 从 OperatorStateStore 中 获 取
ListState 类 型 的 状 态 , 由 OperatorStateStore 负 责 从 对 应 的
StateBackend中读取状态重新赋予算子中的状态变量,如代码清单13-
12所示。
代码清单13-12 异步算子恢复状态示例

see more please visit: https://homeofpdf.com


AsyncWaitOperator 中 有 一 个 名 为 “async wait operator
state”的状态,在算子初始化状态的时候,对其进行了恢复。
各个算子有其特殊的状态,不同算子的恢复过程基本类似,具体
的逻辑可以看其实现方法。
2. 函数State恢复
函数的状态恢复主要是针对有状态函数,这些函数的共同特点是
继承了CheckpointedFunction或者ListCheckpointed接口。在Flink内
置的有状态函数主要是Source、Sink函数,为了支持端到端严格一次
的情况,Source函数需要能够保存数据读取的断点位置,作业故障恢
复后能够从断点位置开始读取,Kafka等连接器中的读取数据的函数就
实现了CheckpointedFunction。同样在Sink写出数据到外部存储的时
候 , 有 时 也 会 需 要 恢 复 状 态 , 如 BucketingSink 和
TwoPhaseCommitSinkFunction。函数状态恢复逻辑各不相同,所以由
各个连接器自己实现,如代码清单13-13所示。
代码清单13-13 恢复函数状态

see more please visit: https://homeofpdf.com


3. Keyed状态恢复
WindowOperator 是 KeyedState 的 典 型 应 用 , 其 窗 口 使 用 了
KeyedState在StateBackend中保存窗口数据、定时器等,在恢复的时
候,除了恢复OperatorState和函数State之外,还进行了窗口定时器
等State的恢复,如代码清单13-14所示。
代码清单13-14 WindowOperator恢复KeyedState

13.8 端到端严格一次
在分布式环境下,保证端到端严格一次是一件非常困难的事情,
尤其是并行输出的时候,如图13-5所示。

see more please visit: https://homeofpdf.com


图13-5 端到端计算示例
1)Sink1已经往Kafka写入了数据,Sink2写入失败。
2)Flink应用此时进行Failover,系统回滚到最近的一次成功的
检查点,但是Sink1已经把数据写入Kafka了。
3)Flink无法回滚Kafka的State.因此,Kafka将在之后再次接收到
一份同样的来自Sink1的数据。
4)虽然在引擎内部保证了严格一次的处理,但是从整体效果来看
是至少一次的处理,在某些情况下这是不可容忍的问题。
为了解决上述问题,Flink设计实现了一种两阶段提交协议,能够
保证从读取、计算到写出整个过程的端到端严格一次,无论是什么原
因导致的作业失败,都严格保证数据只影响结果一次,即不会重复计
算,或者保证重复计算不影响结果的正确性。
下节以一个具体示例来说明Flink的两阶段提交协议。
13.8.1 两阶段提交协议
Flink支持的端到端严格一次并不只限于Kafka,理论上可以使用
任何Source/Sink,只要它们满足如下条件。
(1)数据源支持断点读取
即 能 够 记 录 上 次 读 取 的 位 置 ( offset 或 者 其 他 可 以 标 记 的 信
息),失败之后能够从断点处继续读取。
(2)外部存储支持回滚机制或者满足幂等性
● 回滚机制:即当作业失败之后能够将部分写入的结果回滚到写
入之前的状态。

see more please visit: https://homeofpdf.com


● 幂等性:即当作业失败之后,写入了部分结果,但是当重新写
入全部结果的时候,不会带来负面作用,重复写入不会带来错误的结
果。
如图13-6所示。

图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除了起到触发检查点的作用,在两阶段提交协
议中,还负责将流中所有消息分割成属于本次检查点的消息以及属于
下次检查点的两个集合,每个集合表示一组需要提交的数据,即属于
同一个事务。

see more please visit: https://homeofpdf.com


CheckpointBarrier从Source算子流向中间算子,一直到Sink,整
个过程如图13-7所示。

图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与外部存储通过事务关联起来,在出现异常作业需要恢复的时

see more please visit: https://homeofpdf.com


候,能够通过事务回滚消除重复写入的脏数据,这就是两阶段提交所
要解决的场景。
在预提交阶段,Sink把要写入外部存储的数据以State的形式保存
到状态后端存储(StateBackend)中,同时以事务的方式将数据写入
外部存储,如图13-8所示。

图13-8 两阶段协议-预提交阶段
倘若在预提交阶段任何一个算子发生异常,导致检查点没有备份
到状态后端存储,所有其他算子的检查点也必须被终止,Flink回滚到
最近成功完成的检查点。
2. 提交阶段
预提交阶段完成之后,下一步就是通知所有的算子,确认检查点
已成功完成。然后进入第二阶段——提交阶段。该阶段中JobMaster会
为作业中每个算子发起检查点已完成的回调逻辑。
本例中的Source和Window窗口操作不需要与外部存储交互,因此
在该阶段,这两个算子无须执行任何逻辑,但是Sink需要与外部交
互,所以此时需要将预提交开启的外部事务提交,如图13-9所示。

see more please visit: https://homeofpdf.com


图13-9 两阶段协议——提交阶段
在预提交阶段,数据实际上已经写入外部存储,但是因为事务的
原因是不可读的,所以Sink在事务提交阶段的工作稍微简单一点,当
所有的Sink实例提交成功之后,一旦预提交完成,必须确保提交外部
事务也要成功,此时算子和外部系统协同来保证。倘若提交外部事务
失败(如网络故障等),Flink应用就会崩溃,然后根据用户重启策略
进行回滚,回滚到预提交时的状态,之后再次重试提交。这个过程至
关重要,如果提交外部事务失败,就可能出现数据丢失的情况。
总结来说,两阶段提交协议依赖于Flink的两阶段检查点机制,
JobMaster触发检查点,所有算子完成各自快照备份即预提交阶段,在
这个阶段Sink也要把待写出的数据备份到可靠的存储中,确保不会丢
失,向支持外部事务的存储预提交,当检查点的第一阶段完成之后,
JobMaster确认检查点完成,此时Sink提交才真正写入外部存储。
13.8.2 两阶段提交实现
两阶段协议如果全交给应用开发者实现,会比较烦琐,所以Flink
抽取了公共逻辑并封装进TwoPhaseCommitSinkFunction抽象类,其类
体系如图13-10所示。

see more please visit: https://homeofpdf.com


图13-10 TwoPhaseCommitSinkFunction类体系
从TwoPhaseCommitSinkFunction的类体系来看,其比较特殊的地
方是继承了CheckpointedFunction接口,在预提交阶段,能够通过检
查点将待写出的数据可靠地存储起来;继承了CheckpointListener接
口,在提交阶段,能够接收JobMaster的确认通知,触发提交外部事
务。
下面是TwoPhaseCommitSinkFunction类来实现一个简单的基于文
件的Sink案例。若要实现支持端到端严格一次的文件Sink,最重要的
是以下4种方法。
1)beginTransaction。开启一个事务,在临时目录下创建一个临
时文件,之后写入数据到该文件中。此过程为不同的事务创建隔离,
避免数据混淆。
2)preCommit。在预提交阶段,将缓存数据块写出到创建的临时
文件,然后关闭该文件,确保不再写入新数据到该文件,同时开启一
个新事务,执行属于下一个检查点的写入操作。此过程用于准备需要
提交的数据,并且将不同事务的数据隔离开来。
3)commit。在提交阶段,以原子操作的方式将上一阶段的文件写
入真正的文件目录下。如果提交失败,Flink应用会重启,并调用
TwoPhaseCommitSinkFunction#recoverAndCommit方法尝试恢复并重新
提交事务。
此处注意,两阶段提交可能会导致数据输出的延迟,即用户若想
要看到真正的文件,需要等待JobMaster确认检查点完成才会写入真正
的文件目录,实时性有所降低。

see more please visit: https://homeofpdf.com


4)abort。一旦终止事务,删除临时文件。
Flink应用预提交完成之后,若在提交完成之前崩溃了,会恢复到
预提交的状态,此时很有可能Sink已经部分提交但没有完全完成,需
要进行事务的回滚,所以在State中需要保存足够多的信息,使作业重
启之后能够重新提交事务或者回滚事务。本例中这部分关键信息就是
临时文件所在的路径以及目标目录。
TwoPhaseCommitSinkFunction考虑了这种场景,因此应用从检查
点恢复之后,TwoPhaseCommitSinkFunction总是会发起一个抢占式的
提交。这种提交必须是幂等性的,虽然大部分情况下这都不是问题。
本例中对应的这种场景就是,临时文件不在临时目录下,而是已经被
移动到目标目录下。
Pravega和Kafka 0.11 Producer(或更高版本)支持事务,Flink
针 对 Kafka 0.11 版 本 Connector 基 于 TwoPhaseCommitSinkFunction 实
现,经过验证,端到端严格一次带来的性能开销比较小,基本不会影
响效率。

13.9 总结
Flink有不同可靠性级别的容错语义保证,底层依赖于轻量级异步
快照。轻量级异步快照机制通过在数据流中注入检查点Barrier,随着
数据的流动,在各个算子上执行检查点,保存状态到外部的可靠存储
中,当作业发生异常的时候,从最后一次成功的检查点中恢复状态。
同时在检查点基础上设计了保存点,使得在作业迁移、集群升级
等过程中也能保证作业执行结果的精确性。
另外,基于检查点机制,在框架级别支持两阶段提交协议,实现
了端到端严格一次的语义保证,这是Flink相比其他计算引擎的独特之
处。

see more please visit: https://homeofpdf.com


第14章 Flink SQL
Table API & SQL是Flink中的两种关系型API,是Flink API的一
等公民。在标准SQL中,SQL语句分为如下4类语句。
● DML(Data Manipulation Language):数据操作语言,用来
定义数据库记录(数据)。
● DCL(Data Control Language):数据控制语言,用来定义访
问权限和安全级别。
● DQL(Data Query Language):数据查询语言,用来查询记录
(数据)。
● DDL(Data Definition Language):数据定义语言,用来定
义数据库对象(库、表、列等)。
Flink Table API实现的是DQL数据查询,Flink SQL语句包含DML
数据操作语言、DDL数据定义语言、DQL数据查询语言。两者都没有实
现DCL。从DQL的角度讲,Table API的查询语义是Flink SQL查询语义
的超集。
为什么有了DataStream/DataSet API之后还要提供Table API和
SQL呢?主要原因如下。
首先,使用DataStream或者DataSet API进行开发的时候非常烦
琐,开发应用需要使用Function接口,即便是一个简单的过滤都需要
实现一个FilterFunction匿名类,重写filter函数,而使用Table API
则要简单很多。
其次,Table API和SQL是流批通用的,代码可以完全复用。回想
一下前边提到的,编写流计算应用使用DataStream API,批处理应用
使用DataSet API,在API层上就是不同的。但是使用Table API和SQL
开发Flink应用程序,无论是流还是批,Table API调用和SQL语法都是
一样的,在执行的时候根据ExecutionEnvironment区分是流计算还是
批处理。
最后,DataStream API和DataSet API开发应用的时候,Flink只
能进行非常有限的优化,需要开发者非常谨慎地编写高效的应用程

see more please visit: https://homeofpdf.com


序。而使用Table API和SQL则可以使用Calcite的SQL优化器,相对来
说更容易写出执行效率高的应用。
Flink 1.9版本引入了阿里巴巴的Blink,对Flink Table & SQL模
块做了重大的重构,在保留了Flink Planner的同时,引入了Blink
Planner。
Flink Planner没有考虑流计算作业和批处理作业的统一,针对流
计算作业和批处理作业的实现不同,在底层会分别转换为DataStream
API和DataSet API的调用,其根本原因是在Flink中批和流的算子是完
全独立的两套体系,代码和优化逻辑没有复用,维护成本很高。
Blink Planner基于流批一体的理念,重新设计了算子体系,以流
为核心,流计算作业和批处理作业最终都会转换为Transformation。
Blink Planner 针 对 批 处 理 和 流 计 算 , 分 别 实 现 了 BatchPlanner 和
StreamPlanner,两者共用了大部分代码,共享了很多优化逻辑。
在SQL优化上,Blink引入了更多的优化规则,代码生成的范围更
大。
与Spark自己实现SQL解析器、优化器不同,Flink使用了Apache
Calcite作为SQL解析器和优化器。

14.1 Apache Calcite


14.1.1 Calcite是什么
Apache Calcite是一个动态数据管理框架,它具备很多典型数据
库管理系统的功能,如SQL解析、SQL校验、SQL查询优化、SQL生成以
及数据连接查询等,但是又省略了一些关键的功能,如Calcite并不存
储相关的元数据和基本数据,不完全包含相关处理数据的算法等。
Calcite的设计目标是成为动态的数据管理框架,所以在具有很多
特性的同时,它也舍弃了一些功能,如数据存储、处理数据的算法和
元数据仓库。由于舍弃了这些功能,Calcite可以在应用和数据存储、
数据处理引擎之间很好地扮演中介的角色。用Calcite创建数据库非常
灵活,用户只需要动态地添加数据即可。

see more please visit: https://homeofpdf.com


Calcite使用了基于关系代数的查询引擎,聚焦在关系代数的语法
分析和查询逻辑的规划制定上。它不受上层编程语言的限制,前端可
以使用SQL、Pig、Cascading或者Scalding,只要通过Calcite提供的
SQL API(解析、验证等)将它们转化成关系代数的抽象语法树即可;
并根据一定的规则或成本对AST的算法与关系进行优化,最后推给各个
数据处理引擎来执行。
Calcite也不涉及物理规划层,它通过扩展适配器来连接多种后端
的 数 据 源 和 处 理 引 擎 , 如 Spark 、 Splunk 、 HBase 、 Cassandra 或 者
MangoDB。简单地说,这种架构就是“一种查询引擎,连接多种前端和
后 端 ” 。 目 前 , 使 用 Calcite 作 为 SQL 解 析 与 优 化 引 擎 的 有 Hive 、
Drill、Flink、Phoenix和Storm。
14.1.2 Calcite的技术特点
首先,从一个计算框架的研发者视角来看。SQL语法解析背后需要
对关系代数的深刻理解,本身存在一定技术门槛,而且需要保证SQL解
析的结果与ANSI-SQL等主流SQL流派的语义一致,还是需要下不少工夫
的。而更重要的是,在大数据量的分布式计算场景,一条SQL可以
parse为多棵语义对等的语法树,但彼此间的执行效率可能相差甚远,
且在不同的数据结构、量级和计算逻辑上,优劣选择也不同。
于是,如何优化就成为一个很重要且需要长期积累才能解决的问
题。这两个方面,在分布式批量计算、流式计算、交互式查询等领
域,都或多或少存在共性,尤其是当把优化算法抽象为可插拔的Rules
之后,就更加可能孵化出一个通用的框架来。
其次,从数据分析的视角看,要整合多个计算框架,很可能需要
跨平台的查询分发和优化,如异构数据库的关联,如何方便地集成不同
的数据源就是一个复杂的工作,如果能够有一个框架提供与平台无关
的数据处理方式,数据分析人员不需要关心集成的细节,只需要编写
通用的SQL即可,无疑会极大地降低数据分析的门槛。
14.1.3 Calcite的主要功能
Calcite的主要功能如下。
(1)SQL解析

see more please visit: https://homeofpdf.com


Calcite的SQL解析是通过JavaCC实现的,使用JavaCC编写SQL语法
描述文件,将SQL解析成未经校验的AST语法树。
(2)SQL校验
校验分两部分:
1)无状态的校验:即验证SQL语句是否符合规范。
2)有状态的校验:即通过与元数据结合验证SQL中的Schema、
Field、Function是否存在,输入输出类型是否匹配等。
(3)SQL查询优化
对上个步骤的输出(RelNode,逻辑计划树)进行优化,得到优化
后的物理执行计划。优化有两种:基于规则的优化和基于代价的优
化,后面会详细介绍。
(4)SQL生成
将物理执行计划生成为在特定平台/引擎的可执行程序,如生成符
合MySQL或Oracle等不同平台规则的SQL查询语句等。
(5)数据连接与执行
通过各个执行平台执行查询,得到输出结果。
在Flink或者其他使用Calcite的大数据引擎中,一般到SQL查询优
化即结束,由各个平台结合Calcite的SQL代码生成和平台实现的代码
生成,将优化后的物理执行计划组合成可执行的代码,然后在内存中
编译执行。
14.1.4 Calcite的核心原理
关系代数是关系型数据库操作的理论基础,关系代数支持并、
差、笛卡儿积、投影和选择等基本运算。关系代数是Calcite的核心,
任何一个查询都可以表示成由关系运算符组成的树。可以将SQL转换成
关系代数,或者通过Calcite提供的API直接创建它,如代码清单14-1
所示。
代码清单14-1 SQL查询示例

see more please visit: https://homeofpdf.com


可以表达成如下的关系表达式语法树。

当上层编程语言(如SQL)转换为关系表达式后,就会被送到
Calcite的逻辑规划器进行规则匹配。在这个过程中,Calcite查询优
化引擎会循环使用规划规则对SQL逻辑计划树进行迭代优化。Calcite
中提供了RBO(基于规则)和CBO(基于代价)两种优化器,在保证语
义等价的基础上,生成执行成本最低的SQL逻辑树。
使用逻辑规划规则等同于对关系表达式进行等价变换,比如将一
个过滤器推到Join运算之前执行。如图14-1所示,将Filter操作下推
到Join之前执行,这样做的好处是减少了Join操作记录的数量,同时
降低了CPU、内存、网络等各方面的开销,极端情况下,Join效率可能
会提升上百倍。

图14-1 Filter下推到Join之前
Calcite提供了灵活的机制,任何使用Calcite的框架都可以自定
义关系运算符、优化规则、代价模型、相关优化需要的统计信息,如
数据统计直方图等,从而能够灵活地应用在不同的场景中,这符合
Calcite作为SQL优化框架的目标。

see more please visit: https://homeofpdf.com


14.2 动态表
传统SQL和关系代数为表而设计,表一般表示有界数据集。传统
SQL的关系运算和流计算存在一些差异,所以将传统SQL直接生硬地应
用在流计算中是存在问题的。
1. 传统SQL与流计算的差异
SQL与流计算的对比见表14-1。
表14-1 SQL与流计算的对比

传统SQL比较成熟,且应用广泛,如果能在流上使用SQL进行开
发,那么会带来极大的便利性。将SQL应用于流计算,需要将流和表两
个差异比较大的概念进行融合。在本书开篇的时候讲到有界数据集是
无界数据集的特例,如果把有界数据集当作表,那么无界数据集
(流)就是一个随着时间变化持续写入数据的表,Flink中使用动态表
(Dynamic Table)来表示流,用静态表表示传统的批处理中的数据
集。流是DataStream API中的概念,动态表是Flink SQL中的概念,本
质上来说,两者都表示无界数据集。
2. 动态表与连续查询
引入了动态表的概念之后,将SQL作用于流,实际上变成了将SQL
作用于动态表。Flink的Table API和SQL围绕着动态表构建。动态表在
Flink中抽象为Table接口。
与表示批处理数据的静态表相比,动态表随时间而变化。将SQL查
询作用于动态表,查询会持续执行而不会终止,因此叫作连续查询。
因为数据会持续产生、没有尽头,所以连续查询不会给出一个最终的
不变的结果。以Count运算为例,对于静态表而言,Count是一个确定

see more please visit: https://homeofpdf.com


的值,而连续查询则会随着数据持续进入动态表,持续不断地更新
Count结果。从这个角度来说,流上的SQL实际上给出的总是中间结
果。
此处需要注意,流上的SQL查询运算与批处理的SQL查询运算在语
义上是等同的,对于相同的数据集计算结果也是一样的。
流、动态表和连续查询的关系如图14-2所示。

图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 点击事件流例子

see more please visit: https://homeofpdf.com


14.2.1 流映射为表
在数据流上进行关系查询,需要将流映射为动态表。数据流的每
个记录可以理解为动态表的持续INSERT。
如图14-3所示,点击事件流(左侧)被转化为表(右侧),表会
随着点击事件记录的插入而不断增长。

注意:此处的动态表是逻辑概念,开发时用在Table API和SQL
中,在实际执行的时候,Flink内部动态表的运算仍然会变成DAG,与
数据库中的表不同,数据库中的表虽然也是逻辑概念,但是表最终会
保存到磁盘上。

图14-3 流表映射

14.2.2 连续查询

see more please visit: https://homeofpdf.com


连续查询(Continuous Query),即流上的SQL查询,作用于动态
表且又会产生新的动态表(结果表);批处理的查询执行完毕会中
止,流上的连续查询与批处理不同,连续查询不会终止且会根据其输
入表(动态表)上的数据变化,持续计算并将变化反映到其结果表中
(上游数据变化,下游需要不断更新动态表)。
下面介绍两个在点击事件流上定义的Clicks表上的查询示例,帮
助大家理解。
1. GROUP-BY COUNT聚合查询示例
该示例中,将Clicks表按user字段分组,并统计访问过的URL的数
量。随着数据流不断进入Clicks表,结果表的变化过程如图14-4所
示。

图14-4 Group-By Count聚合查询计算


1)当查询开始时,数据流尚未进入系统,Clicks表为空。
2)当第一行插入到Clicks表中时,查询开始计算结果表(动态
表),如[Mary,./home]插入后,结果表包含一行结果[Mary,1]。
3)当插入第二行[Bob,./cart]时,查询会更新结果表并插入新
记录[Bob,1]。

see more please visit: https://homeofpdf.com


4)第三行[Mary,./prod=id=1]插入时,查询会更新结果表中的
[Mary,1]记录,将其更新为[Mary,2]。
5)最后一行[Liz,1]插入Clicks表后,也会更新到结果表(插
入新记录)。
2. GROUP-BY WINDOW COUNT聚合查询示例
与第一个查询类似,除了用户属性之外,还在小时滚动窗口上对
Clicks表进行了分组,然后对URL进行计数(基于时间的计算,如窗口
基于特殊的时间属性),如图14-5所示。

图14-5 Group-By Window Count聚合查询计算


每个小时查询会计算结果并更新结果表。在cTime为12:00:00—
12:59:59之间时,Clicks表存在4条记录,对应的查询计算出两条结
果;下个时间窗口(13:00:00—13:59:59),Clicks表中存在3条记
录,对应的查询计算出两条结果添加到结果表中;当记录插入至
Clicks表中后,结果表也会被动态更新。
3. 更新和追加查询
上述两个查询虽然有些类似(均计算统计聚合分组),但两者也
有显著不同:
1)第一个查询会向结果表插入新记录、更新旧的记录。

see more please visit: https://homeofpdf.com


2)第二个查询只会向结果表中插入记录。
一个流上SQL查询应用在源动态表上,生成新的结果动态表,该查
询对结果表的操作只有插入行为(INSERT),还是同时包含插入、更
新(UPDATE)行为,有如下不同之处。
1)生成包含更新行为动态表的查询必须维护更多的State,消耗
更多的CPU、内存资源。
2)将仅包含插入行为的结果动态表转化为流,与将包含插入和更
新行为的动态结果表转化为数据流的行为不同,在API层面上体现为不
同的API接口。
14.2.3 流上SQL查询限制
大部分SQL查询语义都可以应用在流计算中,但是有些查询的计算
成本太高,原因一般有两个:
1)需要维护的状态太大。
2)计算更新太昂贵。
1. 维护的状态大小
流上的作业不会停止,所以数据流上的连续查询经常会持续运行
几周或几个月。因此,连续查询处理的数据总量可以很大,如果需要
更新以前的结果(结果表),那么就需要维护该结果表的所有行,否
则就没法更新结果表中以前的记录。
上面的GROUP-BY COUNT聚合查询示例。是无窗口的统计(无窗口
统计理论上来说,所有的结果都是early-fired,所有的计算结果都是
中间结果,而不是最终结果,需要持续的更新),数据需要不断写入
结果表,然后从结果表交给应用系统,因为数据流是永远不中断的,
不能等到计算出最终的结果才交给下游,如果一直等待,那么就意味
着流上的统计永远给不出结果。
例如,第一个查询示例中需要保存每个user的url总数,使得当输
入表(左侧表)接收一行新数据时更新结果表(右侧表)。假设用户
存在注册用户和未注册用户两种,若只跟踪注册用户,那么维护cnt大
小的代价不会太大(注册用户量不太大)。但若非注册用户也分配唯

see more please visit: https://homeofpdf.com


一的用户名,则随着时间的增加,维护cnt大小的代价将增大,最终导
致查询失败,示例如下。

上面的示例中提到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操作,但是有些不
同。

see more please visit: https://homeofpdf.com


在Flink中,将动态表分为3种类型:
1)只有更新行为,只有一行或多行但被持续更新的表。
2)只有插入行为,没有UPDATE、DELETE更改的只插入表。
3)既有插入行为也有更新行为的表。
当将动态表转化为流或将其写入外部系统时,对动态表的更改
(修改)需要被转换为流上的行为,这3种类型的动态表,对应于不同
类型的数据流。
Flink 的 Table API & SQL 支 持 3 种 方 式 的 动 态 表 上 的 更 改 ( 修
改)。
(1)Append流
Append流只支持追加写入行为,即只支持INSERT行为的动态表,
不支持Update、Delete等改变已存在数据的行为。
(2)Retract流
Retract流包含两种类型消息:add消息和retract消息。
动态表的更改行为对应的消息类型如下。
1)INSERT更改转换为流上的add消息。
2)DELETE更改转换为流上的retract消息。
3)UPDATE更改转换为两条消息,即对旧记录的retract消息和新
记录的add消息。
从动态表转化为Retract流的示例如图14-6所示。

see more please visit: https://homeofpdf.com


图14-6 动态表转换为Retract流
(3)Upsert流
Upsert流包含两种类型消息:update消息和delete消息。
动态表转化为Upsert流必须有主键(可以是复合主键),具有主
键的动态表的更改行为对应的消息类型如下。
1)INSERT、UPDATE转换为UPSERT消息。
2)DELETE转换为delete消息。
Upsert流与Retract流的主要区别在于,UPDATE更改使用单一消息
(主键)进行编码,因此效率更高。动态表转化为Upsert流的示例如
图14-7所示。

see more please visit: https://homeofpdf.com


图14-7 动态表到Upsert流的转换
目前从动态表转换到数据流,在API层面上,只提供了Append流和
Retract流两种支持。

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个接口文件的完整路径如下。

see more please visit: https://homeofpdf.com


● org/apache/flink/table/api/TableEnvironment.java。

org/apache/flink/table/api/java/BatchTableEnvironment.java。

org/apache/flink/table/api/java/StreamTableEnvironment.java。

org/apache/flink/table/api/scala/BatchTableEnvironment.scala


org/apache/flink/table/api/scala/StreamTableEnvironment.scala

TableEnvironment接口体系如图14-8所示。

图14-8 TableEnvironment接口体系
TableEnvironment 是 顶 级 接 口 , 是 所 有 TableEnvironment 的 基
类,其有两个BatchTableEnvironment子接口(分别在Flink Table &
SQL中用作批处理)和两个StreamTableEnvironment子接口(分别在
Flink都提供了Java实现和Scala实现,各有两个接口)。

see more please visit: https://homeofpdf.com


StreamTableEnvironment与TableEnvironment接口相比,扩展了
与 DataStream 的 相 互 转 换 能 力 。 BatchTableEnvironment 与
TableEnvironment接口相比,增加了与DataSet的相互转换能力。
TableEnvironment是统一的接口:
1 ) 无 论 是 使 用 Java 开 发 还 是 Scala 开 发 , 统 一 使 用
TableEnvironment接口。
2)对于SQL流计算、批处理开发,在开发者的层面上也是统一
的。
3)目前Flink Table & SQL有Flink Planner和Blink Planner,
对于开发者而言,使用TableEnvironment也无须考虑执行细节,只需
要考虑Planner是否稳定以及Planner内置的Function等。
TableEnvironment 目 前 还 不 支 持 注 册 UDTF 和 UDAF , 用 户 有 注 册
UDTF和UDAF的需求时,可以选择使用TableEnvironment的子类。
如 果 需 要 DataStream/DataSet 与 SQL 混 合 编 程 , 则 需 要 使 用
StreamTableEnvironment和BatchTableEnvironment接口来转换。
目前这个状态下,Table Environment比较复杂,Flink社区正在
逐渐将批处理迁移到流计算上,实现从API接口到执行层面的统一,届
时DataSet API会退出历史的舞台,BatchTableEnvironment也将退出
历史的舞台。同时社区也在努力推动Java和Scala TableEnvironment
的统一。Flink TableEnvironment的未来架构会更加简洁,成为推荐
使 用 的 接 口 , 只 有 当 需 要 与 DataStream 做 转 换 时 , 才 需 要 用 到
StreamTableEnvironment。
14.3.2 TableEnvironment使用示例
根 据 用 户 使 用 的 Planner 和 作 业 的 类 型 , 可 以 把 各 个
TableEnvironment的应用场景分为4类,下面结合代码来说明在不同的
场景下如何使用TableEnvironment。
1.场景一
用户使用Flink Planner进行流计算的Table程序(使用Table API
或 SQL 进 行 开 发 的 程 序 ) 的 开 发 。 这 种 场 景 下 , 用 户 可 以 使 用

see more please visit: https://homeofpdf.com


StreamTableEnvironment 或 TableEnvironment , 两 者 的 区 别 是
StreamTableEnvironment额外提供了与DataStream API交互的接口。
如代码清单14-3所示。
代码清单14-3 使用Flink Planner流计算

2.场景二
用户使用Old Planner进行批处理的Table程序的开发。这种场景
下,用户只能使用BatchTableEnvironment,因为在使用Old Planner
时,批处理程序操作的数据是DataSet,只有BatchTableEnvironment
提供了面向DataSet的接口实现。
如代码清单14-4所示。
代码清单14-4 Flink Planner批处理

see more please visit: https://homeofpdf.com


3.场景三
用户使用Blink Planner进行流计算的Table程序的开发。这种场
景下,用户可以使用StreamTableEnvironment或TableEnvironment,
两者的区别是StreamTableEnvironment额外提供与DataStream API交
互的接口。用户在EnvironmentSettings中声明使用Blink Planner,
将执行模式设置为StreamingMode即可。
如代码清单14-5所示。
代码清单14-5 Blink流计算

see more please visit: https://homeofpdf.com


4.场景四
用户使用Blink Planner进行批处理的Table程序的开发。这种场
景下,用户只能使用TableEnvironment,因为在使用Blink Planner
时,批处理程序操作的数据已经是bounded DataStream,所以不能使
用BatchTableEnvironment。用户在EnvironmentSettings中声明使用
Blink Planner将执行模式设置为BatchMode即可。值得注意的是,
TableEnvironment 接 口 的 具 体 实 现 中 已 经 支 持 了 StreamingMode 和
BatchMode两种模式,而StreamTableEnvironment接口的具体实现中目
前 暂 不 支 持 BatchMode 的 配 置 , 所 以 这 种 场 景 不 能 使 用
StreamTableEnvironment。
如代码清单14-6所示。

see more please visit: https://homeofpdf.com


代码清单14-6 Blink批处理

14.4 Table API


Table的核心抽象是Table,Table是Flink Table API的核心操作
对象,提供了流批统一的数据操作行为定义。对于批处理Table是静态
表,对于流计算Table是动态表。
Table体系如图14-9所示。

图14-9 Table体系
如同DataStream,Table也有各种不同的变体,在Flink中的DQL查
询语义中有7种可以相互转换的Table,不同类型的Table之间的转换关
系如图14-10所示。

see more please visit: https://homeofpdf.com


图14-10 不同Table之间的相互转换关系
从图14-10中可以看到Table API是围绕着Table构建的,大部分
API都构建在Table上,其他类型的Table最终都会回归到Table。
1. Table
Table是Table API的核心接口,提供了常见的SQL数据操作接口,
如代码清单14-7所示。
代码清单14-7 Table API开发代码示例

see more please visit: https://homeofpdf.com


2. GroupedTable
在Table上使用列、表达式(不包含时间窗口),或者两者的组合
进行分组之后的Table。简单理解就是等同于在SQL语句中对Table进行
GroupBy运算。如代码清单14-8所示。
代码清单14-8 Table API GroupBy字段示例
3. GroupWindowedTable
使用时间窗口进行分组之后的Table,按照时间对数据进行切分,
时间窗口必须是GroupBy中的第一项,且每个GroupBy只支持一个窗
口。
4. WindowedGroupTable
GroupWindowdTable 和 WindowedGroupTable 一 般 组 合 使 用 , 在
GroupWindowedTable上再按照字段进行GroupBy运算后的Table,如代
码清单14-9所示。
代码清单14-9 Table API GroupBy时间窗口和字段示例

5. OverWindowTable
使用开窗函数分组之后的Table,如代码清单14-10所示。
代码清单14-10 Table API OverWindow示例

6. AggregatedTable

see more please visit: https://homeofpdf.com


对分组之后的Table(如GroupedTable和WindowedGroupTable)执
行AggregationFunction聚合函数的结果,如代码清单14-11所示。
代码清单14-11 Table API GroupBy后普通聚合示例

7. FlatAggregateTable
对分组之后的Table(如GroupedTable和WindowedGroupTable)执
行TableAggregationFunction(表聚合函数)的结果,如代码清单14-
12所示。
代码清单14-12 Table API GroupBy后表聚合示例

14.5 SQL API


前面提到Flink SQL支持DML数据操作语言、DQL数据查询语言、
DDL数据定义语言,对应的Flink SQL语句如下。
● DQL:查询语句。
● DML:INSERT语句,不包含UPDATE、DELETE语句,后面这两类
语句的运算实际上在Flink SQL中也有体现,通过Retract召回实现了
流上的UPDATE和DELETE。
● DDL:Create、Drop、Alter语句。
Table API围绕着Table构建了API体系,所有的操作都在Table
上,而SQL则不同,其接口在TableEnvironment上。
1. DQL & DML语句

see more please visit: https://homeofpdf.com


DQL语句就是Select查询语句,Flink的Table API和SQL都是关系
型查询API,这就意味着此两者是有紧密关系的。
TableEnvironment#sqlQuery方法是Select查询语句的接口,其返
回是Table,也就是说Select语句查询和Table API可以混合编程。
DML 语 句 在 Flink 中 用 得 比 较 多 的 是 Insert 语 句 ,
TableEnvironment#sqlUpdate方法是Insert语句的接口。
DQL和DML的使用如代码清单14-13所示。
代码清单14-13 Select和Insert语句代码示例

see more please visit: https://homeofpdf.com


2. DDL语句
Flink SQL中的DDL语句包括Create语句、Drop语句、Alter语句。
这一类语句主要是用来操作元数据,如创建表、数据库和Function
等。

14.6 元数据
与其他的数据仓库、数据库系统类似,Flink Table API和Flink
SQL也需要依赖于元数据。元数据描述了Flink处理的读取和写出的数
据的结构以及数据的访问方法等信息,是Flink中实现SQL处理数据的
重要组成部分,没有元数据SQL就无法校验、优化。
接下来将对元数据的结构、管理、集成、使用等方面一一进行介
绍。
14.6.1 元数据管理

see more please visit: https://homeofpdf.com


元数据管理的核心是元数据的组织和元数据类型的定义。Flink中
使用层次结构来保存元数据。
元数据包含的类型有:库、表、视图、UDF、表字段定义。
Catalog用来管理的核心抽象,Catalog接口中定义了一系列操作
元数据的方法。目前Flink中实现了内存型GenericInMemoryCatalog和
HiveCatalog两种Catalog,如图14-11所示。
(1)内存型GenericInMemoryCatalog
内存型Catalog是Flink原来就有的元数据存储机制,元数据在内
存中临时保存,无持久化存储,在SQL校验和优化过程中使用。
(2)HiveCatalog
因为内存型Catalog是临时的,无持久化,所以其中的元数据无法
在团队间共享。对接Hive的元数据,既可以与Hadoop生态直接打通,
又 能 利 用 Hive 存 储 元 数 据 , 一 次 创 建 多 次 使 用 。 对 于 不 同 版 本 的
Hive,Flink定义了HiveShim接口来支持不同版本的Hive MetaStore。

图14-11 Catalog元数据目录体系
Catalog并不是孤立的,其上对开发人员、下对实际元数据的存储
的整体关系如图14-12所示。

see more please visit: https://homeofpdf.com


图14-12 元数据核心对象间的关系
开 发 者 在 使 用 Catalog 的 时 候 , 其 入 口 是 TableEnvironment ,
TableEnvironment 中 保 存 了 Catalog 集 合 , Catalog 集 合 使 用
CatalogManager进行管理。
CatalogManager是封装、管理所有Catalog的容器,支持Table路
径解析。Table路径有3级,如“select ∗ from catalog. database.
table”。
对 于 HiveCatalog , 实 际 上 是 通 过 Hive 的 MetaStoreClient 访 问
Hive的元数据。

注意:Flink1.9版本之前用ExternalCatalog对接外部元数据,
现在已经移除。
14.6.2 元数据分类
Catalog定义了对4种元数据类型的接口,每种元数据的用途和操
作不同,所以Flink定义了4类接口分别对应于4种元数据类型,元数据
类型之间的层次关系如图14-13所示。
最顶层的Catalog是元数据的容器,从Catalog向下是实际的不同
类型元数据的定义。
1.数据库

see more please visit: https://homeofpdf.com


此处的数据库等同于数据库中的库实例,接口定义为
CatalogDatabase,定义数据库实例的元数据,一个数据库实例中包含
表、视图、函数等多种对象,其类体系如图14-14所示。

图14-13 元数据层次关系

图14-14 数据库实例元数据类体系
2.表和视图
CatalogTable对应于数据库中的表,CatalogView对应于数据库中
的视图,两者相似,所以继承了共同的CatalogBaseTable接口,其类
体系如图14-15所示。

see more please visit: https://homeofpdf.com


图14-15 表和视图元数据类体系
表是一种存储的实体,表的元数据包含了表的字段信息、表的分
区信息、表的属性、表的描述信息等。表的字段定义与传统数据库是
类似的。表的分区信息主要是描述分区表,Flink可以分区信息进行数
据的并行访问。表的属性与Hive类似,在大数据领域中,表可能存储
在Kafka、Hive、HBase等不同的存储引擎中,对于Flink而言,所有的
表都是外部数据源,只有表的字段信息、分区信息还不够,还需要表
的访问信息,如用户名密码、IP地址端口、协议、存储引擎的类型
等,这些都是使用表的属性来记录的。表的属性是KV类型的结构。
视图是一个虚拟的概念,本质上是一条SQL查询语句,底层对应于
一张或者多张表。视图的元数据包含了视图的SQL查询语句、视图的字
段信息、视图的属性。
3.函数CatalogFunction
Catalog中的函数元数据的接口。函数元数据包含了函数所在的类
信息和编程语言。目前在Flink中支持Java、Scala、Python三种语
言。
其类体系如图14-16所示。

see more please visit: https://homeofpdf.com


图14-16 函数元数据类体系
这里的函数是SQL UDF,详见SQL函数相关章节。

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.时间属性

see more please visit: https://homeofpdf.com


作为流计算系统,时间窗口和Watermark是必须支持的,在Table
API 和 SQL 中 也 不 例 外 , 所 以 需 要 在 TableSchema 中 定 义 时 间 属 性 和
Watermark。Table API和SQL中支持两种事件属性:处理时间和事件时
间。
事件时间有3种来源:
1)来自一个当前字段。
2)来自数据源。
3)来自自定义的事件提取。
事件时间的使用如代码清单14-15所示。
代码清单14-15 事件时间声明

see more please visit: https://homeofpdf.com


声明了时间属性之后,还要设置Watermark策略。Watermark的生
成策略在第4.4.2节介绍过,此处不再赘述。
3. Watermark
在Flink中Watermark用来应对乱序的数据,使用处理时间的时
候,其实不存在乱序,只有在使用事件时间的时候才会使用到
Watermark。
Flink SQL中Watermark有两种定义方式:
(1)代码方式
使 用 代 码 的 方 式 在 构 建 Table 的 元 数 据 的 时 候 , 可 以 设 定 其
Watermark策略,如代码清单14-16所示。
代码清单14-16 代码方式设置Watermark策略

(2)SQL语句
使用SQL语句同样也能在表上设定Watermark策略,所有使用这张
表的SQL语句共享此Watermark策略,如代码清单14-17所示。
代码清单14-17 SQL创建Watermark示例

14.7.1 Table Source


数据从外部存储的时候,需要面对不同的协议、API、数据的格
式,所以需要将读取行为进行抽象,Table Source就是数据读取的顶
层抽象,用来描述如下信息。

see more please visit: https://homeofpdf.com


1)外部数据源的元信息。
2)如何从外部存储读取数据并转换为Flink的内部数据结构等。
Table Source体系如图14-17所示。

图14-17 Table Source继承体系


Table Source定义了从外部存储读取数据的行为,从大的方面分
为3类:
1)StreamTableSource:流数据源抽象,区分了是无界数据还是
有界数据,在Blink中实现了批流统一,批和流的数据源均使用该接口
的实现类。
2)BatchTableSource:原有的批数据源抽象,已经标记为废弃,
不再赘述。
3)流表Join中的维表:对于流表Join这一类的运算,本质上来说
是 对 流 上 的 数 据 查 询 外 部 维 表 进 行 字 段 补 全 , Flink 提 供 了
LookupableTableSource,按照Join条件中的字段进行关联。
除了基本的数据读取行为之外,为了提高效率,降低IO吞吐,
Table Source体系中还提供了数据读取的优化行为接口,通过将计算
条件下推到数据源执行,能够极大地减少读取到内存的数据集规模,
显著提升计算效率。
● FilterableTableSource:过滤不符合条件的记录。
● LimitableTableSource:限制记录条数。

see more please visit: https://homeofpdf.com


● ProjectableTableSource:过滤不会被使用的字段。
对于分布式存储的分区类型的表(如Hive表、Kafka Topic等),
定义了PartitionableTableSource来决定如何在计算任务之间分配要
读取的表分区。
14.7.2 Table Slink
数据写出到外部存储的时候,需要面对不同的协议、API、数据的
格式,所以需要将写出行为进行抽象,Table Sink就是数据输出的顶
层抽象,用来描述如何将表的数据写出到外部存储系统。
Table Sink体系如图14-18所示。

图14-18 Table Sink体系


Table Sink 体 系 , 从 大 的 方 面 分 为 两 类 : 面 向 流 的
StreamTableSink和面向批的BatchTableSink。未来批流的底层统一使
用Stream,所以BatchTableSink已经标记为废弃,不再赘述。
在前文中介绍动态表的时候,提到过从动态表到流的转换对应于3
种不同类型的流,可以看到StreamTableSink有3个子接口,分别与3种
不同类型的流相对应:
1)AppendStreamTableSink:追加模式的TableSink,支持追加写
入,不支持更新。
2)RetractStreamTableSink:支持召回模式的TableSink,召回
模式其实就是流上的update的核心。
3)UpsertStreamTableSink:Upsert,有则更新,无则插入。目
前有ElasticSearch、HBase、JDBC 3种实现。

see more please visit: https://homeofpdf.com


14.8 SQL函数
SQL函数的目标是扩展SQL的运算能力,可以实现特定的业务逻
辑。Flink在其Table/SQL API中支持自定义函数,可以将自定义函数
注册到元数据中,在开发过程中直接使用函数,SQL解析器会对函数名
称进行解析。
SQL函数有几种分类方式:
1.内置函数和自定义函数
● Flink中内置了大量的函数,可以直接使用,具体使用可以看
官方文档。
● 自定义函数(SQL UDF)是用户根据业务逻辑实现的特定函
数,也可能具备一定的通用性。
2.标量函数、聚合函数、表函数、表聚合函数
按照函数计算行为的不同,即输入数据和输出数据的不同分为这4
类。
本书主要介绍这4类自定义函数,Flink的自定义函数体系如图14-
19所示。
(1)标量函数
在Flink中叫作Scalar Function,简称UDF。
标量函数就是对一行数据中的一个或者多个字段做处理,计算之
后给出一个单值,其实就是对每一列计算出一个新字段。

图14-19 SQL自定义函数体系
(2)聚合函数
在Flink中叫作Aggregation Function,简称UDAF。

see more please visit: https://homeofpdf.com


聚合函数很好理解,Count、Sum等统计函数都是此类,可将多行
数据的一列值经过运算之后输出单个值。
(3)表函数
在 Flink 中 有 两 个 表 函 数 , 分 别 叫 作 TableFunction 和
AsyncTableFunction(异步执行表函数),简称UDTF。
表函数可以接收一个或者多个字段作为参数,输出多行多列数
据。可以是0行、1行或者多行,每行中包含一个或者多个字段。
(4)表聚合函数
在Flink中叫作TableAggregationFunction,简称UDTAF。
表聚合函数其实就是把表函数和聚合函数的行为融合在一起,对
多行数据进行计算,输出多行多列数据。

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所示。

see more please visit: https://homeofpdf.com


图14-20 Flink的Planner类体系
PlannerBase是Blink Table模块中的规划器基类,其有两个子类
StreamPlanner和BatchPlanner,分别为流和批提供不同的优化逻辑。
核 心 的 优 化 、 转 换 流 程 定 义 在 PlannerBase#translate 中 。
StreamPlanner和BatchPlanner最重要的区别是使用了不同的优化器。
Flink StreamPlanner是Flink Table中的优化器,在当前实现中
只有流上的优化,没有批上的优化。
14.9.1 Expression
Expression是所有表达式的顶层接口,用来表示尚未经过解析的
表达式,解析、验证之后成为ResolvedExpression。Expression可以
表达树形的表达式结构。Expression的子类特别多,数学运算、条件
运算、逻辑运算、函数调用等都是用Expression表示的。
Expression可以表达以下类型。
1)常量值。
2)字段引用。
3)函数调用。
所有的表达式运算都用函数来表示(如数学运算),DIV函数表示
除法运算、EqualTo函数表示相等判断。表达式Expression可以有子表
达式。
Expression的类体系如图14-21所示。

see more please visit: https://homeofpdf.com


图14-21 Expression类体系
1. PlannerExpression
PlannerExpression有3个子类,表示3类运算:
1)BinaryExpression(二元运算)。
2)UnaryExpression(一元运算)。
3)LeafExpression(叶子节点)。
所有的常量值、字段引用、函数调用等都属于这3类运算。
2. ResolvedExpression
ResolvedExpression与Expression相比,不包含未解析的子表达
式,并且添加了数据的输出类型。
ResolvedExpression包含了图14-22所示的实现。

图14-22 ResolvedExpression包含的实现
(1)CallExpression
CallExpression表示一个解析、验证后的函数调用表达式。其基
本属性如下。
1)输出类型。

see more please visit: https://homeofpdf.com


2)函数定义(FunctionDefinition)表达被调用的函数。
3)可选的ObjectIdentifier,用来追溯函数。ObjectIdentifier
用来表示Catalog中的表、视图、函数、类型等,使用全路径表示。由
CatalogManager负责将ObjectIdentifier解析为具体的对象实体。
(2)ValueLiteralExpression
表示任何类型常量值的对象,属性包含了对象的类型和其数据的
值。
(3)TypeLiteralExpression
将DataType包装为一个常量值,主要用在类型转换运算中。简化
了Expression的设计,只有CallExpression接受子表达式。
(4)LocalReferenceExpression
用来引用QueryOperation中的本地实体,该实体不是来自上游的
输入,如引用窗口聚合中的Group Window。
(5)TableReferenceExpression
引用另一张表的表达式,该表达式只是用在了API层面上,被
Planner转换为不相关的子查询。
(6)FieldReferenceExpression
引用上游输入中的字段的表达式,包含如下属性。
1)输入字段的名称和类型。
2)字段所属的上游输入编号(如Join运算),左输入编号为0,
右输入编号为1。
3)字段在对应输入中的索引。
14.9.2 ExpressionResolver
ExpressionResolver在Table API中使用,将Table API中原始的
Expression表达式(未解析)解析成ResolvedExpression。解析的内
容 包 括 一 般 的 表 达 式 和 函 数 调 用
(BuiltInFunctionDefinitions#OVER)。
ExpressionResolver内置了一组解析规则,解析行为如下。
1) 展平∗号,并解析函数中的列名对下层输入列的引用。

see more please visit: https://homeofpdf.com


2)将Over聚合和Over Window合并到一个函数调用。
3)解析所有未经解析的引用,这些引用可能是对字段、表的引用
或本地引用(LocalReferenceExpression参见上节)。
4 ) 替 换 函 数 调 用 , 如 BuiltInFunctionDefinitions#FLATTEN ,
BuiltInFunctionDefinitions#WITH_COLUMNS。
5)执行所有函数调用的入参类型校验,如果有必要则进行类型转
换。
14.9.3 Operation
Operation是SQL操作的抽象,包含数据查询(DQL)、数据操作
(DML)、数据定义(DDL)、数据控制(DCL),Table API的调用会
直接生成Operation,SQL语句操作使用Planner.parse(sql)方法解
析SQL语句后生成Operation。
Operation类体系如图14-23所示。

图14-23 Operation类体系
● CreateOperation : 创 建 操 作 , 可 以 定 义 Database 、 表 、 视
图、函数等的元数据。
● AlterOperation:修改操作,可以修改Database、表、视图、
函数等的元数据。
● DropOperation:删除操作,可以删除Database、表、视图、
函数等的元数据。

see more please visit: https://homeofpdf.com


● UseOperation:切换Schema。
● ModifyOperator:SQL插入操作,在使用的时候,该操作会将
Table转换为数据流写入到外部存储中。
● QueryOperation:SQL查询,一般在批处理中使用。在流计算
中很少单独使用SQL查询,一般是配合SQL Insert语句使用。
14.9.4 QueryOperation
SQL查询Operation的抽象接口,表示关系查询树的节点,Table
API的调用最终会转换为QueryOperation,每个节点带有Schema,用来
校验QueryOperation的合法性。
QueryOperation的实现有以下几类。
1.常规的SQL运算操作
包 括 Join 、 Filter 、 Project 、 Sort 、 Distinct 、 Aggregate 、
WindowAggregate、Set集合等常规的SQL运算操作。
2. UDF运算
CalculatedQueryOperation是表示在表上应用TableFunction的数
据结构,一般包含TabeFunction、入参、返回类型等信息。
3. Catalog查询运算
CatalogQueryOperation,表示对元数据目录的查询运算。
4. StreamQueryOperation运算
包 含 DataStreamQueryOperation 、
JavaDataStreamQueryOperation 、 ScalaDataStreamQueryOperation 三
个实现,用来表达从DataStream读取数据的QueryOperation。
5. DataSetQueryOperation
用来表达从DataSet中读取数据的QueryOperation。
6. 读取数据源的运算
TableSourceQueryOperation 、
RichTableSourceQueryOperation ( 带 有 统 计 信 息 的
TableSourceQueryOperation,未来会删除)用来表示从数据源读取数
据 , 由

see more please visit: https://homeofpdf.com


org.apache.flink.table.api.TableEnvironment.fromTableSource (
Table Source)调用生成。
14.9.5 物理计划节点
Blink的物理计划在实现层面上分为流处理物理计划和批处理物理
计划,其接口体系如图14-24所示。
ExecNode 表 示 物 理 计 划 , StreamExecNode 表 示 流 处 理 的 物 理 计
划,所有流处理物理计划节点都继承此接口。BatchExecNode表示批处
理的物理计划,所有的物理计划节点都继承此接口。
Flink物理计划抽象如图14-25所示。

图14-24 Blink物理执行计划抽象

图14-25 Flink物理执行计划抽象
DataStreamRel表示流上的物理计划树的节点,DataSetRel表示批
上的物理计划树的节点。

14.10 Blink Planner和Flink Planner对比


在深入介绍Blink Planner和Flink Planner之前,首先先了解一
下两者到底有什么不同之处。
1)Blink将批处理作业视为流的特殊情况,因此,还不支持Table
和DataSet之间的转换,并且批处理作业不使用DataSet的执行框架和

see more please visit: https://homeofpdf.com


算子体系,而是基于流作业的底层执行框架和算子体系。
2 ) Blink Planner 不 支 持 BatchTableSource , 而 是 使 用
BoundedStreamTableSource代替。
3 ) Blink Planner 仅 支 持 新 的 Catalog , 不 支 持
ExternalCatalog。
4 ) 为 Flink Planner 和 Blink Planner 实 现 的
FilterableTableSource 不 兼 容 。 Flink Planner 会 将
PlannerExpression下推到FilterableTableSource,而Blink Planner
将下推Expressions。
5)基于字符串的键值配置选项(详细信息参阅有关配置的文档)
仅用于Blink Planner。
6)两个Planner的实现(CalciteConfig)PlannerConfig不同。
7 ) Blink Planner 会 将 多 个 接 收 器 优 化 为 一 个 DAG ( 仅 在
TableEnvironment 上 支 持 , 而 不 在 StreamTableEnvironment 上 支
持)。Flink Planner始终将每个接收器优化为一个新的DAG,其中所
有DAG彼此独立。
8 ) Flink Planner 现 在 不 支 持 catalog 统 计 信 息 , 而 Blink
Planner则支持。

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树进行优
化、转换,最终变成了流计算作业。

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
图14-26 Blink与Calcite关系图
下面分别从SQL和Table API两个视角来了解其过程。

14.12 Blink SQL执行过程


SQL的执行过程分为两个大的阶段:从SQL语句到Operation;从
Operation到Transformation,然后就进入到分布式执行阶段。
14.12.1 从SQL到Operation
TableEnvironment是SQL语句的入口,提供了DQL、DML、DDL 3个
接口。以SQL查询为例,首先会通过Planner提供的SQL解析器将SQL语
句从文本转换为Operatoion。
TableEnvironment#sqlQuery 是 查 询 语 句 ,
TableEnvironment#sqlUpdate对应INSERT、CREATE、DROP语句。对于
SQL查询语句,Blink ParserImpl#parse将SQL语句生成为Operation
树,生成新的Table对象。
以SQL查询语句为例,总体过程如下。
1) 解析SQL字符串转换为QueryOperation。
2) SQL字符串解析为SqlNode。
3)校验SqlNode。
4)调用Calcite SQLToRelConverter将SqlNode转换为RelNode逻
辑树。
5)RelNode转换为Operation。
下边以SQL查询语句为例说明其过程(DML语句、DDL在逻辑上是类
似的)。Blink中SQL查询语句的执行入口如代码清单14-18所示。
代码清单14-18 Blink的SQL查询入口

see more please visit: https://homeofpdf.com


Planner在解析SQL的过程中,首先会使用Calcite将SQL语句进行
解析,获取SQL Node,然后根据不同的SQL语句类型分别进行转换。转
换的细节在SqlToOperationConverter.java中,如代码清单14-19所
示。
代码清单14-19 SQL解析

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
在Planner中,校验语句的合法性,再根据语句类型(DQL、DML、
DDL ) 转 换 成 对 应 的 算 子 树 , 对 于 SQL 查 询 语 句 而 言 , 会 转 换 为
QueryOperation树,如代码清单14-20所示。
代码清单14-20 SQLNode到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 转换入口

see more please visit: https://homeofpdf.com


具体的转换在PlannerBase#translate中进行,整个转换过程如
下。
1)从Operation转换为Calcite RelNode,使用Calcite提供的优
化器。
2 ) 使 用 Flink 定 制 的 Calcite 优 化 器 优 化 , 对 于 流 而 言 使 用
StreamPlanner 进 行 优 化 , 实 际 上 最 终 使 用 的 是
StreamCommonSubGraphBasedOptimizer , 对 于 批 而 言 使 用
BatchPlanner , 实 际 上 具 体 的 工 作 交 给 了
BatchGommonSubGraphBasedOptimizer。
转换的逻辑实现如代码清单14-22所示。
代码清单14-22 PlannerBase优化核心过程

see more please visit: https://homeofpdf.com


整个转换过程从Operation开始,先转换为Calcite的逻辑计划
树,再对应地转换为Flink的逻辑计划树,然后进行优化。优化后的逻
辑树转换成Flink的物理计划,然后物理计划通过代码生成算子、
UDF、表达式等代码,包装到Transformation中,形成Transformation
流水线。
至 此 , SQL 语 句 被 转 换 为 Transformation 流 水 线 , 有 了
Transformation就可以进入到转换为StreamGraph的过程中,最终交给
Flink集群真正的执行起来。具体的执行参考前边相应的作业提交、作
业调度、作业执行章节内容。

14.13 Blink Table API执行过程

see more please visit: https://homeofpdf.com


Table API与SQL API在Operation处进行了统一,两者的差别主要
在生成Operation树之前。
14.13.1 Table API到Operation
下面以Table的Filter运算为例,说明Table API的调用如何生成
Operation树。
1. Filter调用入口
Table#filter有两种使用方式,可以直接使用,构造表达式文
本,如“val > 100”;也可以自己构造条件表达式。表达式文本底层
使用了条件表达式,如代码清单14-23所示。
代码清单14-23 TableImpl#filter接口

2.将Filter添加到QueryOperation树

see more please visit: https://homeofpdf.com


对Table API的调用最终都会体现在Operation树上,把Filter添
加到Operation树中的过程如代码清单14-24所示。
代码清单14-24 Filter运算添加到Operation树

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层面进行了语义的统一,但是在执行层面又

see more please visit: https://homeofpdf.com


使用了DataSet和DataStream各自的Runtime。
在1.9版本及之后的Flink Table和SQL模块的实现中,Table API
和SQL两者表达的都是关系型运算,在面向开发者API的层面不同,但
是底层对于Flink引擎而言,本质上是一样的,在Operation层面上进
行了统一。
如图14-27所示,Table API的调用被转换成Operation树,SQL语
句经过解析、验证最终也变成了一棵Operation树,Flink Planner实
际上是对Operation树进行优化、转换,最后再根据任务的不同,分别
转换为DataStream作业和DataSet作业,DataSet不做详细阐述。

see more please visit: https://homeofpdf.com


图14-27 Flink与Calcite关系图
下面分别从SQL和Table API两个视角分别去了解其过程。

see more please visit: https://homeofpdf.com


在Calcite中解析生成的RelNode在转换成Flink的物理计划前,都
要经过一次转换为Flink定义的Flink RelNode。
Flink RelNode与Calcite的RelNode几乎是一一对应的。Flink中
的RelNode类使用Scala编写,所有带有FlinkLogicalRel特质的类都是
Flink 中 的 RelNode , 命 名 为 FlinkLogicalXXX , 位 于 包
org.apache.flink.table.plan.nodes.logical中。

14.15 Flink SQL执行过程


在Flink StreamPlanner中根据表要转换到的流类型进行分别转
换,即从SQL语句到Operation,从Operation到DataStream/DataSet的
调用,然后进入分布式执行阶段。
14.15.1 SQL到Operation
Flink和Blink Planner中把SQL语句转换为Operation的过程是类
似的,差别在于实现类不同,Flink Planner使用的是flink-table-
planner模块中的StreamPlanner。完成转换动作之后,开始进入优化
和转换阶段。
14.15.2 Operation到DataStream/DataSet
Flink Planner模块中的实现与Blink Planner中的实现不同,核
心的区别在于使用了不同的Planner。
Flink Planner 支 持 将 Table 转 换 为 Append 流 ( 追 加 写 入 流 ) 、
Retract流(支持Update的流),但是不支持将Table转换为Upsert
流。
DQL语句本质上产生了一个新的Table,是一种中间状态,所以在
开 发 过 程 中 , DQL 语 句 需 要 转 换 为 DataStream , 其 触 发 点 为
StreamTableEnvironmentImpl#toDataStream。
DML 语 句 和 DDL 语 句 表 示 一 个 完 整 的 运 算 过 程 , 其 触 发 点 为
StreamTableEnvironmentImpl#sqlUpdate 或 者
StreamTableEnvironmentImpl#insertInto,在调用API的时候直接就
触发了解析转换过程,如代码清单14-25所示。

see more please visit: https://homeofpdf.com


代码清单14-25 分类触发转换

see more please visit: https://homeofpdf.com


以writeToAppendSink为例,从表转换为只有INSERT行为的操作。
与Blink不同,Flink的Planner优化之后生成的是DataStream,如代码
清单14-26所示。

see more please visit: https://homeofpdf.com


代码清单14-26 Append类型Sink的转换

see more please visit: https://homeofpdf.com


优化过程在优化章节介绍。

14.16 Flink Table API执行过程


Table API到Operation的过程,与Blink中Table API到Operation
过 程 基 本 一 致 , 区 别 在 于 前 者 使 用 的 是 flink-table-planner 中 的
StreamPlanner#parse实现解析,此处不再赘述。
Operation 到 DataStream/DataSet 的 过 程 , 与 上 边 flink-table-
planner中SQL执行过程章节中Operation到DataStream/DataSet的过程
一样,此处不再赘述。

14.17 SQL优化

see more please visit: https://homeofpdf.com


SQL查询优化是来自数据库的概念,查询优化器是关系型数据库管
理系统的核心之一,决定对特定的查询使用哪些索引、哪些关联算
法,从而使其高效运行。它是优化器中最重要的组件之一。以数据库
为例,一个典型的数据库系统包含3个重要的组件:查询分析引擎、执
行引擎和存储引擎。Flink作为分布式计算引擎,数据依赖于外部存
储,所以在Flink主要是查询优化器和执行引擎,前面已经对Flink的
内部机制做过大量介绍,此处不再赘述,下来介绍一下查询优化器。
1.查询优化器
查询优化器是SQL分析和执行的优化工具,负责生成、制订SQL的
执行计划,查询优化器的优劣很大程度上决定了一个系统的性能。
2.查询优化器分类
查 询 优 化 器 分 为 两 类 : 基 于 规 则 的 优 化 器 ( Rule-Based
Optimizer,RBO) 和基于代价的优化器(Cost-Based Optimizer,
CBO)。
(1)RBO
RBO根据事先设定好的优化规则对SQL计划树进行转换,降低计算
成本。只要SQL语句相同,应用完规则就会得到相同的SQL物理执行计
划,也就是说RBO并不考虑数据的规模、数据倾斜等问题,对数据不敏
感,导致优化后的执行计划往往并不是最优的。这就要求SQL的使用者
了解更多的RBO的规则,使用门槛更高。
(2)CBO
CBO优化器根据事先设定好的优化规则对SQL计划树反复应用规
则,SQL语句生成一组可能被使用的执行计划,然后CBO会根据统计信
息和代价模型(Cost Model)计算每个执行计划的代价,从中挑选代
价最小的执行计划。由上可知,CBO中有两个依赖:统计信息和代价模
型。统计信息的准确与否、代价模型的合理与否都会影响CBO选择最优
计划。
一般情况下,CBO是优于RBO的,原因是RBO是一种只认规则,只针
对数据不敏感的过时的优化器。在实际场景中,数据往往是有变化
的,通过RBO生成的执行计划很有可能不是最优的。
目前各大数据库和大数据计算引擎都倾向于使用CBO。

see more please visit: https://homeofpdf.com


3.查询优化器执行过程
无论是RBO还是CBO,都包含了一系列优化规则,这些优化规则可
以对关系表达式进行等价转换,常见的优化规则包含谓词下推、列裁
剪、常量折叠等。
在这些优化规则的基础上,就能对关系表达式做相应的等价转
换,从而生成执行计划。下面介绍RBO和CBO两种优化器的执行过程。
(1)RBO规则优化
RBO的执行过程比较简单,主要包含两个步骤。
1)规则转换:等价改变查询语句的形式,以便产生更好的执行计
划。它决定是否重写用户的查询(包括视图合并、谓词推进、非嵌套
子查询/子查询反嵌套、物化视图重写),以生成更好的查询计划。
2)计划生成:步骤1)的转换生成了一个逻辑执行计划,还需要
将 逻 辑 执 行 计 划 构 建 成 物 理 执 行 计 划 , 对 应 在 Flink 中 就 是 构 建
Transformation。
(2)CBO代价优化
CBO查询优化主要包含三个步骤。
1)规则转换:等价改变查询语句的形式,以便产生更好的执行计
划。它决定是否重写用户的查询(包括视图合并、谓词推进、非嵌套
子查询/子查询反嵌套、物化视图重写),以生成更好的查询计划。
2)代价评估:通过复杂的算法来统计信息,进而评估各个执行计
划 的 总 体 成 本 , 包 括 选 择 性 ( Selectivity ) 、 基 数
(Cardinality)、成本(Cost),同时考虑可能的访问路径(Access
Path)、关联方法和关联顺序,生成不同的执行计划,让查询优化器
从这些计划中选择出执行代价最小的一个计划。
3)计划生成:生成大量的执行计划,然后选择其总体代价或总体
成本最低的一个执行计划,转换为Flink的执行计划。
CBO 实 现 有 两 种 模 型 , 即 Volcano 模 型 和 Cascades 模 型 , 其 中
Calcite使用的是Volcano模型,而Orca使用的是Cascades模型。这两
种模型的思想基本相同,不同点在于Cascades模型一边遍历SQL逻辑
树,一边优化,从而进一步裁剪掉一些执行计划。

see more please visit: https://homeofpdf.com


14.18 Blink优化
Blink SQL引入了自成一体的优化体系,其优化规则集、优化器完
全独立于Flink SQL。引入Blink SQL之后,对Calcite的使用方式变
了。当前版本中,SQL优化不是单纯地使用Calcite的HepPlanner或者
是 VolcanoPlanner , 而 是 在 不 同 的 优 化 阶 段 使 用 不 同 的 Calcite
Planner。
14.18.1 优化器
Blink中并没有直接使用Calcite的优化器,而是通过规则组合和
Calcite优化器的组合,分别为流和批实现了自定义的优化器,优化过
程分阶段进行,不同阶段使用不同的优化器和规则集。
Blink的优化器体系如图14-28所示。

图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单独优化。

see more please visit: https://homeofpdf.com


优化算法的过程如下。
1 ) 首 先 将 RelNode DAG 分 解 为 多 棵 子 树 ( Flink 中 对 应 的 类 为
RelNodeBlock ) , 然 后 生 成 一 个 RelNodeBlock DAG 。 每 个
RelNodeBlock只有一个Sink,代表一棵子树。
2)递归优化RelNodeBlock,优化顺序是从叶子结点(Source)到
根 结 点 ( Sink ) 。 非 根 子 树 ( RelNodeBlock ) 包 装 为 一 个
IntermediateRelTable。
3)优化完成之后,将IntermediateRelTable重新展开生成优化后
的RelNode DAG。
目前,选择这种优化策略主要基于以下考虑。
1)一般来说,使用多Sink的用户倾向于使用View,View天然就是
一个公共子图。
2)经过优化之后,如Project下推、filter下推,在最终的DAG中
可能就没有公共子图了。
当前策略可以改进的地方:
1)如何找到公共子图的切分点,如一些Physical RelNode可能是
从多个Logical RelNode转换而来,所以一个合法的切分点一定不在这
几个Logical RelNode之间。
2)优化结果是局部最优(每个子图内最优),不是全局最优。
2. StreamCommonSubGraphBasedOptimizer
流上的基于子图的优化器,用来优化Blink的流SQL和Table。
3. BatchCommonSubGraphBasedOptimizer
流上的基于子图的优化器,用来优化Blink的流SQL和Table。
14.18.2 代价计算
Flink的优化器混合使用了Calcite的优化器,在使用基于代价优
化的Volcano模型的时候,影响优化最重要的一点就是每个计划节点的
代价计算。代价计算涉及了5种资源:数据量、CPU资源使用、内存资
源使用、IO资源使用和网络资源使用。
14.18.3 优化过程

see more please visit: https://homeofpdf.com


在Blink的Planner中定义了流、批的优化规则,以流计算为例,
调用过程如下所示。

从总体上来说,优化分为两个阶段,逻辑优化阶段和物理优化阶
段,优化过程如图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作为顶层的容器,将优化规
则集按照顺序组织起来的。

see more please visit: https://homeofpdf.com


图14-29 Blink优化过程

see more please visit: https://homeofpdf.com


图14-30 FlinkOptimizeProgram体系
2. FlinkGroupProgram
优化程序组、容器类型,将一组FlinkOptimizeProgram顺序组织
起 来 , 当 执 行 优 化 的 时 候 , 依 照 顺 序 依 次 执 行 。 与
FlinkChainedProgram的不同之处在于,该优化程序支持迭代执行多
次。
3. FlinkRuleSetProgram
基 于 规 则 集 的 优 化 程 序 , 其 有 两 个 实 现 类 :
FlinkHepRuleSetProgram 和 FlinkVolcanoProgram 。
FlinkHepRuleSetProgram基于Calcite的HepPlanner进行优化(RBO,
基于规则)。FlinkVolcanoProgram基于Calcite的VolcanoPlanner进
行优化(CBO,基于代价)。
4. FlinkDecorrelateProgram
进行去相关优化规则的包装,对Flink SQL计划进行去相关优化。
5. FlinkUpdateAsRetractionTraitInitProgram
在对Flink SQL计划执行Retract推断之前,进行一些初始化动
作。
6. FlinkHepProgram
FlinkHepRuleSetProgram 底 层 依 赖 于 FlinHepProgram 。
FinkHepProgram提供了更底层的接口和控制。
7. FlinkVolcanoProgram

see more please visit: https://homeofpdf.com


包装优化规则,在底层依赖于Calcite Volcano优化器对Flink的
SQL计划进行优化。
8. FlinkMiniBatchIntervalTraitInitProgram
对于启用Mini-Batch模式的Flink SQL逻辑计划,在推断之前进行
一些初始化。
流计算和批处理的优化差异较大,所以各自有一套优化规则集和
优化的顺序。流计算应用的优化在FlinkStreamProgram中定义,批处
理的优化在FlinkBatchProgram中定义。
14.18.4 优化规则
Blink在API和执行层面上已经统一了流计算和批处理,其优化规
则分为流模式下优化和批模式优化规则。规则在Blink Planner中使
用。
1. Blink流模式优化规则
在执行优化之前,FlinkStreamProgram首先将逻辑优化规则和物
理优化规则全部分门别类地加载进来,为规则集分配优化器。
规则集如下。
1)SUBQUERY_REWRITE:子查询重写规则集,基于规则的优化器。
2)TEMPORAL_JOIN_REWRITE:历史记录表重写规则集,基于规则
的优化器。
3)TIME_INDICATOR:时间属性规则集,基于规则的优化器。
4)DEFAULT_REWRITE:默认重写规则集,基于规则的优化器。
5)PREDICATE_PUSHDOWN:断言下推规则集,基于规则的优化器。
6)JOIN_REORDER:Join重排序规则集,基于规则的优化器。
7)LOGICAL:逻辑优化规则集,基于规则的优化器。
8)LOGICAL_REWRITE:逻辑规则集,基于规则的优化器。
9)PHYSICAL:物理优化规则集,基于规则的优化器。
10)PHYSICAL_REWRITE:物理重写规则,基于规则的优化器。
序号即优化规则的执行顺序,其中1)~8)是逻辑优化规则,9)
~10)是物理优化规则。

see more please visit: https://homeofpdf.com


2. Blink批模式优化规则
在执行优化之前,FlinkBatchProgram首先将逻辑优化规则和物理
优化规则全部分门别类地加载进来,为规则集分配优化器。
规则集如下。
1)SUBQUERY_REWRITE:子查询重写规则集。
2)TEMPORAL_JOIN_REWRITE:历史记录表Join重写规则集。
3)DECORRELATE:去相关规则集。
4)DEFAULT_REWRITE:默认重写规则集。
5)PREDICATE_PUSHDOWN:断言下推规则集。
6)JOIN_REORDER:Join重排序规则集。
7)JOIN_REWRITE:Join重写规则集。
8)WINDOW:窗口优化规则集。
9)LOGICAL:逻辑优化规则集,包含Limit、过滤、投影、空结果
集裁剪。
10)LOGICAL_REWRITE:逻辑重写规则集。
11 ) PHYSICAL : 物 理 优 化 规 则 , 将 Flink 逻 辑 节 点 树 ( 由
FlinkLogicalRel 构 成 ) 转 换 为 Flink 物 理 节 点 树 ( 由 ExecNode 构
成),经过此转换之后,下一步就是转换为Flink的Transformation
了。
12)PHYSICAL_REWRITE:物理重写规则,对于排序和聚合使用本
地Hash聚合。
序号即优化规则的执行顺序,1)~10)是逻辑优化规则,11)
~12)是物理优化规则。
14.18.5 公共子图
公共子图重用是Blink中的一个特性。在Blink中支持一个作业中
执行多条SQL语句,如果多条SQL语句的计划树子树(带有叶子结点)
节点的摘要(digest)相同,那么只保留1个(最左侧的子树),即使
是不同的树的子树也可以重用。这样可以降低数据读取、处理的成
本,如图14-31所示。

see more please visit: https://homeofpdf.com


图14-31 公共子图优化示例
图14-31中左侧Project1-Scan1和Project2-Scan2经过计算,其摘
要(digest)完全相同,也就是说读取同样的数据,执行完全相同的
处理逻辑,那么只保留1个就可以,否则会导致算力的浪费,最终优化
后的结果如右图所示。
实现公共子图优化,要能够切分子图,并判断哪些子图是等价
的。
1.切分子图
做公共子图优化的前提是将逻辑节点树切分,在Blink中引入了
RelNodeBlock , 每 一 个 RelNodeBlock 就 是 RelNode DAG 的 子 树 ( 子
图),一个RelNodeBlock只有一个Sink输出。
RelNode的切分算法如下。
1 ) 如 果 只 有 一 个 RelNode 树 , 那 么 整 个 RelNode 树 就 是 一 个
RelNodeBlock。
2)重用不同RelNode树中的公共子树,生成一个RelNode DAG。
3)从Root节点(Sink节点)开始遍历每一棵树,为每个RelNode
标记其Sink节点。
4)从Root节点开始遍历每一棵树,遇到包含多个Sink节点的
RelNode,该RelNode节点就是一个新的RelNodeBlock的输出节点(也
叫作DAG的切分点break-point)。
步骤4)中不能当作切分点的几个特例:

see more please visit: https://homeofpdf.com


1 ) 当
RelNodeBlockPlanBuilder.TABLE_OPTIMIZER_UNIONALL_AS_BREAKPOIN
T_DISABLED设置为True的时候,UnionAll不能当作切分点。
2)TableFunctionScan、Snapshot或者Window Agg(带有Window
属性的Aggregate)不能当作切分点,因为它们的Physical RelNode是
RelNode 的 组 合 , 组 合 中 的 每 一 个 RelNode 不 能 单 独 优 化 。 如
FlinkLogicalTableFunctionScan 和 FlinkLogicalCorrelate 会 被 合 并
成 一 个 BatchExecCorrelate 或 StreamExecCorrelate , 视 运 行 模 式 而
定。RelNodeBlock切分示例如代码清单14-27所示。
代码清单14-27 RelNodeBlock切分示例代码

上 面 的 Table API 示 例 代 码 对 应 的 RelNode 逻 辑 计 划 树 和 经 过


RelNodeBlock的转换之后的RelNodeBlock树,如图14-32所示。

see more please visit: https://homeofpdf.com


图14-32 RelNodeBlock切分结果
图14-32左侧是该代码示例TableAPI调用最终形成的RelNode逻辑
计划树,右侧是该RelNode计划树被切分成的RelNodeBlock树,该树一
共有3个RelNodeBlock节点,切分点为Join(a1=b2),在这个切分点
上开始出现了两个Sink。
虽然Project(a,b,c)也有两个父节点,但是优化之后会被合并
到Join(a1=b2)中,所以Project(a,b,c)不是切分点,
对RelNodeBlock树进行优化的时候,从叶子节点开始向上依次优
化每一个RelNodeBlock。
2.摘要(digest)的计算

see more please visit: https://homeofpdf.com


逻辑节点的摘要(digest)的作用类似于Object#hashCode,用于
进 行 RelNode 逻 辑 树 的 等 价 判 断 , 摘 要 ( digest ) 相 同 则 认 为 两 个
RelNode或者RelNode树逻辑等价。
摘要(digest)的计算实际上是使用RelTreeWriterImpl#explain
方法进行的,顾名思义,explain方法就是把RelNode树对象递归拼接
成字符串,该字符串就是所谓的摘要(digest),RelNode树等价的比
较变成了字符串相等的比较。计算过程中需要考虑以下要素。
1)需要进行字段级别的digest,两个结构完全一样的RelNode
树,如果字段类型不同,就视为不等价。
2)需要将Retract行为包含到digest中,如果两个RelNode树除了
Retract行为外其他完全一样,就视为不等价。
如果不把字段类型考虑进去,不等价的RelNode的摘要可能是相同
的,重用Subplan的结果是错误的。对于如代码清单14-28所示的SQL语
句,生成的SQL物理计划如代码清单14-29 SQL物理计划所示。
代码清单14-28 SQL语句示例

代码清单14-29 SQL物理计划

see more please visit: https://homeofpdf.com


从 物 理 计 划 中 可 以 看 到 , 该 物 理 计 划 的 两 个 Group By 生 成 的
HashAggregate子树完全是等价的,但是从SQL明显看出来是不等价
的,两个Group By中“CAST(a) AS a”,一个将a转换成了BIGINT类
型,一个将a转换成了DOUBLE类型。所以类型必须在考虑摘要的时候包
含进去。
3.公共子图重用优化过程
公共子图的优化过程包含在Blink SQL物理计划到执行计划的转换
过程中,最核心的就是找到是否存在等价子图,如果有多个等价子
图,只保留其中1个,为了避免因为不同子图引用了相同对象导致摘要
相等,首先要重写相同的RelNode逻辑计划节点对象,使得不同的子图
不会共享相同的RelNode对象,然后再判断子图是否等价,如代码清单
14-30所示。
代码清单14-30 公共子图重用优化过程

see more please visit: https://homeofpdf.com


4.公共子图的优化效果
前面介绍了公共子图优化的原理,接下来通过一个示例来看一下
公共子图优化的实际效果。如代码清单14-31所示,在同一个作业中执
行了3个Insert语句,Insert语句存在相同的Select子句。
代码清单14-31 3个Insert语句示例代码

see more please visit: https://homeofpdf.com


1.9之前版本Flink生成的StreamGraph如图14-33所示,从图中明
显可见,Insert语句只重用了DataSource算子。

see more please visit: https://homeofpdf.com


图14-33 1.9版本之前的StreamGraph
1.9版本之后增加了Subplan重用,能够带来更大范围的重用,生
成的StreamGraph如图14-34所示,从DataSource开始,优化掉4个计算
步骤,再考虑并行度的话,相比之前版本,优化的算子个数= 4×并行
度,显著减少了重复计算的CPU、内存成本,也降低了数据传输的成
本。

see more please visit: https://homeofpdf.com


图14-34 1.9版本及以后的StreamGraph

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版本及之后会根据

see more please visit: https://homeofpdf.com


不同的优化阶段,混合使用Calcite的Hep规则优化器和Volcano代价优
化器。流使用StreamOptimizer,批使用BatchOptimizer。
Flink Planner优化器体系如图14-35所示。
Flink Planner没有使用FlinkOptimizeProgram进行优化,而是直
接使用了Calcite的Program进行优化。

图14-35 Flink Planner优化器体系

14.19.2 优化过程
Flink Planner的优化过程如图14-36所示。

see more please visit: https://homeofpdf.com


图14-36 Flink Planner优化过程
Flink在旧版本中直接使用了Calcite的优化器,而在当前版本
中,与Blink类似,定制了优化过程,并不是全部托管给Calcite的优
化器,混合使用了Calcite的Hep规则优化器和Volcano代价优化器。逻
辑优化使用Calcite的Hep优化器(基于规则),物理优化阶段使用了
Calcite的Hep规则优化器和Volcano优化器(基于代价)。
在Flink SQL中FlinkOptimzeProgram将具体的优化交给Calcite来
执行。FlinkOptimzeProgram将不同的优化规则组合到优化器中,按照
顺序应用优化规则。

see more please visit: https://homeofpdf.com


StreamOptimizer中的优化过程如代码清单14-32所示,批的优化
过程与之类似,规则集优化过程略有不同。
代码清单14-32 Flink Planner流优化过程

Flink Planner的优化过程与Blink Planner优化过程差异最大的


地方,是Flink Planner最终将SQL语义转换为对DataStream API的调

see more please visit: https://homeofpdf.com


用。
14.19.3 优化规则
Flink 流 模 式 优 化 规 则 集 定 义 在 Flink Table 模 块 中 , 在 Flink
Planner中使用,流优化规则集内容如下。
1)FlinkRuleSets.TABLE_SUBQUERY_RULES:子查询优化规则集,
使用规则优化模型。
2 ) FlinkRuleSets.EXPAND_PLAN_RULES ,
FlinkRuleSets.POST_EXPAND_CLEAN_UP_RULES:展开规则集。
3)去相关优化规则:此处Flink使用Calcite内置规则进行去相关
优化。
4)时间虚拟列转换:对于时间虚拟列,如果在计算中使用,应尽
早进行计算,对虚拟字段的使用转换为对字段的引用。
5 ) FlinkRuleSets.DATASTREAM_NORM_RULES : 使 用 代 价 优 化 模
型。
6)FlinkRuleSets.LOGICAL_OPT_RULES:优化逻辑计划,调整节点
间的上下游到达优化计算逻辑的效果,同时将基于Calcite的逻辑节点
树转换成Flink逻辑节点树(FlinkLogi calRel),使用代价优化模
型。
7)FlinkRuleSets.LOGICAL_REWRITE_RULES:逻辑重写规则,使
用规则优化模型。
8)FlinkRuleSets.DATASTREAM_OPT_RULES:物理优化规则,将逻
辑节点转换为对DataStream的API调用,使用代价优化模型。
序号即优化规则的应用顺序,其中1)~7)是逻辑优化,8)是物
理优化。

14.20 代码生成
优化器负责全局的优化,从提升全局资源利用率、消除数据倾
斜、降低IO等角度进行优化,包括Join重写等。代码生成负责局部优

see more please visit: https://homeofpdf.com


化,优化具体Task的执行效率,主要依赖Codegen技术,具体包括
Expression表达式级别和执行逻辑级别的代码生成。
在Blink Planner和Flink Planner模块中,都包含了的SQL语句的
Java代码生成,直接生成合法的Java类,在类中实现处理。然后使用
Janino将Java代码编译成字节码,直接在JVM中执行。
Janino是一个超级小但又超级快的Java编译器,经常作为嵌入式
编译器来使用。不仅能像javac一样将Java代码源文件编译成字节码文
件,还可以对一些Java表达式、代码块、类中的文本(class body)
或者内存中的源文件进行编译,并把编译后的字节码直接加载到同一
个JVM中运行。
14.20.1 为什么进行代码生成
如果没有代码生成技术,则所有的SQL运算都要定义成函数,如对
于表达式a+b+1,其求值过程如图14-37所示。

图14-37 表达式a+b+1的求值过程
首先需要定义add函数,add函数层层调用。在这个过程中涉及虚
函数调用、对象创建等额外逻辑,这些额外成本远超对表达式求值本
身 。 而 通 过 代 码 生 成 , 则 可 以 将 过 程 简 化 为 row.get ( a )
+row.get(b)+1,Java执行简单的加法效率要高得多。Spark在其
Tungsten项目中也引入了代码生成技术,Spark SQL的执行效率获得了
极大的提升。
14.20.2 代码生成范围

see more please visit: https://homeofpdf.com


Blink Table Planner 和 Flink Table Planner 有 各 自 的 代 码 生
成,代码生成分为两个部分:Calcite代码生成和Flink代码生成。
Flink Planner 因 为 最 终 将 SQL 转 换 为 了 对 DataStream API 的 调
用,所以Flink Planner的代码生成主要用来生成Flink的Function代
码,包括UDF类(类、方法体)、表达式的代码生成等。
1)表达式运算。包括数学运算、条件运算、逻辑运算表达式、
SQL内置函数调用。
2)函数类。包括SQL子句(如Where、Project、Join和维表Join
等 ) , 如 MapFunction 、 FlatMapFunction 、 JoinFunction 、
FlatJoinFunction、ProcessFunction和AsyncFunction等。
Blink Table Planner的代码生成范围更广,包含了算子、UDF、
表达式的代码生成。整个生成过程自内而外,首先生成表达式代码,
组装成方法,然后生成函数类,再生成算子代码。
1)表达式运算。包含数学运算、条件运算、逻辑运算、SQL内置
函数调用。
2)处理逻辑。SQL子句(如Where、Project、Join和维表Join
等)一般会生成单个UDF类,如生成MapFunction、FlatMapFunction、
JoinFunction、FlatJoinFunction和ProcessFunction等UDF实现类。
对于一些比较复杂的子句,如Group By子句,则可能会进行两阶
段优化,其会生成本地聚合、远端聚合两个UDF类,对应的也就会生成
两个算子。
3)算子代码生成。算子代码生成是Blink Planner增加的,生成
OneInputStreamOperator、TwoInputStreamOperator两类算子。
14.20.3 代码生成示例
代码清单14-33所示是Flink单元测试代码中的一段示例,接下来
以此为例解释其代码生成过程,便于读者理解Flink的代码生成是如何
实现的。
代码清单14-33 Blink SQL CalcITCase单元测试片段

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
经过Blink Planner转换生成最终的执行计划之后,进入到将物理
计划转换为Flink Transformation的阶段,在这个阶段中,SQL语句转
换为动态生成的Java代码。
对物理执行计划中的每一个ExecNode进行转换,下边对示例代码
中SQL语句的Where部分生成StreamOperator代码的过程进行解读:
首先是生成UDF部分的代码,然后将UDF包装到StreamOperator的
代码中,最后封装为Transformation,如代码清单14-34所示。
代码清单14-34 Calc代码生成

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
1.生成Where条件处理逻辑Java代码
generateProcessCode方法负责生成核心的处理代码片段,但是该
片段并不是完整的方法,如代码清单14-35所示。
代码清单14-35 Where条件生成代码示例

2.生成OneInputStreamOperator子类Java代码
生成了处理片段之后,需要将处理代码封装到算子的实现类中,
如代码清单14-36所示。
代码清单14-36 生成OneInputStreamOperator算子

see more please visit: https://homeofpdf.com


see more please visit: https://homeofpdf.com
see more please visit: https://homeofpdf.com
感兴趣的读者可以在IDE环境中debug,跟踪代码生成的过程,加
深理解。

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一样经历优化、转换,然后进入执行阶段。

see more please visit: https://homeofpdf.com


在SQL优化方面,Flink并没有直接使用Calcite,而是自己定义了
优化过程和优化规则,混合使用了Calcite的HepPlanner(RBO规则优
化)和VolcanoPlanner两种优化器。为了提高代码执行效率,采用了
代码生成方式直接生成执行代码。

see more please visit: https://homeofpdf.com


第15章 运维监控
Flink Metrics指作业在Flink集群运行过程中的各项指标,包括
机器系统指标,如主机名、CPU、内存、线程、GC垃圾回收、网络、
IO、JVM和 任务运行组件(JobManager、TaskManager、作业、Task、
算子)等相关指标。
Flink的指标模块有两个作用:
1)实时采集监控数据,在Flink Web UI展示监控信息,用户可以
在页面上看到自己提交任务的状态、延迟等信息。
2)对外提供监控数据收集接口,用户可以将整个Flink集群的监
控数据主动上报至第三方监控系统。
第2个作用对IT规模很大的公司很有用(如互联网公司、大型金融
机构、运营商),它们的集群规模一般比较大,Flink UI的展示和易
用性不够,并且大型机构一般都有集中的运维管理系统,所以就通过
上报的方式与现有的IT运维系统进行集成,利用现有工具对Flink监
控。

15.1 监控指标
Flink提供了Counter、Gauge、Histogram和Meter 4类监控指标。
指标接口体系如图15-1所示。

图15-1 指标分类
1. Counter计数器
用来统计一个指标的总量。以Flink中的指标为例,算子的接收记
录 总 数 ( numRecordsIn ) 和 发 送 记 录 总 数 ( numRecordsOut ) 属 于

see more please visit: https://homeofpdf.com


Counter类型。
2. Gauge指标瞬时值
用 来 记 录 一 个 指 标 的 瞬 间 值 。 以 Flink 中 的 指 标 为 例 ,
TaskManager中的JVM堆内存使用量(JVM.Heap.Used),记录某个时刻
TaskManager进程的JVM堆内存使用量。
3. Histogram直方图
指标的总量或者瞬时值都是单个值的指标,当想得到指标的最大
值、最小值、中位数等统计信息时,需要用到Histogram。Flink中属
于Histogram的指标很少,其中最重要的一个是算子的延迟。此项指标
会记录数据处理的延迟信息,对任务监控起到很重要的作用。
4. Meter平均值
用来记录一个指标在某个时间段内的平均值。Flink中类似的指标
有Task/算子中的numRecordsInPerSecond,记录此Task或者算子每秒
接收的记录数。

15.2 指标组
Flink的指标体系是一个树形结构,域相当于树上的顶层分支,表
示指标的大的分类。
每个指标被分配一个标识符,该标识符将基于3个组件进行汇报:
注册指标时用户提供的名称、可选的用户自定义域和系统提供的域。
例如,如果A.B是系统域,C.D是用户域,E是名称,那么指标的标识符
将 是 A.B.C.D.E. 可 以 通 过 设 置 conf/flink-conf.yam 里 面 的
metrics.scope.delimiter参数来配置标识符的分隔符,默认为“.”
(点)。
以算子的指标组结构为例,其默认为:

算子的输入记录数指标为:

关于指标组的配置参数的使用参见官方手册。

see more please visit: https://homeofpdf.com


15.3 监控集成
生产环境中,为了保证业务的正常运行,一般都需要对Flink集群
和其中作业的运行状态进行监控,Flink提供了主动和被动两种集成方
式。主动方式是MetricReporter,主动将监控数据写入第三方监控接
口;被动方式是提供Rest接口,供外部监控系统调用。
1. MetricReporter主动集成
MetricReporter是用来向外披露Metric的监测结果的接口,定义
了MetricReporter的生命周期管理、添加和删除指标时的行为。
MetricReporter接口如代码清单15-1。
代码清单15-1 MetricReporter接口

在Flink中使用MetricReporter的时候,有两种实例化的方式:
1 ) 使 用 Java 反 射 机 制 实 例 化 MetricReporter , 要 求
MetricReporter的实现类必须是public的访问修饰符,不能是抽象
类,必须有一个无参构造函数。
2 ) 使 用 MetricReporterFactory 工 厂 模 式 实 例 化
MetricReporter,推荐使用这种实例化的方式,相比发射机制,限制
更少。
Flink 提 供 了 JMX 、 Graphite 、 InfluxDB 、 Prometheus 、
PrometheusPushGateway、StatsD、Datadog和Slf4j共8种Reporter,
在配置文件中进行配置就可以直接使用。具体配置参数的使用参见官
方文档。
2. Rest API监控接口被动集成

see more please visit: https://homeofpdf.com


Flink提供了Rest API监控接口,被动接收外部应用的调用,可以
返回集群、组件、作业、Task、算子的状态。Flink自带的Web UI管理
界面中,也是通过这个接口来采集数据呈现的,它也被设计用于定制
监视工具。
Rest API的核心实现类是WebMonitorEndpoint,该类也支持HA高
可用。

15.4 指标注册中心
指标注册中心在Flink中叫作MetricRegistry,追踪所有已注册的
指标(Metric)。指标组(MetricGroup)用来对指标进行分组管理,
指标汇报器(MetricReporter)用来对外披露指标,而指标注册中心
就是这两者的中介,通过指标注册中心就可以让指标汇报器感知到在
指标组中有哪些指标、指标的值是多少,然后指标汇报器可以采集指
标数据,并写入第三方监控系统中。
指标组、指标注册中心、指标汇报器之间的关系如图15-2所示。

图15-2 指标组、指标注册中心、指标汇报器的关系
以 在 指 标 组 添 加 指 标 为 例 , 其 过 程 为
AbstractMetricGroup.addGroup→AbstractMetricGroup.addMetric→
MetricRegistry.register→MetricReporter.notifyOfAddedMetric 。
删除指标的过程也是类似的。

15.5 指标查询服务
指标查询服务在Flink中的接口是MetricQueryService,其通过继
承 RpcEndpoint 提 供 了 堆 外 调 用 的 能 力 , 实 现 了

see more please visit: https://homeofpdf.com


MetricQueryServiceGateway接口,提供了指标查询的方法。
与其他Flink组件相比,MetricQueryService在一个独立的线程池
中执行,线程池中的线程池优先级比较低,启动时使用低优先级线
程。
MetricQueryService中,使用KV的形式,按照指标类型来保存注
册到Flink中的所有指标,如代码清单15-2所示。
代码清单15-2 MetricQueryService接口

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所示。

see more please visit: https://homeofpdf.com


图15-3 延迟追踪原理
在Source算子中周期性发送LatencyMarker,其中记录了当前时间
戳t1。延迟标记在处理过程中被当作一般的事件处理,如果存在背
压,LatencyMaker跟普通的事件一样会被阻塞,所以当LatencyMarker
流 转 到 StreamSink ( Sink 算 子 ) 的 时 候 , 本 地 处 理 时 间 t2 和
LatencyMarker的时间戳t1相比较,t2-t1就是数据处理的延迟。
此处需要注意,延迟标记无法体现出数据处理的延迟,因为
LatencyMarker在算子中并不会处理,直接就发送给下游。例如,对于
窗口运算,延迟标记会直接绕过窗口,而一般的元素处理则需要等待
窗口触发。只有算子无法接收新的数据,如检查点Barrier对齐、反压
等时,才会导致数据被缓冲等待的延迟体现在延迟标记中。
延迟标记用来跟踪DAG图中从Source端到下游算子之间延迟分布,
延迟分布最终作为Histogram类型的指标。指标的粒度使用metrics-
latency-interval参数配置。最细粒度是Task,启用Task级别的延迟
跟踪,可能会导致产生大量的Histogram计算。
LatencyMarker 计 算 过 程 中 使 用 的 是 TaskManager 的 本 地 系 统 时
间,需要确保Flink集群中的所有机器的时钟是同步的,否则计算出来
的延迟会与实际的延迟有偏差。
LatencyMarker最终计算出来的是数据延迟,体现为Histogram类
型的指标,在Flink中定义了3种计算粒度:
(1)单值粒度
指标按照算子ID + Task编号粒度进行统计,无论上游使用了多少
个数据源。例如,对于JOIN,不区分数据源。
(2)算子

see more please visit: https://homeofpdf.com


指标按照Source算子ID + 算子ID + Task编号粒度进行统计,区
分数据源,不区分Source算子的Task。
(3)Task
指标按照数据源算子ID + 数据源Task编号 + 算子ID + Task编
号,区分数据源且区分数据源算子的Task。
注意
启用延迟跟踪可能严重影响集群的性能(特别是以Task为粒度
时)。强烈建议只在必要时使用,如debug排错。

15.7 总结
Flink的监控指标分为4类,为了更好地区分指标,采用指标分组
组织指标体系。监控数据提供了被动读取和主动汇报两种方式,可以
非常方便地与其他运维管理系统进行集成。

see more please visit: https://homeofpdf.com


第16章 RPC框架
在前面的章节里,讲解了Flink作业的开发、执行、调度等方面的
知识,除了数据的交换之外,Flink集群组件之间的心跳、作业调度消
息等也需要进行RPC消息通信。Flink选择了Akka作为Flink自身消息的
RPC通信框架。

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。尽

see more please visit: https://homeofpdf.com


管单个的Actor是自然有序的,但一个包含若干个Actor的系统却是高
度并发且极具扩展性的,因为那些处理线程是所有Actor之间共享的。
这也是为什么不该在Actor线程里调用可能导致阻塞的“调用”的原
因,这样的调用可能会阻塞该线程,使它们无法替其他Actor处理消
息。
16.1.2 使用Akka
Akka 系 统 的 核 心 是 ActorSystem 和 Actor , 若 需 构 建 一 个 Akka 系
统,首先需要创建Ac torSystem,创建完ActorSystem后,可通过其创
建Actor(注意:Akka不允许直接创建一个Actor,只能通过Akka提供
的某些API才能创建或查找Actor,一般会通过ActorSystem#actorOf和
ActorContext#actorOf 来 创 建 Actor ) ; 另 外 , 只 能 通 过
ActorRef(Actor的引用,其对原生的Actor实例做了良好的封装,外
界不能随意修改其内部状态)来与Actor进行通信,如代码清单16-1所
示。
代码清单16-1 Akka系统构建和消息发送

1. Actor路径

see more please visit: https://homeofpdf.com


在 Akka 中 , 创 建 的 每 个 Actor 都 有 自 己 的 路 径 , 该 路 径 遵 循
ActorSystem的层级结构,大致如下。
(1)本地路径
代 码 清 单 16-1 中 本 地 Actor 的 路 径 为
akka://sys/user/helloActor,路径含义如下。
1)Akka:表示Akka本地协议。
2)Sys:创建的ActorSystem的名字。
3)user:通过ActorSystem#actorOf和ActorContext#actorOf方
法创建的Actor都属于/user,与/user对应的是/system,其是系统层
面创建的,与系统整体行为有关,在开发阶段并不需要对其过多关
注。
4)helloActor:创建的本地HelloActor。
(2)远程路径
代 码 清 单 16-1 中 远 程 Actor 的 路 径 为
akka.tcp://sys@l27.0.0.1:2020/user/remoteActor 。 路 径 含 义 如
下。
1)akka.tcp:Akka远程协议,通信方式为TCP。
2)sys@127.0.0.1:2020:ActorSystem名字及远程主机IP和端口
号。
3)user:与本地协议中的含义一样。
4)remoteActor:创建的远程Actor。
2.获取Actor
在与Actor通信之前需要获取ActorRef,获取ActorRef需要提供
Actor的路径,如代码清单16-2所示。
代码清单16-2 获取ActorRef发送消息

see more please visit: https://homeofpdf.com


上 述 代 码 中 是 获 取 本 地 Actor 的 ActorRef , 如 果 想 获 取 远 程
ActorRef,使用Actor路径小节中与远程路径格式类似的路径即可,注
意IP地址和端口号是必须有的。
16.1.3 Akka的通信
Akka有两种核心的异步通信方式:tell和ask。
1. tell方式
当使用tell方式时,表示仅仅使用异步方式给某个Actor发送消
息,无须等待Actor的响应结果,并且也不会阻塞后续代码的运行,
如:

see more please visit: https://homeofpdf.com


其中,第一个参数为消息,它可以是任何可序列化的数据或对
象 , 第 二 个 参 数 表 示 发 送 者 , 一 般 是 另 外 一 个 Actor 的 引 用 ,
ActorRef.noSender ( ) 表 示 无 发 送 者 ( 实 际 上 是 一 个 叫 作
deadLetters的Actor)。
2. ask方式
当需要从Actor获取响应结果时,可使用ask方法,ask方法会将返
回结果包装在scala.concurrent.Future中,然后通过异步回调获取返
回结果,调用方逻辑如代码清单16-3所示。
代码清单16-3 调用方发送ask消息响应处理

see more please visit: https://homeofpdf.com


16.2 RPC消息的类型
1.握手消息
1)RemoteHandshakeMessage:与Actor握手消息。
2)HandshakeSuccessMessage:与Actor握手成功消息。
2. Fenced消息
1)LocalFencedMessage:本地Fence Token消息,同一个JVM内的
调用。
2)RemoteFencedMessage:远程Fence Token消息,包括本地不同
JVM和跨节点的JVM调用。
Fenced消息用来防止集群的脑裂(Brain Split)问题,如配置
JobMaster HA,开始的时候JobMaster1作为Leader,JobMaser1宕掉,
JobMaster2 被 选 为 Leader , 此 时 如 果 JobMaster1 恢 复 , 可 能 会 向
TaskManager发送消息,TaskManager必须有一种机制能够识别哪个是
当前的JobMaster Leader,此时Fence Token使用JobMaster ID鉴别。
每 个 TaskManager 向 JobMaster 注 册 之 后 , 都 会 拿 到 当 前 Leader
JobMaster的ID作为Fence Token,其他JobMaster发送的消息因为其
JobMaster ID与期望的Fence Token不一样,就会被忽略掉。在当前的
实 现 中 , JobMaster 、 Dispatcher 、 ResourceManager 实 现 了 Fence
Token验证机制。
3.调用消息
1)LocalRpcInvocation:本地RpcEndpoint调用消息,同一个JVM
内的调用。
2)RemoteRpcInvocation:远程RpcEndpoint的调用消息,包括本
地不同JVM和跨节点的JVM调用。
4.执行消息
执行消息是RunAsync,带有Runnable对象的异步执行请求消息。
RpcEndpoint.runAsync方法调用RpcService.runAsync,然后调用
RpcService.scheduleRunAsync;RpcService.scheduleRunAsync 调 用
AkkaInvocationHanlder.tell方法发送RunAsync消息。

see more please visit: https://homeofpdf.com


16.3 RPC通信组件
16.3.1 RpcGateway
RpcGateway又叫作远程调用网关,是Flink远程调用的接口协议,
对外提供可调用的接口,所有实现RPC的组件、类都实现了此接口。
其接口定义如代码清单16-4所示。
代码清单16-4 RpcGateway接口

其接口继承体系如图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。

see more please visit: https://homeofpdf.com


16.3.2 RpcEndpoint
RpcGateway提供了行为定义,RpcEndpoint则在RpcGateway的基础
上提供了RPC服务组件的生命周期管理,Flink中所有提供远程调用服
务 的 组 件 ( Dispatcher 、 JobMaster 、 ResourceManager 、
TaskExecutor等)都继承自RpcEndpoint。
其类体系如图16-3所示。

图16-3 RpcEndpoint类体系
在Flink的设计中,同一个RpcEndpoint中的所有调用只有一个线
程处理,叫作Endpoint的主线程。与Akka的Actor模型一样,所有对状
态数据的修改在同一个线程中执行,所以不存在并发的问题。
上面的重要子类都使用了RpcService作为参数,用来启动RPC通信
服 务 , RpcEndpoint 提 供 远 程 通 信 特 性 , RpcService 负 责 管 理
RpcEndpoint生命周期。RPCEndpoint是RpcService、RPCServer的结合
之处,如代码清单16-5所示。
代码清单16-5 RpcEndpoint构造函数

see more please visit: https://homeofpdf.com


RpcEndpoint 在 其 本 身 的 构 造 过 程 中 使 用
RpcService.startServer()启动RpcServer,进入可以接收处理请求
的状态,最后再将RpcSever绑定到主线程上真正执行起来。
RpcServer负责接收消息,分发给对应的处理逻辑。
16.3.3 RpcService
RpcService是RpcEndpoint的成员变量,RpcService的作用如下。
1)启动和停止RpcServer和连接RpcEndpoint。
2 ) 根 据 指 定 的 连 接 地 址 , 连 接 到 RpcServer 会 返 回 一 个
RpcGateway。分为带FencingToken和不带FencingToken的版本。
3)延迟/立刻调度Runnable、Callable。
其类体系如图16-4所示。

see more please visit: https://homeofpdf.com


图16-4 RpcService类体系
RpcService 会 在 ClusterEntrypoint ( JobMaster ) 和
TaskManagerRunner(TaskExecutor)启动的过程中被初始化并启动。
AkkaRpcService是RpcService的唯一实现。AkkaRpcService中包
含了一个ActorSystem,保存了ActorRef和RpcEndpoint之间的映射关
系。RpcService跟RpcGateway类似,也提供了获取地址和端口的方
法。
RpcService会根据RpcEndpoint(Fenced和非Fenced)的类型构建
不同的AkkaRpcActor(Fenced和非Fenced),并保存AkkaRpcActor引
用和RpcEndpoint的对应关系。创建出来的AkkaRpcActor是底层Akka调
用的实际接收者,RPC的请求在客户端被封装成RpcInvocation对象,
以Akka消息的形式发送。
同 时 也 要 完 成 RpcServer 的 构 建 , RpcServer 也 分 为 Fenced 与 非
Fenced两类,详情参见下一小节。最终通过Java的动态代理将所有的
消息调用转发到InvocationHandler,如代码清单16-6所示。
代码清单16-6 通过多动态代理转发至InvocationHandler

see more please visit: https://homeofpdf.com


16.3.4 RpcServer
RpcServer是RpcEndpoint的成员变量,负责接收响应远端的RPC消
息 请 求 。 其 有 RpcServer 有 两 个 实 现 : AkkaInvocationHandler 和
FencedAkkaInvocationHandler。
RpcServer类体系如图16-5所示。

图16-5 RpcServer类体系
RpcServer的启动实质上是通知底层的AkkaRpcActor切换到START
状态,开始处理远程调用请求,如代码清单16-7所示。

see more please visit: https://homeofpdf.com


代码清单16-7 通知AkkaRpcActor进入START状态

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所示。

see more please visit: https://homeofpdf.com


图16-6 AkkaRpcActor类体系
AkkaRpcActor在RpcService.startServer()中被创建。从图16-
6中可以看到FencedAkkaRpcActor支持脑裂消息的处理。

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处理调用

see more please visit: https://homeofpdf.com


AkkaInvocationHandler中判断方法所属的类,如果是RPC方法,
则调用invokeRpc方法。将方法调用封装为RPCInvocation消息。如果
是本地则生成LocaRPCInvocation,本地消息不需要序列化,如果是远
程调用则创建RemoteRpcInvocation。
判断远程方法调用是否需要等待结果,如果无须等待(void),
则使用向Actor发送tell类型的消息,如果需要返回结果,则向Actor
发送ask类型的消息,如代码清单16-9所示。
代码清单16-9 RPC远程调用

see more please visit: https://homeofpdf.com


16.4.2 RPC请求响应
RPC消息是通过RpcEndpoint所绑定的Actor的ActorRef发送的,
AkkaRpcActor是消息接收的入口,AkkaRpcActor在RpcEndpoint中构造
生成,负责将消息交给不同的方法进行处理,如代码清单16-10所示。
代码清单16-10 消息处理入口

see more please visit: https://homeofpdf.com


AkkaRpcActor接收到的消息总共有3种。
(1)握手消息
如上文所述,在客户端构造时会通过ActorSelection发送过来。
收到消息后会检查接口、版本是否匹配,如果一致就返回成功。握手
消息处理如代码清单16-11所示。
代码清单16-11 握手消息处理

(2)控制消息
例 如 , 在 RpcEndpoint 调 用 start 方 法 后 , 会 向 自 身 发 送 一 条
Processing.START消息来转换当前Actor的状态为STARTED,STOP也类
似,并且只有在Actor状态为STARTED时才会处理RPC请求。控制消息处
理如代码清单16-12所示。

see more please visit: https://homeofpdf.com


代码清单16-12 控制消息处理

(3)RPC消息
通 过 解 析 RpcInvocation 获 取 方 法 名 和 参 数 类 型 , 并 从
RpcEndpoint类中找到Method对象,通过反射调用该方法。如果有返回
结果,会以Akka消息的形式发送回发送者。RPC消息处理如代码清单
16-13所示。
代码清单16-13 RPC消息处理

see more please visit: https://homeofpdf.com


16.5 总结
Flink集群内部通信框架的最底层依赖于Akka,定义了不同类型的
消息,并且设计了通用的RPC通信组件,Flink的所有提供RPC请求的集
群组件(如JobMaster、TaskManager、ResourceManager、Dispatcher
等)都使用了这些RPC通信基础组件来提供对外的RPC接口。不同的分
布式组件需要频繁地通信,所以本章也介绍了其RPC交换过程。

see more please visit: https://homeofpdf.com


专家寄语
传统的数据仓库主要处理T+1的数据,即今天产生的数据,其分析
结果明天才能看到。随着数据时效性在企业运营中的重要性日益凸
现,如实时推荐、精准营销、广告投放效果监控、实时物流等应用的
发展,以T+0为目标的实时数据仓库建设已提上日程。实时数据仓库解
决方案的核心就是数据的实时处理能力,以Flink为代表的分布式流数
据处理技术已经成为构建实时数据仓库的基础技术之一。本书可以拓
展传统数据仓库从业者的思路,建议大家仔细阅读,为构建国产自主
的新一代实时数据仓库添砖加瓦。
唐世渭 北京大学博士生导师,中国自主数据库奠基人
中国数据库及数据仓库研究开拓者
随着AI技术的广泛应用,业务运营正在从基于规则向基于AI过
渡,同时带来了对AI时效性的要求,模型在线训练和海量数据在线推
理成为从业者的必备能力。谷歌论文中所提出的以流为核心的大数据
模型代表了未来的大数据技术发展趋势,在开源技术中Flink与该处理
模型最为相似。本书超越技术的表象,将Flink底层技术原理清晰地呈
现给读者,值得大家花时间去研读。
戴文渊 第四范式CEO
批流 一体 是 大 数 据引 擎的发展趋势,在开源大数据引擎中 ,
Flink、Spark尤其突出,但两者实现批流一体的思路却有所不同:
Flink以流为核心实现,Spark以批为核心实现。在实际应用中,Flink
能够兼具高吞吐、低延迟的特性,更加符合未来的需要。本书抽丝剥
茧将Flink层层解构,把Flink的核心原原本本地呈现给读者,是读者
由初级进阶高阶的必读佳作。
徐国市 小米大数据部副总经理
大数据已经从T+1时代进入了T+0时代,Flink就是其中最有代表的
组件。本书深入浅出地讲解了Flink内核原理,从底层到运行方法均做
了全面的介绍,是学习Flink的必读佳作。
郭炜 易观CTO,TGO鲲鹏会北京分会会长

see more please visit: https://homeofpdf.com


ClickHouse华人社区发起人
Flink前身是柏林理工大学2008年提出的一个研究性项目,2014年
成为Apache的顶级孵化项目,同时已经成为大数据处理领域的明星。
基于批处理实现批流一体的大数据处理引擎,在Spark的光环之下,
Flink选择了实时计算作为突破方向,经过持续数年的不断发力,终于
从Spark的光环下走出来,成为实时计算领域的首选技术,并且逐渐在
批处理方向上持续完善,成为与Spark并驾齐驱的大数据计算引擎。随
着实时数据处理的需求越来越广泛,Flink也将会得到广泛的使用。本
书将Flink立体剖析呈现给读者,结合Flink源码可以帮助开源代码爱
好者、喜欢研究源码的读者找到比较好的学习切入点。
胡时伟 第四范式首席架构师
我从事保险科技近20年,感受着大数据、云计算、物联网、人工
智能、区块链等科技的不断发展,也认识到保险科技正在深刻地改变
着行业。其中,大数据实时计算技术帮助行业极大地提升了效率和服
务能力。天下武功,唯快不破,以Flink为代表的实时计算技术未来将
会成为大数据技术的核心基石之一。说来也巧,我正在考虑用实时计
算处理历史上做过的保全、理赔和满期等关联保单信息,同时准备采
用实时计算技术做一个保险中台的底层框架。拿到这本书后,一口气
读完,整书言简意赅,从入门到精通,讲解了Flink的内核原理和实
现,更深刻地理解了分布式数据的底层原理、计算模型,其中很多方
法、思路给了我很多启发。我也相信能够帮助大家穿越迷雾洞悉本
质,从而更好地将技术应用在工作中。在这个充满机遇、变革的数字
时代,技术层出不穷、眼花缭乱,让人难以选择。这里感谢本书作者
为我们提供了大数据时代的一个更好的技术选择。
杜鹏飞 君康人寿CIO
未来战争是智能化的战争,其重要特征就是对抗节奏明显加快,
制胜机理由以能制胜转变为以快制胜,谁掌握了先机,谁就掌握了战
争的主动权。实时计算技术通过实时融合处理敌情、我情和战场环境
等数据,可以有效帮助指挥员全面掌握情况、捕捉细微变化、发现重
大征候,从而准确掌握战场实时态势,快速、精准做出决策,动态高
效调控部队行动,准确评估作战效果。Flink是开源领域最优秀的大数
据实时计算引擎,本书值得各位军工领域的大数据技术从业人员仔细

see more please visit: https://homeofpdf.com


阅读,了解其原理与机制,应用在军事领域,从而带来指挥控制效率
的革命性提升、加速作战指挥体系变革、引领指挥决策模式转变。
王立军 军委战规办、信息化办主任
作为流式处理的一颗新星,Flink在众多大数据处理框架中脱颖而
出,以其流批一体的能力,同时兼具高吞吐、低延迟和容错的特性,
成为流式处理的事实标准。作者具有多年大数据领域实践经验,特别
在流式处理方面沉淀颇深。本书重点介绍了Flink的设计思想和基本原
理,并扩展到运维、管理等诸多方面,是广大大数据开发、运维、架
构人员及技术爱好者不错的选择。
韩锋CCIA(中国计算机行业协会)常务理事,腾讯云TVP
DBA-Plus社群联合创始人

see more please visit: https://homeofpdf.com


参考文献
[1]Flink. Flink官方文档[EB/OL].(2020-02-11)[2020-
04-09 ] . https://ci.apache.org/projects/flink/flink-docs-
release-1.10/.
[ 2 ] Flink. Data Streaming Fault Tolerance [ EB/OL ] .
( 2020-02-11 ) [ 2020-04-09 ] .
https://ci.apache.org/projects/flink/flink-docs-release-
1.10/internals/stream_checkpointing.html.
[3]Flink. Jobs and Scheduling[EB/OL].(2020-02-11)
[ 2020-04-09 ] . https://ci.apache.org/projects/flink/flink-
docs-release-1.10/internals/job_scheduling.html.
[4]Flink. Task Lifecycle[EB/OL].(2020-02-11)[2020-
04-09 ] . https://ci.apache.org/projects/flink/flink-docs-
release-1.10/internals/task_lifecycle.html.
[ 5 ] Flink. Component Stack [ EB/OL ] . ( 2020-02-11 )
[ 2020-04-09 ] . https://ci.apache.org/projects/flink/flink-
docs-release-1.10/internals/components.html .
[ 6 ] Flink. A Deep-Dive into Flink's Network
Stack [ EB/OL ] . ( 2019-06-05 ) [ 2020-04-09 ] .
https://flink.apache.org/2019/06/05/flink-network-stack.html.
[7]Akka. Akka官方文档[EB/OL].(2020-04-09)[2020-04-
09]. https://doc.akka.io//docs/akka/current/index.html.
[8]Flink. Flink 1.10版本源码[EB/OL].(2020-02-11)
[2020-04-09]. https://github.com/apache/flink/tree/release-
1.10.
[9]老白讲互联网.追源索骥:透过源码看懂Flink核心框架的执
行 流 程 [ EB/OL ] . ( 2018-09-27 ) [ 2020-04-
09].https://github.com/bethunebtj/flink_tutorial.

see more please visit: https://homeofpdf.com


[10]伍翀(云邪). Flink原理与实现:Window机制[EB/OL].
( 2016-05-25 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/05/25/flink-internals-window-
mechanism/.
[ 11 ] 伍 翀 ( 云 邪 ) . Flink 原 理 与 实 现 : Session
Window [ EB/OL ] . ( 2016-06-06 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/06/06/flink-internals-session-
window/.
[12]伍翀(云邪). Flink原理与实现:数据流上的类型和操作
[ EB/OL ] . ( 2016-05-20 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/05/20/flink-internals-streams-
and-operations-on-streams/.
[13]伍翀(云邪). Flink原理与实现:内存管理[EB/OL].
( 2016-04-29 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/04/29/flink-internals-memory-
manage/.
[ 14 ] 伍 翀 ( 云 邪 ) . Flink 原 理 与 实 现 : 架 构 和 拓 扑 概 览
[ EB/OL ] . ( 2016-05-03 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/05/03/flink-internals-overview/.
[ 15 ] 伍 翀 ( 云 邪 ) . Flink 原 理 与 实 现 : 如 何 生 成
StreamGraph [ EB/OL ] . ( 2016-05-04 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/05/04/flink-internal-how-to-
build-streamgraph/.
[ 16 ] 伍 翀 ( 云 邪 ) . Flink 原 理 与 实 现 : 如 何 生 成
JobGraph [ EB/OL ] . ( 2016-05-10 ) [ 2020-04-09 ] .
http://wuchong.me/blog/2016/05/10/flink-internals-how-to-
build-jobgraph/.
[ 17 ] 杨 华 ( vinoyang ) . Flink 运 行 时 之 生 产 端 结 果 分 区
[ EB/OL ] . ( 2016-12-30 ) [ 2020-04-09 ] .
https://blog.csdn.net/yanghua_kobe/article/details/53946640.

see more please visit: https://homeofpdf.com


[ 18 ] 杨 华 ( vinoyang ) . Flink 运 行 时 之 结 果 分 区 消 费 端
[ EB/OL ] . ( 2016-12-30 ) [ 2020-04-09 ] .
https://blog.csdn.net/yanghua_kobe/article/details/54089128.
[19]徐榜江(雪尽). Flink SQL系列|5个TableEnvironment我
该 用 哪 个 ? [ EB/OL ] . ( 2019-09-29 ) [ 2020-04-09 ] .
https://yq.aliyun.com/articles/719760.
[20]Gordon Tai. 4 characteristics of Timers in Apache
Flink to keep in mind[EB/OL].(2019-01-18)[2020-04-09].
https://www.ververica.com/blog/4-characteristics-of-timers-
in-apache-flink.
[ 21 ] Stefan Richter. 3 differences between Savepoints
and Checkpoints in Apache Flink [ EB/OL ] . ( 2018-11-02 )
[ 2020-04-09 ] . https://www.ververica.com/blog/differences-
between-savepoints-and-checkpoints-in-flink.
[22]Stefan Richter. An Overview of End-to-End Exactly-
Once Processing in Apache Flink [ EB/OL ] . ( 2018-02-15 )
[ 2020-04-09 ] . https://www.ververica.com/blog/end-to-end-
exactly-once-processing-apache-flink-apache-kafka.

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
see more please visit: https://homeofpdf.com

You might also like