大家好,我是老兵。
本系列为大数据技术栈面试体系
系列,每期将分享一个技术组件的知识全体系,并结合面试的形式由浅入深讲解。
本期将介绍大数据实时计算利器Flink面试体系,全文内容已制作成PDF。
Apache Flink是开源的大数据实时计算框架,具有分布式、高性能、内存计算等特点。Flink因其独特的流批一体
设计模式,被广泛应用于实时
和离线
数据应用场景。
Flink被称为第四代大数据计算引擎,在其前面存在Mapreduce、Storm、Spark等计算框架。在流处理领域中,Flink是目前最全面、最强大的实时计算引擎。
结合官网的示意图,我们来看下Flink的工作场景。
数据源:支持多种数据源接入。包含事务型数据库、日志、IOT设备、点击事件等数据。
处理层:基于Yarn|K8s调度引擎和HDFS|S3存储组件,提供完整的事件驱动
、时间语义
、流&批一体
的Flink计算服务。
应用层:输出端提供应用系统、事件日志、存储系统等数据对接。
1)Flink分层模型
Flink底层通过封装和抽象,提供四级分层编程模型,以此支撑业务开发实时和批处理程序。
结合示意图,我们由下而上进行介绍。
Runtime层:
Flink程序的最底层入口。提供基础的核心接口完成流、状态、事件、时间等复杂操作,功能灵活但使用成本较高,一般面向源码研发人员。
DataStream/Dataset API层:
这一层主要面向开发者。基于Runtime层抽象为两类API,其中DataStream API处理实时流程序;Dataset API处理批数据程序。
Table API:
统一DataStream/DataSet API,抽象成带有Schema信息的表结构API。通过Table操作和注册表完成数据计算,支持与DataStream/Dataset相互转换。
SQL:
面向数据分析和开发人员,抽象为SQL操作,降低开发门槛和平台化。
2)Flink计算模型
Flink的计算模型和Spark的模型有些类似。包含输入端(source)、转换(Transform)、输出端(sink)。
source端
:Flink程序的输入端,支持多个数据源对接
Transformation
:Flink程序的转换过程,实现DataStream/Dataset的计算和转换
sink端
: Flink的输出端,支持内部和外部输出源
具体的Flink计算模型(算子)详情,可以参考我的文章:一网打尽Flink算子大全
主要考察对Flink的内部运行机制的了解程度,需要重点注意Flink中的重要角色组件及其协作机制。
Flink底层执行分为客户端
(Client)、Job管理器
(JobManager)、任务执行器
(TaskManager)三种角色组件。其中Client负责Job提交;JobManager负责协调Job执行和Task任务分配;TaskManager负责Task任务执行。
Flink常见执行流程如下(调度器不同会有所区别):
1)用户提交流程序Application。
2)Flink解析StreamGraph
。Optimizer
和Builder模块解析程序代码,生成初始StreamGraph
并提交至Client
。
3)Client生成JobGraph
。上述StreamGraph由一系列operator chain构成,在client中会被转换为JobGraph
,即优化多个chain为一个节点,最终提交到JobManager
。
4)JobManager调度Job
。JobManager和Client的ActorSystem
保持通信,并生成ExecutionGraph
(并行化JobGraph)。随后Schduler和Coordinator模块协调并调度Jobz执行。
5)TaskManager部署Task
。TaskManager
和JobManager
的ActorSystem
保持通信,接受job调度计划
并在内部划分TaskSlot
部署执行Task任务
。
6)Task执行
。Task执行期间,JobManager、TaskManager和Client之间保持通信,回传任务状态
和心跳信息
,监控任务执行。
顾名思义,这里涉及Flink的部署模式内容。一般Flink部署模式除了Standalone之外,最常见的为Flink on Yarn和Flink on K8s模式,其中Flink on Yarn模式在企业中应用最广。
Flink on Yarn模式细分由可以分为Flink session、Flink per-job和Flink application模式,下面我们逐一说明。
1)Flink session模式
Flink Session模式会首先启动一个集群
,按照配置约定,集群中包含一定数量的JobManager
和TaskManager
。后面所有提交的Flink Job均共享该集群内的JobManager和TaskManager,即所有的Flink Job竞争相同资源。
这样做的好处是节省作业提交资源开销(集群已存在),减少资源和线程切换工作。但是所有作业共享一个JobManager,导致JobManager
压力激增,同时一旦某Job发生故障时会影响到其他作业(中断或重启)。一般仅适用于短周期
、小容量
作业。
看下Flink-session模式的作业提交流程:
(1)整体流程分为两部分:yarn-session集群启动、Job提交。
(2)yarn-session集群启动
。请求YarnRM
启动JobManager
,随后JobManager内部启动Dispatcher
和Flink-yarnRM
进程,等待后续Job提交
。
(3)Client提交Job。Client连接Dispatcher
开始提交Job,包含jars
和解析过的JobGraph
拓扑数据结构。
(4)Dispatcher启动JobMaster
,JobMaster向Yarn RM请求slots资源
。
(5)Flink-Yarn RM
向Yarn RM请求Container
资源,准备启动TaskManager。
(6)Yarn启动TaskManager进程
。TaskManager同时向Flink RM反向注册(自身可用的slots槽数)
(7)TaskManager为新的作业提供slots,与JobMaster通信。
(8)JobMaster将执行的任务分发
给TaskManager,开始部署执行任务
2)Flink Per-job模式
Flink Per-job模式为每个提交的作业启动集群,各集群间相互独立,并在各自作业完成后销毁,最大限度保障资源隔离。每个Job均衡分发
自身的JobManager,单独进行job的调度和执行。
虽然该模式提供了资源隔离,但是每个job均维护一个集群,启动、销毁以及资源请求消耗时间长,因此比较适用于长时间的任务执行(批处理任务)。
Per-job模式在Flink 1.15中弃用,目前推荐使用applicaiton模式。
看下Flink Per-job模式的作业提交流程:
(1)首先Client提交作业到YarnRM
,包括jars和JobGraph
等信息。
(2)YarnRM分配Container启动AppMaster
。AppMaster中启动JobManager
和FlinkRM
,并将作业提交给JobMaster
。
(3)JobMaster向YarnRM请求资源(slots
)。
(4)FlinkRM向YarnRM
请求container
并启动TaskManager
。
(6)TaskManager启动之后,向FlinkRM
注册自己的可用任务槽。
(7)TaskManager向FlinkRM反向注册
(自身可用的slots槽数
)
(8)TaskManager为新的作业提供slots
,与JobMaster通信。
(9)JobMaster将执行的任务分发
给TaskManager,开始部署执行任务
3)Flink application模式
Flink application模式综合Per-job和session的优点,为每个·Application·创建独立的集群(JobManager),允许每个Application中包含多个job作业提交(可开启异步提交),当application应用完成时集群关闭。
该模式和前面两种模式的最大区别是Main()方法此时在JobManager
中执行,即在JobManager中完成文件下载
、jobGraph解析
、提交资源
等事项。前面两种模式的main()方法在Client端执行,该模式将大大减少Client压力。
看下Flink application模式的作业提交流程:
(1)流程与Per-job模式的提交流程非常相似。
(2)提交Application。此时首先是提交整个Application应用,应用中包含多个Job。
(3)每个Job启动各自的JobManager,可选择异步启动执行。
(4)其余步骤与Per-job模式类似,可参考上述步骤详解。
由于目前云原生和K8s容器化的快速发展,很多Flink程序开始转向容器化部署。首先需要了解下K8s的相关知识,这是个加分项。
1)K8s容器编排技术
k8s全称kubernete,是一种强大的
、可移植
的高性能容器编排工具
。这里的容器指的是Docker容器化
技术,它通过将执行环境和配置打包成镜像服务
,在任意环境下快速部署docker容器
,提供基础的环境服务。解决之前部署服务速度慢、迁移难和高成本等问题。
由于Docker容器技术的普及,基于容器构建的云原生架构越来越多,同时也带来了很多容器运维管理问题。K8s提供了一套完整的容器编排解决方案,实现容器发现及调度
、负载均衡
、弹性扩容
和数据卷挂载
等服务。
2)Flink on K8s部署模式
整体过程和Flink on Yarn的提交模式比较类似,主要是环境切换成K8s,此时的TaskManager和JobManager等组件变成了K8s Pod角色(镜像)。
首先提前定义各组件的服务配置文件并提交到K8s集群;K8s集群会自动根据配置启动相应的Pod服务,最后Flink程序开始运行。
session模式示例
(1)K8s集群根据提交的配置文件启动K8sMaster
和TaskManager
(K8s Pod
对象)
(2)依次启动Flink的JobManager
、JobMaster
、Deploy
和K8sRM
进程(K8s Pod对象);过程中完成slots请求
和ExecutionGraph
的生成动作。
(3)TaskManager注册Slots、JobManager请求Slots并分配任务
(4)部署Task执行并反馈状态
Flink中的执行图一般是可以分为四类,按照生成顺序分别为:StreamGraph
-> JobGraph
-> ExecutionGraph
->物理执行图
。
1)StreamGraph
顾名思义,这里代表的是我们编写的流程序图。通过Stream API生成,这是执行图的最原始拓扑数据结构。
2)JobGraph
StreamGraph在Client中经过算子chain链合并等优化,转换为JobGraph拓扑图,随后被提交到JobManager中。
3)ExecutionGraph
JobManager中将JobGraph进一步转换为ExecutionGraph
,此时ExecutuonGraph根据算子配置的并行度转变为并行化的Graph拓扑结构。
4)物理执行图
比较偏物理执行概念,即JobManager进行Job调度,TaskManager最终部署Task的图结构。
Flink一般根据固定时间或长度把数据流切分到不同的窗口,并提供相应的窗口Window算子,在窗口内进行聚合运算。
Flink的窗口一般分为三种类型:滚动窗口、滑动窗口、会话窗口和全局窗口等。
滚动窗口
滑动窗口
会话窗口
Flink中的窗口算子一般会配置Keyed类型数据集操作,并结合watermark和定时器,提供时间语义的统计,Windows算子的定义如下:
Windows Assigner:定义窗口的类型(数据流分配到多长时间间隔的哪种窗口),比如1min的滚动窗口。
Trigger:指派触发器,即窗口满足什么条件触发
Evictor:数据剔除(非必须)
Lateness:是否处理延迟数据标志,可在watermark之后再次触发
OutputTag:侧输出流输出标签,和getOutputTag配合使用。
WindowFunction:windows内的处理逻辑(程序核心)
// 计算过去30s窗口的uv/pv
dataStream.keyBy(x => x.getString("position_id"))
.window(TumblingEventTimeWindows.of(Time.minutes(30)))
.aggregate(new PVResultFunc(), new UVResultFunc())
Flink中的waternark(水印)是处理延迟数据的优化机制。一般数据顺序进入系统,但是存在网络等外部因素导致数据乱序或者延迟达到,这部分数据即不能丢弃也不能无限等待,watermark的出现解决了这个两难问题。
watermark的定义是:比如在一个窗口内,当位于窗口最小watermark
(水位线)的数据达到后,表明(约定)该窗口内的所有数据均已达到,此时不再等待数据,直接触发窗口计算。
watermark:最新事件事件 - 固定时间间隔
1)watermark的作用
规定了数据延迟处理的最优判定,即watermark时间间隔
较为完善的处理了数据乱序的问题,从而输出预期结果
结合最大延迟时间和侧输出流等机制,彻底解决数据延迟
2)watermark的生成
Flink中的watermark生成形式分为两种,即PeriodicWatermarks(周期性的生成水印)、PunctuatedWatermarks(每条信息/数据量生成水印)。
AssignerWithPeriodicWatermarks
// 设置5s周期性生成watermark
env.getConfig.setAutoWatermarkInterval(5000)
// 周期性生成watermark
val periodicWatermarkStream = dataStream.assignTimestampsAndWatermarks(new XXPeriodicAssigner(10))
AssignerWithPunctuatedWatermarks
class xxx extends AssignerWithPunctuatedWatermarks[(String, Long, Int)] {
override def extractTimestamp(element: (String, Long, Int), previousElementTimestamp: Long): Long = {
element._2
}
override def checkAndGetNextWatermark(lastElement:(St ring, Long, Int), extractTimestamp: Long): Watermark = {
// 判断字段状态生成watermark
if (lastElement._1 != 0) new Watermark(extractTimesta mp) else null
}
}
分布式快照即所谓的一致性检查点
(Checkpoints)。定义为某个时间点上所有任务状态的一份拷贝(快照),该时间点也是所有任务刚好处理完一个相同数据的时间。
Flink间隔时间自动执行
一致性检查点
程序,异步插入barrier检查点分界线,内存状态存储为cp进程文件。
从source
(Input)端开始,JobManager
会向每个source端发送检查点barrier消息
并启动检查点checkpoints
。在保证所有的source端数据处理完成后,Flink开始保存一致性检查点checkpoints
,过程启用barrier检查点分界线。
接收数据和barrier
消息,两个过程异步进行。在所有的source数据都处理完成后,开始将自己的检查点checkpoints保存到状态后端StateBackend
中,并通知JobManager将barrier分发到下游。
barrier向下游传递时会进行barrier对齐
确认。待barrier都到齐后才进行checkpoints检查点保存。
重复以上操作,直到整个流程完成。
Flink重要的特性就是其支持有状态计算。什么是有状态计算呢?即将中间的计算结果进行保存,便于后面的数据回溯和计算。
这个很好理解,因为Flink一般使用场景大多数为窗口实时计算,计算的是即时数据,当存在一个计算历史数据累计的需求时显得捉襟见肘,因此需要有方法能够保持前面的数据状态。Flink的底层很多机制默认开启了状态管理,比如checkpoint过程、二阶段提交均存在状态保存的操作。
在实际操作中Flink状态分为Keyed State 与 Operator State。
1)Operator State
算子状态的作用范围限定为算子任务,同一并行任务的所有数据都可以访问到相同的状态。状态对于同一任务而言是共享的。
List State。列表状态算子,将状态存储为列表数据
Union List State。联合列表状态算子,与List State类似,但是当出现故障时可恢复。
Broadcast State。广播状态算子,即存在多个task任务共享状态。
private var listState : ListState[Person] = _
override def open(parameters: Configuration): Unit = {
val listStateDesc: ListStateDescriptor[Person] = new ListStateDescriptor[Person]("personState", classOf[Person])
listState = getRuntimeContext.getListState(listStateDesc)
}
2)Keyed State
顾名思义,此类型的State状态保存形式为K-V键值对,通过K值管理和维护状态数据。
Flink对每个key维护自身状态,相同Key的数据划分到同一任务中,由Key管理其对应的状态。
Value State。值状态算子,将状态存储为K-单个值
List State。和上面的List State类似,状态被存储为k-数组列表
Map State。映射状态算子,状态被存储为K-Map
聚合State。状态存储为Aggregating聚合操作列表
MapState userMapState;
userMapState = getRuntimeContext().getMapState(
new MapStateDescriptor(
"Usercount",Long.class,Long.class));
在介绍内存管理之前,先介绍一下JVM中的堆内存和堆外内存。
通常来说。JVM堆
空间概念,简单描述就是在程序中,关于对象实例|数组的创建
、使用
和释放
的内存,都会在JVM中的一块被称作为"JVM堆"内存区域内进行管理分配。
Flink程序在创建对象后,JVM会在堆内内存中
分配
一定大小的空间,创建Class对象
并返回对象引用,Flink保存对象引用,同时记录占用的内存信息。
而堆外内存如果你有过Java相关编程经历的话,相信对堆外内存的使用并不陌生。其底层调用基于C
的JDK Unsafe类方法,通过指针
直接进行内存的操作,包括内存空间的申请、使用、删除释放等。
介绍完了堆内内存和堆外内存的概念,下面我们来看下Flink的内存管理。
1)JobManager内存管理
JobManager进程总内存包括JVM堆内内存、JVM堆外内存以及JVM MetaData内存,其中涉及的内存配置参数为:
# JobManager总进程内存
jobmanager.memory.process.size:
# 作业管理器的 JVM 堆内存大小
jobmanager.memory.heap.size:
#作业管理器的堆外内存大小。此选项涵盖所有堆外内存使用。
jobmanager.memory.off-heap.size:
2)TaskManager内存管理
TaskManager内存同样包含JVM堆内内存、JVM堆外内存以及JVM MetaData内存三大块。其中JVM堆内内存又包含Framework Heap和Task Heap,即框架堆内存和任务Task堆内存。
JVM堆外内存包含Memory memory托管内存,主要用于保存排序、结果缓存、状态后端数据等。另一块为Direct Memory直接内存,包含如下:
Framework Off-Heap Memory:Flink框架的堆外内存,即Flink中TaskManager的自身内存,和slot无关。
Task Off-Heap:Task的堆外内存
Network Memory:网络内存
其中涉及的内存配置参数为:
// tm的框架堆内内存
taskmanager.memory.framework.heap.size=
// tm的任务堆内内存
taskmanager.memory.task.heap.size
// Flink管理的原生托管内存
taskmanager.memory.managed.size=
taskmanager.memory.managed.fraction=
// Flink 框架堆外内存
taskmanager.memory.framework.off-heap.size=
// Task 堆外内存
taskmanager.memory.task.off-heap.size=
// 网络数据交换所使用的堆外内存大小
taskmanager.memory.network.min: 64mb
taskmanager.memory.network.max: 1gb
taskmanager.memory.network.fraction: 0.1
1)设计理念
Spark是批处理框架,其中的SparkStreaming在Spark的基础上实现的微批处理工作,支持秒级别延迟。
Flink是彻底的流处理框架,可以处理有界流和无流数据,达到流批一体,延迟低,真正做到来一条数据立马处理。
spark本身是无状态的,基于RDD计算。Flink基于事件驱动,既能进行有状态计算,也可以进行无状态计算。
2)流批一体
Spark通过逼近最小微批的方式达到近实时的效果,本质上还是批处理。
Flink本身内部就是处理无界的实时流,通过时间间隔限制,将无界流转换为有界流,实现流批一体。
3)应用场景
Spark擅长处理数据量非常大而且逻辑复杂的批数据处理、基于历史数据的交互式查询等
Flink擅长处理低延迟实时数据处理场景,比如实时日志报表分析等。
Spark社区更为活跃,且生态比较丰富,特别是机器学习方面;Flink正在逐渐完善社区和生态影响力。
4)相同点
均提供统一的批处理和流处理API,支持高级编程语言和SQL
都基于内存计算,速度快
都支持Exactly-once一致性
都有完善的故障恢复机制
这里我把三个组件SQL执行原理放到了一起,通过对比加深一下印象。
1)Hive SQL的执行原理
Hive SQL是Hive提供的SQL查询引擎,底层由MapReduce实现。Hive根据输入的SQL语句执行词法分析、语法树构建、编译、逻辑计划、优化逻辑计划以及物理计划等过程,转化为Map Task和Reduce Task最终交由Mapreduce
引擎执行。
执行引擎。具有mapreduce的一切特性,适合大批量数据离线处理,相较于Spark而言,速度较慢且IO操作频繁
有完整的hql
语法,支持基本sql语法、函数和udf
对表数据存储格式有要求,不同存储、压缩格式性能不同
2)Spark SQL的执行原理
Spark SQL底层基于Spark
引擎,使用Antlr
解析语法,编译生成逻辑计划和物理计划,过程和Hive SQL执行过程类似,只不过Spark SQL产生的物理计划为Spark程序。
输入编写的Spark SQL
SqlParser
分析器。进行语法检查、词义分析,生成未绑定的Logical Plan逻辑计划(未绑定查询数据的元数据信息,比如查询什么文件,查询那些列等)
Analyzer
解析器。查询元数据信息并绑定,生成完整的逻辑计划。此时可以知道具体的数据位置和对象,Logical Plan 形如from table -> filter column -> select 形式的树结构
Optimizer
优化器。选择最好的一个Logical Plan,并优化其中的不合理的地方。常见的例如谓词下推、剪枝、合并等优化操作
Planner
使用Planing Strategies将逻辑计划转化为物理计划,并根据最佳策略选择出的物理计划作为最终的执行计划
调用Spark Plan Execution
执行引擎执行Spark RDD任务
3)Flink SQL的执行原理
Flink SQL的执行原理和Hive以及Spark SQL的执行原理大同小异,均存在解析、校验、编译生成语法树、优化生成逻辑计划等步骤。
Parser:SQL解析
。底层通过JavaCC解析SQ语法,并将SQL解析为未经校验的AST语法树。
Validate:SQL校验
。这里会校验SQL的合法性,比如Schema、字段、数据类型等是否合法(SQL匹配程度),过程需要与sql存储的元数据结合查验。
Optimize:SQL优化
。Flink内部使用多种优化器,将前面步骤的语法树进一步优化,针对RelNode和生成的逻辑计划,随后生成物理执行计划。
Produce:SQL生成
。将物理执行计划生成在特定平台的可执行程序。
Execute:SQL执行
。执行SQL得到结果。
Flink背压是生产应用中常见的情况,当程序存在数据倾斜、内存不足状况经常会发生背压,我将从如下几个方面去分析。
1)Flink背压表现
1)运行开始时正常,后面出现大量Task任务等待
2)少量Task任务开始报checkpoint
超时问题
3)大量Kafka数据堆积,无法消费
4)Flink UI的BackPressure页面出现红色High
标识
2) 反压一般有哪些情况
一般可以细分两种情况:
当前Task
任务处理速度慢,比如task任务中调用算法处理等复杂逻辑,导致上游申请不到足够内存。
下游Task
任务处理速度慢,比如多次collect()输出到下游,导致当前节点无法申请足够的内存。
3) 频繁反压的影响是什么
频繁反压会导致流处理作业数据延迟增加,同时还会影响到Checkpoint
。
Checkpoint时需要进行Barrier
对齐,此时若某个Task出现反压
,Barrier流动速度会下降,导致Checkpoint变慢甚至超时,任务整体也变慢。
长期或频繁出现反压才需要处理,如果由于
网络波动
或者GC
出现的偶尔反压可以不必处理。
4)Flink的反压机制
背压时一般下游速度慢于上游速度,数据久积成疾
,需要做限流。但是无法提前预估下游实际速度,且存在网络波动情况。
需要保持上下游动态反馈,如果下游速度慢,则上游限速;否则上游提速。实现动态自动反压的效果。
下面看下Flink内部是怎么实现反压机制的。
1)每个TaskManager
维护共享Network BufferPool
(Task共享内存池),初始化时向Off-heap Memory
中申请内存。
2)每个Task创建自身的Local BufferPool
(Task本地内存池),并和Network BufferPool交换内存。
3)上游Record Writer
向 Local BufferPool申请buffer(内存)写数据。如果Local BufferPool没有足够内存则向Network BufferPool
申请,使用完之后将申请的内存返回Pool
。
4)Netty Buffer
拷贝buffer并经过Socket Buffer
发送到网络,后续下游端按照相似机制处理。
5)当下游申请buffer失败时,表示当前节点内存
不够,则逐层发送反压信号
给上游,上游慢慢停止数据发送,直到下游再次恢复。
5)反压如何处理
查看Flink UI界面,定位哪些Task出现反压问题
查看代码和数据,检查是否出现数据倾斜
如果发生数据倾斜,进行预聚合key或拆分数据
加大执行内存,调整并发度和分区数
其他方式。。。
由于篇幅有限,更多Flink反压内容请查看我的相关文章:万字趣解Flink背压
精准一次消费需要整个系统各环节均保持强一致性,包括可靠的数据源端
(数据可重复读取、不丢失) 、可靠的消费端
(Flink)、可靠的输出端
(幂等性、事务)。
Flink保持精准一次消费主要依靠checkpoint一致性快照
和二阶段提交
机制。
1)数据源端
Flink内置FlinkKafkaConsumer
类,不依赖于 kafka 内置的消费组offset管理,在内部自行记录并维护
kafka consumer 的offset
。
(1)管理offset(手动提交)并保存到checkpoint中
(2)FlinkKafkaConsumer API内部集成Flink的Checkpoint机制,自动实现精确一次的处理语义。
从源码中看到stateBackend
中把offset state恢复到restoredState,然后从fetcher拉取最新的offset数据,随后将offset存入到stateBackend中;最后更新xcheckpoint。
2)Flink消费端
Flink内部采用一致性快照机制
来保障Exactly-Once的一致性语义。
通过间隔时间自动执行一致性检查点
(Checkpoints)程序,b并异步插入barrier检查点分界线。整个流程所有的operator均会进行barrier对齐
->数据完成确认
->checkpoints状态保存
,从而保证数据被精确一次处理。
3)输出端
Flink内置二阶段事务提交
机制和目标源支持幂等写入。
幂等
写入就是多次写入会产生相同的结果,结果具有不可变性。在Flink中saveAsTextFile
算子就是一种比较典型的幂等写入。
二阶段提交
则对于每个checkpoint创建事务,先预提交数据到sink中,然后等所有的checkpoint全部完成后再真正提交请求到sink, 并把状态改为已确认,从而保证数据仅被处理一次。
为checkpoint创建事务,等到所有的checkpoint全部真正的完成后,才把计算结果写入到sink中。
Flink内置watermark机制,可在一定程度上允许数据延迟
程序可在watermark的基础上再配置最大延迟时间
开启侧输出流,将延迟的数据输出到侧输出流
程序内部控制,延迟过高的数据单独进行后续处理
Flink双流JOIN主要分为两大类。一类是基于原生State的Connect算子操作,另一类是基于窗口的JOIN操作。其中基于窗口的JOIN可细分为window join
和interval join
两种。
实现原理:底层原理依赖Flink的
State状态存储
,通过将数据存储到State中进行关联join, 最终输出结果。
1)基于Window Join的双流JOIN实现机制
通俗理解,将两条实时流中元素分配到同一个时间窗口中完成Join。两条实时流数据缓存在Window State
中,当窗口触发计算时执行join操作。
join算子操作
两条流数据按照关联主键在(滚动、滑动、会话)窗口内进行inner join
, 底层基于State存储,并支持处理时间和事件时间两种时间特征,看下源码:
windows窗口、state存储和双层for循环执行join()实现双流JOIN操作,但是此时仅支持inner join类型。
coGroup算子操作
coGroup算子也是基于window窗口机制,不过coGroup算子比Join算子更加灵活,可以按照用户指定的逻辑匹配左流或右流数据并输出,达到left join和right join的目的。
orderDetailStream
.coGroup(orderStream)
.where(r -> r.getOrderId())
.equalTo(r -> r.getOrderId())
.window(TumblingProcessingTimeWindows.of(Time.seconds(60)))
.apply(new CoGroupFunction>() {
@Override
public void coGroup(Iterable orderDetailRecords, Iterable orderRecords, Collector> collector) {
for (OrderDetail orderDetaill : orderDetailRecords) {
boolean flag = false;
for (Order orderRecord : orderRecords) {
// 右流中有对应的记录
collector.collect(new Tuple2<>(orderDetailRecords.getGoods_name(), orderDetailRecords.getGoods_price()));
flag = true;
}
if (!flag) {
// 右流中没有对应的记录
collector.collect(new Tuple2<>(orderDetailRecords.getGoods_name(), null));
}
}
}
})
.print();
2)基于Interval Join的双流JOIN实现机制
Interval Join根据右流相对左流偏移的时间区间(interval
)作为关联窗口,在偏移区间窗口中完成join操作。
满足数据流stream2在数据流stream1的 interval
(low, high)偏移区间内关联join。interval越大,关联上的数据就越多,超出interval的数据不再关联。
实现原理:interval join也是利用Flink的state存储数据,不过此时存在state失效机制
ttl
,触发数据清理操作。
val env = ...
// kafka 订单流
val orderStream = ...
// kafka 订单明细流
val orderDetailStream = ...
orderStream.keyBy(_.1)
// 调用intervalJoin关联
.intervalJoin(orderDetailStream._2)
// 设定时间上限和下限
.between(Time.milliseconds(-30), Time.milliseconds(30))
.process(new ProcessWindowFunction())
class ProcessWindowFunction extends ProcessJoinFunction...{
override def processElement(...) {
collector.collect((r1, r2) => r1 + " : " + r2)
}
}
订单流在流入程序后,等候(low,high)时间间隔内的订单明细流数据进行join, 否则继续处理下一个流。interval join目前也仅支持inner join。
3)基于Connect的双流JOIN实现机制
对两个DataStream执行connect操作,将其转化为ConnectedStreams, 生成的Streams可以调用不同方法在两个实时流上执行,且双流之间可以共享状态。
两个数据流被connect之后,只是被放在了同一个流中,内部依然保持各自的数据和形式,两个流相互独立。
[DataStream1, DataStream2] -> ConnectedStreams[1,2]
我们可以在Connect算子底层的ConnectedStreams中编写代码,自行实现双流JOIN的逻辑处理。
1)调用connect算子,根据orderid进行分组,并使用process算子分别对两条流进行处理。
orderStream.connect(orderDetailStream)
.keyBy("orderId", "orderId")
.process(new orderProcessFunc());
2)process方法内部进行状态编程, 初始化订单、订单明细和定时器的ValueState状态。
private ValueState orderState;
private ValueState orderDetailState;
private ValueState timeState;
// 初始化状态Value
orderState = getRuntimeContext().getState(
new ValueStateDescriptor
("order-state",Order.class));
····
3)为每个进入的数据流保存state状态并创建定时器。在时间窗口内另一个流达到时进行join并输出,完成后删除定时器。
@Override
public void processElement1(Order value, Context ctx, Collector> out){
if (orderDetailState.value() == null){
//明细数据未到,先把订单数据放入状态
orderState.update(value);
//建立定时器,60秒后触发
Long ts = (value.getEventTime()+10)*1000L;
ctx.timerService().registerEventTimeTimer(
ts);
timeState.update(ts);
}else{
//明细数据已到,直接输出到主流
out.collect(new Tuple2<>(value,orderDetailS
tate.value()));
//删除定时器
ctx.timerService().deleteEventTimeTimer
(timeState.value());
//清空状态,注意清空的是支付状态
orderDetailState.clear();
timeState.clear();
}
}
...
@Override
public void processElement2(){
...
}
4)未及时达到的数据流触发定时器输出到侧输出流,左流先到而右流未到,则输出左流,反之输出右连流。
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector> out) {
// 实现左连接
if (orderState.value() != null){
ctx.output(new OutputTag("left-jo
in") {},
orderState.value().getTxId());
// 实现右连接
}else{
ctx.output(new OutputTag("left-jo
in") {},
orderDetailState.value().getTxId());
}
orderState.clear();
orderDetailState.clear();
timeState.clear();
}
4)Flink双流JOIN问题处理总结
1)为什么我的双流join时间到了却不触发,一直没有输出
检查一下
watermark
的设置是否合理,数据时间
是否远远大于watermark和窗口时间,导致窗口数据经常为空
2)state数据保存多久,会内存爆炸吗
state自带有
ttl机制
,可以设置ttl过期策略,触发Flink清理过期state数据。建议程序中的state数据结构
用完后手动clear掉。
3)我的双流join倾斜怎么办
join倾斜三板斧: 过滤异常key、拆分表减少数据、打散key分布。当然可以的话我建议加内存!加内存!加内存!!
4)想实现多流join怎么办
目前无法一次实现,可以考虑先union然后再二次处理;或者先进行connnect操作再进行join操作,仅建议~
5)join过程延迟、没关联上的数据会丢失吗
这个一般来说不会,join过程可以使用侧输出流存储延迟流;如果出现节点网络等异常,Flink checkpoint也可以保证数据不丢失。
由于篇幅有限,更多Flink双流JOIN内容请查看我的相关文章:万字直通Flink双流JOIN面试
数据倾斜一般都是数据Key分配不均,比如某一类型key数量过多,导致shuffle过程分到某节点数据量过大,内存无法支撑。
1)数据倾斜可能的情况
那我们怎么发现数据倾斜了呢?一般是监控某任务Job执行情况,可以去Yarn UI或者Flink UI观察,一般会出现如下状况:
发现某subTask执行时间过慢
传输数据量和其他task相差过大
BackPressure页面出现反压问题(红色High标识)
结合以上的排查定位到具体的task中执行的算子,一般常见于Keyed类型算子:比如groupBy()、rebance()等产生shuffle过程的操作。
2)数据倾斜的处理方法
数据拆分。如果能定位的数据倾斜的key,总结其规律特征。比如发现包含某字符,则可以在代码中把该部分数据key拆分出来,单独处理后拼接。
key二次聚合。两次聚合,第一次将key加前缀聚合,分散单点压力;随后去除前缀后再次聚合,得到最终结果。
调整参数。加大TaskManager内存、keyby均衡等参数,一般效果不是很好。
自定义分区或聚合逻辑。继承分区划分、聚合计算接口,根据数据特征和自定义逻辑,调整数据分区并均匀打散数据key。
一般来说Flink可以开启exactly-once机制,可保证精准一次消费。但是如果存在数据处理过程异常导致数据重复,可以借助一些工具或者程序来处理。
建议数据量不大的话可以使用flink自身的state或者借助bitmap结构;稍微大点可以用布隆过滤器或hyperlog工具;其次使用外部介质(redis或hbase)设计好key就行自动去重,只不过会增加处理过程。
总结一下Flink的去重方式:
内存去重。采用Hashset
等数据结构,读取数据中类似主键等唯一性标识字段,在内存中存储并进行去重判断。
使用Redis Key
去重。借助Redis的Hset
等特殊数据类型,自动完成Key去重。
DataFrame/SQL场景,使用group by
、over()
、window
开窗等SQL函数去重
利用groupByKey等聚合算子去重
实时数仓数据规整为层级存储,每层独立加工。整体遵循由下向上建设思想,最大化数据赋能。
1)数仓分层设计
数据源: 分为日志数据
和业务数据
两大类,包括结构化和非结构化数据。
数仓类型:根据及时性分为离线
数仓和实时
数仓
采集(Sqoop、Flume、CDC)
存储(Hive、Hbase、Mysql、Kafka、数据湖)
加工(Hive、Spark、Flink)
OLAP查询(Kylin、Clickhous、ES、Dorisdb)等。
2)数仓架构设计
整体采用Lambda架构。保留实时、离线两条处理流程,即最终会同时构建实时数仓和离线数仓。
1. 技术实现
使用Flink和Kafka、Hive为主要技术栈
实时技术流程。通过实时采集程序同步数据到Kafka消息队列
Flink实时读取Kafka数据,回写到kafka ods
贴源层topic
Flink实时读取Kafka的ods层数据,进行实时清洗和加工,结果写入到kafka dwd
明细层topic
同样的步骤,Flink读取dwd层数据写入到kafka dws
汇总层topic
离线技术流程和前面章节一致
实时olap引擎查询分析、报表展示
2. 优缺点
两套技术流程,全面保障实时性和历史数据完整性
同时维护两套技术架构,维护成本高,技术难度大
相同数据源处理两次且存储两次,产生大量数据冗余和操作重复
容易产生数据不一致问题
3)数据流程设计
整体从上而下,数据经过采集
-> 数仓明细加工
、汇总
-> 应用
步骤,提供实时数仓服务。
这里列举用户分析的数据流程和技术路线:
采集用户行为数据,统计用户曝光点击信息,构建用户画像。
》》更多好文,请关注gzh:大数据兵工厂