Flink面试题

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 数据交换和 Redistribute 详解?

➤ 深入原理-Flink 执行计划 (Shuffle)?

➤ 深入理解-Flink 内存模型?

Flink 原理与实现:内存管理

在本文中,分为以下几个部分:
第一部分:Flink 中的核心概念和基础篇,包含了 Flink 的整体介绍、核心概念、算子等考察点。
第二部分:Flink 进阶篇,包含了 Flink 中的数据传输、容错机制、序列化、数据热点、反压等实际生产环境中遇到的问题等考察点。
第三部分:Flink 源码篇,包含了 Flink 的核心代码实现、Job 提交流程、数据交换、分布式快照机制、Flink SQL 的原理等考察点。

➤ Flink 相比传统的 Spark Streaming 有什么区别?

这个问题是一个非常宏观的问题,因为两个框架的不同点非常之多。但是在面试时有非常重要的一点一定要回答出来:Flink 是标准的实时处理引擎,基于事件驱动。而 Spark Streaming 是微批(Micro-Batch)的模型
下面我们就分几个方面介绍两个框架的主要区别:

  1. 架构模型
  • Spark Streaming 在运行时的主要角色包括:Master、Worker、Driver、Executor。
  • Flink 在运行时主要包含:Jobmanager、Taskmanager和Slot。
  1. 任务调度
  • Spark Streaming 连续不断的生成微小的数据批次,构建有向无环图DAG,Spark Streaming 会依次创建 DStreamGraph、JobGenerator、JobScheduler。
  • Flink 根据用户提交的代码生成 StreamGraph,经过优化生成 JobGraph,然后提交给 JobManager进行处理,JobManager 会根据 JobGraph 生成 ExecutionGraph,ExecutionGraph 是 Flink 调度最核心的数据结构,JobManager 根据 ExecutionGraph 对 Job 进行调度。
  1. 时间机制
  • Spark Streaming 支持的时间机制有限,只支持处理时间
  • Flink 支持了流处理程序在时间上的三个定义:处理时间、事件时间、注入时间。同时也支持 watermark 机制来处理滞后数据。
  1. 容错机制
  • 对于 Spark Streaming 任务,我们可以设置 checkpoint,然后假如发生故障并重启,我们可以从上次 checkpoint 之处恢复,但是这个行为只能使得数据不丢失,可能会重复处理,不能做到恰好一次处理语义。
  • Flink 则使用两阶段提交协议来解决这个问题。
  1. 维表join和异步IO
    Structured Streaming不直接支持与维表的join操作,但是可以使用map、flatmap及udf等来实现该功能,所有的这些都是同步算子,不支持异步IO操作。但是Structured Streaming直接与静态数据集的join,可以也可以帮助实现维表的join功能,当然维表要不可变。

Flink支持与维表进行join操作,除了map,flatmap这些算子之外,flink还有异步IO算子,可以用来实现维表,提升性能。

➤ 为什么说flink统一了流和批处理?

因为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面试题_第1张图片

本道面试题考察的其实就是一句话:Flink的开发者认为**批处理流处理**的一种特殊情况。批处理是有限的流处理。Flink 使用一个引擎支持了DataSet API 和 DataStream API。


➤ 你们的Flink集群规模多大?

大家注意,这个问题看起来是问你实际应用中的Flink集群规模,其实还隐藏着另一个问题:Flink可以支持多少节点的集群规模?
在回答这个问题时候,可以将自己生产环节中的集群规模、节点、内存情况说明,同时说明部署模式(一般是Flink on Yarn),除此之外,用户也可以同时在小集群(少于5个节点)和拥有 TB 级别状态的上千个节点上运行 Flink 任务。


➤ Flink的基础编程模型了解吗?

Flink面试题_第2张图片

上图是来自Flink官网的运行流程图。通过上图我们可以得知,Flink 程序的基本构建是数据输入来自一个 Source,Source 代表数据的输入端,经过 Transformation 进行转换,然后在一个或者多个Sink接收器中结束。数据流(stream)就是一组永远不会停止的数据记录流,而转换(transformation)是将一个或多个流作为输入,并生成一个或多个输出流的操作。执行时,Flink程序映射到 streaming dataflows,由流(streams)和转换操作(transformation operators)组成。


➤ Flink集群有哪些角色?各自有什么作用?

Flink面试题_第3张图片

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。


➤ Flink的架构?

主从结构 Jobmanager,taskmanager两个进程(可以把client也加进去)。
集群模式:standalone,on yarn(在yarn上运行一个flink集群/提交到yarn上运行flink job)

Jobmanager: 作用
  1. registerTaskManager:在flink集群启动时,taskmanager会向jobmanager注册
  2. submitjob:flink程序内部通过client向jobmanager提交job,job是以jobgraph形式提交
  3. canceljob:请求取消一个flinkjob
  4. updateTaskExcutionStage:更新taskmanager中excution的状态信息
  5. requestnextinputsplit:运行在taskmanager上的task请求获取下一个要处理的split
  6. jobstatuschanged:executionGraph向jobmanager发送该消息,用来表示job的状态变化
Taskmanager: 作用
  1. 注册:向jobmnager注册自己
  2. 可操作阶段:该阶段taskmanager可以接受并处理与task有关的消息
client: 作用

client对用户提交的代码进行预处理,client将程序组装成一个 jobgraph,它是由多个jobvertex组成的DAG。

关于flink生成dag、提交job、分发task等细节 在任务提交面试题会整理。


➤ Flink 的组件栈有哪些?

根据 Flink 官网描述,Flink 是一个分层架构的系统,每一层所包含的组件都提供了特定的抽象,用来服务于上层组件。

Flink面试题_第4张图片

自下而上,每一层分别代表:

  • Deploy 层:该层主要涉及了Flink的部署模式,在上图中我们可以看出,Flink 支持包括local、Standalone、Cluster、Cloud等多种部署模式。
  • Runtime 层:Runtime层提供了支持 Flink 计算的核心实现,比如:支持分布式 Stream 处理、JobGraph到ExecutionGraph的映射、调度等等,为上层API层提供基础服务。
  • API层:API 层主要实现了面向流(Stream)处理和批(Batch)处理API,其中面向流处理对应DataStream API,面向批处理对应DataSet API,后续版本,Flink有计划将DataStream和DataSet API进行统一。
  • Libraries层:该层称为Flink应用框架层,根据API层的划分,在API层之上构建的满足特定应用的实现计算框架,也分别对应于面向流处理和面向批处理两类。面向流处理支持:CEP(复杂事件处理)、基于SQL-like的操作(基于Table的关系操作);面向批处理支持:FlinkML(机器学习库)、Gelly(图处理)。

➤ JobManger在集群中扮演了什么角色?

JobManager 负责整个 Flink 集群任务的调度以及资源的管理,从客户端中获取提交的应用,然后根据集群中 TaskManager 上 TaskSlot 的使用情况,为提交的应用分配相应的 TaskSlot 资源并命令 TaskManager 启动从客户端中获取的应用。

  1. JobManager 相当于整个集群的 Master 节点,且整个集群有且只有一个活跃的 JobManager ,负责整个集群的任务管理和资源管理。
  2. JobManager 和 TaskManager 之间通过 Actor System 进行通信,获取任务执行的情况并通过 Actor System 将应用的任务执行情况发送给客户端。
  3. 同时在任务执行的过程中,Flink JobManager 会触发 Checkpoint 操作,每个 TaskManager 节点 收到 Checkpoint 触发指令后,完成 Checkpoint 操作,所有的 Checkpoint 协调过程都是在 Fink JobManager 中完成。
  4. 当任务完成后,Flink 会将任务执行的信息反馈给客户端,并且释放掉 TaskManager 中的资源以供下一次提交任务使用。

➤ JobManger在集群启动过程中起到什么作用?

JobManager的职责主要是接收Flink作业,调度Task,收集作业状态和管理TaskManager。它包含一个Actor,并且做如下操作:

  • RegisterTaskManager: 它由想要注册到JobManager的TaskManager发送。注册成功会通过AcknowledgeRegistration消息进行Ack。
  • SubmitJob: 由提交作业到系统的Client发送。提交的信息是JobGraph形式的作业描述信息。
  • CancelJob: 请求取消指定id的作业。成功会返回CancellationSuccess,否则返回CancellationFailure。
  • UpdateTaskExecutionState: 由TaskManager发送,用来更新执行节点(ExecutionVertex)的状态。成功则返回true,否则返回false。
  • RequestNextInputSplit: TaskManager上的Task请求下一个输入split,成功则返回NextInputSplit,否则返回null。
  • JobStatusChanged: 它意味着作业的状态(RUNNING, CANCELING, FINISHED,等)发生变化。这个消息由ExecutionGraph发送。

➤ TaskManager在集群中扮演了什么角色?

TaskManager 相当于整个集群的 Slave 节点,负责具体的任务执行和对应任务在每个Node上的资源申请管理

  1. 客户端通过将编写好的 Flink 应用编译打包,提交到 JobManager,然后 JobManager 会根据已注册在 JobManager 中 TaskManager 的资源情况,将任务分配给有资源的 TaskManager节点,然后启动并运行任务。
  2. TaskManager 从 JobManager 接收需要部署的任务,然后使用 Slot 资源启动 Task,建立数据接入的网络连接,接收数据并开始数据处理。同时 TaskManager 之间的数据交互都是通过数据流的方式进行的。

可以看出,Flink 的任务运行其实是采用多线程的方式,这和 MapReduce 多 JVM 进行的方式有很大的区别,Flink 能够极大提高 CPU 使用效率,在多个任务和 Task 之间通过 TaskSlot 方式共享系统资源,每个 TaskManager 中通过管理多个 TaskSlot 资源池进行对资源进行有效管理。


➤ TaskManager在集群启动过程中起到什么作用?

TaskManager的启动流程较为简单:
启动类:org.apache.flink.runtime.taskmanager.TaskManager
核心启动方法 : selectNetworkInterfaceAndRunTaskManager 启动后直接向JobManager注册自己,注册完成后,进行部分模块的初始化。


