目录
第一部分:Flink 的整体介绍、核心概念、算子等
第二部分:Flink 的数据传输、容错机制、序列化、数据热点、反压等实际生产环境中遇到的问题。Job 提交流程、数据交换、分布式快照机制、Flink SQL 的原理等
N0.1
Flink 中的核心概念
Flink 是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。并且 Flink 提供了数据分布、容错机制以及资源管理等核心功能。Flink提供了诸多高抽象层的API以便用户编写分布式任务:
- DataSet API
对静态数据进行批处理操作,将静态数据抽象成分布式的数据集,用户可以方便地使用Flink提供的各种操作符对分布式数据集进行处理,支持Java、Scala和Python - DataStream API
对数据流进行流处理操作,将流式的数据抽象成分布式的数据流,用户可以方便地对分布式数据流进行各种操作,支持Java和Scala - Table API
对结构化数据进行查询操作,将结构化数据抽象成关系表,并通过类SQL的DSL对关系表进行各种查询操作,支持Java和Scala
此外,Flink 还针对特定的应用领域提供了领域库,例如: Flink ML(Flink 的机器学习库),提供了机器学习Pipelines API并实现了多种机器学习算法。 Gelly(Flink 的图计算库),提供了图计算的相关API及多种图计算算法实现。
根据官网的介绍,Flink 的特性包含:
1、支持高吞吐、低延迟、高性能的流处理,支持带有事件时间的窗口 (Window) 操作,支持有状态计算的 Exactly-once语义
2、支持高度灵活的窗口 (Window) 操作
3、支持基于 time、count、session 以及 data-driven 的窗口操作
4、支持具有 Backpressure 功能的持续流模型
5、支持基于轻量级分布式快照(Snapshot)实现的容错,运行时同时支持 Batch on Streaming 处理和Streaming 处理,Flink 在 JVM 内部实现了自己的内存管理
6、支持迭代计算
7、支持程序自动优化:避免特定情况下 Shuffle、排序等昂贵操作,中间结果有必要进行缓存
Flink与Spark Streaming的区别
首先,Flink 是标准的实时处理引擎,基于事件驱动。而 Spark Streaming 是微批(Micro-Batch)的模型。下面列出两个框架的主要区别:
- 架构模型
Spark Streaming 在运行时的主要角色包括:Master、Worker、Driver、Executor,Flink 在运行时主要包含:Jobmanager、Taskmanager和Slot - 任务调度
Spark Streaming 连续不断的生成微小的数据批次,构建有向无环图DAG,Spark Streaming 会依次创建 DStreamGraph、JobGenerator、JobScheduler。Flink 根据用户提交的代码生成 StreamGraph,经过优化生成 JobGraph,然后提交给 JobManager进行处理,JobManager 会根据 JobGraph 生成 ExecutionGraph,ExecutionGraph 是 Flink 调度最核心的数据结构,JobManager 根据 ExecutionGraph 对 Job 进行调度 - 时间机制
Spark Streaming 支持的时间机制有限,只支持处理时间。Flink 支持了流处理程序在时间上的三个定义:处理时间、事件时间、注入时间。同时也支持 watermark 机制来处理滞后数据 - 容错机制
对于 Spark Streaming 任务,我们可以设置 checkpoint,然后假如发生故障并重启,我们可以从上次 checkpoint 之处恢复,但是这个行为只能使得数据不丢失,可能会重复处理,不能做到恰好一次处理语义。Flink 则使用两阶段提交协议来解决这个问题
Flink 的组件栈
Flink 是一个分层架构的系统,每一层所包含的组件都提供了特定的抽象,用来服务于上层组件,如图所示。
自下而上,每一层分别代表:Deploy 层:该层主要涉及了Flink的部署模式,在上图中可以看出,Flink 支持包括local、Standalone、Cluster、Cloud等多种部署模式。Runtime 层:Runtime层提供了支持 Flink 计算的核心实现,比如:支持分布式 Stream 处理、JobGraph到ExecutionGraph的映射、调度等等,为上层API层提供基础服务。API层:API 层主要实现了面向流(Stream)处理和批(Batch)处理API,其中面向流处理对应DataStream API,面向批处理对应DataSet API。Libraries层:该层称为Flink应用框架层,根据API层的划分,在API层之上构建的满足特定应用的实现计算框架,也分别对应于面向流处理和面向批处理两类。面向流处理支持:CEP(复杂事件处理)、基于SQL-like的操作(基于Table的关系操作);面向批处理支持:FlinkML(机器学习库)、Gelly(图处理)。
Flink可以完全独立于Hadoop,在不依赖Hadoop组件下运行。但是作为大数据的基础设施,Hadoop体系是任何大数据框架都绕不过去的。Flink可以集成众多Hadooop 组件,例如Yarn、Hbase、HDFS等等。例如,Flink可以和Yarn集成做资源调度,也可以读写HDFS,或者利用HDFS做检查点。
Flink编程模型
根据Flink官网的运行流程图可知,Flink 程序的基本构建是数据输入来自一个 Source,Source 代表数据的输入端,经过 Transformation 进行转换,然后在一个或者多个Sink接收器中结束。数据流(stream)就是一组永远不会停止的数据记录流,而转换(transformation)是将一个或多个流作为输入,并生成一个或多个输出流的操作。执行时,Flink程序映射到 streaming dataflows,由流(streams)和转换操作(transformation operators)组成。
Flink集群角色如图所示:
Flink 程序在运行时主要有 TaskManager,JobManager,Client三种角色。其中JobManager扮演着集群中的管理者Master的角色,它是整个集群的协调者,负责接收Flink Job,协调检查点,Failover 故障恢复等,同时管理Flink集群中从节点TaskManager。JobManager 从客户端中获取提交的应用,然后根据集群中 TaskManager 上 TaskSlot 的使用情况,为提交的应用分配相应的 TaskSlot 资源并命令 TaskManager 启动从客户端中获取的应用。JobManager 和 TaskManager 之间通过 Actor System 进行通信,获取任务执行的情况并通过 Actor System 将应用的任务执行情况发送给客户端。同时在任务执行的过程中,Flink JobManager 会触发 Checkpoint 操作,每个 TaskManager 节点 收到 Checkpoint 触发指令后,完成 Checkpoint 操作,所有的 Checkpoint 协调过程都是在 Fink JobManager 中完成。
TaskManager是实际负责执行计算的Worker,在其上执行Flink Job的一组Task,每个TaskManager负责管理其所在节点上的资源信息,如内存、磁盘、网络,在启动的时候将资源的状态向JobManager汇报。TaskManager 相当于整个集群的 Slave 节点,负责具体的任务执行和对应任务在每个节点上的资源申请和管理。客户端通过将编写好的 Flink 应用编译打包,提交到 JobManager,然后 JobManager 会根据已注册在 JobManager 中 TaskManager 的资源情况,将任务分配给有资源的 TaskManager节点,然后启动并运行任务。TaskManager 从 JobManager 接收需要部署的任务,然后使用 Slot 资源启动 Task,建立数据接入的网络连接,接收数据并开始数据处理。同时 TaskManager 之间的数据交互都是通过数据流的方式进行的。可以看出,Flink 的任务运行其实是采用多线程的方式,这和 MapReduce 多 JVM 进行的方式有很大的区别,Flink 能够极大提高 CPU 使用效率,在多个任务和 Task 之间通过 TaskSlot 方式共享系统资源,每个 TaskManager 中通过管理多个 TaskSlot 资源池进行对资源进行有效管理。
Client是Flink程序提交的客户端,当用户提交一个Flink程序时,会首先创建一个Client,该Client首先会对用户提交的Flink程序进行预处理,并提交到Flink集群中处理,所以Client需要从用户提交的Flink程序配置中获取JobManager的地址,并建立到JobManager的连接,将Flink Job提交给JobManager。
Flink 资源管理中 Task Slot
的概念
在Flink架构角色中,TaskManager是实际负责执行计算的Worker,TaskManager 是一个 JVM 进程,并会以独立的线程来执行一个task或多个subtask。为了控制一个 TaskManager 能接受多少个 task,Flink 提出了 Task Slot 的概念。TaskManager会将自己节点上管理的资源分为不同的Slot:固定大小的资源子集。这样就避免了不同Job的Task互相竞争内存资源,但是需要主要的是,Slot只会做内存的隔离。没有做CPU的隔离。
Flink分区策略
分区策略是用来决定数据如何发送至下游。目前 Flink 支持了8种分区策略:
GlobalPartitioner :数据会被分发到下游算子的第一个实例中进行处理。
ShufflePartitioner :数据会被随机分发到下游算子的每一个实例中进行处理。
RebalancePartitioner :数据会被循环发送到下游的每一个实例中进行处理。
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。
BroadcastPartitioner :广播分区会将上游数据输出到下游算子的每个实例中。适合于大数据集和小数据集做join的场景。
ForwardPartitioner ForwardPartitioner :用于将记录输出到下游本地的算子实例。它要求上下游算子并行度一样。
KeyGroupStreamPartitioner :Hash分区器。会将数据按 Key 的 Hash 值输出到下游算子实例中。
CustomPartitionerWrapper :用户自定义分区器。需要用户自己实现Partitioner接口,来定义自己的分区逻辑。例如:
static classCustomPartitionerimplementsPartitioner<String> {
@Override
publicintpartition(String key, int numPartitions) {
switch (key){
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
default:
return 4;
}
}
}
Flink SQL解析
Flink 的SQL解析是基于Apache Calcite这个开源框架,构建抽象语法树的事情交给了 Calcite 去做。
一次完整的SQL解析过程如下:
- 用户使用对外提供Stream SQL的语法开发业务应用
- 用calcite对StreamSQL进行语法检验,语法检验通过后,转换成calcite的逻辑树节点;最终形成calcite的逻辑计划Logical Plan
- 采用Flink自定义的优化规则和calcite火山模型、启发式模型共同对逻辑树进行优化,生成最优的Flink物理计划
- 对物理计划采用janino codegen生成代码,生成用低阶API DataStream 描述的流应用,提交到Flink平台执行
另一边,Table API 上的调用会构建成 Table API 的抽象语法树,并通过 Calcite 提供的RelBuilder 转变成 Calcite 的抽象语法树。然后依次被转换成逻辑执行计划和物理执行计划。在提交任务后会分发到各个 TaskManager 中运行,在运行时会使用 Janino 编译器编译代码后运行。
No.2
Flink是如何支持批流一体的?
Flink的开发者认为批处理是流处理的一种特殊情况,批处理是有限的流处理,Flink 使用一个引擎支持了DataSet API 和 DataStream API,以此来实现批流一体。
Flink是如何做到高效的数据交换的?
在一个Flink Job中,数据需要在不同的task中进行交换,整个数据交换是有 TaskManager 负责的,TaskManager 的网络组件首先从缓冲buffer中收集records,然后再发送。Records 并不是一个一个被发送的,而是积累一个批次再发送,batch 技术可以更加高效的利用网络资源。
Flink如何实现容错
Flink 实现容错主要靠强大的CheckPoint机制和State机制。Checkpoint机制指的是Flink会定期的持久化的状态数据,将状态数据持久化到远程⽂件系统。Checkpoint 负责定时制作分布式快照、对程序中的状态进行备份;State 用来存储计算过程中的中间状态。Checkpoint是由JobManager负责实施,JobManager会定期向下游的计算节点发送Barrier(栅栏),下游计算节点收到该Barrier信号之后,会预先提交自己的状态信息,并且给JobManage应答,同时会继续将接收到的Barrier继续传递给下游的任务节点,依次类推,所有的下游计算节点在收到该Barrier信号的时候都会做预提交自己的状态信息。等到所有的下游节点都完成了状态的预提交,并且JobManager收集完成所有下游节点的应答之后,JobManager才会认定此次的Checkpoint是成功的,并且会⾃动删除上⼀次检查点数据。
Savepoint会使用Flink的Checkpoint机制来创建一个非增量的Snapshot,里面包含Streaming程序的状态,并将Checkpoint的数据存储到外部存储系统中。Savepoint由两部分组成:稳定存储(例如HDFS)上的二进制文件和元数据文件的目录。稳定存储上的文件表示作业执行状态图像的净数据;Savepoint的元数据文件以(绝对路径)的形式包含(主要)指向作为Savepoint一部分的稳定存储上的所有文件的指针。
savepoint 和 checkpoint 区别
Savepoint与Checkpoint的不同之处在于备份与传统数据库系统中的恢复日志不同。Checkpoint的主要目的是在意外的作业失败时提供恢复机制,生命周期由Flink管理,作为一种恢复和定期触发的方法。Savepoints由用户创建,拥有和删除。
Flink的分布式快照是根据Chandy-Lamport算法量身定做的,通过持续创建分布式数据流及其状态的一致快照。核心思想是在 input source 端插入 barrier,控制 barrier 的同步来实现 snapshot 的备份和 exactly-once 语义。Flink 中的分布式快照机制实现如下图所示:
将流计算看作成一个流式的拓扑,定期在这个拓扑的头部source点开始插入特殊的barriers(栅栏)。从上游开始不断向下游广播这个Barriers。每一个节点收到所有的barriers,会将state做一次snapshot(快照)。当每个节点都做完Snapshot之后,整个拓扑就算做完一次checkpoint。接下来不管出现任何故障,都会从最近的checkpoint进行恢复。Flink用这套Chandy-Lamport算法,保证了强一致性的语义,也是Flink区别于其他无状态流计算引擎的核心区别。
Flink的内存管理
Flink 并不是将大量对象存在堆上,而是将对象都序列化到一个预分配的内存块上。此外,Flink大量的使用了堆外内存。如果需要处理的数据超出了内存限制,则会将部分数据存储到硬盘上。Flink 为了直接操作二进制数据实现了自己的序列化框架。
理论上Flink的内存管理分为三部分:
- Network
Buffers:这个是在TaskManager启动的时候分配的,这是一组用于缓存网络数据的内存,每个块是32K,默认分配2048个,可以通过taskmanager.network.numberOfBuffers
设置 - Memory Manage pool
大量的MemorySegment块,用于运行时的算法(Sort/Join/Shuffle等),这部分启动的时候就会分配。内存的分配支持预分配和lazy load,默认懒加载的方式。 - User Code
这部分是除了Memory Manager之外的内存用于User code和TaskManager本身的数据结构。
由于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 为了避免JVM的固有缺陷,例如:java对象存储密度低、GC影响吞吐和响应等,实现了自主管理内存。MemorySegment就是Flink的内存抽象,默认情况下,一个MemorySegment可以被看做是一个32KB大的内存块的抽象,既可以是JVM里的一个byte[],也可以是堆外内存(DirectByteBuffer)。在MemorySegment这个抽象之上,Flink在数据从operator内的数据对象在向TaskManager上转移,预备被发给下个节点的过程中,使用的抽象或者说内存对象是Buffer。对接从Java对象转为Buffer的中间对象是另一个抽象StreamRecord。
Flink的Window
Flink 支持两种划分窗口的方式,按照time和count。如果根据时间划分窗口,那么它就是一个time-window 如果根据数据划分窗口,那么它就是一个count-window。
Flink支持窗口的两个重要属性(size和interval)。如果size=interval,那么就会形成tumbling-window(无重叠数据) 如果size>interval,那么就会形成sliding-window(有重叠数据) 如果size< interval, 那么这种窗口将会丢失数据。比如每5秒钟,统计过去3秒的通过路口汽车的数据,将会漏掉2秒钟的数据。通过组合可以得出四种基本窗口:
time-tumbling-window 无重叠数据的时间窗口,设置方式举例:timeWindow(Time.seconds(5))
time-sliding-window 有重叠数据的时间窗口,设置方式举例:timeWindow(Time.seconds(5),Time.seconds(3))
count-tumbling-window无重叠数据的数量窗口,设置方式举例:countWindow(5)
count-sliding-window 有重叠数据的数量窗口,设置方式举例:countWindow(5,3)
window产生 数据倾斜 指的是数据在不同的窗口内堆积的数据量相差过多。本质上产生这种情况的原因是数据源头发送的数据量速度不同导致的。出现这种情况一般通过两种方式来解决:1、在数据进入窗口前做预聚合 2、重新设计窗口聚合的key
Flink中在使用聚合函数 GroupBy、Distinct、KeyBy 等函数时出现数据倾斜问题,主要从3个方面入手:
- 在业务上规避这类问题
例如一个假设订单场景,北京和上海两个城市订单量增长几十倍,其余城市的数据量不变。这时候我们在进行聚合的时候,北京和上海就会出现数据堆积,我们可以单独数据北京和上海的数据。 - Key的设计上
把热key进行拆分,比如上个例子中的北京和上海,可以把北京和上海按照地区进行拆分聚合。 - 参数设置
Flink 1.9.0 SQL(Blink Planner) 性能优化中一项重要的改进就是升级了微批模型,即 MiniBatch。原理是缓存一定的数据后再触发处理,以减少对State的访问,从而提升吞吐和减少数据的输出量。
如何解决Flink任务延迟高
在Flink的后台任务管理中,可以看到Flink的哪个算子和task出现了反压。最主要的手段是资源调优和算子调优。资源调优即是对作业中的Operator的并发数(parallelism)、CPU(core)、堆内存(heap_memory)等参数进行调优。作业参数调优包括:并行度的设置,State的设置,checkpoint的设置。
Flink在处理反压时,内部是基于 producer-consumer 模型来进行消息传递的,Flink的反压设计也是基于这个模型。Flink 使用了高效有界的分布式阻塞队列,就像 Java 通用的阻塞队列(BlockingQueue)一样。下游消费者消费变慢,上游就会受到阻塞。
Flink的反压和Strom有哪些不同?
Storm 是通过监控 Bolt 中的接收队列负载情况,如果超过高水位值就会将反压信息写到 Zookeeper ,Zookeeper 上的 watch 会通知该拓扑的所有 Worker 都进入反压状态,最后 Spout 停止发送 tuple。
Flink中的反压使用了高效有界的分布式阻塞队列,下游消费变慢会导致发送端阻塞。二者最大的区别是Flink是逐级反压,而Storm是直接从源头降速。
Operator Chains(算子链)
为了更高效地分布式执行,Flink会尽可能地将operator的subtask链接(chain)在一起形成task。每个task在一个线程中执行。将operators链接成task是非常有效的优化:它能减少线程之间的切换,减少消息的序列化/反序列化,减少数据在缓冲区的交换,减少了延迟的同时提高整体的吞吐量,即算子链。Flink把Operator chain放在一起形成算子链的条件:
1、上下游的并行度一致
2、下游节点的入度为1 (也就是说下游节点没有来自其他节点的输入)
3、上下游节点都在同一个 slot group 中(下面会解释 slot group)
4、下游节点的 chain 策略为 ALWAYS(可以与上下游链接,map、flatmap、filter等默认是ALWAYS)
5、上游节点的 chain 策略为 ALWAYS 或 HEAD(只能与下游链接,不能与上游链接,Source默认是HEAD)
6、两个节点间数据分区方式是 forward(参考理解数据流的分区)
7、用户没有禁用 chain
DAG计算图
Flink所谓"三层图"结构生成大致经历以下三个过程:
- 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。
watermark
watermark是一种衡量Event Time进展的机制,它是数据本身的一个隐藏属性。watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、背压等原因,导致乱序的产生。但是对于late element,不能无限期的等下去,因此就存在watermark机制来保证一个特定的时间后,必须触发window去进行计算了。
在 Flink 的窗口处理过程中,如果确定全部数据到达,就可以对 Window 的所有数据做窗口计算操作(如汇总、分组等),如果数据没有全部到达,则继续等待该窗口中的数据全部到达才开始处理。这种情况下就需要用到水位线(WaterMarks)机制,它能够衡量数据处理进度,保证事件数据(全部)到达Flink或者在乱序及延迟到达时,也能够像预期一样计算出正确并且连续的结果。当任何 Event 进入到 Flink 系统时,会根据当前最大事件时间产生 Watermarks 时间戳。Watermark 的使用存在三种情况:
【1】本来有序的 Stream 中的 Watermark:如果数据元素的事件时间是有序的,Watermark 时间戳会随着数据元素的事件时间按顺序生成,此时水位线的变化和事件时间保持一直(因为既然是有序的时间,就不需要设置延迟了,即 watermark=maxtime-0 = maxtime)。当Watermark 时间大于 Windows 结束时间就会触发对 Windows 的数据计算。
【2】乱序事件中的 Watermark
【3】并行数据流中的 Watermark:在多并行度的情况下,Watermark 会有一个对齐机制,这个对齐机制会取所有 Channel中最小的 Watermark。
提取watermark:watermark的提取工作在taskManager中完成,即提取watermark是并行进行的。生成watermark的方式主要有2大类:
1、AssignerWithPeriodicWatermarks:定时提取watermark,这种方式会定时提取更新wartermark。
2、AssignerWithPunctuatedWatermarks:伴随event的到来就提取watermark,就是每一个event到来的时候,就会提取一次Watermark。这样的方式当然设置watermark更为精准,但是当数据量大的时候,频繁的更新wartermark会比较影响性能。
Flink如何处理乱序?
watermark+window机制。window中可以对input进行按照Event Time排序,使得完全按照Event Time发生的顺序去处理数据,以达到处理乱序数据的目的。
如何计算WaterMark的值?
Watermark = 进入 Flink 的最大的事件时间(mxtEventTime) - 指定的延迟时间
Flink何时触发window?
如果窗口的停止时间小于或者等于maxEventTime – t(当时的 warkmark),那么这个窗口被触发执行
Flink 状态管理
Flink有两种基本类型的状态:托管状态(Managed State)和原生状态(Raw State)。两者的区别:Managed State是由Flink管理的,Flink帮忙存储、恢复和优化,Raw State是用户自定义的状态,只支持字节,任何上层数据结构需要序列化为字节数组。对Managed State继续细分,它又有两种类型:Keyed State和Operator State。
Keyed State
Flink 为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态。Flink 提供了以下数据格式来管理和存储键控状态 (Keyed State):
- ValueState:存储单值类型的状态。可以使用 update(T) 进行更新,并通过 T value() 进行检索
- ListState:存储列表类型的状态。可以使用 add(T) 或 addAll(List) 添加元素;并通过 get() 获得整个列表
- ReducingState:用于存储经过 ReduceFunction 计算后的结果,使用 add(T) 增加元素
- AggregatingState:用于存储经过 AggregatingState 计算后的结果,使用 add(IN) 添加元素
- FoldingState:已被标识为废弃,会在未来版本中移除,官方推荐使用 AggregatingState 代替
- MapState:维护 Map 类型的状态。
Operator State
Operator State可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。Flink为算子状态提供三种基本数据结构:
- ListState:存储列表类型的状态
- UnionListState:存储列表类型的状态,与 ListState 的区别在于:如果并行度发生变化,ListState 会将该算子的所有并发的状态实例进行汇总,然后均分给新的 Task;而 UnionListState 只是将所有并发的状态实例汇总起来,具体的划分行为则由用户进行定义
- BroadcastState:用于广播的算子状态。如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态
Flink中的状态一致性
1、Flink是如何保证状态一致性的?
Flink使用了一种轻量级快照机制(Checkpoint)来保证exactly-one语义,在出现故障时将系统重置回正确状态;所有任务的状态,在某个时间点的一份快照,而这个时间点是所有任务都恰好处理完一个相同的输入数据的时候。
2、端到端(end-to-end)状态一致性
- 在flink的流处理器内部由流处理器实现一致性,一致性除了需要在flink内部保证还需要保证数据源和输出端的一致性
- 端到端的一致性,意味着整个数据流,每个组件都有自己的一致性
- 一致性的级别取决于所有组件中一致性最弱的组件
端到端的保证:
- 内部保证 — checkpoint
- source端 — 可重设数据的读取位置;可重新读取偏移量
- sink端 – 从故障恢复时,数据不会重复写入外部系统,有两种实现方式:幂等写入和事务性写入;幂等写入:一个操作多次执行,但是只改一次结果,重复的不起作用。事务写入:构建的事务对应着checkpoint,等到checkpoint真正完成的时候,才把对应的结果写入sink中。
两种事务写入的实现方式
预写日志( Write- Ahead-Log , WAL )
●把结果数据先当成状态保存,然后在收到checkpoint完成的通知时,一次性写入sink系统
●简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么sink系统,
都能用这种方式一批搞定
●DataStream API提供了-个模板类: GenericWriteAheadSink, 来实现这种事务性sink
两阶段提交(Two- Phase-Commit,2PC )
●对于每个checkpoint, sink任务会启动一个事务,并将接下来所有接收的数据添加到事务里
●然后将这些数据写入外部sink系统,但不提交它们--这时只是“预提交”)
●当它收到checkpoint完成的通知时,它才正式提交事务,实现结果的真正写入
➢这种方式真正实现了exactly-once,它需要一个提供事 务支持的外部sink系统。
Flink 提供了TwoPhaseCommitSinkFunction接口。
2PC对外部sink系统的要求:
1、外部sink系统必须提供事务支持,或者sink任务必须能够模拟外部系统上的事务.
2、在checkpoint的间隔期间里,必须能够开启一个事 务并接受数据写入
3、在收到checkpoint完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况下,这可能需要-些时间。如果这个时候sink系统关闭事务(例如超时了) ,那么未提交的数据就会丢失
4、sink任务必须能够在进程失败后恢复事务
5、提交事务必须是幂等操作
3、Flink+kafka端到端的exactly-once语义
Flink和kafka天生就是一对,用kafka做为source,用kafka做完sink <===> 实现端到端的一致性
- 内部 – 利用checkpoint机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
- source – kafka consumer作为source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
- sink – kafka producer作为sink,采用两阶段提交sink,需要实现一个TwoPhaseCommitSinkFunction
Flink由JobManager协调各个TaskManager进行checkpoint存储,checkpoint保存在 StateBackend中。当 checkpoint 启动时,JobManager 会将检查点分界线(barrier)注入数据流;barrier会在算子间传递下去。每个算子会对当前的状态做个快照,保存到状态后端;对于source任务而言,就会把当前的offset作为状态保存起来。下次从checkpoint恢复时,source任务可以重新提交偏移量,从上次保存的位置开始重新消费数据。每个内部的 transform 任务遇到 barrier 时,都会把状态存到 checkpoint 里。sink 任务首先把数据写入外部 kafka,这些数据都属于预提交的事务(还不能被消费);当遇到 barrier 时,把状态保存到状态后端,并开启新的预提交事务。(以barrier为界之前的数据属于上一个事务,之后的数据属于下一个新的事务);
当所有算子任务的快照完成,也就是这次的 checkpoint 完成时,JobManager 会向所有任务发通知,确认这次 checkpoint 完成。当sink 任务收到确认通知,就会正式提交之前的事务,kafka 中未确认的数据就改为“已确认”,数据就真正可以被消费了。
所以执行过程实际上是一个两段式提交,每个算子执行完成,会进行“预提交”,直到执行完sink操作,会发起“确认提交”,如果执行失败,预提交会放弃掉。具体的两阶段提交步骤总结如下:
- 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
- jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanager
- sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据
- jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
- sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
- 外部kafka关闭事务,提交的数据可以正常消费了。
所以,如果宕机需要通过StateBackend进行恢复,只能恢复所有确认提交的操作。
Flink 如何保证Exactly-once?
Flink通过实现两阶段提交和状态保存来实现端到端的一致性语义。 分为以下几个步骤:
- 开始事务(beginTransaction)创建一个临时文件夹,来写把数据写入到这个文件夹里面
- 预提交(preCommit)将内存中缓存的数据写入文件并关闭
- 正式提交(commit)将之前写完的临时文件放入目标目录下。这代表着最终的数据会有一些延迟
- 丢弃(abort)丢弃临时文件
若失败发生在预提交成功后,正式提交前。可以根据状态来提交预提交的数据,也可删除预提交的数据。
No.3
Flink SQL 如何实现数据流的 Join
实时领域 Streaming SQL 中的 Join 与离线 Batch SQL 中的 Join 最大不同点在于无法缓存完整数据集,而是要给缓存设定基于时间的清理条件以限制 Join 涉及的数据范围。根据清理策略的不同,Flink SQL 分别提供了 Regular Join、Time-Windowed Join 和 Temporal Table Join 来应对不同业务场景。
离线 Batch SQL Join 的实现
传统的离线 Batch SQL (面向有界数据集的 SQL)有三种基础的实现方式,分别是 Nested-loop Join、Sort-Merge Join 和 Hash Join。
- Nested-loop Join将两个数据集加载到内存,并用内嵌遍历的方式来逐个比较两个数据集内的元素是否符合 Join 条件。Nested-loop Join 虽然时间效率以及空间效率都是最低的,但胜在比较灵活适用范围广,因此其变体 BNL 常被传统数据库用作为 Join 的默认基础选项
- Sort-Merge Join分为两个 Sort 和 Merge 阶段。首先将两个数据集进行分别排序,然后对两个有序数据集分别进行遍历和匹配,类似于归并排序的合并。Sort-Merge 只适用于 Equi-Join(Join 条件均使用等于作为比较算子)。Sort-Merge Join 要求对两个数据集进行排序,成本很高,通常作为输入本就是有序数据集的情况下的优化方案
- Hash Join 同样分为两个阶段,首先将一个数据集转换为 Hash Table,然后遍历另外一个数据集元素并与 Hash Table 内的元素进行匹配。第一阶段和第一个数据集分别称为 build 阶段和 build table,第二个阶段和第二个数据集分别称为 probe 阶段和 probe table。Hash Join 效率较高但对空间要求较大,通常是作为 Join 其中一个表为适合放入内存的小表的情况下的优化方案。和 Sort-Merge Join 类似,Hash Join 也只适用于 Equi-Join
实时 Streaming SQL Join
相对于离线的 Join,实时 Streaming SQL(面向无界数据集的 SQL)无法缓存所有数据,因此 Sort-Merge Join 要求的对数据集进行排序基本是无法做到的。Flink SQL 提供的三种 Join 的主要区别在于缓存剔除策略如何实现。
- Regular Join
Regular Join 是最为基础的没有缓存剔除策略的 Join。Regular Join 中两个表的输入和更新都会对全局可见,影响之后所有的 Join 结果。因为历史数据不会被清理,所以 Regular Join 允许对输入表进行任意种类的更新操作(insert、update、delete)。然而因为资源问题 Regular Join 通常是不可持续的,一般只用做有界数据流的 Join。 - Time-Windowed Join
Time-Windowed Join 利用窗口给两个输入表设定一个 Join 的时间界限,超出时间范围的数据则对 JOIN 不可见并可以被清理掉。这里涉及到的时间的语义可以指计算发生的系统时间( Processing Time),也可以指从数据本身的时间字段提取的 Event Time。如果是 Processing Time,Flink 根据系统时间自动划分 Join 的时间窗口并定时清理数据;如果是 Event Time,Flink 分配 Event Time 窗口并依据 Watermark 来清理数据。 - Temporal Table Join
虽然 Timed-Windowed Join 解决了资源问题,但也限制了使用场景: Join 两个输入流都必须有时间下界,超过之后则不可访问。这对于很多 Join 维表的业务来说是不适用的,因为很多情况下维表并没有时间界限。针对这个问题,Flink 提供了 Temporal Table Join 来满足用户需求。
Temporal Table Join 类似于 Hash Join,将输入分为 Build Table 和 Probe Table。前者一般是纬度表的 changelog,后者一般是业务数据流,典型情况下后者的数据量应该远大于前者。在 Temporal Table Join 中,Build Table 是一个基于 append-only 数据流的带时间版本的视图,所以又称为 Temporal Table。Temporal Table 要求定义一个主键和用于版本化的字段(通常就是 Event Time 时间字段),以反映记录在不同时间的内容。
不同于在 Regular Join 和 Time-Windowed Join 中两个表是平等的,任意一个表的新记录都可以与另一表的历史记录进行匹配,在 Temporal Table Join 中,Temoparal Table 的更新对另一表在该时间节点以前的记录是不可见的。这意味着只需要保存 Build Side 的记录直到 Watermark 超过记录的版本字段。因为 Probe Side 的输入理论上不会再有早于 Watermark 的记录,这些版本的数据可以安全地被清理掉。