1, flink和spark有什么区别?flink的优势体现在什么地方?
2, flink的checkpoint是怎么实现的?
3, flink on yarn的启动流程?
4, flink如何实现端到端的exactly-once?
5, 谈谈你对flink状态的认识?
6, 怎么合理的配置flink任务的资源?
7, flink的反压是怎么实现的?
8, flink的watermark是干什么的?具体怎么用?
9, flink的延迟高,怎么调优?
10, flink的双流join是怎么实现的?
Flink 原理与实现:内存管理
在本文中,分为以下几个部分:
第一部分:Flink 中的核心概念和基础篇,包含了 Flink 的整体介绍、核心概念、算子等考察点。
第二部分:Flink 进阶篇,包含了 Flink 中的数据传输、容错机制、序列化、数据热点、反压等实际生产环境中遇到的问题等考察点。
第三部分:Flink 源码篇,包含了 Flink 的核心代码实现、Job 提交流程、数据交换、分布式快照机制、Flink SQL 的原理等考察点。
这个问题是一个非常宏观的问题,因为两个框架的不同点非常之多。但是在面试时有非常重要的一点一定要回答出来:Flink 是标准的实时处理引擎,基于事件驱动。而 Spark Streaming 是微批(Micro-Batch)的模型
。
下面我们就分几个方面介绍两个框架的主要区别:
处理时间
。Flink支持与维表进行join操作,除了map,flatmap这些算子之外,flink还有异步IO算子,可以用来实现维表,提升性能。
因为flink无论是批处理还是流处理,底层都是有状态的流处理,flink执行批处理实际上是流处理的一种特例,只不过此时的流式有界的,而流处理的流式无界的,应用于流处理上的transformation完全可以应用在batch上并且table API和sql都可以用在批处理和流处理上只不过区别在于
a. 容错并不是采用的流式处理的checkpoint,而是直接重新计算
b. dataset api处理的数据是很简单的数据结构,而stream处理的是key/value
c. 流处理在应用transformation和table api和sql的时候不支持topN、limit、sort普通字段等操作
另外从计算模型上来说:批处理每个stage只有完全处理完才会把缓存中(缓存+磁盘)序列化的数据发往下一个stage,而流处理是一条一条,批处理吞吐量大,流处理时效性强,而flink则是采用了折中的方式,在内存中划分缓冲小块,当小块满了就发往下一个stage。如果缓存块无限大,那么就是批处理了。
本道面试题考察的其实就是一句话:Flink的开发者认为**批处理
是流处理
**的一种特殊情况。批处理是有限的流处理。Flink 使用一个引擎支持了DataSet API 和 DataStream API。
大家注意,这个问题看起来是问你实际应用中的Flink集群规模,其实还隐藏着另一个问题:Flink可以支持多少节点的集群规模?
在回答这个问题时候,可以将自己生产环节中的集群规模、节点、内存情况说明,同时说明部署模式(一般是Flink on Yarn),除此之外,用户也可以同时在小集群(少于5个节点)和拥有 TB 级别状态的上千个节点上运行 Flink 任务。
上图是来自Flink官网的运行流程图。通过上图我们可以得知,Flink 程序的基本构建是数据输入来自一个 Source,Source 代表数据的输入端,经过 Transformation 进行转换,然后在一个或者多个Sink接收器中结束。数据流(stream)就是一组永远不会停止的数据记录流,而转换(transformation)是将一个或多个流作为输入,并生成一个或多个输出流的操作。执行时,Flink程序映射到 streaming dataflows,由流(streams)和转换操作(transformation operators)组成。
Flink 程序在运行时主要有 TaskManager,JobManager,Client三种角色。
JobManager扮演着集群中的管理者Master的角色,它是整个集群的协调者,负责接收Flink Job
,协调检查点
,Failover 故障恢复
等,同时管理Flink集群中从节点TaskManager。
a. JobManager 接收待执行的 application。application 包含一个 JobGraph 和 JAR (包含所有需要的classes,libraries 和其他资源)。
b. JobManager 将 JobGraph 转成 ExecutionGraph,ExecutionGraph中包含可以并发执行的 tasks。
c. JobManager 向 ResourceManager 申请需要的资源(TaskManager slots),一旦分配到足够的slots,则分发 tasks 到 TaskManager 执行。
d. 执行期间,JobManager 负责中央协调,如协调checkpoint等
TaskManager是实际负责执行计算的Worker,在其上执行Flink Job的一组Task,每个TaskManager负责管理其所在节点上的资源信息,如内存、磁盘、网络,在启动的时候将资源的状态向JobManager汇报。
a. 启动之后,TaskManager 向 ResourceManager 注册 slots 数,当接收到 ResourceManager 的分配通知后,会向 JobManager 提供一个或多个slots
b. 紧接着 JobManager 将 tasks 分配到 slots 执行。
c. 执行期间,不同的 TaskManager 之间会进行数据交换
Client是Flink程序提交的客户端,当用户提交一个Flink程序时,会首先创建一个Client,该Client首先会对用户提交的Flink程序进行预处理,并提交到Flink集群中处理,所以Client需要从用户提交的Flink程序配置中获取JobManager的地址,并建立到JobManager的连接,将Flink Job提交给JobManager。
主从结构 Jobmanager,taskmanager两个进程(可以把client也加进去)。
集群模式:standalone,on yarn(在yarn上运行一个flink集群/提交到yarn上运行flink job)
client对用户提交的代码进行预处理,client将程序组装成一个 jobgraph,它是由多个jobvertex组成的DAG。
关于flink生成dag、提交job、分发task等细节 在任务提交面试题会整理。
根据 Flink 官网描述,Flink 是一个分层架构的系统,每一层所包含的组件都提供了特定的抽象,用来服务于上层组件。
自下而上,每一层分别代表:
JobManager 负责整个 Flink 集群任务的调度
以及资源的管理
,从客户端中获取提交的应用,然后根据集群中 TaskManager 上 TaskSlot 的使用情况,为提交的应用分配相应的 TaskSlot 资源并命令 TaskManager 启动从客户端中获取的应用。
JobManager的职责主要是接收Flink作业,调度Task,收集作业状态和管理TaskManager。它包含一个Actor,并且做如下操作:
TaskManager 相当于整个集群的 Slave 节点,负责具体的任务执行
和对应任务在每个Node上的资源申请
和管理
。
可以看出,Flink 的任务运行其实是采用多线程的方式,这和 MapReduce 多 JVM 进行的方式有很大的区别,Flink 能够极大提高 CPU 使用效率,在多个任务和 Task 之间通过 TaskSlot 方式共享系统资源,每个 TaskManager 中通过管理多个 TaskSlot 资源池进行对资源进行有效管理。
TaskManager的启动流程较为简单:
启动类:org.apache.flink.runtime.taskmanager.TaskManager
核心启动方法 : selectNetworkInterfaceAndRunTaskManager 启动后直接向JobManager注册自己,注册完成后,进行部分模块的初始化。
我们要知道一般来说在使用一个类的时候,一般是要创建对象的,所以我们在sql里使用UDF的时候会创建对象,如果是多线程并行操作sql,那么就是多个UDF对象。那么如何保证一个executer进程中共享一个UDF呢,在scala中就用Object即可。如果是class就写一个单例模式,关于单例模式算法题中我会详细整理!
面试官说:只有当不会改变DAG的修改才会正常恢复!!!有机会试一下。
Flink可以完全独立于Hadoop,在不依赖Hadoop组件下运行。但是做为大数据的基础设施,Hadoop体系是任何大数据框架都绕不过去的。Flink可以集成众多Hadooop 组件,例如Yarn、Hbase、HDFS等等。例如,Flink可以和Yarn集成做资源调度,也可以读写HDFS,或者利用HDFS做检查点。
Flink 实现容错主要靠强大的 CheckPoint机制
和 State机制
。
flink是通过checkpoint机制实现容错,它的原理是不断的生成分布式streaming数据流snapshot快照。在流处理失败时通过这些snapshot可以恢复数据流处理。而flink的快照有两个核心:
barrier 机制
:barrier是实现checkpoint的机制。state 状态保存
:state保存则是通过barrier这种机制进行 分布式快照
的实现。barrier是checkpoint的核心,他会当做记录
打入数据流
,从而将数据流分组
,并沿着数据流方向向前推进
,每个barrier会携带一个snapshotID
,属于该snapshot的记录会被推向该barrier的前方。所以barrier之后的属于下一个ckeckpoint期间(snapshot中)的数据。然后当中间的operation接收到barrier后,会发送barrier到属于该barrier的snapshot的数据流中,等到sink operation接收到该barrier后会向checkpoint coordinator确认该snapshot,直到所有的sink operation都确认了该snapshot,才会认为完成了本次checkpoint或者本次snapshot。
理解:可以认为barrier这种机制是flink实现分布式快照的手段。那么在这个过程中要记录state快照信息,到底有哪些信息需要序列话呢?
在说state保存之前我们要知道flink的三种方式,
同步
进行分布式快照);异步
进行分布式快照)。除了第3种其他两种都是同步快照。也就是说用hdfs
这种方式快照是会阻塞数据处理
的,只有当两个barrier之间数据处理完成并完成快照之后才向下一个task发送数据并打入barrier n。我们不管异步快照,我们现在只说同步快照。
state状态保存分为两种:
用户自定义状态
:也就是我们为了实现需求敲的代码(算子),他们来创建和修改的state;系统状态
:此状态可以认为数据缓冲区,比如window窗口函数,我们要知道数据处理的情况。生成的快照现在包含:
barrier k对齐
)这个情况出现的很少,用于解决同一个Operation处理多个输入流
的情况(不是同一个数据源),这种情况下operation将先收到barrier k
的数据缓存起来不进行处理,只有当另一个流的barrier k
到达之后再进行处理,同时opeartion会向checkpoint coordinator
上报snapshot。这就是barrier k对齐
。
spark的checkpoint的方式没有这么复杂,直接通过记录metadata和data的方式来进行checkpoint。从checkpoint中恢复时ss是决不允许修改代码的,而sss是有些情况可以接受修改代码的。
a. metadata checkpoint
将定义流式计算的信息保存到hdfs:配置、dstream操作、尚未完成的批次
b. data checkpoint
这就比较直接了,直接持久化RDD到hdfs,因为我们知道spark的容错就是基于rdd的血缘关系的,而为了避免依赖关系链太长,spark会定期从最新的rdd中持久化数据到hdfs。
注意:::如果spark程序中没有updateStateByKey或reduceByKeyAndWindow这种带有状态持续改变的算子操作的时候完全可以不用对rdd进行持久化,只需要利用metadata来恢复程序即可,因为数据的丢失时可以接受的,但是如果存在状态转换的算法就不行了。
Flink的分布式快照是根据Chandy-Lamport算法量身定做的。简单来说就是持续创建分布式数据流
及其状态
的一致快照。
核心思想:是在 input source 端插入 barrier
,控制 barrier 的同步来实现 snapshot 的备份 和 exactly-once 语义。
Flink通过实现 两阶段提交
和 状态保存
来实现 端到端
的一致性语义。 分为以下几个步骤:
若失败发生在预提交成功后,正式提交前。可以根据状态来提交预提交的数据,也可删除预提交的数据。
可以从两方面阐述:
第一:flink的checkpoint机制可以保证at least once消费语义
第二:flink的两段式提交commit保证了端对端的exactly once消费语义(TwoPhaseCommitSinkFunction)
尤其是在kafka0.11版本开始,支持两段式提交
Flink1.4之前只能在flink内存保证exactly once语义,但是很多时候flink要对接其他系统,那么就要实现commit提交和rollback回滚机制,而分布式系统中两段提交和回滚就是实现方式。因为很多算子包括sink都是并行的,我们不能通过sink的一次commit就完成了最终的commit,因为假如有10的sink,其中9个sink commit了第十个失败了,那么这个过程我们还是无法回滚!!所以需要分布式两段提交策略。
所谓pre-commit指的是第一阶段,也就是checkpoint阶段完成时进行pre-commit,如果所有的pre-commit成功,jobmanager会通知所有跟外部系统有联系的比如sink,通知他们进行第二阶段的commit!这就是两段式提交实现的flink的exactly once消费语义。
开启 Checkpoint 机制主要是为了实现 实时任务处理的容错
。
实时任务不同于批处理任务,除非用户主动停止,一般会一直运行,运行的过程中可能存在机器故障、网络问题、外界存储问题等等,要想实时任务一直能够稳定运行,实时任务要有自动容错恢复的功能。
而批处理任务在遇到异常情况时,在重新计算一遍即可。实时任务因为会一直运行的特性,如果在从头开始计算,成本会很大,尤其是对于那种运行时间很久的实时任务来说。
实时任务开启 Checkpoint 功能,也能够减少容错恢复的时间。因为每次都是从最新的 Chekpoint 点位开始状态恢复,而不是从程序启动的状态开始恢复。
Flink Checkpoint 失败有很多种原因,常见的失败原因如下:
从目前的具体实践情况来看,Flink Checkpoint 异常觉大多数还是用户代码逻辑的问题,对于程序异常没有正确的处理导致。所以在编写 Flink 实时任务时,一定要注意处理程序可能出现的各种异常。这样,也会让实时任务的逻辑更加的健壮。
checkpoint 的执行间隔要根据实际业务情况配置,checkpoint次数据太频繁,容易给后端系统造成压力。checkpoint 间隔时间太久,状态数据恢复时间较长。
checkpoint执行成功后,会自动删除之前保存的 checkpoint 数据。有多少个 SubTask就会生成多少个 checkpoint 文件。每个 SubTask 保存自己的数据。
自下而上
的背压检测
从而 控制流量。如果下层的operation压力大那么上游的operation就会让它慢下来。Jobmanager会反复调用一个job的task运行所在线程的Thread.getStackTrace(),默认情况下,jobmanager会每个50ms触发对一个job的每个task依次进行100次堆栈跟踪调用,根据调用结果来确定backpressure,flink是通过计算得到一个比值radio来确定当前运行的job的backpressure状态。在web页面可以看到这个radio值,它表示在一个内部方法调用中阻塞的堆栈跟踪次数,例如radio=0.01表示100次中仅有一次方法调用阻塞。数据倾斜
:在实践中,很多情况下的反压是由于数据倾斜造成的,这点我们可以通过 Web UI 各个 SubTask 的 Records Sent 和 Record Received 来确认,另外 Checkpoint detail 里不同 SubTask 的 State size 也是一个分析数据倾斜的有用指标。
用户代码执行效率
:此外,最常见的问题可能是用户代码的执行效率问题(频繁被阻塞或者性能问题)。最有用的办法就是对 TaskManager 进行 CPU profile,从中我们可以分析到 Task Thread 是否跑满一个 CPU 核:如果是的话要分析 CPU 主要花费在哪些函数里面,比如我们生产环境中就偶尔遇到卡在 Regex 的用户函数(ReDoS);如果不是的话要看 Task Thread 阻塞在哪里,可能是用户函数本身有些同步的调用,可能是 checkpoint 或者 GC 等系统活动导致的暂时系统暂停。
数据量过大,资源不足
:当然,性能分析的结果也可能是正常的,只是作业申请的资源不足而导致了反压,这就通常要求拓展并行度。值得一提的,在未来的版本 Flink 将会直接在 WebUI 提供 JVM 的 CPU 火焰图[5],这将大大简化性能瓶颈的分析。
TaskManager 的内存以及 GC 问题也可能会导致反压
,包括 TaskManager JVM 各区内存不合理导致的频繁 Full GC 甚至失联。推荐可以通过给 TaskManager 启用 G1 垃圾回收器来优化 GC,并加上 -XX:+PrintGCDetails 来打印 GC 日志的方式来观察 GC 的问题。
在Flink的后台任务管理中,我们可以看到Flink的 哪个算子
和 task
出现了反压。最主要的手段是 资源调优
和 算子调优
。
Flink 内部是基于 producer-consumer 模型来进行消息传递的,Flink的反压设计也是基于这个模型。Flink 使用了高效有界的分布式阻塞队列,就像 Java 通用的阻塞队列(BlockingQueue)一样。下游消费者消费变慢,上游就会受到阻塞。
Per-Job 一个 Job 对应一个 JobManager。这种模式需要的资源比较多,JobManager 频繁的做checkpoint的,通常是实时任务。
flink-version/bin/flink run -m yarn-cluster
启动 Client。文件
上传到HDFS上去,TaskManager在启动的过程中也会去下载这个文件获取JobManager的地址,然后与JobManager进行通信。ApplicationMaster
启动后,ApplicationMaster
向 ResourceManager
申请资源,假如一个 TaskManager 中启动4个 TaskSlot,7个并行度,启动2个 TaskManager就可以了。ResourceManager
分配Container资源后,由ApplicationMaster
通知NodeManager
启动TaskManager
,NodeManager 从 HDFS 中下载 Jar 包和配置,然后启动 TaskManager,TaskManager 的进程名称是YarnTaskExecutorRunner
。今天说一下另一种非精准去重:基数估计(概率算法)
关于上述的东西涉及到概率的很多东西,等以后真的用到再说吧。
在Flink架构角色中我们提到,TaskManager是实际负责执行计算的Worker
,TaskManager 是一个 JVM 进程,并会以独立的线程来执行一个task或多个subtask。为了控制一个 TaskManager 能接受多个 task,Flink 提出了 Task Slot 的概念。简单的说,TaskManager会将自己节点上管理的资源分为不同的Slot:固定大小的资源子集
。这样就避免了不同Job的Task互相竞争内存资源,但是需要主要的是,**Slot
只会做内存
**的隔离。没有做CPU的隔离。
Flink 最常用的常用算子包括:
Slot 是指 TaskManager 最大能并发执行 的能力。
parallelism 是指 TaskManager 实际使用的并发能力。也是 Flink-Job 的实际并发能力。
在 Spark 中是通过 Shuffle,即依赖关系 来划分 Stage , 宽依赖肯定是分划分Stage,而 Flink 通过 算子操作
来划分 Stage,Task 主要有4种 方式:
并行度 parallelism
发生改变的时候触发。Task (Spark :-> Stage)
。Flink 中 TaskSlot 默认的名子是 default
。
可以通过slotSharingGroup()
方法指定 TaskSlot 槽位的名称。
如果在某个DataStream后改变了资源槽名称,后续的DataStream在没有改回default前,所有DataStream的资源槽名称将与改变的资源槽保持一致。
资源槽名称不同的 subTask 不能在同一个 TaskSlot 中运行;
一个 TaskSlot 中只能运行:
分区策略是用来决定数据如何发送至下游。目前 Flink 支持了8中分区策略的实现。
上图是整个Flink实现的分区策略继承图:
上游数据
输出到下游算子
的每个实例(本人测试是每个 Task中)
中。适合于大数据集和小数据集做Jion的场景。用户自定义分区器
。需要用户自己实现Partitioner接口,来定义自己的分区逻辑。例如:记录
输出到下游本地
的算子实例。它要求上下游算子并行度一样。简单的说,ForwardPartitioner用来做数据的控制台打印。循环发送
到下游的每一个实例Task
中进行处理。随机分发
到下游算子
的每一个实例
中进行处理。static class CustomPartitioner implements Partitioner {
@Override
publicintpartition(String key, int numPartitions) {
switch (key){
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
default:
return 4;
}
}
}
Flink的Application 被分为多个并行任务来执行,其中每个并行的实例处理一部分数据。这些并行实例的数量被称为并行度。
我们在实际生产环境中可以从四个不同层面设置并行度:
需要注意的优先级:算子层面>环境层面>客户端层面>系统层面
。
Flink 中默认并行度Parallel是1,可以在flink-conf.yaml中设置全局的并行度。也可能通过 env.setParallelism(10);
来设置并行度,这样设置的并行度是针对本 Job 的,后续没有算子特意更改的话,会应用整个 Flink-Job。但 Flink 也支持针对某个单独的Operator设置并行度。
优先级是什么样的子?
配置文件默认并行度 < env 设置并行度 < 算子设置并行度
slot是指taskmanager的并发执行能力,假设我们将 taskmanager.numberOfTaskSlots
配置为3 那么每一个 taskmanager 中分配3个 TaskSlot, 3个 taskmanager 一共有9个TaskSlot。
parallelism
是指 taskmanager
实际使用的并发能力。假设我们把 parallelism.default 设置为1,那么9个 TaskSlot 只能用1个,有8个空闲。
Flink 实现了多种重启策略。
Flink实现的分布式缓存和Hadoop有异曲同工之妙。目的是在本地读取文件,并把他放在 taskmanager 节点中,防止task重复拉取
。
val env = ExecutionEnvironment.getExecutionEnvironment
// register a file from HDFS
env.registerCachedFile("hdfs:///path/to/your/file", "hdfsFile")
// register a local executable file (script, executable, ...)
env.registerCachedFile("file:///path/to/exec/file", "localExecFile", true)
// define your program and execute
val input: DataSet[String] = ...
val result: DataSet[Integer] = input.map(new MyMapper())
env.execute()
我们知道Flink是并行的,计算过程可能不在一个 Slot 中进行,那么有一种情况即:当我们需要访问同一份数据。那么Flink中的广播变量就是为了解决这种情况。
我们可以把广播变量理解为是一个公共的共享变量,我们可以把一个dataset 数据集广播出去,然后不同的task在节点上都能够获取到,这个数据在 每个节点
上只会存在一份。
Broadcast State 是 Flink 支持的另一种扩展方式,Broadcast State 将流数据广播到下游所有的 Task (All-Task)
中,数据会全部存储在下流 Task 内存中,接收到 Broadcast 流的正常流可以利用这些数据,一般是广播系统配置,或动态规则。
Broadcast 流中的数据是可以动态修改的。
正常流
是 keyedStream,需要实现 KeyedBroadcastProcessFunction。public abstract class KeyedBroadcastProcessFunction extends BaseBroadcastProcessFunction {
public abstract void processElement(final IN1 value, final ReadOnlyContext ctx, final Collector out) throws Exception;
public abstract void processBroadcastElement(final IN2 value, final Context ctx, final Collector out) throws Exception;
}
正常流
是non-keyedStream,需要实现BroadcastProcessFunctionpublic abstract class BroadcastProcessFunction extends BaseBroadcastProcessFunction {
public abstract void processElement(final IN1 value, final ReadOnlyContext ctx, final Collector out) throws Exception;
public abstract void processBroadcastElement(final IN2 value, final Context ctx, final Collector out) throws Exception;
}
上面两个接口暂且称为:广播者
,与 非广播者
。它们的不同之处在于: 广播者对 Broadcast 有读,写权限。而非广播者
只有读权限。这样主要是为了保证Broadcast state 在算子的所有并行实例中是一样的。由于 Flink 中没有跨任务的通信机制,在一个任务实例中的修改不能在并行任务间传递,而广播端在所有并行任务中都能看到相同的数据元,只对广播端提供可写的权限。同时要求在广播端的每个并行任务中,对接收数据的处理是相同的。如果忽略此规则会破坏 State 的一致性保证,从而导致不一致且难以诊断的结果。也就是说,processBroadcast() 的实现逻辑必须在所有并行实例中具有相同的确定性行为。
参看 Flink__20__Broadcast & Broadcast State
。
广播流用connect
方式连接。
Flink在做计算的过程中经常需要存储中间状态,来避免数据丢失和状态恢复。选择的状态存储策略不同,会影响状态持久化如何和 checkpoint 交互。
Flink提供了三种状态存储方式:
同步
进行分布式快照)同步
进行分布式快照)异步
进行分布式快照)RocksDBStateBackend
除了第3种其他2种都是同步快照
。也就是说用hdfs这种方式快照是会阻塞数据处理的,只有当两个barrier之间数据处理完成并完成快照之后才向下一个task发送数据并打入barrier n。我们不管异步快照,我们现在只说同步快照。
Flink 中的时间和其他流式计算系统的时间一样分为三类:事件时间
,摄入时间
,处理时间
三种。
① Processing time:根据task所在节点的本地时间来切分时间窗口
② event time:消息自带时间戳,但是这种时间是有延时的,也就是乱序的,为了防止同一个窗口的message被正确处理,所以需要其他方法如watermark,说白了就是给一个延时容忍度,然后根据watermark来判断窗口的划分,然后再根据trigger的类型判断什么时候进行计算
③ ingestion time:有的消息本身不携带时间戳,但是用户依然希望按照消息而不是节点时钟划分窗口,在message进入flink的时候给他一个递增的时间,是event time的一种特例,用的很少
EventTime Window
计算提出的一种机制,本质上是**一种时间戳
**。 一般来讲Watermark经常和 EventTime,Window一起被用来处理乱序事件。事件
一个级别的抽象,其内部包含一个成员变量时间戳timestamp,标识当前数据的时间进度
。Watermark实际上作为数据流的一部分随数据流流动。@Internal
public abstract class StreamElement {
/**
* Checks whether this element is a watermark.
* @return True, if this element is a watermark, false otherwise.
*/
public final boolean isWatermark() {
return getClass() == Watermark.class;
}
/**
* Checks whether this element is a stream status.
* @return True, if this element is a stream status, false otherwise.
*/
public final boolean isStreamStatus() {
return getClass() == StreamStatus.class;
}
/**
* Checks whether this element is a record.
* @return True, if this element is a record, false otherwise.
*/
public final boolean isRecord() {
return getClass() == StreamRecord.class;
}
/**
* Checks whether this element is a latency marker.
* @return True, if this element is a latency marker, false otherwise.
*/
public final boolean isLatencyMarker() {
return getClass() == LatencyMarker.class;
}
/**
* Casts this element into a StreamRecord.
* @return This element as a stream record.
* @throws java.lang.ClassCastException Thrown, if this element is actually not a stream record.
*/
@SuppressWarnings("unchecked")
public final StreamRecord asRecord() {
return (StreamRecord) this;
}
/**
* Casts this element into a Watermark.
* @return This element as a Watermark.
* @throws java.lang.ClassCastException Thrown, if this element is actually not a Watermark.
*/
public final Watermark asWatermark() {
return (Watermark) this;
}
/**
* Casts this element into a StreamStatus.
* @return This element as a StreamStatus.
* @throws java.lang.ClassCastException Thrown, if this element is actually not a Stream Status.
*/
public final StreamStatus asStreamStatus() {
return (StreamStatus) this;
}
/**
* Casts this element into a LatencyMarker.
* @return This element as a LatencyMarker.
* @throws java.lang.ClassCastException Thrown, if this element is actually not a LatencyMarker.
*/
public final LatencyMarker asLatencyMarker() {
return (LatencyMarker) this;
}
}
Flink 引入了 WaterMark机制 ,再加上EventTime 完美的解决了 数据乱序
的问题。
当基于 EventTime 的数据流进入窗口时,最困难的一点是 如何确定对应窗口时间的所有数据都已经到达了
。然后实际上并不能百分百的准确判断,因此业界常用的方法是: 基于已经收集的消息来估算是不是还有新的消息,这就是 WaterMark 的思想
。是 WaterMark 触发发窗口的计算。
WaterMark 是一种衡量 EventTime 进展的机制, 它是数据本身隐藏属性
,数据本身携带着对应的 WaterMark。WaterMark 本身就是一个时间戳
,代表着比这时间早的事件已经全部到达窗口,也就是: 假设不会再有比这个时间还小的事件到达
。这个假设是触发窗口计算的基础,只有 WaterMark 大于窗口的结束时间
,窗口才会关闭,进行计算。
public final class Watermark extends StreamElement {
/** The watermark that signifies end-of-event-time. */
public static final Watermark MAX_WATERMARK = new Watermark(Long.MAX_VALUE);
// ------------------------------------------------------------------------
/** The timestamp of the watermark in milliseconds. */
private final long timestamp;
/**
* Creates a new watermark with the given timestamp in milliseconds.
*/
public Watermark(long timestamp) {
this.timestamp = timestamp;
}
/**
* Returns the timestamp associated with this {@link Watermark} in milliseconds.
*/
public long getTimestamp() {
return timestamp;
}
// ------------------------------------------------------------------------
@Override
public boolean equals(Object o) {
return this == o ||
o != null && o.getClass() == Watermark.class && ((Watermark) o).timestamp == this.timestamp;
}
@Override
public int hashCode() {
return (int) (timestamp ^ (timestamp >>> 32));
}
@Override
public String toString() {
return "Watermark @ " + timestamp;
}
}
在 Flink 内部,Flink 默认开启了多个窗口
,来接收需要的数据。把相对应的数据放到 指定的窗口中。
A. AssignerWithPeriodicWatermarks:
周期性的(一时的时间间隔或达到一定的记录条数)产生一个 WaterMark。可通过env.getConfig().setAutoWatermarkInterval()
进行修改。在实际生产环境中使用比较多,会周期性的产生 WaterMark,但是必须结合时间
或数据累积条数
两个维度,否则在极端情况下会有较大的延迟。
使用这种方式生成水印可以指定:
env.getConfig().setAutoWatermarkInterval(...);
设置生成水印的间隔(毫秒)。@Override
public void run(SourceContext ctx) throws Exception {
while (/* condition */) {
MyType next = getNext();
ctx.collectWithTimestamp(next, next.getEventTimestamp());
if (next.hasWatermarkTime()) {
ctx.emitWatermark(new Watermark(next.getWatermarkTime()));
}
}
}
DateStream.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks>() {
long currentMaxTimestamp = 0L;
long maxOutOfOrderness = 2000L;
Watermark watermark = null;
//最大允许的乱序时间是10s
@Override
public Watermark getCurrentWatermark() {
watermark = new Watermark(currentMaxTimestamp - maxOutOfOrderness);
return watermark;
}
@Override
public long extractTimestamp(Tuple4 element,
long previousElementTimestamp) {
currentMaxTimestamp = Math.max(element.f1, previousElementTimestamp);
System.out.println(" Assigner - extractTimestamp - transaction = " + element.f1 + ", previousElementTimestamp = " + previousElementTimestamp);
return element.f1;
}
});
extractTimestamp
方法是从 数据本身 提取时间 EventTime,该方法会返回之前时间戳 previousElementTimestamp
与数据的 EventTime
的比较,如果 数据的 EventTime
比 previousElementTimestamp 之前的时间戳
大,则返回数据的 EventTime
。getCurrentWatermark()
方法是获取当前WaterMark,maxOutOfOrderness
是2000毫秒,表示允许延迟的最大时间,来了超过2000毫秒的数据,Flink 就会丢弃了。
B. AssignerWithPunctuatedWatermarks
数据流中每一个递增的 EventTime 都会产生一个 WaterMark。在实际生产环境中,在 TPS很高的情况下会产生大量的 WaterMark,可能会在一定程序上对下流算子造成一定的压力,所以一般只在实时性要求很高的场景才会选择这种方式。
checkAndGetNextWatermark
方法在 extractTimestamp()
方法被调用后调用,它可以决定是否要生成一个新水印,返回的水印只有在不为 null,并且大于
先前返回到系统的水印时间戳的时候才会发送出去,如果返回水印是 null,或者 返回的水印戳比之前返回的小,则不生成新的水印。
**注意**: 这种生成机制可以为每个事件生成一个水印,但因为水印要在下游参与计算的,所以过多的话会导致整体计算性能下降
。DateStream.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks>() {
@Nullable
@Override
public Watermark checkAndGetNextWatermark(Tuple4 lastElement, long extractedTimestamp) {
return null;
}
@Override
public long extractTimestamp(Tuple4 element, long previousElementTimestamp) {
return 0;
}
})
A. 丢弃
: 在 Flink 中,对这种延迟数据的默认处理方式是丢弃。
B. allowedLateness
: 再次指定允许数据延迟的时间。
allowedLateness
表示允许数据延迟的时间,这个方法是在 WindowedStream 中的,用来设置允许窗口数据延迟的时间,超过这个时间的元素就会被丢弃,这个的默认值是 0,该设置仅针对于以EventTime
开的Window
。
那 WaterMark 允许数据延迟时间与这个数据延迟的区区别是:allowedLateness
允许延迟时间是在 Watermark 允许延迟时间的基础上增加的时间。
所谓延迟数据,即窗口已经因为watermark进行了触发,则在此之后如果还有数据进入窗口,则默认情况下不会对
窗口Window
进行再次触发和聚合计算。要想在数据进入已经被触发过的窗口后,还能继续触发窗口计算,则可以使用延迟数据处理机制。
第二次(或多次)触发
的条件是 watermark < window_end_time + allowedLateness
,只要满足该条件,延迟数据已进入窗口就会触发窗口计算。dataStream.assignTimestampsAndWatermarks(new TestWatermarkAssigner())
.keyBy(new TestKeySelector())
.timeWindow(Time.milliseconds(1), Time.milliseconds(1))
.allowedLateness(Time.milliseconds(2)) //表示允许再次延迟 2 毫秒
.apply(new WindowFunction() {
//计算逻辑
});
C. sideOutputLateData
: 收集迟到的数据
sideOutputLateData 这个方法同样是 WindowedStream 中的方法,该方法会将延迟的数据发送到给定 OutputTag 的 side output 中去,然后你可以通过 SingleOutputStreamOperator.getSideOutput(OutputTag) 来获取这些延迟的数据。具体的操作方法如下:
//定义 OutputTag
OutputTag lateDataTag = new OutputTag("late"){};
SingleOutputStreamOperator windowOperator = dataStream
.assignTimestampsAndWatermarks(new TestWatermarkAssigner())
.keyBy(new TestKeySelector())
.timeWindow(Time.milliseconds(1), Time.milliseconds(1))
.allowedLateness(Time.milliseconds(2))
.sideOutputLateData(lateDataTag) //指定 OutputTag
.apply(new WindowFunction() {
//计算逻辑
});
windowOperator.addSink(resultSink);
// 通过指定的 OutputTag 从 Side Output 中获取到延迟的数据之后,你可以通过 addSink() 方法存储下来,这样可以方便你后面去排查哪些数据是延迟的。
windowOperator.getSideOutput(lateDataTag).addSink(lateResultSink);
TableEnvironment是Table API和SQL集成的核心概念。
这个类主要用来:
基于此,一次完整的SQL解析过程如下:
Flink 将 SQL 校验、SQL 解析以及 SQL 优化交给了Apache Calcite。Calcite 在其他很多开源项目里也都应用到了,譬如 Apache Hive, Apache Drill, Apache Kylin, Cascading。Calcite 在新的架构中处于核心的地位,如下图所示。
构建抽象语法树的事情交给了 Calcite 去做。SQL query 会经过 Calcite 解析器转变成 SQL 节点树,通过验证后构建成 Calcite 的抽象语法树(也就是图中的 Logical Plan)。另一边,Table API 上的调用会构建成 Table API 的抽象语法树,并通过 Calcite 提供的 RelBuilder 转变成 Calcite 的抽象语法树。然后依次被转换成逻辑执行计划和物理执行计划。在提交任务后会分发到各个 TaskManager 中运行,在运行时会使用 Janino 编译器编译代码后运行
在一个Flink Job中,数据需要在不同的task中进行交换,整个数据交换是有 TaskManager
负责的,TaskManager 的网络组件首先从缓冲buffer中收集records,然后再发送。Records 并不是一个一个被发送的,二是积累一个批次再发送,batch 技术可以更加高效的利用网络资源。
Flink源码中有一个独立的 connector模块,所有的其他connector都依赖于此模块,Flink 在1.9版本发布的全新kafka连接器,摒弃了之前连接不同版本的kafka集群需要依赖不同版本的connector这种做法,只需要依赖一个connector即可。
可以在处理前加一个fliter算子,将不符合规则的数据过滤出去。
Flink 并不是将大量对象存在堆上,而是将对象都 序列化
到一个 预分配
的内存块上。此外,Flink大量的使用了堆外内存
。如果需要处理的数据超出了内存限制,则会将部分数据存储到硬盘上。Flink 为了直接操作二进制数据实现了自己的序列化框架。
理论上Flink的内存管理分为三部分:
taskmanager.network.numberOfBuffers
修改。用户代码
以及 TaskManager 的数据结构
使用的。①自带的序列化工具:序列化后的对象就是字节数组是连续存储,占用空间大大降低。又例如cpu多级缓存的命中,避免oom
使用定制的序列化工具前提是待处理的数据类型一样,这样可以再内存中存一份共享的schema,并且在操作对象时不用反序列化整个对象,而是根据字节数组的偏移量来反序列化一部分——访问对象的成员变量。
②显式内存管理:批量的申请内存和释放。避免了频繁申请释放导致的内存碎片和资源消耗,减少垃圾回收次数
③off-heap的使用:off-heap有三个特点:off-heap的数据可以与其他程序共享;off-heap的数据进行磁盘IO或者网络IO的时候支持zero-copy(零拷贝)技术,不需要至少一次的内存拷贝;off-heap可想而知可以延缓gc回收。
你既然提到了zero-copy(零拷贝)技术,你能详细说说什么是零拷贝技术吗?
首先明确两点:
而零拷贝zero-copy就是想不要内核内存数据向用户空间中拷贝的过程,实现:
通过找一块内存作为用户空间和内核的共享。(epoll中有应用),所以说白了零拷贝技术就是共享内存页!!!回想一下在解决fast-fail问题的时候有一个集合叫做copyonwritearraylist写时复制的技术就是应用了零拷贝技术。当发生修改list的操作的时候会fork子进程来操作,而此时并不是将整个list复制,而是只复制修改的内存页,其他内存页采用父子进程共享内存页来实现共享!
在kafka中详细介绍了zero-copy的发展历程。
Flink 为了避免JVM的固有缺陷例如java对象存储密度低,FGC影响吞吐和响应等,实现了自主管理内存。MemorySegment
就是Flink的内存抽象。默认情况下,一个MemorySegment可以被看做是一个32kb大的内存块的抽象。这块内存既可以是JVM里
的一个byte[]
,也可以是堆外内存(DirectByteBuffer)
。
在MemorySegment这个抽象之上,Flink在数据从operator内的数据对象在向TaskManager上转移,预备被发给下个节点的过程中,使用的抽象或者说内存对象是Buffer。
对接从Java对象转为Buffer的中间对象是另一个抽象StreamRecord。
Java本身自带的序列化和反序列化的功能,但是辅助信息
占用空间比较大
,在序列化对象时记录了过多的类信息。
Apache Flink摒弃了Java原生的序列化方法,以独特的方式处理数据类型
和序列化
,包含自己的类型描述符,泛型类型提取和类型序列化框架。
TypeInformation
是所有类型描述符的基类
。它揭示了该类型的一些基本属性,并且可以生成序列化器。
针对前六种类型数据集,Flink皆可以自动生成对应的TypeSerializer,能非常高效地对数据集进行序列化和反序列化。
window产生数据倾斜指的是数据在不同的窗口内堆积的数据量相差过多。本质上产生这种情况的原因是: 数据源头发送的数据量不同导致
的。出现这种情况一般通过两种方式来解决:
数据倾斜和数据热点是所有大数据框架绕不过去的问题。处理这类问题主要从3个方面入手:
为了更高效地分布式执行,Flink会尽可能地将operator的subtask链接(chain)在一起形成task。每个task在一个线程中执行。将operators链接成task是非常有效的优化:
这就是我们所说的算子链。
两个operator chain在一起的的条件:
数据分区
方式是 forward(参考理解数据流的分区)Flink 在内部会将多个 Operator 算子 串在一起作为一个 Operator chain(执行链)
来执行,每个 Operator-Chain
会在 TaskManager 上的一个 独立线程(Thread)
中执行,这样不仅可以减少线程的数量及线程切换带来的资源消耗,还能降低数据在Operator算子之间传输序列化与反序列化带来的消耗。
可以通过env.disableOperatorChaining()
语句,来禁止 Operator-Chain,这样每个Operator 算子
都会单独分配一个 Task(Spark中的 State)
。
在一些资源密集,计算密集的情况下,设置disableChaining,可以将一些复杂算子独立出来,使其独立运行,保证任务的正常运行。
one-to-one
: 类似于 Spark 中的 窄依赖
。这种类型的operatorFlink会放到一个SubTask中执行,一个 SubTask 就对应一个Thread,一个 SubTask就运行在一个 TaskSlot 中,运行在一个SubTask中的程序逻辑都是相同的,只是运行的数据不同。这样设计可以减少资源浪费,数据传输开销。redistributing
: 类假于 Spark 中的 宽依赖
,Shuffle。这种类型的operator会把数据在算子之间传输。用户提交的Flink Job会被转化成一个DAG任务运行,分别是:StreamGraph、JobGraph、ExecutionGraph。
Flink 中 JobManager与TaskManager,JobManager与Client的交互是基于Akka工具包的,是通过消息驱动。
整个Flink Job的提交还包含着ActorSystem的创建,JobManager的启动,TaskManager的启动和注册。
一个Flink任务的DAG生成计算图大致经历以下三个过程:
代码所表达的逻辑层面
的计算拓扑结构,按照用户代码的执行顺序向StreamExecutionEnvironment添加StreamTransformation构成流式图。串联合并的节点
进行合并
,设置节点之间的边,安排资源共享slot槽位
和放置相关联的节点
,上传任务所需的文件,设置检查点配置等。相当于经过部分初始化和优化处理的任务图。任务具体执行所需的内容
,是最贴近底层实现的执行图。TaskManager中最细粒度的资源是Task slot,代表了一个固定大小的资源子集,每个TaskManager会将其所占有的资源平分给它的slot。
通过调整 task slot 的数量,用户可以定义task之间是如何相互隔离的。每个 TaskManager 有一个slot,也就意味着每个task运行在独立的 JVM 中。每个 TaskManager 有多个slot的话,也就是说多个task运行在同一个JVM中。而在同一个JVM进程中的task,可以共享TCP连接(基于多路复用)
和心跳消息
,可以减少数据的网络传输,也能共享一些数据结构,一定程度上减少了每个task的消耗。 每个slot可以接受单个task,也可以接受多个连续task组成的pipeline,如下图所示,FlatMap函数占用一个taskslot,而key Agg函数和sink函数共用一个taskslot:
迭代计算一般出现在机器学习和图计算的应用中,flink通过迭代的Operator中定义step函数来实现迭代算法,包括Iterate和Delta Iterate两种类型,实现他们就是反复在当前迭代状态上调用step函数,知道满足给定条件才会停止迭代。
①Ierate
是一种简单的迭代,每一轮迭代step函数的输入或者是输入的整个数据集或者是上一个迭代的结果,通过该轮迭代计算出下一轮计算所需要的输入(next partial solution),满足迭代终止条件后会输出迭代最终结果。说白了就是只要有不满足条件的元素,所有元素都一视同仁全部在进行迭代。
②Delta Iterate
增量迭代,它有2个输入,其中一个是初始workset,表示输入待处理的增量stream数据,另一个是初始solution set,他是经过stream方向上Operator处理过的结果。第一轮迭代会将step函数作用在初始workset上,得到的结果workset作为下一轮迭代的输入,同时还要增量更新初始solution set,如果反复迭代直到满足终止条件,最后会根据solution set的结果输出最终结果。说白了就是当前结果跟之前的结果和现在的数据是有关系的,是在原有结果上进行增量更改的。
需要注意的是如果是dataset的迭代需要设置终止条件,如果是stream的迭代就不需要给出终止条件。
其实这个题我差不多思考了5分钟吧,想了两个方案,第二个方案得到面试官的赞赏(自淫);第一个方案就随便说说吧。
通过改变结果表的方式,比如表结构时eventtime windowtime value currenttime,eventtime代表数据产生时间,windowtime代表结果窗口的时间,value就是聚合值啦, currenttime代表即将要写入mysql的时间。我们一方面把这个结果表持久化到mysql或者其他的地方,另一方面将结果表重新写回kafka,专门这样的话我就可以粗略地认为某个或者某些窗口的那些数据延迟了。当然了不用重写回kafka也应该可以处理。
但是这个方案不太好,而且不能一举两得,因为这种方式不能知道数据丢失情况。
我借鉴了flink的barrier来实现的。面试官把这种方案叫做类似哨兵的方式。具体如下:我们在数据源头自己写一个生产者,这个生产者每分钟产生60或者600条数据,类似于采样的方式确定总体丢失率,比如60条数据在写结果时发现丢了1条,那么就认为该窗口内数据丢失了1/60的数据。
另外针对数据延时如何界定,跟第一种方案差不多,将这些自己的message写入mysql时打上当前时间,看一看是否延时,如果是就认为当前窗口数据延时。
注意:这里面有很多可以优化的地方,毕竟没有自己真正实现,所以等待机会吧。
其实这个东西很好做,只不过唯一需要考虑的时候内存的情况,或者说是checkpoint的问题。因为这个流程无外乎就是接入kafka数据源,然后窗口时间是24小是,采用滑动窗口,滑动间隔是1分钟,写一个count() group by vedio_id,与tags表join,得到多行的video_id tag counts,然后再根据tag group by 进行count,再取一个topN就可以得到结果。
注意:第一:与tag表join操作可以换成其他的方式,如使用类似广播变量的方式或者缓存文件等等方式都ok,比如我的项目中采用的是缓存文件的方式,然后对于每条message,在数据进来的时候就把tag打上了。第二:取topN这里有根据具体业务逻辑相关,这里可以看出数据量很大,所以使用最小堆的方式,每次计算都得到一个topN。
关键点:这个问题的关键点在于这些数据量到底按照我所说的想法能否真正的实现。因为数据量很大,每秒的qps可以达到几百万。
可能出现的问题:24小时的window到底flink能否承受住。
我的回答:
因为我们假定flink在内存中存的是24小时/1分钟=1440条数据,分组之后1440*1000000条数据,100w代表的每分钟都会有100w的短视频被观看。所以总体会占有很大的内存,粗略计算需要24G(按照内存中对象24byte来计算的)。当然还没有达到非常大,flink无法支撑的地步。(说白了这里我一定要去源码中看一看到底checkpoint存的是什么东西!!!只需要知道pv和uv记录的东西即可,就可以知道全部了。)
说白了就是分区partition操作,吧数据根据某个key(这里应该是video_id)分布到多台服务器上去,这样会缓解压力,其实我觉得这歌说明没什么意义,因为当我们调用group by操作的时候flink自己就会并行处理,把数据shuffle到多台服务器上去操作了!!
以一分钟为滑动窗口,每一分钟的结果都记录到hbase,每次trigger的时候取出过去24小时的1440条记录进行合并、计算、聚合操作得到结果。但这无疑是增加了网络io、失败概率、延迟。但这种方式是不会存在内存问题的。
当然了类似于现在很多查询性能非常好的es、clickhouse我觉得只要逻辑正确都可以应用!
说归说,真正实现可没这么简单。
print 函数中创建了 PrintSinkFunction 函数,并添加到 addSink中。
用户可以实现 RichSinkFunction,SinkFunction来自定义 Sink。
自定义 Sink 要实现 invoke 方法.
RichFunction 接口中有 open(), close() 方法。
RichSinkFunction 有 open(), close() 方法。
insert into t_activities_count (aid, event_type, `count`) values (?, ?, ?) ON DUPLICATE KEY UPDATE `count` = `count` + ?
SourceFunction: run(), cancel() 方法。
RichSourceFunction, 抽像类, 空类。
ParallelSourceFunction, 接口, 空接口。
RichParallelSourceFunction, 抽象类, 空类。
RichFunction 接口中有 open(), close() 方法。
默认kafka会自动提交偏移量到 __consumer_offsets
Topic 中,该 Topic 默认有 50
个分区。Flink程序没有启动checkpoint的情况下,会将 topic 消费的 offset 提交到 __consumer_offsets
中。假如Flink程序运行失败了,即使是设置了 auto.offset.reset=earliest
,Flink 程序也不会从最开始的位点消费。使用该模式,无法保证 exactly-once。
通过如下语句来关闭 Kafka source 往 __consumer_offset
topic 中写 offset 的功能。
FlinkKafkaConsumer stringFlinkKafkaConsumer = new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), kafkaProperties);
## 默认值是 true.
stringFlinkKafkaConsumer.setCommitOffsetsOnCheckpoints(false);
保存在__consumer_offset
Topic中的 offset一般有两个作用:
__consumer_offset
中恢复数据。可以通过FlinkKafkaConsumer.setCommitOffsetsOnCheckpoints(false);
来关闭该选项,默认是打开的。
如果从指定的 offset 读取数据,offset 不存在了,默认使用 setStartFromGroupOffsets
来消费分区中的数据。
如果有 checkpoint, savepoint 配置,则指定 offset 无效,作业会根据状态从存储的 offset中恢复数据。
要实现 checkpointFunction 接口,并重写 snapshotState, initializeState 方法。
run() 方法中,对 offset 的更新需要加锁,因为 checkpoint 线程,与 source 线程是两个线程。
initializeState() 方法,在函数初始化的时候调用一次。
snapshotState() 是为了将 状态数据持久化到 hdfs 上。
TwoPhaseCommitSinkFunction
抽象类。FlinkKafkaProducerBase
类来实现的,@PublicEvolving
public abstract class TwoPhaseCommitSinkFunction
extends RichSinkFunction
implements CheckpointedFunction, CheckpointListener {
protected final LinkedHashMap> pendingCommitTransactions = new LinkedHashMap<>();
protected transient Optional userContext;
protected transient ListState> state;
private final Clock clock;
private final ListStateDescriptor> stateDescriptor;
private TransactionHolder currentTransactionHolder;
/**
* Specifies the maximum time a transaction should remain open.
*/
private long transactionTimeout = Long.MAX_VALUE;
/**
* If true, any exception thrown in {@link #recoverAndCommit(Object)} will be caught instead of
* propagated.
*/
private boolean ignoreFailuresAfterTransactionTimeout;
/**
* If a transaction's elapsed time reaches this percentage of the transactionTimeout, a warning
* message will be logged. Value must be in range [0,1]. Negative value disables warnings.
*/
private double transactionTimeoutWarningRatio = -1;
/**
* Use default {@link ListStateDescriptor} for internal state serialization. Helpful utilities for using this
* constructor are {@link TypeInformation#of(Class)}, {@link org.apache.flink.api.common.typeinfo.TypeHint} and
* {@link TypeInformation#of(TypeHint)}. Example:
*
* {@code
* TwoPhaseCommitSinkFunction(TypeInformation.of(new TypeHint>() {}));
* }
*
*
* @param transactionSerializer {@link TypeSerializer} for the transaction type of this sink
* @param contextSerializer {@link TypeSerializer} for the context type of this sink
*/
public TwoPhaseCommitSinkFunction(
TypeSerializer transactionSerializer,
TypeSerializer contextSerializer) {
this(transactionSerializer, contextSerializer, Clock.systemUTC());
}
@VisibleForTesting
TwoPhaseCommitSinkFunction(
TypeSerializer transactionSerializer,
TypeSerializer contextSerializer,
Clock clock) {
this.stateDescriptor =
new ListStateDescriptor<>(
"state",
new StateSerializer<>(transactionSerializer, contextSerializer));
this.clock = clock;
}
protected Optional initializeUserContext() {
return Optional.empty();
}
protected Optional getUserContext() {
return userContext;
}
@Nullable
protected TXN currentTransaction() {
return currentTransactionHolder == null ? null : currentTransactionHolder.handle;
}
@Nonnull
protected Stream> pendingTransactions() {
return pendingCommitTransactions.entrySet().stream()
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), e.getValue().handle));
}
// ------ methods that should be implemented in child class to support two phase commit algorithm ------
/**
* Write value within a transaction.
*/
protected abstract void invoke(TXN transaction, IN value, Context context) throws Exception;
/**
* Method that starts a new transaction.
*
* @return newly created transaction.
*/
protected abstract TXN beginTransaction() throws Exception;
/**
* Pre commit previously created transaction. Pre commit must make all of the necessary steps to prepare the
* transaction for a commit that might happen in the future. After this point the transaction might still be
* aborted, but underlying implementation must ensure that commit calls on already pre committed transactions
* will always succeed.
*
* Usually implementation involves flushing the data.
*/
protected abstract void preCommit(TXN transaction) throws Exception;
/**
* Commit a pre-committed transaction. If this method fail, Flink application will be
* restarted and {@link TwoPhaseCommitSinkFunction#recoverAndCommit(Object)} will be called again for the
* same transaction.
*/
protected abstract void commit(TXN transaction);
/**
* Invoked on recovered transactions after a failure. User implementation must ensure that this call will eventually
* succeed. If it fails, Flink application will be restarted and it will be invoked again. If it does not succeed
* eventually, a data loss will occur. Transactions will be recovered in an order in which they were created.
*/
protected void recoverAndCommit(TXN transaction) {
commit(transaction);
}
/**
* Abort a transaction.
*/
protected abstract void abort(TXN transaction);
/**
* Abort a transaction that was rejected by a coordinator after a failure.
*/
protected void recoverAndAbort(TXN transaction) {
abort(transaction);
}
// ------ entry points for above methods implementing {@CheckPointedFunction} and {@CheckpointListener} ------
/**
* This should not be implemented by subclasses.
*/
@Override
public final void invoke(IN value) throws Exception {}
@Override
public final void invoke(
IN value, Context context) throws Exception {
invoke(currentTransactionHolder.handle, value, context);
}
@Override
public final void notifyCheckpointComplete(long checkpointId) throws Exception {
}
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
}
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
}
}
使用 FlinkKafkaProducer 往 kafka 中写数据时,如果不单独设置 partition 策略,会默认使用 FlinkFixedPartitioner
,该 partitioner 分区的方式是 task 所在的并发 id 对 topic 总 partition 数取余:parallelInstanceId % partitions.length
每个 task
都会 轮循
的写下游的 所有 partition
。该方式下游的 partition 数据会比较均衡,但是缺点是: partition 个数过多的情况下需要维持过多的网络连接,即每个 task 都会维持跟所有 partition 所在 broker 的连接
。max,min算子在底层其实是调用aggregate()
,通过传递不同的AggregationType (SUM, MIN, MAX, MINBY, MAXBY,) 实现不同的功能。
max 和 maxBy 之间的区别在于 max 返回流中的最大值,但 maxBy 返回具有最大值的键, min 和 minBy 同理。
根据数据类型的划分,分为: keyedState 和 Operator State。
keyedState: 表示与 Key 相关的一种 State,只能用于 KeyedStream 类型数据集,对应的 Function 和 Operator 之上。KeyedState 中的 Key 是我们在 SQL语句中对应的 GroupBy/PartitionBy 里面的字段。 KeyedState 是 OperatorState 的特例,区别是 KeyedState 事先按 Key 对数据进行分区,每个 KeyState 仅对应一个 Operator 和 Key 的组合
。下图中根据 Key 分组,分组后 一个 SubTask 中会包含1到n 个KeyedState,每个 KeyedState 对应一个 Key。
OperatorState
只与算子实例绑定,每个算子实例 Operator 实例
中持有所有数据元素
的一部分状态数据
。
如下图,Kafka Topic有3个分区,Flink-Job 有4个并行度,每个 SubTask 都会维护自己读取 Kafka 相应 Partition 中的 offset,类似这种 offset 数据与具体的Operator相关的数据,就是OperatorState。
当并行度发生变化时,Operator State 可以将状态在所有的并行实例中进行重分配,并且提供了多种方式来进行重分配。
假如现在是求WordCount,结合上面两张图片:
KeyedSatte
与 OperatorState
都支持并行度发生变化时,进行状态数据的重新分配。
Keyed State 和 Operator State 都有两种存在形式,即 Raw State(原始状态),和 Managed State(托管状态)。
DataStream 所有 function 都可以使用Managed State 托管状态,但是原生状态只能在实现 operator 的时候使用。相对原生状态,推荐使用托管状态
,使用托管状态当并行度发生变化时,Flink 可以自动帮助你重分配 state,同时还可以更好的管理内存。
要使用一个状态对象,需要先创建一个 StateDescriptor,它包含了状态的名字(你可以创建若干个 state,但是它们必须要有唯一的值以便能够引用它们),状态的值的类型,想使用的 state 类型,你可以创建
OperatorState 只支持一种数据结构,即 ListState。
为了使用 OperatorState托管状态
,可以实现
CheckpointedFunction
ListCheckpointed
。public interface CheckpointedFunction {
void snapshotState(FunctionSnapshotContext context) throws Exception;
void initializeState(FunctionInitializationContext context) throws Exception;
}
@PublicEvolving
public interface ListCheckpointed {
List snapshotState(long checkpointId, long timestamp) throws Exception;
void restoreState(List state) throws Exception;
}
CheckpointedFunction
接口或者ListCheckpointed
接口来使用managed operator state;对于managed operator state,目前仅仅支持list-style
的形式,即要求state是serializable objects的List结构,方便在rescale的时候进行redistributed,关于redistribution schemes的模式目前有两种:Even-split redistribution 进一步重分配
:每个算子会返回一个状态元素列表,整个状态在逻辑上是所有列表的连接。在重新分配State
或者恢复State
的时候,这个状态元素列表会被按照并行度
分为子列表,每个算子会得到一个子列表。这个子列表可能为空,或包含一个或多个元素。举个例子,如果使用并行性 1,算子的检查点状态包含元素 element1 和 element2,当将并行性增加到 2 时,element1 可能最终在算子实例 0 中,而 element2 将转到算子实例 1 中。Union redistribution: 整体重分配
: 每个算子会返回一个状态元素列表,整个状态在逻辑上是所有列表的连接。在重新分配State
或恢复State
的时候,每个算子都会获得完整的状态元素列表。ListCheckpointed是CheckpointedFunction的限制版,它只能支持Even-split redistribution模式的list-style state
ListCheckpointed定义了两个方法,分别是snapshotState方法及restoreState方法;
TTL 可以分配给任何类型的 KeyedState
,如果一个 State 设置了 TTL,那么当 State 过期时,之前存储的值会清除。所有的 State 集合类型都支持单个TTL设置,这就是说 List 和 Map 的集合元素,都支持独立到期。
StateTtlConfig valueStateTtl = StateTtlConfig.newBuilder(Time.seconds(5))
.setUpdateType(UpdateType.OnCreateAndWrite)
.setStateVisibility(StateVisibility.NeverReturnExpired).build();
目前,堆State后端依赖于增量清理,RocksDB 后端使用压缩过滤器进行后台清理。
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupFullSnapshot().build()
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupInBackground()
.build();
CleanupStrategies:
TTL 清理策略: CleanupStrategies 类中有字段 isCleanupInBackground(是否在后台清理),相关的清理 Strategies。
Strategies:
CleanupStrategy Interface:
a. Class -> EmptyCleanupStrategy(不清理,为空)
b. Class -> IncrementalCleanupStrategy(增量的清除)
c. Class -> RocksdbCompactFilterCleanupStrategy(在 RocksDB 中自定义压缩过滤器)
Flink 程序开启了 Checkpoint 后,Flink 程序会按照一定的时间间隔对程序State进行备份,当Flink故障恢复时,可从 Checkpoint中恢复之前的状态。
程序中的 ValueState 被声名为 transient
意思是不对 ValueState 序列化,Flink 的状态数据都保存在StateBackEnd 中,ValueState 的值应该是在 open()中通过 StateBackEnd 来获取的。如果不加 transient
关键字,有可能把State状态数据
序列化的磁盘
或其他介质
中,在程序下次启动时,造成数据不一致;另一种情况是,ValueState 的值有可能很大,比如统计网站的 UV 数据,需要保存所有用户的 UID,如果序列化,很可能状态数据特别大。
滚动窗口
,滑动窗口
。前闭后开
的。Flink 支持两种划分窗口的方式,按照**time
** 和 count
。如果根据时间划分窗口,那么它就是一个time-window。如果根据数据划分窗口,那么它就是一个count-window。
flink支持窗口的两个重要属性(size和interval)
:
通过组合可以得出四种基本窗口:
Kafka 在没有指定 分区Key时,默认的分区策略是 轮询
,即把所有数据按分区,依次发送到Topic的所有分区,以减少数据倾斜。wc10Topic 有4个分区,所以,我们测试了2次发现,只有当所有 Kafka Partition 都满足触发条件时,才会触发整个窗口计算;只要有一个没有达到条件,都不会触发窗口计算。为了更好的理解其原理,请看下图。图中每一个箭头都代表一个 kafka partition。
数据时间
大于 窗口的结束时间(边界)
,就会触发窗口计算。数据最大时间
减去 允许延迟时间
大于等于 窗口的结束时间(边界)
时,才会触发窗口计算。为了更好的理解其,原理,参阅数据延迟示意图:在DataSource的并行度等于1的情况下:
a. WaterMark = 数据所携带的时间 - 延迟时间的时间
b. WaterMark >= 上一个窗口的结束边界就会触发窗口执行
在DataSource的并行度大于1的情况下:
a. 每一个分区 WaterMark = 数据所携带的时间 - 延迟时间的时间
b. 每一个分区 WaterMark >= 上一个窗口的结束边界就会触发窗口执行
public class QueryActivityNameAsyncMySql {
public static void main(String[] args) throws Exception {
args = new String[]{"localhost:9092", "activity_topic", "gid-wc10-test1"};
// 正常流
DataStreamSource dataStreamSource = DYFlinkUtilV1.createDataStreamSource(args, new SimpleStringSchema());
// 在设置容量的时候,不能超过, 异步 function 的最大连接数量.
SingleOutputStreamOperator activityBeanAsyncStream = AsyncDataStream
.unorderedWait(dataStreamSource, new DataToActivityBeanAsyncMySqlFunction(), 0, TimeUnit.MILLISECONDS, 10);
activityBeanAsyncStream.print();
DYFlinkUtilV1.getEnv().execute("QueryActivityNameAsyncMySql");
}
}
当异步 I/O 请求超时时,默认情况下会引发超时异常
并重新启动作业
。如果要自定义超时处理策略,可以重写 AsyncFunction 接口的 timeout 方法。
AsyncFunction
发出的并发请求
通常以某种未定义的顺序完成,具体结果取决于首先完成的请求。 Flink提供了两种模式:
AsyncDataStream.unorderedWait(…)
:异步请求完成后立即发出结果记录,在异步 I/O 操作后,DataStream中记录的顺序与以前不同,当时间策略使用的是ProcessTime处理时间
时,这种情况下的延迟
和开销
都会很小。AsyncDataStream.orderedWait(…)
:在这种情况下,会保证流数据的顺序,结果记录发出去的顺序
与触发异步请求
的顺序相同,为此,如果有记录的结果先返回,也会在队列中缓存着,直到其前面的结果记录都发出(或者超时)了。这样的话就会导致部分数据会有一定的延迟和等待开销,因为和无序的情况下对比,这些结果会在状态中保持更长的时间。join()
函数方式:Flink Operator之CoGroup、Join以及Connect
在DataStream和DataSet中都存在CoGroup、Join这两个Operator。而Connect只适用于处理DataStream。
DataStream> joinedStream = leftStreamOperator.join(rightStreamOperator)
.where(new LeftKeySelector())
.equalTo(new RightKeySelector())
.window(TumblingEventTimeWindows.of(Time.milliseconds(windowSize)))
.apply(new JoinedFunction());
public static class JoinedFunction implements
JoinFunction, Tuple3, Tuple5> {
@Override
public Tuple5 join(Tuple3 first, Tuple3 second)
throws Exception {
return Tuple5.of(first.f0, first.f1, second.f1, first.f2, second.f2);
}
}
CoGroup
函数方式:Flink 中实现LeftJoin, RightJoin 是通过调用coGroup
方法实现的。coGroup
方法需要用户实现 CoGroupFunction
接口,并定义LeftJoin, RightJoin的实现。
coGroup
方法获取同一个计算窗口left-Stream
的数据和 right-Stream
的数据, 匹配相同的 Key 值,输出匹配结果 。
coGroup
方法的触发一定满足以下几个条件:coGroup
方法两个Stream的数据, 一定在同一个窗口中。public interface CoGroupFunction extends Function, Serializable {
/**
* This method must be implemented to provide a user implementation of a
* coGroup. It is called for each pair of element groups where the elements share the
* same key.
*
* @param first The records from the first input.
* @param second The records from the second.
* @param out A collector to return elements.
*
* @throws Exception The function may throw Exceptions, which will cause the program to cancel,
* and may trigger the recovery logic.
*/
void coGroup(Iterable first, Iterable second, Collector out) throws Exception;
}
// join
DataStream> joinedStream = leftStreamOperator.coGroup(rightStreamOperator)
.where(new LeftKeySelector())
.equalTo(new RightKeySelector())
.window(TumblingEventTimeWindows.of(Time.milliseconds(windowSize)))
.apply(new CoGroupedLeftJoinFunction());
joinedStream.print("leftjoin--------");
public static class CoGroupedLeftJoinFunction implements
CoGroupFunction, Tuple3, Tuple5> {
// coGroup 获取同一个计算窗口 left-Stream的数据和 right-Stream 的数据, 匹配相同的 Key 值.
// 能够coGroup的数据满足以下几个条件:
// 1. 两个Stream 的数据, 一定在同一个窗口中.
// 2. 窗口已经被触发了.
@Override
public void coGroup(Iterable> firstIterator, Iterable> secondIterator,
Collector> collector) throws Exception {
for (Tuple3 first : firstIterator) {
boolean isJoined = false;
for (Tuple3 second : secondIterator) {
if (first.f0.equals(second.f0)) {
collector.collect(Tuple5.of(first.f0, first.f1, second.f0, first.f2, second.f2));
isJoined = true;
// TODO do not break;
// join 上之后, 继续与 右流 join, 因为有可能 匹配右流多条.
}
}
// 左流没有 join 上右流
if (!isJoined) {
collector.collect(Tuple5.of(first.f0, first.f1, "null", first.f2, -1L));
}
}
}
}
双流 Join 也是一个非常常见的应用场景。深入源码你可以发现,JoinedStreams 和 CoGroupedStreams 的代码实现有80%是一模一样的,JoinedStreams 在底层又调用了 CoGroupedStreams 来实现 Join 功能。除了名字不一样,一开始很难将它们区分开来,而且为什么要提供两个功能类似的接口呢??
group
,是对同一个key上的两组集合进行操作,而 join 侧重的是pair
,是对同一个key上的每对元素
进行操作。JoinedStreams 和 CoGroupedStreams 是基于 Window 上实现的,所以 CoGroupedStreams 最终又调用了 WindowedStream 来实现。
val firstInput: DataStream[MyType] = ...
val secondInput: DataStream[AnotherType] = ...
val result: DataStream[(MyType, AnotherType)] = firstInput.join(secondInput)
.where("userId").equalTo("id")
.window(TumblingEventTimeWindows.of(Time.seconds(3)))
.apply (new JoinFunction () {...})
上述 JoinedStreams 的样例代码在运行时会转换成如下的执行图:
双流上的数据在同一个key的会被分别分配到同一个window窗口的左右两个篮子里,当window结束的时候,会对左右篮子进行笛卡尔积从而得到每一对pair,对每一对pair应用 JoinFunction。不过目前(Flink 1.1.x)JoinedStreams 只是简单地实现了流上的join操作而已,距离真正的生产使用还是有些距离。因为目前 join 窗口的双流数据都是被缓存在内存中的,也就是说如果某个key上的窗口数据太多就会导致 JVM OOM(然而数据倾斜是常态)。双流join的难点也正是在这里,这也是社区后面对 join 操作的优化方向,例如可以借鉴Flink在批处理join中的优化方案,也可以用ManagedMemory来管理窗口中的数据,并当数据超过阈值时能spill到硬盘。
在 DataStream 上有一个 union 的转换 dataStream.union(otherStream1, otherStream2, ...)
,用来合并多个流,新的流会包含所有流中的数据。union 有一个限制,就是所有合并的流的类型必须是一致的。ConnectedStreams
提供了和 union 类似的功能,用来连接两个流,但是与 union 转换有以下几个区别:
如下 ConnectedStreams 的样例,连接 input
和 other
流,并在input
流上应用map1
方法,在other
上应用map2
方法,双流可以共享状态(比如计数)。
val input: DataStream[MyType] = ...
val other: DataStream[AnotherType] = ...
val connected: ConnectedStreams[MyType, AnotherType] = input.connect(other)
val result: DataStream[ResultType] =
connected.map(new CoMapFunction[MyType, AnotherType, ResultType]() {
override def map1(value: MyType): ResultType = { ... }
override def map2(value: AnotherType): ResultType = { ... }
})
当并行度为2时,其执行图如下所示:
RocksDBStateBackend 和上面两种都有点不一样,RocksDB 是一种嵌入式的本地数据库,它会在本地文件系统中维护状态,KeyedStateBackend
等会直接写入本地 RocksDB 中,它还需要配置一个文件系统(一般是 HDFS),比如 hdfs://namenode:40010/flink/checkpoints
,当触发 checkpoint 的时候,会把整个 RocksDB 数据库复制到配置的文件系统中去,当 failover 时从文件系统中将数据恢复到本地。
enableIncrementalCheckpointing
来确认是否开启增量的 checkpoint,默认是不开启
的,在 CheckpointingOptions 类中有个 state.backend.incremental
参数来表示,增量 checkpoint 非常适合于超大状态的场景。序列化
和反序列化
才能完成的,跟状态直接存储在内存中,性能可能会略低些