➤ flink中应用在tableAPI中的UDF有几种?

  1. scalar function:针对一条record的一个字段的操作,返回一个字段
  2. table function:针对一条record的一个字段的操作,返回多个字段
  3. aggregate function:针对多条记录的一个字段操作,返回一条记录

➤ 你知道UDF吧,请问我们注册UDF到底是每个计算线程一份还是每个executor一份?或者说是多个对象还是共享一个对象?如果答的对的话,面试官会问你如何保证共享呢,这就涉及单例对象的问题了。这个问题有点乱,请自行整理。(头条)

我们要知道一般来说在使用一个类的时候,一般是要创建对象的,所以我们在sql里使用UDF的时候会创建对象,如果是多线程并行操作sql,那么就是多个UDF对象。那么如何保证一个executer进程中共享一个UDF呢,在scala中就用Object即可。如果是class就写一个单例模式,关于单例模式算法题中我会详细整理!


➤ 你知道flink可以修改代码恢复吧!但是不是所有的修改都可以恢复哦,请问什么样的代码修改会导致无法flink任务恢复?(头条)

面试官说:只有当不会改变DAG的修改才会正常恢复!!!有机会试一下。


➤ Flink 的运行必须依赖 Hadoop组件吗?

Flink可以完全独立于Hadoop,在不依赖Hadoop组件下运行。但是做为大数据的基础设施,Hadoop体系是任何大数据框架都绕不过去的。Flink可以集成众多Hadooop 组件,例如Yarn、Hbase、HDFS等等。例如,Flink可以和Yarn集成做资源调度,也可以读写HDFS,或者利用HDFS做检查点。


➤ Flink是如何做容错的?

Flink 实现容错主要靠强大的 CheckPoint机制State机制

  1. Checkpoint 负责定时制作分布式快照、对程序中的 State 状态进行备份;
  2. State 用来存储计算过程中的中间状态。

➤ flink的 Checkpoint 机制详细讲一下?注意与spark的区别?

flink是通过checkpoint机制实现容错,它的原理是不断的生成分布式streaming数据流snapshot快照。在流处理失败时通过这些snapshot可以恢复数据流处理。而flink的快照有两个核心:

  • barrier 机制:barrier是实现checkpoint的机制。
  • state 状态保存:state保存则是通过barrier这种机制进行 分布式快照 的实现。
1. 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的三种方式,

  1. jobmanager内存,不建议;
  2. hdfs(可以使用,同步 进行分布式快照);
  3. rocksDB(异步 进行分布式快照)。

除了第3种其他两种都是同步快照。也就是说用hdfs这种方式快照是会阻塞数据处理的,只有当两个barrier之间数据处理完成并完成快照之后才向下一个task发送数据并打入barrier n。我们不管异步快照,我们现在只说同步快照。

2. state状态保存

state状态保存分为两种:

  1. 一种是用户自定义状态:也就是我们为了实现需求敲的代码(算子),他们来创建和修改的state;
  2. 一种是系统状态:此状态可以认为数据缓冲区,比如window窗口函数,我们要知道数据处理的情况。

生成的快照现在包含:

  1. 对于每个并行流数据源,创建快照时流中的偏移/位置
  2. 对于每个运算符,存储在快照中的状态指针
3. stream aligning (barrier k对齐)

这个情况出现的很少,用于解决同一个Operation处理多个输入流的情况(不是同一个数据源),这种情况下operation将先收到barrier k的数据缓存起来不进行处理,只有当另一个流的barrier k到达之后再进行处理,同时opeartion会向checkpoint coordinator上报snapshot。这就是barrier k对齐

SparkCheckpoint

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 分布式快照 Checkpoint 的原理是什么?

Flink的分布式快照是根据Chandy-Lamport算法量身定做的。简单来说就是持续创建分布式数据流及其状态的一致快照。

Flink面试题_第5张图片

核心思想:是在 input source 端插入 barrier,控制 barrier 的同步来实现 snapshot 的备份exactly-once 语义

➤ Flink 是如何保证Exactly-once语义的?

Flink通过实现 两阶段提交状态保存 来实现 端到端 的一致性语义。 分为以下几个步骤:

  1. 开始事务(beginTransaction)创建一个临时文件夹,来写把数据写入到这个文件夹里面
  2. 预提交(preCommit)将内存中缓存的数据写入文件并关闭
  3. 正式提交(commit)将之前写完的临时文件放入目标目录下。这代表着最终的数据会有一些延迟
  4. 丢弃(abort)丢弃临时文件
自己整理的版本
  • 使用 支持 Exactly-Once 数据源
  • 使用 FlinkKafkaConsumer, 开启 checkpointing,把偏移量通过 checkpoint 机制保存到 StateBackend 中,并且默认会将偏移量写入到 Kafka __consumer_offset Topic 中。
  • FlinkKafkaConsumer.setCommitOffsetsOnCheckpoints 默认为 true,即将偏移量写入到 Kafka __consumer_offset 中,目的是为了监控或重启任务时,没有指定 savepoint 可以接着一起的偏移量继续消费。
  • 设置 CheckpointingMode.EXACTLY_ONCE
  • 存储系统支持覆盖(Redis, Hbase, ES) 使用幂等性,覆盖原来的数据。
  • Barrier 可以保证一个流水线中所有算子都处理完了,再对该条数据做 Checkpoint.
  • 存储系统不支持覆盖,要支持事务,成功:提交事务,更新偏移量;失败:回滚事务,不更新偏移量,放弃这次 Checkpoint ,标记为失败状态,继续下一次Checkpoint。
    a. 特点是:可以保证提交事务和 Checkpointing 同时成功。

若失败发生在预提交成功后,正式提交前。可以根据状态来提交预提交的数据,也可删除预提交的数据。

➤ 请问Flink到底是如何保证端到端的exactly once语义的?请从source——算子——算子——sink整个流程说明。可以从kafka的sink或者producer说起。需要注意的是ckeckpoint和offset提交的先后顺序,可以看一下源码。貌似与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了第十个失败了,那么这个过程我们还是无法回滚!!所以需要分布式两段提交策略。

2阶段提交的思想?
  1. pre-commit
  2. commit,

所谓pre-commit指的是第一阶段,也就是checkpoint阶段完成时进行pre-commit,如果所有的pre-commit成功,jobmanager会通知所有跟外部系统有联系的比如sink,通知他们进行第二阶段的commit!这就是两段式提交实现的flink的exactly once消费语义。


➤ 为什么要开启 Checkpoint?

开启 Checkpoint 机制主要是为了实现 实时任务处理的容错
实时任务不同于批处理任务,除非用户主动停止,一般会一直运行,运行的过程中可能存在机器故障、网络问题、外界存储问题等等,要想实时任务一直能够稳定运行,实时任务要有自动容错恢复的功能。
而批处理任务在遇到异常情况时,在重新计算一遍即可。实时任务因为会一直运行的特性,如果在从头开始计算,成本会很大,尤其是对于那种运行时间很久的实时任务来说。
实时任务开启 Checkpoint 功能,也能够减少容错恢复的时间。因为每次都是从最新的 Chekpoint 点位开始状态恢复,而不是从程序启动的状态开始恢复。


➤ Flink Checkpoint 常见失败原因分析?

Flink Checkpoint 失败有很多种原因,常见的失败原因如下:

  • 用户代码逻辑没有对于异常处理,让其直接在运行中抛出。比如解析 Json 异常,没有捕获,导致 Checkpoint失败,或者调用 Dubbo 超时异常等等。
  • 依赖外部存储系统,在进行数据交互时,出错,异常没有处理。比如输出数据到 Kafka、Redis、HBase等,客户端抛出了超时异常,没有进行捕获,Flink 任务容错机制会再次重启。
  • 内存不足,频繁GC,超出了 GC 负载的限制。比如 OOM 异常
  • 网络问题、机器不可用问题等等。

从目前的具体实践情况来看,Flink Checkpoint 异常觉大多数还是用户代码逻辑的问题,对于程序异常没有正确的处理导致。所以在编写 Flink 实时任务时,一定要注意处理程序可能出现的各种异常。这样,也会让实时任务的逻辑更加的健壮。


➤ Flink Checkpoint 太频繁,或时间太久会有什么影响?

checkpoint 的执行间隔要根据实际业务情况配置,checkpoint次数据太频繁,容易给后端系统造成压力。checkpoint 间隔时间太久,状态数据恢复时间较长。


➤ Flink checkpoint 成功后,会不会删除之前的 checkpoint 数据?Flink 会生成多少个 checkpoint文件?

checkpoint执行成功后,会自动删除之前保存的 checkpoint 数据。有多少个 SubTask就会生成多少个 checkpoint 文件。每个 SubTask 保存自己的数据。
Flink面试题_第6张图片

Flink面试题_第7张图片


➤ Flink的 BackPressure 背压机制,跟spark有什么区别?

  1. Flink是通过自下而上背压检测从而 控制流量。如果下层的operation压力大那么上游的operation就会让它慢下来。Jobmanager会反复调用一个job的task运行所在线程的Thread.getStackTrace(),默认情况下,jobmanager会每个50ms触发对一个job的每个task依次进行100次堆栈跟踪调用,根据调用结果来确定backpressure,flink是通过计算得到一个比值radio来确定当前运行的job的backpressure状态。在web页面可以看到这个radio值,它表示在一个内部方法调用中阻塞的堆栈跟踪次数,例如radio=0.01表示100次中仅有一次方法调用阻塞。
  • OK: 0 <= Ratio <= 0.10
  • LOW: 0.10 < Ratio <= 0.5
  • HIGH: 0.5 < Ratio <= 1。
  1. spark的背压检测和处理方式跟flink不同,在spark1.5之前需要自己配置接收速率来限速,所以这个值需要人为测试来决定,spark1.5版本之后只需要开启backpressure功能即可,spark会自己根据计算时间、延时时间等来确定是否限流。

➤ Flink BackPressure 背压 (反压) 问题及如何排查?如何解决?

  1. 数据倾斜:在实践中,很多情况下的反压是由于数据倾斜造成的,这点我们可以通过 Web UI 各个 SubTask 的 Records Sent 和 Record Received 来确认,另外 Checkpoint detail 里不同 SubTask 的 State size 也是一个分析数据倾斜的有用指标。

  2. 用户代码执行效率:此外,最常见的问题可能是用户代码的执行效率问题(频繁被阻塞或者性能问题)。最有用的办法就是对 TaskManager 进行 CPU profile,从中我们可以分析到 Task Thread 是否跑满一个 CPU 核:如果是的话要分析 CPU 主要花费在哪些函数里面,比如我们生产环境中就偶尔遇到卡在 Regex 的用户函数(ReDoS);如果不是的话要看 Task Thread 阻塞在哪里,可能是用户函数本身有些同步的调用,可能是 checkpoint 或者 GC 等系统活动导致的暂时系统暂停。

  3. 数据量过大,资源不足:当然,性能分析的结果也可能是正常的,只是作业申请的资源不足而导致了反压,这就通常要求拓展并行度。值得一提的,在未来的版本 Flink 将会直接在 WebUI 提供 JVM 的 CPU 火焰图[5],这将大大简化性能瓶颈的分析。

  4. TaskManager 的内存以及 GC 问题也可能会导致反压,包括 TaskManager JVM 各区内存不合理导致的频繁 Full GC 甚至失联。推荐可以通过给 TaskManager 启用 G1 垃圾回收器来优化 GC,并加上 -XX:+PrintGCDetails 来打印 GC 日志的方式来观察 GC 的问题。


➤ Flink任务延迟高,想解决这个问题,你会如何入手?

在Flink的后台任务管理中,我们可以看到Flink的 哪个算子task 出现了反压。最主要的手段是 资源调优算子调优

  • 资源调优:对作业中的Operator的并发数(parallelism)、CPU(core)、堆内存(heap_memory)等参数进行调优。
  • 作业参数调优:并行度的设置,State的设置,checkpoint的设置。

➤ Flink 是如何处理 BackPressure 背压(反压)的?

Flink 内部是基于 producer-consumer 模型来进行消息传递的,Flink的反压设计也是基于这个模型。Flink 使用了高效有界的分布式阻塞队列,就像 Java 通用的阻塞队列(BlockingQueue)一样。下游消费者消费变慢,上游就会受到阻塞。


➤ Flink的job在standalone和on yarn模式下的提交流程?

A. standalone:
  1. client会将flink代码转换成DAG jobgraph提交给jobmanager
  2. jobmanager会将JobGraph转换映射为一个ExecutionGrapg,区别在于JobGraph是用户层面的DAG,顶点表示transformation的算子,箭头表示数据流动方向,从web页面可以看到;而executionGrapg则是并行执行这个job的DAG,其中每一个定点都代表这一个exector将要运行的task任务。
  3. jobmanager将task分发给taskmanager上启动的各个exector线程(这是跟spark不同的,spark是动态的,而flink是提前安排好,目的可以理解就是为了dataflow而设计的)
  4. 在此期间各个executor会向jobmanager汇报进度、snapshot等信息
  5. 任务执行完毕后jobmanager删除任务
B. on yarn:Flink 作业提交有两种模式:
  • Session: 如果是以session的方式预先在yarn中启动一个flink集群的话跟standalone类似,只不过运行致谢jobmanager和taskmanager是yarn中的container。
  • Per-Job: 如果是直接提交job任务到yarn集群的话,yarn会启动applicationmaster用来承载jobmanager,然后动态分配taskmanager。

Per-Job 一个 Job 对应一个 JobManager。这种模式需要的资源比较多,JobManager 频繁的做checkpoint的,通常是实时任务。

Flink面试题_第8张图片

Flink面试题_第9张图片

2020-05-17 古月整理
  1. 使用 flink-version/bin/flink run -m yarn-cluster启动 Client。
  2. Client 向 ResourceManager 申请资源,ResourceManager 向 Client 返回一个 Job_ID,随后 Client 将 Flink-Jar 包及配置上传到 HDFS。
  3. 接着 ResourceManager 在集群中随机寻找一个NodeManager,让 NodeManager下载 Flink-Job 的 Jar 包,下载完 Jar 包后,启动一个Container,在 Container 中启动ApplicationMaster,在这个过程中会启动JobManager,因为 JobManager 和 ApplicationMaster 在同一进程里面,它会把JobManager的地址重新作为一个文件上传到HDFS上去,TaskManager在启动的过程中也会去下载这个文件获取JobManager的地址,然后与JobManager进行通信。
  4. ApplicationMaster启动后,ApplicationMasterResourceManager 申请资源,假如一个 TaskManager 中启动4个 TaskSlot,7个并行度,启动2个 TaskManager就可以了。
  5. ResourceManager 分配Container资源后,由ApplicationMaster通知NodeManager启动TaskManager,NodeManager 从 HDFS 中下载 Jar 包和配置,然后启动 TaskManager,TaskManager 的进程名称是YarnTaskExecutorRunner
  6. TaskManager 启动成功后,与 JobManager 通信,发送心跳包,反射注册自己。
  7. JobManager 中生成任务的执行计划,然后把具体的执行计划调度到具体的 TaskSlot中。

a. Flink任务提交后,Client向HDFS上传Flink的Jar包和配置,
b. 之后向Yarn ResourceManager提交任务,
c. ResourceManager分配Container资源并通知对应的NodeManager启动ApplicationMaster,
d. ApplicationMaster启动后加载Flink的Jar包和配置构建环境,然后启动JobManager,
e. 之后ApplicationMaster向ResourceManager申请资源启动TaskManager,
f. ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager,
g. NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager,
h. TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务。

➤ 非精准去重的实现方式?除了bloomfilter。(头条)

  • bloomfilter的非精准去重
    1亿条数据占用内存百兆而已。粗略算一下:位图Bitset底层是用long来实现的。一亿个bit/8/1024/1024=11兆,然后根据比例以及多个hash函数扩展一下百兆可以解决。如果说数据量非常大,那么就用分布式的。

今天说一下另一种非精准去重:基数估计(概率算法)

  • Linear Counting: 简单的几大似然估计,适用于基数N比较小的时候,优点是相同数据量下是Bloomfilter十分之一的空间使用。
  • LogLog Counting: 解决了基数较大时LC的空间复杂度仍是线性的问题,刚才说了bloomfilter一亿数据要用11m,但是LLC只需要几KB!!
  • HyperLogLog Counting: 解决了当基数不是很大时LLC的误差较大的问题
  • HyperLogLog++(spark中就有HLL++的实现!!这就是面试中面试官提到的。)
    是google对HLLC的进一步工程优化。减小了误差抖动问题。

关于上述的东西涉及到概率的很多东西,等以后真的用到再说吧


➤ 说说 Flink 资源管理中 Task Slot 的概念

Flink面试题_第10张图片

在Flink架构角色中我们提到,TaskManager是实际负责执行计算的Worker,TaskManager 是一个 JVM 进程,并会以独立的线程来执行一个task或多个subtask。为了控制一个 TaskManager 能接受多个 task,Flink 提出了 Task Slot 的概念。简单的说,TaskManager会将自己节点上管理的资源分为不同的Slot:固定大小的资源子集。这样就避免了不同Job的Task互相竞争内存资源,但是需要主要的是,**Slot只会做内存**的隔离。没有做CPU的隔离


➤ 说说 Flink 的常用算子?

Flink 最常用的常用算子包括:

  • Map:DataStream → DataStream,输入一个参数产生一个参数,map的功能是对输入的参数进行转换操作。
  • Filter:过滤掉指定条件的数据。
  • KeyBy:按照指定的key进行分组。
  • Reduce:用来进行结果汇总合并。
  • Window:窗口函数,根据某些特性将每个key的数据进行分组(例如:在5s内到达的数据)

➤ Flink 中的 TaskSlot 是什么?

  • Flink 中Task 与 Spark 中的 TaskSet 相对应.
  • Flink 的 TaskSlot 只对内存隔离,不对 cpu 隔离。
  • Flink 允许 子任务 SubTask 共享 Slot,即使 SubTask 不属于同一个 Task(Spark 中的 TaskSet, Stage).

➤ TaskSlot 与 Parallelism的关系?

Slot 是指 TaskManager 最大能并发执行 的能力。
parallelism 是指 TaskManager 实际使用的并发能力。也是 Flink-Job 的实际并发能力。


➤ Flink 中通过什么来划分 Task? 而在 Spark 中通过什么来划分 Stage? Flink 有几种划分 Task 的方式? 4种。

在 Spark 中是通过 Shuffle,即依赖关系 来划分 Stage , 宽依赖肯定是分划分Stage,而 Flink 通过 算子操作 来划分 Stage,Task 主要有4种 方式:

  1. REBALANCE: 重平衡,并行度;在算子的并行度 parallelism发生改变的时候触发。
  2. HASH (Redistributing)。
    keyBy:根据 key 值重分区(which re-partitions by hashing the key);
    broadcast: 广播变量;
    rebalance: 随机重分区(which re-partitions randomly);
  3. startNewChain() 方法被调用。终止之前的operator-chain,从当前算子开启新的operator-chain。
  4. disableChainning() 方法被调用。operator-chain,把当前operator划分成独立的 Task (Spark :-> Stage)

➤ Flink 中 TaskSlot 的默认的名子是什么? TaskSlot 任务槽 运行 Task 有什么限制?

  • Flink 中 TaskSlot 默认的名子是 default

  • 可以通过slotSharingGroup()方法指定 TaskSlot 槽位的名称。

  • 如果在某个DataStream后改变了资源槽名称,后续的DataStream在没有改回default前,所有DataStream的资源槽名称将与改变的资源槽保持一致。
    资源槽名称不同的 subTask 不能在同一个 TaskSlot 中运行;

  • 一个 TaskSlot 中只能运行:

    1. 来自同一个 application 的 subTask。如 Application-A,与 Application-B 的 subTask 不能运行在一个 TaskSlot 中。
    2. TaskSlot 名称相同。
    3. 不同 Task (Spark :-> Stage)的 subTask 任务。同一个 Task 的不同 subTask 不能运行在一个资源槽中。

    Flink面试题_第11张图片

    修改了任务槽之后:
    Flink面试题_第12张图片


➤ 说说你知道的Flink分区策略?

分区策略是用来决定数据如何发送至下游。目前 Flink 支持了8中分区策略的实现。

Flink面试题_第13张图片

上图是整个Flink实现的分区策略继承图:

  • BroadcastPartitioner 广播分区会将上游数据输出到下游算子每个实例(本人测试是每个 Task中) 中。适合于大数据集和小数据集做Jion的场景。
  • CustomPartitionerWrapper 用户自定义分区器。需要用户自己实现Partitioner接口,来定义自己的分区逻辑。例如:
  • ForwardPartitioner 用于将记录输出到下游本地的算子实例。它要求上下游算子并行度一样。简单的说,ForwardPartitioner用来做数据的控制台打印。
  • GlobalPartitioner 数据会被分发到下游算子的第一个实例中进行处理。
  • KeyGroupStreamPartitioner Hash分区器。会将数据按 Key 的 Hash 值输出到下游算子实例中。
  • RebalancePartitioner 数据会被循环发送到下游的每一个实例Task中进行处理。
  • RescalePartitioner 这种分区器会根据上下游算子的并行度,循环的方式输出到下游算子的每个实例。这里有点难以理解,假设上游并行度为2,编号为A和B。下游并行度为4,编号为1,2,3,4。那么A则把数据循环发送给1和2,B则把数据循环发送给3和4。假设上游并行度为4,编号为A,B,C,D。下游并行度为2,编号为1,2。那么A和B则把数据发送给1,C和D则把数据发送给2。
  • ShufflePartitioner 数据会被随机分发下游算子每一个实例中进行处理。
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的并行度了解吗?Flink的并行度设置是怎样的?

Flink的Application 被分为多个并行任务来执行,其中每个并行的实例处理一部分数据。这些并行实例的数量被称为并行度。
我们在实际生产环境中可以从四个不同层面设置并行度:

  • 操作算子层面(Operator Level)
  • 执行环境层面(Execution Environment Level)
  • 客户端层面(Client Level)
  • 系统层面(System Level)

需要注意的优先级:算子层面>环境层面>客户端层面>系统层面


➤ Flink 中默认的并行度是几? Flink中有几种设置并行度的方式? 优先级是什么样的子?

Flink 中默认并行度Parallel是1,可以在flink-conf.yaml中设置全局的并行度。也可能通过 env.setParallelism(10); 来设置并行度,这样设置的并行度是针对本 Job 的,后续没有算子特意更改的话,会应用整个 Flink-Job。但 Flink 也支持针对某个单独的Operator设置并行度。

优先级是什么样的子?
配置文件默认并行度 < env 设置并行度 < 算子设置并行度


➤ Flink的Slot和parallelism有什么区别?官网上十分经典的图:

Flink面试题_第14张图片

slot是指taskmanager的并发执行能力,假设我们将 taskmanager.numberOfTaskSlots 配置为3 那么每一个 taskmanager 中分配3个 TaskSlot, 3个 taskmanager 一共有9个TaskSlot。

Flink面试题_第15张图片

parallelism 是指 taskmanager 实际使用的并发能力。假设我们把 parallelism.default 设置为1,那么9个 TaskSlot 只能用1个,有8个空闲。


➤ Flink有没有重启策略?说说有哪几种?

Flink 实现了多种重启策略。

  • 固定延迟重启策略(Fixed Delay Restart Strategy)
  • 故障率重启策略(Failure Rate Restart Strategy)
  • 没有重启策略(No Restart Strategy)
  • Fallback重启策略(Fallback Restart Strategy)

➤ 用过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中的广播变量,使用时需要注意什么?

我们知道Flink是并行的,计算过程可能不在一个 Slot 中进行,那么有一种情况即:当我们需要访问同一份数据。那么Flink中的广播变量就是为了解决这种情况。
我们可以把广播变量理解为是一个公共的共享变量,我们可以把一个dataset 数据集广播出去,然后不同的task在节点上都能够获取到,这个数据在 每个节点 上只会存在一份。


➤ Flink Broadcast State 简介

Broadcast State 是 Flink 支持的另一种扩展方式,Broadcast State 将流数据广播到下游所有的 Task (All-Task)中,数据会全部存储在下流 Task 内存中,接收到 Broadcast 流的正常流可以利用这些数据,一般是广播系统配置,或动态规则。
Broadcast 流中的数据是可以动态修改的。


➤ Flink Broadcast State 特点

  • 使用 Map 类型数据结构
  • 仅适用于同时具有广播流和非广播流作为数据输入的特定算子
  • 可以具有多个不同名称的 Broadcast state

➤ Flink 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,需要实现BroadcastProcessFunction
public 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 Broadcast State 需要注意

  • 没有跨任务的通信,这就是为什么只有广播方可以修改 Broadcast state 的原因。
  • 用户必须确保所有任务以相同的方式为每个传入的数据元更新 Broadcast state,否则可能导致结果不一致。
  • 跨任务的 Broadcast state 中的事件顺序可能不同,虽然广播的元素可以保证所有元素都将转到所有下游任务,但元素到达的顺序可能不一致。因此,Broadcast state 更新不能依赖于传入事件的顺序。
  • 所有任务都会把 Broadcast state 存入 checkpoint,虽然 checkpoint 发生时所有任务都具有相同的 Broadcast state。这是为了避免在恢复期间所有任务从同一文件中进行恢复(避免热点),然而代价是 state 在 checkpoint 时的大小成倍数(并行度数量)增加。
  • Flink 确保在恢复或改变并行度时不会有重复数据,也不会丢失数据。在具有相同或改小并行度后恢复的情况下,每个任务读取其状态 checkpoint。在并行度增大时,原先的每个任务都会读取自己的状态,新增的任务以循环方式读取前面任务的检查点。
  • 不支持 RocksDB state backend,Broadcast state 在运行时保存在内存中。

➤ Flink 如何实现 Broadcast 广播流?

参看 Flink__20__Broadcast & Broadcast State
广播流用connect方式连接。


➤ 说说Flink中的 StateBackend 存储有几种?

Flink在做计算的过程中经常需要存储中间状态,来避免数据丢失和状态恢复。选择的状态存储策略不同,会影响状态持久化如何和 checkpoint 交互。
Flink提供了三种状态存储方式:

  1. MemoryStateBackend,(同步 进行分布式快照)
  2. FsStateBackend,(可以使用,同步 进行分布式快照)
  3. RocksDBStateBackend,(异步 进行分布式快照)

RocksDBStateBackend
除了第3种其他2种都是同步快照。也就是说用hdfs这种方式快照是会阻塞数据处理的,只有当两个barrier之间数据处理完成并完成快照之后才向下一个task发送数据并打入barrier n。我们不管异步快照,我们现在只说同步快照。


➤ Flink 中的时间有哪几类?

Flink 中的时间和其他流式计算系统的时间一样分为三类:事件时间摄入时间处理时间三种。

  1. 如果以 EventTime 为基准来定义时间窗口将形成EventTimeWindow,要求消息本身就应该携带EventTime。
  2. 如果以 IngesingtTime 为基准来定义时间窗口将形成 IngestingTimeWindow,以 source 的systemTime为准。
  3. 如果以 ProcessingTime 基准来定义时间窗口将形成 ProcessingTimeWindow,以 operator 的systemTime 为准。

➤ Flink的三种时间 Window?

① Processing time:根据task所在节点的本地时间来切分时间窗口
② event time:消息自带时间戳,但是这种时间是有延时的,也就是乱序的,为了防止同一个窗口的message被正确处理,所以需要其他方法如watermark,说白了就是给一个延时容忍度,然后根据watermark来判断窗口的划分,然后再根据trigger的类型判断什么时候进行计算
③ ingestion time:有的消息本身不携带时间戳,但是用户依然希望按照消息而不是节点时钟划分窗口,在message进入flink的时候给他一个递增的时间,是event time的一种特例,用的很少


➤ Flink 中 水印 Watermark 是什么概念,起到什么作用?

  • Watermark 是 Apache Flink 为了处理 EventTime Window 计算提出的一种机制,本质上是**一种时间戳**。 一般来讲Watermark经常和 EventTime,Window一起被用来处理乱序事件。
  • Watermark继承了StreamElement。Watermark 是和事件一个级别的抽象,其内部包含一个成员变量时间戳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 是为了解决什么问题?

Flink 引入了 WaterMark机制 ,再加上EventTime 完美的解决了 数据乱序 的问题。


➤ Flink WaterMark解决问题的思想是什么?

当基于 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面试题_第16张图片

在 Flink 内部,Flink 默认开启了多个窗口,来接收需要的数据。把相对应的数据放到 指定的窗口中。


➤ Flink 中设置 Watermark 有2种方式?

A. AssignerWithPeriodicWatermarks:
周期性的(一时的时间间隔或达到一定的记录条数)产生一个 WaterMark。可通过env.getConfig().setAutoWatermarkInterval() 进行修改。在实际生产环境中使用比较多,会周期性的产生 WaterMark,但是必须结合时间数据累积条数两个维度,否则在极端情况下会有较大的延迟。

使用这种方式生成水印可以指定:

  1. 可以通过env.getConfig().setAutoWatermarkInterval(...);设置生成水印的间隔(毫秒)。
  2. 通常建议在数据源(source)之后就进行生成水印,或者做些简单操作比如 filter/map/flatMap 之后再生成水印,越早生成水印的效果会更好,也可以直接在数据源头就做生成水印。比如你可以在 source 源头类中的 run() 方法里面这样定义:
@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的比较,如果 数据的 EventTimepreviousElementTimestamp 之前的时间戳 大,则返回数据的 EventTimegetCurrentWatermark()方法是获取当前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;
            }
        })

➤ Flink 中处理数据如何延迟的3种方法是什么?

A. 丢弃: 在 Flink 中,对这种延迟数据的默认处理方式是丢弃。
B. allowedLateness: 再次指定允许数据延迟的时间。
allowedLateness 表示允许数据延迟的时间,这个方法是在 WindowedStream 中的,用来设置允许窗口数据延迟的时间,超过这个时间的元素就会被丢弃,这个的默认值是 0,该设置仅针对于以EventTime开的Window
那 WaterMark 允许数据延迟时间与这个数据延迟的区区别是:allowedLateness允许延迟时间是在 Watermark 允许延迟时间的基础上增加的时间。

所谓延迟数据,即窗口已经因为watermark进行了触发,则在此之后如果还有数据进入窗口,则默认情况下不会对 窗口Window进行再次触发和聚合计算。要想在数据进入已经被触发过的窗口后,还能继续触发窗口计算,则可以使用延迟数据处理机制。

  1. 触发条件
    延迟数据对窗口进行第二次(或多次)触发的条件是 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);

➤ Flink Table & SQL 熟悉吗?TableEnvironment这个类有什么作用

TableEnvironment是Table API和SQL集成的核心概念。
这个类主要用来:

  1. 在内部catalog中注册表
  2. 注册外部catalog
  3. 执行SQL查询
  4. 注册用户定义(标量,表或聚合)函数
  5. 将DataStream或DataSet转换为表
  6. 持有对ExecutionEnvironment或StreamExecutionEnvironment的引用

➤ Flink SQL的实现原理是什么?是如何实现 SQL 解析的呢?首先大家要知道 Flink 的SQL解析是基于Apache Calcite这个开源框架。

Flink面试题_第17张图片

基于此,一次完整的SQL解析过程如下:

  1. 用户使用对外提供Stream SQL的语法开发业务应用
  2. 用calcite对StreamSQL进行语法检验,语法检验通过后,转换成calcite的逻辑树节点;最终形成calcite的逻辑计划
  3. 采用Flink自定义的优化规则和calcite火山模型、启发式模型共同对逻辑树进行优化,生成最优的Flink物理计划
  4. 对物理计划采用janino codegen生成代码,生成用低阶API DataStream 描述的流应用,提交到Flink平台执行

➤ 简单说说FlinkSQL的是如何实现的?

Flink 将 SQL 校验、SQL 解析以及 SQL 优化交给了Apache Calcite。Calcite 在其他很多开源项目里也都应用到了,譬如 Apache Hive, Apache Drill, Apache Kylin, Cascading。Calcite 在新的架构中处于核心的地位,如下图所示。

Flink面试题_第18张图片

构建抽象语法树的事情交给了 Calcite 去做。SQL query 会经过 Calcite 解析器转变成 SQL 节点树,通过验证后构建成 Calcite 的抽象语法树(也就是图中的 Logical Plan)。另一边,Table API 上的调用会构建成 Table API 的抽象语法树,并通过 Calcite 提供的 RelBuilder 转变成 Calcite 的抽象语法树。然后依次被转换成逻辑执行计划和物理执行计划。在提交任务后会分发到各个 TaskManager 中运行,在运行时会使用 Janino 编译器编译代码后运行


➤ Flink是如何做到高效的数据交换的?

在一个Flink Job中,数据需要在不同的task中进行交换,整个数据交换是有 TaskManager 负责的,TaskManager 的网络组件首先从缓冲buffer中收集records,然后再发送。Records 并不是一个一个被发送的,二是积累一个批次再发送,batch 技术可以更加高效的利用网络资源。


➤ Flink 的 kafka 连接器有什么特别的地方?

Flink源码中有一个独立的 connector模块,所有的其他connector都依赖于此模块,Flink 在1.9版本发布的全新kafka连接器,摒弃了之前连接不同版本的kafka集群需要依赖不同版本的connector这种做法,只需要依赖一个connector即可。


➤ 消费kafka数据的时候,如何处理脏数据?

可以在处理前加一个fliter算子,将不符合规则的数据过滤出去。


➤ 说说 Flink的内存管理是如何做的?

Flink 并不是将大量对象存在堆上,而是将对象都 序列化 到一个 预分配 的内存块上。此外,Flink大量的使用了堆外内存。如果需要处理的数据超出了内存限制,则会将部分数据存储到硬盘上。Flink 为了直接操作二进制数据实现了自己的序列化框架。
理论上Flink的内存管理分为三部分:

  • Network Buffers:这个是在TaskManager启动的时候分配的,用于 Shuffle、 Broadcast 。这是一组用于缓存网络数据的内存,每个块是32K,默认分配2048个,可以通过taskmanager.network.numberOfBuffers修改。
  • Memory Manager pool:这是一个由 MemoryManager 管理的,大量的Memory Segment块,用于运行时的算法(Sort/Join/Shuffle等),这部分启动的时候就会分配。下面这段代码,根据配置文件中的各种参数来计算内存的分配方法。(heap or off-heap,这个放到下节谈),内存的分配支持预分配和lazy load,默认懒加载的方式。默认情况下,池子占了堆内存的 70% 的大小。
  • User Code,这部分是除了Memory Manager之外的内存用于,这部分的内存是留给用户代码以及 TaskManager 的数据结构使用的。

➤ Flink的内存管理有什么特色?

①自带的序列化工具:序列化后的对象就是字节数组是连续存储,占用空间大大降低。又例如cpu多级缓存的命中,避免oom

使用定制的序列化工具前提是待处理的数据类型一样,这样可以再内存中存一份共享的schema,并且在操作对象时不用反序列化整个对象,而是根据字节数组的偏移量来反序列化一部分——访问对象的成员变量。

②显式内存管理:批量的申请内存和释放。避免了频繁申请释放导致的内存碎片和资源消耗,减少垃圾回收次数

③off-heap的使用:off-heap有三个特点:off-heap的数据可以与其他程序共享;off-heap的数据进行磁盘IO或者网络IO的时候支持zero-copy(零拷贝)技术,不需要至少一次的内存拷贝;off-heap可想而知可以延缓gc回收。

你既然提到了zero-copy(零拷贝)技术,你能详细说说什么是零拷贝技术吗?

首先明确两点:

  • 第一:零拷贝技术是针对内存数据的拷贝而言的;
  • 第二:正常的一次http请求-相应的过程需要四次内存拷贝的过程,服务端和客户端各两次,服务器的网线将数据写入内核缓存内存,此时cpu被迫中断执行中断进程,将数据拷贝到用户进程空间中,当处理完毕返回给客户端的时候还要讲用户空间的内存数据拷贝到内核缓冲中然后在send的时候拷贝到NIC网卡。这样一次http请求响应就是4次拷贝。

而零拷贝zero-copy就是想不要内核内存数据向用户空间中拷贝的过程,实现:

通过找一块内存作为用户空间和内核的共享。(epoll中有应用),所以说白了零拷贝技术就是共享内存页!!!回想一下在解决fast-fail问题的时候有一个集合叫做copyonwritearraylist写时复制的技术就是应用了零拷贝技术。当发生修改list的操作的时候会fork子进程来操作,而此时并不是将整个list复制,而是只复制修改的内存页,其他内存页采用父子进程共享内存页来实现共享!

在kafka中详细介绍了zero-copy的发展历程。


➤ 简述Flink的数据抽象及数据交换过程?

Flink 为了避免JVM的固有缺陷例如java对象存储密度低,FGC影响吞吐和响应等,实现了自主管理内存。MemorySegment 就是Flink的内存抽象。默认情况下,一个MemorySegment可以被看做是一个32kb大的内存块的抽象。这块内存既可以是JVM里的一个byte[],也可以是堆外内存(DirectByteBuffer)
在MemorySegment这个抽象之上,Flink在数据从operator内的数据对象在向TaskManager上转移,预备被发给下个节点的过程中,使用的抽象或者说内存对象是Buffer。
对接从Java对象转为Buffer的中间对象是另一个抽象StreamRecord。


➤ 说说 Flink的序列化如何做的?

Java本身自带的序列化和反序列化的功能,但是辅助信息占用空间比较大,在序列化对象时记录了过多的类信息。
Apache Flink摒弃了Java原生的序列化方法,以独特的方式处理数据类型序列化,包含自己的类型描述符,泛型类型提取和类型序列化框架。
TypeInformation 是所有类型描述符的基类。它揭示了该类型的一些基本属性,并且可以生成序列化器。

TypeInformation 支持以下几种类型:
  • BasicTypeInfo: 任意Java 基本类型或 String 类型
  • BasicArrayTypeInfo: 任意Java基本类型数组或 String 数组
  • WritableTypeInfo: 任意 Hadoop Writable 接口的实现类
  • TupleTypeInfo: 任意的 Flink Tuple 类型(支持Tuple1 to Tuple25)。Flink tuples 是固定长度固定类型的Java Tuple实现
  • CaseClassTypeInfo: 任意的 Scala CaseClass(包括 Scala tuples)
  • PojoTypeInfo: 任意的 POJO (Java or Scala),例如,Java对象的所有成员变量,要么是 public 修饰符定义,要么有 getter/setter 方法
  • GenericTypeInfo: 任意无法匹配之前几种类型的类

针对前六种类型数据集,Flink皆可以自动生成对应的TypeSerializer,能非常高效地对数据集进行序列化和反序列化。


➤ Flink中的Window出现了数据倾斜,你有什么解决办法?

window产生数据倾斜指的是数据在不同的窗口内堆积的数据量相差过多。本质上产生这种情况的原因是: 数据源头发送的数据量不同导致的。出现这种情况一般通过两种方式来解决:

  • 在数据进入窗口前做预聚合
  • 重新设计窗口聚合的key

➤ Flink中在使用聚合函数 GroupBy、Distinct、KeyBy 等函数时出现数据热点该如何解决?

数据倾斜和数据热点是所有大数据框架绕不过去的问题。处理这类问题主要从3个方面入手:

  • 在业务上规避这类问题
    例如一个假设订单场景,北京和上海两个城市订单量增长几十倍,其余城市的数据量不变。这时候我们在进行聚合的时候,北京和上海就会出现数据堆积,我们可以单独数据北京和上海的数据。
  • Key的设计上
    把热key进行拆分,比如上个例子中的北京和上海,可以把北京和上海按照地区进行拆分聚合。
  • 参数设置
    Flink 1.9.0 SQL(Blink Planner) 性能优化中一项重要的改进就是升级了微批模型,即 MiniBatch。原理是缓存一定的数据后再触发处理,以减少对State的访问,从而提升吞吐和减少数据的输出量。

➤ Operator Chains(算子链)这个概念你了解吗?

为了更高效地分布式执行,Flink会尽可能地将operator的subtask链接(chain)在一起形成task。每个task在一个线程中执行。将operators链接成task是非常有效的优化:

  1. 它能减少线程之间的切换
  2. 减少 Message 的序列化/反序列化
  3. 减少数据在缓冲区的交换
  4. 减少了延迟的同时提高整体的吞吐量。

这就是我们所说的算子链。


➤ Flink什么情况下才会把Operator chain在一起形成算子链?

两个operator chain在一起的的条件:

  • 上下游的并行度一致
  • 下游节点的入度为1 (也就是说下游节点没有来自其他节点的输入)
  • 上下游节点都在同一个 slot group 中(下面会解释 slot group)
  • 下游节点的 chain 策略为 ALWAYS(可以与上下游链接,map、flatmap、filter等默认是ALWAYS)
  • 上游节点的 chain 策略为 ALWAYS 或 HEAD(只能与下游链接,不能与上游链接,Source默认是HEAD)
  • 两个节点间数据分区方式是 forward(参考理解数据流的分区)
  • 用户没有禁用 operator-chain

➤ 什么是 Operator-chain? Operator-Chain 占用几个线程? Operator Chain 的好处?

Flink 在内部会将多个 Operator 算子 串在一起作为一个 Operator chain(执行链)来执行,每个 Operator-Chain 会在 TaskManager 上的一个 独立线程(Thread)中执行,这样不仅可以减少线程的数量及线程切换带来的资源消耗,还能降低数据在Operator算子之间传输序列化与反序列化带来的消耗。


➤ 如何禁止 Operator-Chain。

可以通过env.disableOperatorChaining()语句,来禁止 Operator-Chain,这样每个Operator 算子都会单独分配一个 Task(Spark中的 State)


➤ 算子 Operator-Chain 在一起的条件有哪些?

  • 下游节点只有一个输入
  • 下游节点的操作符不为 null
  • 上游节点的操作符不为 null
  • 上下游节点在一个槽位共享组(slotsharinggroup)内,默认是 default
  • 下游节点的连接策略是 ALWAYS(可以与上下游节点连接)
  • 上游节点的连接策略是 HEAD 或者 ALWAYS
  • edge 的分区函数是 ForwardPartitioner 的实例(没有 keyby 等操作)
  • 上下游节点的并行度相等
  • 允许进行节点连接操作(默认允许)

➤ Flink startNewChain(), disableChainning() 两个算子的作用是什么? 有什么好处?

在一些资源密集,计算密集的情况下,设置disableChaining,可以将一些复杂算子独立出来,使其独立运行,保证任务的正常运行。


➤ Flink 中算子 Operator 有两种关系?

  1. one-to-one: 类似于 Spark 中的 窄依赖。这种类型的operatorFlink会放到一个SubTask中执行,一个 SubTask 就对应一个Thread,一个 SubTask就运行在一个 TaskSlot 中,运行在一个SubTask中的程序逻辑都是相同的,只是运行的数据不同。这样设计可以减少资源浪费,数据传输开销。
  2. redistributing: 类假于 Spark 中的 宽依赖,Shuffle。这种类型的operator会把数据在算子之间传输。

➤ Flink Job的提交流程

用户提交的Flink Job会被转化成一个DAG任务运行,分别是:StreamGraph、JobGraph、ExecutionGraph。
Flink 中 JobManager与TaskManager,JobManager与Client的交互是基于Akka工具包的,是通过消息驱动。
整个Flink Job的提交还包含着ActorSystem的创建,JobManager的启动,TaskManager的启动和注册。


➤ Flink所谓"三层图"结构是哪几个"图"?

一个Flink任务的DAG生成计算图大致经历以下三个过程:

  • StreamGraph: 最接近代码所表达的逻辑层面的计算拓扑结构,按照用户代码的执行顺序向StreamExecutionEnvironment添加StreamTransformation构成流式图。
  • JobGraph: 从StreamGraph生成,将可以串联合并的节点进行合并,设置节点之间的边,安排资源共享slot槽位放置相关联的节点,上传任务所需的文件,设置检查点配置等。相当于经过部分初始化和优化处理的任务图。
  • ExecutionGraph: 由JobGraph转换而来,包含了任务具体执行所需的内容,是最贴近底层实现的执行图。

➤ Flink 计算资源的调度是如何实现的?

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面试题_第19张图片


➤ 说一说flink的迭代与增量迭代吧?

迭代计算一般出现在机器学习和图计算的应用中,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的迭代就不需要给出终止条件。


➤ 实时数据情景题1:如何得到实时的数据丢失率和数据延时率,比如数据丢失率是99.9%,延时率是99.99%,具体丢失率和延时率的定义可以自己把握?(金山云)

其实这个题我差不多思考了5分钟吧,想了两个方案,第二个方案得到面试官的赞赏(自淫);第一个方案就随便说说吧。

  • 第一个方案

通过改变结果表的方式,比如表结构时eventtime windowtime value currenttime,eventtime代表数据产生时间,windowtime代表结果窗口的时间,value就是聚合值啦, currenttime代表即将要写入mysql的时间。我们一方面把这个结果表持久化到mysql或者其他的地方,另一方面将结果表重新写回kafka,专门这样的话我就可以粗略地认为某个或者某些窗口的那些数据延迟了。当然了不用重写回kafka也应该可以处理。

但是这个方案不太好,而且不能一举两得,因为这种方式不能知道数据丢失情况。

  • 第二个方案

我借鉴了flink的barrier来实现的。面试官把这种方案叫做类似哨兵的方式。具体如下:我们在数据源头自己写一个生产者,这个生产者每分钟产生60或者600条数据,类似于采样的方式确定总体丢失率,比如60条数据在写结果时发现丢了1条,那么就认为该窗口内数据丢失了1/60的数据。

另外针对数据延时如何界定,跟第一种方案差不多,将这些自己的message写入mysql时打上当前时间,看一看是否延时,如果是就认为当前窗口数据延时。

注意:这里面有很多可以优化的地方,毕竟没有自己真正实现,所以等待机会吧。


➤ 15.实时数据情景题2:如何求得当前时间再过去24小时内的短视频的tags的topN,比如每秒的短视频播放次数在百万次,埋点数据格式是uid, video_id, time;然后业务库中有video_id对应的所有的tags。(头条)

其实这个东西很好做,只不过唯一需要考虑的时候内存的情况,或者说是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能否承受住。

我的回答:

  • 首先24小时窗口的uv必然会崩溃,当时24小时的窗口的pv我猜测是没有问题的!

因为我们假定flink在内存中存的是24小时/1分钟=1440条数据,分组之后1440*1000000条数据,100w代表的每分钟都会有100w的短视频被观看。所以总体会占有很大的内存,粗略计算需要24G(按照内存中对象24byte来计算的)。当然还没有达到非常大,flink无法支撑的地步。(说白了这里我一定要去源码中看一看到底checkpoint存的是什么东西!!!只需要知道pv和uv记录的东西即可,就可以知道全部了。)

  • 其次如果说我们认为这个内存量我们单台服务器支撑不了,那么也没有关系,我们在引入数据源的时候就进行keyed。

说白了就是分区partition操作,吧数据根据某个key(这里应该是video_id)分布到多台服务器上去,这样会缓解压力,其实我觉得这歌说明没什么意义,因为当我们调用group by操作的时候flink自己就会并行处理,把数据shuffle到多台服务器上去操作了!!

  • 最后如果我们真的认为采用24小时窗口不行,那么我们用中间件即可,比如hbase。

以一分钟为滑动窗口,每一分钟的结果都记录到hbase,每次trigger的时候取出过去24小时的1440条记录进行合并、计算、聚合操作得到结果。但这无疑是增加了网络io、失败概率、延迟。但这种方式是不会存在内存问题的。

  • 还有其他性能更好的技术方案比如使用druid,自带实时索引、预聚合特点,而且还有时间戳的索引,很不错的选择。

当然了类似于现在很多查询性能非常好的es、clickhouse我觉得只要逻辑正确都可以应用!

说归说,真正实现可没这么简单


➤ print 函数是怎么实现数据输出的?

print 函数中创建了 PrintSinkFunction 函数,并添加到 addSink中。


➤ 自定义 sink 一般有哪几个可以继承的类?

用户可以实现 RichSinkFunction,SinkFunction来自定义 Sink。
自定义 Sink 要实现 invoke 方法.
RichFunction 接口中有 open(), close() 方法。


➤ RichSinkFunction 有哪几个需要实现的方法?

RichSinkFunction 有 open(), close() 方法。


➤ 实现 MySql sink 的时,使用的 SQL 语句是什么?

insert into t_activities_count (aid, event_type, `count`) values (?, ?, ?) ON DUPLICATE KEY UPDATE `count` = `count` + ?

➤ 自定义 Source 一般有哪几个可以继承的类?

SourceFunction: run(), cancel() 方法。
RichSourceFunction, 抽像类, 空类。
ParallelSourceFunction, 接口, 空接口。
RichParallelSourceFunction, 抽象类, 空类。

RichFunction 接口中有 open(), close() 方法。


➤ Flink 的 Kafka Source 会把 offset 写到哪里? 如何关闭写到 __consumer_offset 中的 offset ?

默认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);

➤ Flink 开启了 checkpoint 机制后,会把 kafka offset 保存在哪2个地方?

  • checkpoint 中。
  • __consumer_offset Topic 中。

保存在__consumer_offsetTopic中的 offset一般有两个作用:

  • 监控 Topic 消费情况。
  • 在 Flink-Job重新启动,但是没有设置 savepoint 时,从__consumer_offset中恢复数据。

可以通过FlinkKafkaConsumer.setCommitOffsetsOnCheckpoints(false);来关闭该选项,默认是打开的。


➤ Flink Kafka 消费数据的 Offset 选择有几种模式?

如果从指定的 offset 读取数据,offset 不存在了,默认使用 setStartFromGroupOffsets 来消费分区中的数据。
如果有 checkpoint, savepoint 配置,则指定 offset 无效,作业会根据状态从存储的 offset中恢复数据。


➤ Flink 如何实现 带 checkpoint 功能的 并行文件读取 Source?

要实现 checkpointFunction 接口,并重写 snapshotState, initializeState 方法。
run() 方法中,对 offset 的更新需要加锁,因为 checkpoint 线程,与 source 线程是两个线程。
initializeState() 方法,在函数初始化的时候调用一次。
snapshotState() 是为了将 状态数据持久化到 hdfs 上。

➤ FlinkKafkaProducer 0.11.x 前后,类结构有什么不同?

  1. Kafka 0.11之后支持了事务,所以 FlinkKafkaProducer011 是继承的 TwoPhaseCommitSinkFunction 抽象类。
  2. Kafka 0.11之前不支持事务,所以 FlinkKafkaProducer010 和 FlinkKafkaProducer09 是基于 FlinkKafkaProducerBase 类来实现的,
    Flink面试题_第20张图片
@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 默认分区实现是?有什么问题?

使用 FlinkKafkaProducer 往 kafka 中写数据时,如果不单独设置 partition 策略,会默认使用 FlinkFixedPartitioner,该 partitioner 分区的方式是 task 所在的并发 id 对 topic 总 partition 数取余:parallelInstanceId % partitions.length

  • 此时如果 sink 为 4,paritition 为 1,则 4 个 task 往同一个 partition 中写数据。但当 sink task < partition 个数时会有部分 partition 没有数据写入,例如 sink task 为2,partition 总数为 4,则后面两个 partition 将没有数据写入。
  • 如果构建 FlinkKafkaProducer 时,partition 设置为 null,此时会使用 kafka producer 默认分区方式,非 key 写入的情况下,使用 round-robin 的方式进行分区,每个 task 都会 轮循 的写下游的 所有 partition。该方式下游的 partition 数据会比较均衡,但是缺点是: partition 个数过多的情况下需要维持过多的网络连接,即每个 task 都会维持跟所有 partition 所在 broker 的连接

Flink面试题_第21张图片


➤ Flink sum, min, max 算子底层是怎么实现的?

max,min算子在底层其实是调用aggregate(),通过传递不同的AggregationType (SUM, MIN, MAX, MINBY, MAXBY,) 实现不同的功能。


➤ Flink max 和 maxBy 之间的区别?

max 和 maxBy 之间的区别在于 max 返回流中的最大值,但 maxBy 返回具有最大值的键, min 和 minBy 同理。


➤ Flink 的 State 分为哪两类?

根据数据类型的划分,分为: 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
    Flink面试题_第22张图片

  • OperatorState
    只与算子实例绑定,每个算子实例 Operator 实例中持有所有数据元素一部分状态数据
    如下图,Kafka Topic有3个分区,Flink-Job 有4个并行度,每个 SubTask 都会维护自己读取 Kafka 相应 Partition 中的 offset,类似这种 offset 数据与具体的Operator相关的数据,就是OperatorState。
    当并行度发生变化时,Operator State 可以将状态在所有的并行实例中进行重分配,并且提供了多种方式来进行重分配。

Flink面试题_第23张图片

假如现在是求WordCount,结合上面两张图片:

  • OperatorState 中保存的是 Kafka Partition 的 offset 信息;共有4个 Partition,所以 checkpoint中会生成 4个对的State文件。
  • KeyedState 中保存的是分组后每个单词的频率;checkpoint 中生成的 State 文件数根据具体的 Parallel 决定。

Flink面试题_第24张图片

KeyedSatteOperatorState 都支持并行度发生变化时,进行状态数据的重新分配。


➤ Flink KeyedSatte 与 OperatorState 状态都是从 什么对象中获取的?

  • keyedState 都是通过调用 getRuntimeContext()方法,获取到 RuntimeContext 对象获取的。
  • operator state 是通过调用 FunctionInitializationContext 中获取的 State 才是 OperatorState.
    通过调用 OperatorStateStore operatorStateStore = context.getOperatorStateStore();

➤ Raw and Managed State

Keyed State 和 Operator State 都有两种存在形式,即 Raw State(原始状态),和 Managed State(托管状态)。

  • Raw State 状态: 是 Operator(算子)保存它们自己的数据结构中的 State,当 checkpoint时,原始状态会以字节流的形式写入 Checkpoint 中。Flink 不知道 State 的数据结构长啥样,仅能看到原生的字节数组。
  • Managed State 托管状态: 可以使用 Flink Runtime 提供的数据结构来表示,如内部哈希表或者 RockedDB。有 ValueState, ListState等。Flink Runtime 会对这些状态进行编码然后写入 Checkpoint 中。

DataStream 所有 function 都可以使用Managed State 托管状态,但是原生状态只能在实现 operator 的时候使用。相对原生状态,推荐使用托管状态,使用托管状态当并行度发生变化时,Flink 可以自动帮助你重分配 state,同时还可以更好的管理内存。


➤ Flink 中 State Descriptor 有几种?

要使用一个状态对象,需要先创建一个 StateDescriptor,它包含了状态的名字(你可以创建若干个 state,但是它们必须要有唯一的值以便能够引用它们),状态的值的类型,想使用的 state 类型,你可以创建

  • ValueStateDescriptor
  • ListStateDescriptor
  • ReducingStateDescriptor
  • FoldingStateDescriptor
  • MapStateDescriptor。

➤ Flink Operator State 有几种类型?

OperatorState 只支持一种数据结构,即 ListState。


➤ Flink OperatorState 支持几种托管状态?

为了使用 OperatorState托管状态,可以实现

  • CheckpointedFunction
  • ListCheckpointed
CheckpointedFunction
public interface CheckpointedFunction {
    
    void snapshotState(FunctionSnapshotContext context) throws Exception;

    void initializeState(FunctionInitializationContext context) throws Exception;
}
  • snapshotState: 执行 Checkpoint 的时候,snapshotState方法调用。
  • initializeState: 初始化自定义函数,或从checkpoint 中恢复 State 时调用。
ListCheckpointed
@PublicEvolving
public interface ListCheckpointed {
    
    List snapshotState(long checkpointId, long timestamp) throws Exception;

    void restoreState(List state) throws Exception;
}
  • ListCheckpointed定义了两个接口,一个是snapshotState方法,一个是restoreState方法
  • snapshotState方法,方法有个checkpointId参数,是唯一单调递增的数字,而timestamp则是master触发checkpoint的时间戳,该方法要返回当前的state(List结构)
  • restoreState方法会在failure recovery的时候被调用,传递的参数为List类型的state,方法里头可以将state恢复到本地
状态总结:
  1. stateful function可以通过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的时候,每个算子都会获得完整的状态元素列表。
  1. ListCheckpointed是CheckpointedFunction的限制版,它只能支持Even-split redistribution模式的list-style state

  2. ListCheckpointed定义了两个方法,分别是snapshotState方法及restoreState方法;

  • snapshotState方法在master触发checkpoint的时候被调用,用户需要返回当前的状态,
  • restoreState方法会在failure recovery的时候被调用,传递的参数为List类型的state,方法里头可以将state恢复到本地

➤Flink State 的 TTL 设置?

TTL 可以分配给任何类型的 KeyedState,如果一个 State 设置了 TTL,那么当 State 过期时,之前存储的值会清除。所有的 State 集合类型都支持单个TTL设置,这就是说 List 和 Map 的集合元素,都支持独立到期。

  StateTtlConfig valueStateTtl = StateTtlConfig.newBuilder(Time.seconds(5))
        .setUpdateType(UpdateType.OnCreateAndWrite)
        .setStateVisibility(StateVisibility.NeverReturnExpired).build();

➤ Flink State 状态值的更新策略有几种?

  • StateTtlConfig.UpdateType.Disabled 状态不会过期
  • StateTtlConfig.UpdateType.OnCreateAndWrite:创建和写入时更新State 时间。
  • StateTtlConfig.UpdateType.OnReadAndWrite: 除了创建和写入,在读时更新 State 时间。

➤ Flink State 状态的可见性有哪几种?

  • StateTtlConfig.StateVisibility.NeverReturnExpired: 永远不返回过期值
  • StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp: 如果仍然可用则返回

➤ Flink 状态清理有几种方式?

目前,堆State后端依赖于增量清理,RocksDB 后端使用压缩过滤器进行后台清理。

  StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupFullSnapshot().build()
  StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupInBackground()
    .build();

➤ Flink 状态清理策略?

CleanupStrategies:
TTL 清理策略: CleanupStrategies 类中有字段 isCleanupInBackground(是否在后台清理),相关的清理 Strategies。
Strategies:

  1. FULL_STATE_SCAN_SNAPSHOT
  2. INCREMENTAL_CLEANUP
  3. ROCKSDB_COMPACTION_FILTER

CleanupStrategy Interface:
a. Class -> EmptyCleanupStrategy(不清理,为空)
b. Class -> IncrementalCleanupStrategy(增量的清除)
c. Class -> RocksdbCompactFilterCleanupStrategy(在 RocksDB 中自定义压缩过滤器)


➤ Flink 状态存储有几种实现方式?

  • 基于内存的实现,MemoryStateBackend,在 debug 模式下使用,不建议在生产环境中使用。
  • 基于 HDFS 的实现,FsStateBackend,分布式文件持久化,每次读写都产生网络 IO,整体性能不佳。
  • 基于 RocksDB 的实现,RocksDBStatebackend,本地文件+异步 HDFS 持久化。
  • 还有一个是基于 Niagara(Alibaba 内部实现),NiagaraStateBackend,分布式持久化,在 Alibaba 生产环境中应用。

Flink 程序开启了 Checkpoint 后,Flink 程序会按照一定的时间间隔对程序State进行备份,当Flink故障恢复时,可从 Checkpoint中恢复之前的状态。


➤ Flink 中 ValueState 有几种实现方式?

  • HeapValueState
  • ImmutableValueState
  • RocksDBValueState
  • TtlValueState

➤ Flink 在定义 State 状态值时应该注意什么?

程序中的 ValueState 被声名为 transient 意思是不对 ValueState 序列化,Flink 的状态数据都保存在StateBackEnd 中,ValueState 的值应该是在 open()中通过 StateBackEnd 来获取的。如果不加 transient 关键字,有可能把State状态数据序列化的磁盘或其他介质中,在程序下次启动时,造成数据不一致;另一种情况是,ValueState 的值有可能很大,比如统计网站的 UV 数据,需要保存所有用户的 UID,如果序列化,很可能状态数据特别大。


➤ Flink 中的 Window 类型? 每种类型又细分为几种类型?

  • Flink 中的窗口分为按时间分配的,按数据分配的,按 Session 分配的。
  • Flink 的窗口根据滚动情况,分为滚动窗口滑动窗口
  • Flink 的窗口是 前闭后开的。

➤ 说说Flink中的Window?来一张官网经典的图:

Flink面试题_第25张图片

Flink 支持两种划分窗口的方式,按照**time** 和 count。如果根据时间划分窗口,那么它就是一个time-window。如果根据数据划分窗口,那么它就是一个count-window。
flink支持窗口的两个重要属性(size和interval):

  • 如果size=interval,那么就会形成tumbling-window(无重叠数据)
  • 如果size>interval,那么就会形成sliding-window(有重叠数据)
  • 如果size< interval, 那么这种窗口将会丢失数据。比如每5秒钟,统计过去3秒的通过路口汽车的数据,将会漏掉2秒钟的数据。

通过组合可以得出四种基本窗口:

  1. time-tumbling-window 无重叠数据的时间窗口,设置方式举例:timeWindow(Time.seconds(5))
  2. time-sliding-window 有重叠数据的时间窗口,设置方式举例:timeWindow(Time.seconds(5), Time.seconds(3))
  3. count-tumbling-window无重叠数据的数量窗口,设置方式举例:countWindow(5)
  4. count-sliding-window 有重叠数据的数量窗口,设置方式举例:countWindow(5,3)

➤ Flink Window 的触发条件是什么? 当有数据延迟时,Window 的触发条件又是什么?

Kafka 在没有指定 分区Key时,默认的分区策略是 轮询,即把所有数据按分区,依次发送到Topic的所有分区,以减少数据倾斜。wc10Topic 有4个分区,所以,我们测试了2次发现,只有当所有 Kafka Partition 都满足触发条件时,才会触发整个窗口计算;只要有一个没有达到条件,都不会触发窗口计算。为了更好的理解其原理,请看下图。图中每一个箭头都代表一个 kafka partition。
Flink面试题_第26张图片


➤ Flink 触发 Window 计算的条件是什么?

  1. 允许延迟的时间为0,即,只要有数据时间 大于 窗口的结束时间(边界),就会触发窗口计算。
  2. 允许延迟的时间不为0,当数据最大时间 减去 允许延迟时间 大于等于 窗口的结束时间(边界)时,才会触发窗口计算。为了更好的理解其,原理,参阅数据延迟示意图:

Flink面试题_第27张图片


➤ Flink Window 计算在 单并行度 与 多并行度 情况下,触发条件是什么?

  1. 在DataSource的并行度等于1的情况下:
    a. WaterMark = 数据所携带的时间 - 延迟时间的时间
    b. WaterMark >= 上一个窗口的结束边界就会触发窗口执行

  2. 在DataSource的并行度大于1的情况下:
    a. 每一个分区 WaterMark = 数据所携带的时间 - 延迟时间的时间
    b. 每一个分区 WaterMark >= 上一个窗口的结束边界就会触发窗口执行


➤ 在 Flink 中,如果要使用 Async I/O API,是非常简单的,需要通过下面三个步骤来执行对数据库的异步操作。

  • 继承 RichAsyncFunction 抽象类或者实现用来分发请求的 AsyncFunction 接口
  • 返回异步请求的结果的 Future
  • 在 DataStream 上使用异步操作
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");
    }
}

➤ Flink 异步 IO 的超时处理

当异步 I/O 请求超时时,默认情况下会引发超时异常重新启动作业。如果要自定义超时处理策略,可以重写 AsyncFunction 接口的 timeout 方法。


➤ Flink Async IO 结果顺序

AsyncFunction 发出的并发请求通常以某种未定义的顺序完成,具体结果取决于首先完成的请求。 Flink提供了两种模式:

  • 无序 AsyncDataStream.unorderedWait(…):异步请求完成后立即发出结果记录,在异步 I/O 操作后,DataStream中记录的顺序与以前不同,当时间策略使用的是ProcessTime处理时间时,这种情况下的延迟开销都会很小。
  • 有序 AsyncDataStream.orderedWait(…):在这种情况下,会保证流数据的顺序,结果记录发出去的顺序与触发异步请求的顺序相同,为此,如果有记录的结果先返回,也会在队列中缓存着,直到其前面的结果记录都发出(或者超时)了。这样的话就会导致部分数据会有一定的延迟和等待开销,因为和无序的情况下对比,这些结果会在状态中保持更长的时间。

➤ Flink 双流 Join 实现 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);
        }
    }

➤ Flink 双流 left join 实现 CoGroup函数方式:

Flink 中实现LeftJoin, RightJoin 是通过调用coGroup方法实现的。coGroup方法需要用户实现 CoGroupFunction接口,并定义LeftJoin, RightJoin的实现。
coGroup方法获取同一个计算窗口left-Stream的数据和 right-Stream 的数据, 匹配相同的 Key 值,输出匹配结果 。

coGroup方法的触发一定满足以下几个条件:
  1. 进入coGroup方法两个Stream的数据, 一定在同一个窗口中。
  2. 窗口已经被触发了。
CoGroupFunction 接口
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));
                }
            }
        }
    }

➤ Flink JoinedStreams & CoGroupedStreams?

双流 Join 也是一个非常常见的应用场景。深入源码你可以发现,JoinedStreams 和 CoGroupedStreams 的代码实现有80%是一模一样的,JoinedStreams 在底层又调用了 CoGroupedStreams 来实现 Join 功能。除了名字不一样,一开始很难将它们区分开来,而且为什么要提供两个功能类似的接口呢??

实际上这两者还是很点区别的:
  1. 首先 co-group 侧重的是group,是对同一个key上的两组集合进行操作,而 join 侧重的是pair,是对同一个key上的每对元素进行操作。
  2. co-group 比 join 更通用一些,因为 join 只是 co-group 的一个特例,所以 join 是可以基于 co-group 来实现的(当然有优化的空间)。
  3. 而在 co-group 之外又提供了 join 接口是因为用户更熟悉 join(源于数据库吧),而且能够跟 DataSet API 保持一致,降低用户的学习成本。

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 的样例代码在运行时会转换成如下的执行图:

Flink面试题_第28张图片

双流上的数据在同一个key的会被分别分配到同一个window窗口的左右两个篮子里,当window结束的时候,会对左右篮子进行笛卡尔积从而得到每一对pair,对每一对pair应用 JoinFunction。不过目前(Flink 1.1.x)JoinedStreams 只是简单地实现了流上的join操作而已,距离真正的生产使用还是有些距离。因为目前 join 窗口的双流数据都是被缓存在内存中的,也就是说如果某个key上的窗口数据太多就会导致 JVM OOM(然而数据倾斜是常态)。双流join的难点也正是在这里,这也是社区后面对 join 操作的优化方向,例如可以借鉴Flink在批处理join中的优化方案,也可以用ManagedMemory来管理窗口中的数据,并当数据超过阈值时能spill到硬盘。


➤ Flink ConnectedStreams?

在 DataStream 上有一个 union 的转换 dataStream.union(otherStream1, otherStream2, ...),用来合并多个流,新的流会包含所有流中的数据。union 有一个限制,就是所有合并的流的类型必须是一致的。ConnectedStreams 提供了和 union 类似的功能,用来连接两个流,但是与 union 转换有以下几个区别:

  1. ConnectedStreams 只能连接两个流,而 union 可以连接多于两个流。
  2. ConnectedStreams 连接的两个流类型可以不一致,而 union 连接的流的类型必须一致。
  3. ConnectedStreams 会对两个流的数据应用不同的处理方法,并且双流之间可以共享状态。这在第一个流的输入会影响第二个流时, 会非常有用。

如下 ConnectedStreams 的样例,连接 inputother 流,并在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时,其执行图如下所示:

Flink面试题_第29张图片


➤ Flink RocksDBStateBackend 及剖析

RocksDBStateBackend 和上面两种都有点不一样,RocksDB 是一种嵌入式的本地数据库,它会在本地文件系统中维护状态,KeyedStateBackend 等会直接写入本地 RocksDB 中,它还需要配置一个文件系统(一般是 HDFS),比如 hdfs://namenode:40010/flink/checkpoints,当触发 checkpoint 的时候,会把整个 RocksDB 数据库复制到配置的文件系统中去,当 failover 时从文件系统中将数据恢复到本地。


➤ Flink 官方其实也是推荐使用 RocksDB 来作为状态的后端存储,

  • state 直接存放在 RocksDB 中,不需要存在内存中,这样就可以减少 Task Manager 的内存压力,如果是存内存的话大状态的情况下会导致 GC 次数比较多,同时还能在 checkpoint 时将状态持久化到远端的文件系统,那么就比较适合在生产环境中使用
  • RocksDB 本身支持 checkpoint 功能
  • RocksDBStateBackend 支持增量的 checkpoint,在 RocksDBStateBackend 中有一个字段 enableIncrementalCheckpointing来确认是否开启增量的 checkpoint,默认是不开启的,在 CheckpointingOptions 类中有个 state.backend.incremental 参数来表示,增量 checkpoint 非常适合于超大状态的场景。

➤ Flink RocksDBStateBackend 属性:

  • checkpointStreamBackend:用于创建 checkpoint 流的状态后端
  • localRocksDbDirectories:RocksDB 目录的基本路径,默认是 Task Manager 的临时目录
  • enableIncrementalCheckpointing:是否增量 checkpoint
  • numberOfTransferingThreads:用于传输(下载和上传)状态的线程数量,默认为 1
  • enableTtlCompactionFilter:是否启用压缩过滤器来清除带有 TTL 的状态

➤ Flink 那么在使用 RocksDBStateBackend 时该注意什么呢:

  • 当使用 RocksDB 时,State大小将受限于磁盘可用空间的大小
  • 状态存储在 RocksDB 中,整个更新和获取State的操作都是要通过序列化反序列化才能完成的,跟状态直接存储在内存中,性能可能会略低些
  • 如果你应用程序的状态很大,那么使用 RocksDB 无非是最佳的选择


你可能感兴趣的:(面试准备,Flink学习实战)