flink在前年的时候就已经学习过,但是在系统中没有广泛的应用,只是当成大数据组件与其他的技术进行预研,没有把整体的代码改为flink的代码,主要没有这方面的需求,只是当成一个预研的项目写过一部分。
学习flink必须先要了解它的应用场景,Flink应用场景:
1.时间驱动型:
它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。
事件驱动型应用是在计算存储分离的传统应用基础上进化而来。在传统架构中,应用需要读写远程事务型数据库。
2.数据分析应用
从原始数据中提取有价值的信息和指标。传统的分析方式通常是利用批查询,或将事件记录下来并基于此有限数据集构建应用来完成。为了得到最新数据的分析结果,必须先将它们加入分析数据集并重新执行查询或运行应用,随后将结果写入存储系统或生成报告。
- 电信网络质量监控
- 移动应用中的产品更新及实验评估分析
- 消费者技术中的实时数据即席分析
- 大规模图分析
3.数据管道应用
提取-转换-加载(ETL)是一种在存储系统之间进行数据转换和迁移的常用方法。ETL 作业通常会周期性地触发,将数据从事务型数据库拷贝到分析型数据库或数据仓库。
数据管道和 ETL 作业的用途相似,都可以转换、丰富数据,并将其从某个存储系统移动到另一个。但数据管道是以持续流模式运行,而非周期性触发。因此它支持从一个不断生成数据的源头读取记录,并将它们以低延迟移动到终点。例如:数据管道可以用来监控文件系统目录中的新文件,并将其数据写入事件日志;另一个应用可能会将事件流物化到数据库或增量构建和优化查询索引。
1.flink特点
①同时支持高吞吐、低延迟、高性能
②支持事件时间概念(Event Time):基于时间事件时间(Event Time)语义进行窗口计算,也就是时间产生的时间。这种基于时间驱动的机制使得事件即使是乱序到达,流系统也能够计算出精确的结果,保持了时间原本产生的时序性。尽量避免网络传输或硬件系统影响。
③支持有状态计算:流式计算过程中将算子的中间结果数据保存在内存中或者是文件系统中,等下一个时间进入算子后可以从之前的状态中获取中间结果中计算当前的结果,从而无需每次都基于全部的原始数据来统计结果,这种方式极大提升了系统的性能,并降低了计算过程资源的消耗,对于数据量大且运算逻辑非常复杂的流式计算场景,有状态发挥了非常重要的作用。
④支持高度灵活的窗口(Window)操作:在流处理应用中,数据是连续不断的,需要通过窗口的方式对数据进行一定范围的聚合计算,Flink将窗口划分为基于Time,count,Session,以及Data-driven等类型的窗口操作,窗口可以用灵活的触发条件定制化达到对复杂的流传输模式的支持,用户可以定义不同窗口触发机制来满足不同的需求。
⑤基于轻量级分布式快照(CheckPoint)实现的容错:Flink能够分布式运行在上千个节点上,将一个大型计算任务的流程拆解成小的计算过程,然后将tesk分布到并行节点上进行处理,在执行任务过程中,能够自动发现事件处理过程中的错误而导致数据不一致的问题。比如:节点宕机、网路传输问题,或是由于用户因为升级或修复问题导致计算服务重启等。在这些情况下,通过基于分布式快照技术CheckPoints,将执行过程中的状态信息进行持久化存储,一但任务出现异常停止,Flink就能够从Checkpoint中进行任务的自动恢复,以确保数据在处理过程中的一致性(Exactly-ONCE)
⑥基于JVM实现独立的内存管理: 内存管理是所有计算框架需要重点考虑的部分,尤其对于计算量比较大的计算场景,数据在内存中该如何进行管理显得至关重要,针对内存管理,Flink实现了自身管理内存机制,尽可能减少JVM GC对系统的影响,另外,Flink通过序列化/反序列化方法将所有的数据对象转化为二进制在内存中存储,降低GC带来的性能下降或任务异常的风险,因此Flink较其他分布式处理的框架显得更加稳定,不会因为JVM GC等问题而影响整个应用的运行
⑦Save Point(保存点) 对于7*24小时运行的流式运用,数据源源不断地接入,在一段时间内应用的终止有可能导致数据丢失或者计算结果不准确,例如进行集群版本的升级、停机运维操作等。值得一提的是,Flink通过Save Points技术将任务执行的快照保存在存储介质上,当任务重启的时候可以直接从事保存的Save Points恢复原有的计算状态,使得任务继续按照停机之前的状态运行,Sava Point技术可以让用户更好的管理和运维
2. Flink与Spark和Storm的区别
①SparkStreaming:高吞吐、不能保证低延迟。
② Storm不能做到高吞吐、低延迟。
③Flink高吞吐、低延迟。
- 数据模型
- Spark采用RDD模型,spark streaming的DStream实际上也就是一组组小批数据RDD的集合
- flink基本数据模型是数据流,以及事件(Event)序列
- 运行时架构
-
spark是批计算,将DAG划分为不同的stage,一个完成后才可以计算下一个
-
flink是标准的流执行模式,一个事件在一个节点处理完后可以直接发往下一个节点处理
-
3.flink部署
Local本地模式|Standalone独立集群模式|Standalone-HA高可用集群模式|Flink On Yarn模式|Kubernetes部署
4. flink 运行架构
4.1 Flink运行时的组件
Flink运行时架构主要包括四个不同的组件,它们会在运行流处理应用程序时协同工作:
- 作业管理器(JobManager)
- 资源管理器(ResourceManager)
- 任务管理器(TaskManager)
- 分发器(Dispatcher)
1. job manager
控制一个应用程序执行的主进程,也就是说,每个应用程序都会被一个不同的JobManager所控制执行。
JobManager会先接收到要执行的应用程序,这个应用程序会包括:
- 作业图(JobGraph)
- 逻辑数据流图(logical dataflow graph)
- 打包了所有的类、库和其它资源的JAR包。
JobManager会把JobGraph转换成一个物理层面的数据流图,这个图被叫做“执行图”(ExecutionGraph),包含了所有可以并发执行的任务。
JobManager会向资源管理器(ResourceManager)请求执行任务必要的资源,也就是任务管理器(TaskManager)上的插槽(slot)。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。
在运行过程中,JobManager会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。
2.ResourceManager
主要负责管理任务管理器(TaskManager)的插槽(slot),TaskManger插槽是Flink中定义的处理资源单元。
Flink为不同的环境和资源管理工具提供了不同资源管理器,比如YARN、Mesos、K8s,以及standalone部署。
当JobManager申请插槽资源时,ResourceManager会将有空闲插槽的TaskManager分配给JobManager。如果ResourceManager没有足够的插槽来满足JobManager的请求,它还可以向资源提供平台发起会话,以提供启动TaskManager进程的容器。
另外,ResourceManager还负责终止空闲的TaskManager,释放计算资源。
3.TaskManager
Flink中的工作进程。通常在Flink中会有多个TaskManager运行,每一个TaskManager都包含了一定数量的插槽(slots)。插槽的数量限制了TaskManager能够执行的任务数量。
启动之后,TaskManager会向资源管理器注册它的插槽;收到资源管理器的指令后,TaskManager就会将一个或者多个slot提供给JobManager调用。JobManager就可以向插槽分配任务(tasks)来执行了。
在执行过程中,一个TaskManager可以跟其它运行同一应用程序的TaskManager交换数据。
4.Dispatcher
可以跨作业运行,它为应用提交提供了REST接口。
当一个应用被提交执行时,分发器就会启动并将应用移交给一个JobManager。由于是REST接口,所以Dispatcher可以作为集群的一个HTTP接入点,这样就能够不受防火墙阻挡。Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。
Dispatcher在架构中可能并不是必需的,这取决于应用提交运行的方式。
4.2 任务提交流程
我们来看看当一个应用提交执行时,Flink的各个组件是如何交互协作的:
ps:上图中7.指TaskManager为JobManager提供slots,8.表示JobManager提交要在slots中执行的任务给TaskManager。
上图是从一个较为高层级的视角来看应用中各组件的交互协作。
如果部署的集群环境不同(例如YARN,Mesos,Kubernetes,standalone等),其中一些步骤可以被省略,或是有些组件会运行在同一个JVM进程中。
具体地,如果我们将Flink集群部署到YARN上,那么就会有如下的提交流程:
-
Flink任务提交后,Client向HDFS上传Flink的Jar包和配置
-
之后客户端向Yarn ResourceManager提交任务,ResourceManager分配Container资源并通知对应的NodeManager启动ApplicationMaster
-
ApplicationMaster启动后加载Flink的Jar包和配置构建环境,去启动JobManager,之后JobManager向Flink自身的RM进行申请资源,自身的RM向Yarn 的ResourceManager申请资源(因为是yarn模式,所有资源归yarn RM管理)启动TaskManager
-
Yarn ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager
-
NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager,TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务。
4.3 任务调度原理
-
客户端不是运行时和程序执行的一部分,但它用于准备并发送dataflow(JobGraph)给Master(JobManager),然后,客户端断开连接或者维持连接以等待接收计算结果。而Job Manager会产生一个执行图(Dataflow Graph)
-
当 Flink 集群启动后,首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager,JobManager 再调度任务到各个 TaskManager 去执行,然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。
-
Client 为提交 Job 的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交 Job 后,Client 可以结束进程(Streaming的任务),也可以不结束并等待结果返回。
-
JobManager 主要负责调度 Job 并协调 Task 做 checkpoint,职责上很像 Storm 的 Nimbus。从 Client 处接收到 Job 和 JAR 包等资源后,会生成优化后的执行计划,并以 Task 的单元调度到各个 TaskManager 去执行。
-
TaskManager 在启动的时候就设置好了槽位数(Slot),每个 slot 能启动一个 Task,Task 为线程。从 JobManager 处接收需要部署的 Task,部署启动后,与自己的上游建立 Netty 连接,接收数据并处理。
注:如果一个Slot中启动多个线程,那么这几个线程类似CPU调度一样共用同一个slot
4.3.1 TaskManger与Slots
要点:
-
考虑到Slot分组,所以实际运行Job时所需的Slot总数 = 每个Slot组中的最大并行度。
eg(1,1,2,1),其中第一个归为组“red”、第二个归组“blue”、第三个和第四归组“green”,那么运行所需的slot即max(1)+max(1)+max(2,1) = 1+1+2 = 4
- Flink中每一个worker(TaskManager)都是一个JVM进程,它可能会在独立的线程上执行一个或多个subtask。
- 为了控制一个worker能接收多少个task,worker通过task slot来进行控制(一个worker至少有一个task slot)。
**上图这个每个子任务各自占用一个slot,可以在代码中通过算子的.slotSharingGroup("组名")
指定算子所在的Slot组名,默认每一个算子的SlotGroup和上一个算子相同,而默认的SlotGroup就是"default"**。
同一个SlotGroup的算子能共享同一个slot,不同组则必须另外分配独立的Slot。
-
默认情况下,Flink允许子任务共享slot,即使它们是不同任务的子任务(前提需要来自同一个Job)。这样结果是,一个slot可以保存作业的整个管道pipeline。
-
不同任务共享同一个Slot的前提:这几个任务前后顺序不同,如上图中Source和keyBy是两个不同步骤顺序的任务,所以可以在同一个Slot执行。
-
一个slot可以保存作业的整个管道的好处:
- 如果有某个slot执行完了整个任务流程,那么其他任务就可以不用继续了,这样也省去了跨slot、跨TaskManager的通信损耗(降低了并行度)
- 同时slot能够保存整个管道,使得整个任务执行健壮性更高,因为某些slot执行出异常也能有其他slot补上。
- 有些slot分配到的子任务非CPU密集型,有些则CPU密集型,如果每个slot只完成自己的子任务,将出现某些slot太闲,某些slot过忙的现象。
-
假设拆分的多个Source子任务放到同一个Slot,那么任务不能并行执行了=>因为多个相同步骤的子任务需要抢占的具体资源相同,比如抢占某个锁,这样就不能并行。
-
-
Task Slot是静态的概念,是指TaskManager具有的并发执行能力,可以通过参数
taskmanager.numberOfTaskSlots
进行配置。而并行度parallelism是动态概念,即TaskManager运行程序时实际使用的并发能力,可以通过参数
parallelism.default
进行配置。
每个task slot表示TaskManager拥有资源的一个固定大小的子集。假如一个TaskManager有三个slot,那么它会将其管理的内存分成三份给各个slot。资源slot化意味着一个subtask将不需要跟来自其他job的subtask竞争被管理的内存,取而代之的是它将拥有一定数量的内存储备。
需要注意的是,这里不会涉及到CPU的隔离,slot目前仅仅用来隔离task的受管理的内存。
通过调整task slot的数量,允许用户定义subtask之间如何互相隔离。如果一个TaskManager一个slot,那将意味着每个task group运行在独立的JVM中(该JVM可能是通过一个特定的容器启动的),而一个TaskManager多个slot意味着更多的subtask可以共享同一个JVM。而在同一个JVM进程中的task将共享TCP连接(基于多路复用)和心跳消息。它们也可能共享数据集和数据结构,因此这减少了每个task的负载。
4.3.2 Slot和并行度
- 一个特定算子的 子任务(subtask)的个数被称之为其并行度(parallelism),我们可以对单独的每个算子进行设置并行度,也可以直接用env设置全局的并行度,更可以在页面中去指定并行度。
- 最后,由于并行度是实际Task Manager处理task 的能力,而一般情况下,一个 stream 的并行度,可以认为就是其所有算子中最大的并行度,则可以得出在设置Slot时,在所有设置中的最大设置的并行度大小则就是所需要设置的Slot的数量。(如果Slot分组,则需要为每组Slot并行度最大值的和)
假设一共有3个TaskManager,每一个TaskManager中的分配3个TaskSlot,也就是每个TaskManager可以接收3个task,一共9个TaskSlot,如果我们设置parallelism.default=1
,即运行程序默认的并行度为1,9个TaskSlot只用了1个,有8个空闲,因此,设置合适的并行度才能提高效率。
ps:上图最后一个因为是输出到文件,避免多个Slot(多线程)里的算子都输出到同一个文件互相覆盖等混乱问题,直接设置sink的并行度为1。
4.3.3 程序和数据流(DataFlow)
-
所有的Flink程序都是由三部分组成的: Source 、Transformation 和 Sink。
-
Source 负责读取数据源,Transformation 利用各种算子进行处理加工,Sink 负责输出
-
在运行时,Flink上运行的程序会被映射成“逻辑数据流”(dataflows),它包含了这三部分
-
每一个dataflow以一个或多个sources开始以一个或多个sinks结束。dataflow类似于任意的有向无环图(DAG)
-
在大部分情况下,程序中的转换运算(transformations)跟dataflow中的算子(operator)是一一对应的关系
4.3.4 执行图(ExecutionGraph)
由Flink程序直接映射成的数据流图是StreamGraph,也被称为逻辑流图,因为它们表示的是计算逻辑的高级视图。为了执行一个流处理程序,Flink需要将逻辑流图转换为物理数据流图(也叫执行图),详细说明程序的执行方式。
-
Flink 中的执行图可以分成四层:StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图。
-
StreamGraph:是根据用户通过Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。
-
JobGraph:StreamGraph经过优化后生成了JobGraph,提交给JobManager 的数据结构。主要的优化为,将多个符合条件的节点chain 在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
-
ExecutionGraph:JobManager 根据JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
-
物理执行图:JobManager 根据ExecutionGraph 对Job 进行调度后,在各个TaskManager 上部署Task 后形成的“图”,并不是一个具体的数据结构。
-
4.3.5 数据传输形式
-
一个程序中,不同的算子可能具有不同的并行度
-
算子之间传输数据的形式可以是 one-to-one (forwarding) 的模式也可以是redistributing 的模式,具体是哪一种形式,取决于算子的种类
-
One-to-one:stream维护着分区以及元素的顺序(比如source和map之间)。这意味着map 算子的子任务看到的元素的个数以及顺序跟 source 算子的子任务生产的元素的个数、顺序相同。map、fliter、flatMap等算子都是one-to-one的对应关系。
-
Redistributing:stream的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如,keyBy 基于 hashCode 重分区、而 broadcast 和 rebalance 会随机重新分区,这些算子都会引起redistribute过程,而 redistribute 过程就类似于 Spark 中的 shuffle 过程。
-
4.3.6 任务链(OperatorChains)
Flink 采用了一种称为任务链的优化技术,可以在特定条件下减少本地通信的开销。为了满足任务链的要求,必须将两个或多个算子设为相同的并行度,并通过本地转发(local forward)的方式进行连接
- 相同并行度的 one-to-one 操作,Flink 这样相连的算子链接在一起形成一个 task,原来的算子成为里面的 subtask
- 并行度相同、并且是 one-to-one 操作,两个条件缺一不可
为什么需要并行度相同,因为若flatMap并行度为1,到了之后的map并行度为2,从flatMap到map的数据涉及到数据由于并行度map为2会往两个slot处理,数据会分散,所产生的元素个数和顺序发生的改变所以有2个单独的task,不能成为任务链
如果前后任务逻辑上可以是OneToOne,且并行度一致,那么就能合并在一个Slot里(并行度原本是多少就是多少,两者并行度一致)执行。
-
keyBy需要根据Hash值分配给不同slot执行,所以只能Hash,不能OneToOne。
-
逻辑上可OneToOne但是并行度不同,那么就会Rebalance,轮询形式分配给下一个任务的多个slot。
-
代码中如果
算子.disableChaining()
,能够强制当前算子的子任务不参与任务链的合并,即不和其他Slot资源合并,但是仍然可以保留“Slot共享”的特性。 -
如果
StreamExecutionEnvironment env.disableOperatorChaining()
则当前执行环境全局设置算子不参与"任务链的合并"。 -
如果
算子.startNewChain()
表示不管前面任务链合并与否,从当前算子往后重新计算任务链的合并。通常用于前面强制不要任务链合并,而当前往后又需要任务链合并的特殊场景。
ps:如果算子.shuffle()
,能够强制算子之后重分区到不同slot执行下一个算子操作,逻辑上也实现了任务不参与任务链合并=>但是仅为“不参与任务链的合并”,这个明显不是最优解操作
5. flink的window
5.1 window
streaming流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而window是一种切割无限数据为有限块进行处理的手段。
Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。
5.2 Window类型
- 时间窗口(Time Window)
- 滚动时间窗口
- 滑动时间窗口
- 会话窗口
- 计数窗口(Count Window)
- 滚动计数窗口
- 滑动计数窗口
TimeWindow:按照时间生成Window
CountWindow:按照指定的数据条数生成一个Window,与时间无关
滚动窗口(Tumbling Windows)
-
依据固定的窗口长度对数据进行切分
-
时间对齐,窗口长度固定,没有重叠
滑动窗口(Sliding Windows)
-
可以按照固定的长度向后滑动固定的距离
-
滑动窗口由固定的窗口长度和滑动间隔组成
-
可以有重叠(是否重叠和滑动距离有关系)
-
滑动窗口是固定窗口的更广义的一种形式,滚动窗口可以看做是滑动窗口的一种特殊情况(即窗口大小和滑动间隔相等)
会话窗口(Session Windows)
- 由一系列事件组合一个指定时间长度的timeout间隙组成,也就是一段时间没有接收到新数据就会生成新的窗口
- 特点:时间无对齐
6.2 Window API
6.2.1 概述
-
窗口分配器——
window()
方法 -
我们可以用
.window()
来定义一个窗口,然后基于这个window去做一些聚合或者其他处理操作。注意
window()
方法必须在keyBy之后才能使用。 -
Flink提供了更加简单的
.timeWindow()
和.countWindow()
方法,用于定义时间窗口和计数窗口。DataStream<Tuple2<String,Double>> minTempPerWindowStream = datastream .map(new MyMapper()) .keyBy(data -> data.f0) .timeWindow(Time.seconds(15)) .minBy(1);
窗口分配器(window assigner)
window()
方法接收的输入参数是一个WindowAssigner- WindowAssigner负责将每条输入的数据分发到正确的window中
- Flink提供了通用的WindowAssigner
- 滚动窗口(tumbling window)
- 滑动窗口(sliding window)
- 会话窗口(session window)
- 全局窗口(global window)
创建不同类型的窗口
-
滚动时间窗口(tumbling time window)
.timeWindow(Time.seconds(15))
-
滑动时间窗口(sliding time window)
.timeWindow(Time.seconds(15),Time.seconds(5))
-
会话窗口(session window)
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
-
滚动计数窗口(tumbling count window)
.countWindow(5)
-
滑动计数窗口(sliding count window)
.countWindow(10,2)
DataStream的windowAll()
类似分区的global操作,这个操作是non-parallel的(并行度强行为1),所有的数据都会被传递到同一个算子operator上,官方建议如果非必要就不要用这个API
6.2.2 TimeWindow
TimeWindow将指定时间范围内的所有数据组成一个window,一次对一个window里面的所有数据进行计算。
滚动窗口
Flink默认的时间窗口根据ProcessingTime进行窗口的划分,将Flink获取到的数据根据进入Flink的时间划分到不同的窗口中。
DataStream<Tuple2<String, Double>> minTempPerWindowStream = dataStream
.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
@Override
public Tuple2<String, Double> map(SensorReading value) throws Exception {
return new Tuple2<>(value.getId(), value.getTemperature());
}
})
.keyBy(data -> data.f0)
.timeWindow( Time.seconds(15) )
.minBy(1);
时间间隔可以通过Time.milliseconds(x)
,Time.seconds(x)
,Time.minutes(x)
等其中的一个来指定。
滑动窗口
滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。
下面代码中的sliding_size设置为了5s,也就是说,每5s就计算输出结果一次,每一次计算的window范围是15s内的所有元素。
DataStream<SensorReading> minTempPerWindowStream = dataStream
.keyBy(SensorReading::getId)
.timeWindow( Time.seconds(15), Time.seconds(5) )
.minBy("temperature");
时间间隔可以通过Time.milliseconds(x)
,Time.seconds(x)
,Time.minutes(x)
等其中的一个来指定。
6.2.3 CountWindow
CountWindow根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果。
注意:CountWindow的window_size指的是相同Key的元素的个数,不是输入的所有元素的总数。
滚动窗口
默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。
DataStream<SensorReading> minTempPerWindowStream = dataStream
.keyBy(SensorReading::getId)
.countWindow( 5 )
.minBy("temperature");
滑动窗口
滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。
下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围是10个元素。
DataStream<SensorReading> minTempPerWindowStream = dataStream
.keyBy(SensorReading::getId)
.countWindow( 10, 2 )
.minBy("temperature");
6.2.4 window function
window function 定义了要对窗口中收集的数据做的计算操作,主要可以分为两类:
- 增量聚合函数(incremental aggregation functions)
- 全窗口函数(full window functions)
增量聚合函数
- 每条数据到来就进行计算,保持一个简单的状态。(来一条处理一条,但是不输出,到窗口临界位置才输出)
- 典型的增量聚合函数有ReduceFunction, AggregateFunction。
全窗口函数
- 先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。(来一个放一个,窗口临界位置才遍历且计算、输出)
- ProcessWindowFunction,WindowFunction。
6.2.5 其它可选API
Flink-Window概述 | Window类型 | TimeWindow、CountWindow、SessionWindow、WindowFunction
-
.trigger()
——触发器定义window 什么时候关闭,触发计算并输出结果
-
.evitor()
——移除器定义移除某些数据的逻辑
-
.allowedLateness()
——允许处理迟到的数据 -
.sideOutputLateData()
——将迟到的数据放入侧输出流 -
.getSideOutput()
——获取侧输出流
6. 时间语义和Watermark
6.1 Flink中的时间语义
-
Event Time:事件创建时间;
-
Ingestion Time:数据进入Flink的时间;
-
Processing Time:执行操作算子的本地系统时间,与机器相关;
Event Time是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。
6.2 EventTime的引入
在Flink的流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,才会被迫使用ProcessingTime或者IngestionTime。
(虽然默认环境里使用的就是ProcessingTime,使用EventTime需要另外设置)
如果要使用EventTime,那么需要引入EventTime的时间属性,引入方式如下所示:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 从调用时刻开始给env创建的每一个stream追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
注:具体的时间,还需要从数据中提取时间戳。
7.3 Watermark
Flink流计算编程--watermark(水位线)简介 <= 不错的文章,建议阅读
7.3.1 概念
- Flink对于迟到数据有三层保障,先来后到的保障顺序是:
- WaterMark => 约等于放宽窗口标准
- allowedLateness => 允许迟到(ProcessingTime超时,但是EventTime没超时)
- sideOutputLateData => 超过迟到时间,另外捕获,之后可以自己批处理合并先前的数据
我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。
那么此时出现一个问题,一旦出现乱序,如果只根据eventTime决定window的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark。
-
当Flink以Event Time模式处理数据流时,它会根据数据里的时间戳来处理基于时间的算子。
(比如5s一个窗口,那么理想情况下,遇到时间戳是5s的数据时,就认为[0,5s)时间段的桶bucket就可以关闭了。)
-
实际由于网络、分布式传输处理等原因,会导致乱序数据的产生
-
乱序数据会导致窗口计算不准确
(如果按照前面说法,获取到5s时间戳的数据,但是2s,3s乱序数据还没到,理论上不应该关闭桶)
-
怎样避免乱序数据带来的计算不正确?
-
遇到一个时间戳达到了窗口关闭时间,不应该立即触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口
-
Watermark是一种衡量Event Time进展的机制,可以设定延迟触发
-
Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现
-
数据流中的Watermark用于表示”timestamp小于Watermark的数据,都已经到达了“,因此,window的执行也是由Watermark触发的。
-
Watermark可以理解成一个延迟触发机制,我们可以设置Watermark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime - t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。
Watermark = maxEventTime-延迟时间t
-
watermark 用来让程序自己平衡延迟和结果正确性
watermark可以理解为把原本的窗口标准稍微放宽了一点。(比如原本5s,设置延迟时间=2s,那么实际等到7s的数据到达时,才认为是[0,5)的桶需要关闭了)
有序流的Watermarker如下图所示:(延迟时间设置为0s)
此时以5s一个窗口,那么EventTime=5s的元素到达时,关闭第一个窗口,下图即W(5),W(10)同理。
乱序流的Watermarker如下图所示:(延迟时间设置为2s)
乱序流,所以可能出现EventTime前后顺序不一致的情况,这里延迟时间设置2s,第一个窗口则为5s+2s
,当EventTime=7s的数据到达时,关闭第一个窗口。第二个窗口则是5*2+2=12s
,当12s这个EventTime的数据到达时,关闭第二个窗口。
当Flink接收到数据时,会按照一定的规则去生成Watermark,这条Watermark就等于当前所有到达数据中的maxEventTime-延迟时长,也就是说,Watermark是基于数据携带的时间戳生成的,一旦Watermark比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行。
由于event time是由数据携带的,因此,如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发。
上图中,我们设置的允许最大延迟到达时间为2s,所以时间戳为7s的事件对应的Watermark是5s,时间戳为12s的事件的Watermark是10s,如果我们的窗口1是1s~5s
,窗口2是6s~10s
,那么时间戳为7s的事件到达时的Watermarker恰好触发窗口1,时间戳为12s的事件到达时的Watermark恰好触发窗口2。
Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。
只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。
6.3.2 Watermark的特点
-
watermark 是一条特殊的数据记录
-
watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退
-
watermark 与数据的时间戳相关
6.3.3 Watermark的传递
- 图一,当前Task有四个上游Task给自己传输WaterMark信息,通过比较,只取当前最小值作为自己的本地Event-time clock,上图中,当前Task[0,2)的桶就可关闭了,因为所有上游中2s最小,能保证2s的WaterMark是准确的(所有上游Watermark都已经>=2s)。这时候将Watermark=2广播到当前Task的下游。
- 图二,上游的Watermark持续变动,此时Watermark=3成为新的最小值,更新本地Task的event-time clock,同时将最新的Watermark=3广播到下游
- 图三,上游的Watermark虽然更新了,但是当前最小值还是3,所以不更新event-time clock,也不需要广播到下游
- 图四,和图二同理,更新本地event-time clock,同时向下游广播最新的Watermark=4
6.3.4 Watermark的引入
watermark的引入很简单,对于乱序数据,最常见的引用方式如下:
dataStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.milliseconds(1000)) {
@Override
public long extractTimestamp(element: SensorReading): Long = {
return element.getTimestamp() * 1000L;
}
});
**Event Time的使用一定要指定数据源中的时间戳。否则程序无法知道事件的事件时间是什么(数据源里的数据没有时间戳的话,就只能使用Processing Time了)**。
我们看到上面的例子中创建了一个看起来有点复杂的类,这个类实现的其实就是分配时间戳的接口。Flink暴露了TimestampAssigner接口供我们实现,使我们可以自定义如何从事件数据中抽取时间戳。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置事件时间语义 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStream<SensorReading> dataStream = env.addSource(new SensorSource()) .assignTimestampsAndWatermarks(new MyAssigner());
MyAssigner有两种类型
-
AssignerWithPeriodicWatermarks
-
AssignerWithPunctuatedWatermarks
以上两个接口都继承自TimestampAssigner。
TimestampAssigner
AssignerWithPeriodicWatermarks
-
周期性的生成 watermark:系统会周期性的将 watermark 插入到流中
-
默认周期是200毫秒,可以使用
ExecutionConfig.setAutoWatermarkInterval()
方法进行设置 -
升序和前面乱序的处理 BoundedOutOfOrderness ,都是基于周期性 watermark 的。
AssignerWithPunctuatedWatermarks
- 没有时间周期规律,可打断的生成 watermark(即可实现每次获取数据都更新watermark)
6.3.5 Watermark的设定
- 在Flink中,Watermark由应用程序开发人员生成,这通常需要对相应的领域有一定的了解
- 如果Watermark设置的延迟太久,收到结果的速度可能就会很慢,解决办法是在水位线到达之前输出一个近似结果
- 如果Watermark到达得太早,则可能收到错误结果,不过Flink处理迟到数据的机制可以解决这个问题
一般大数据场景都是考虑高并发情况,所以一般使用周期性生成Watermark的方式,避免频繁地生成Watermark。
注:一般认为Watermark的设置代码,在里Source步骤越近的地方越合适。
7. Flink状态管理
Flink状态管理详解:Keyed State和Operator List State深度解析 <= 不错的文章,建议阅读
- 算子状态(Operator State)
- 键控状态(Keyed State)
- 状态后端(State Backends)
7.1 状态概述
-
由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
-
可以认为任务状态就是一个本地变量,可以被任务的业务逻辑访问
-
Flink 会进行状态管理,包括状态一致性、故障处理以及高效存储和访问,以便于开发人员可以专注于应用程序的逻辑
-
在Flink中,状态始终与特定算子相关联
-
为了使运行时的Flink了解算子的状态,算子需要预先注册其状态
总的来说,有两种类型的状态:
- 算子状态(Operator State)
- 算子状态的作用范围限定为算子任务(也就是不能跨任务访问)
- 键控状态(Keyed State)
- 根据输入数据流中定义的键(key)来维护和访问
7.2 算子状态 Operator State
-
算子状态的作用范围限定为算子任务,同一并行任务所处理的所有数据都可以访问到相同的状态。
-
状态对于同一任务而言是共享的。(不能跨slot)
-
状态算子不能由相同或不同算子的另一个任务访问。
算子状态数据结构
-
列表状态(List state)
- 将状态表示为一组数据的列表
-
联合列表状态(Union list state)
- 也将状态表示未数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复
-
广播状态(Broadcast state)
-
如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态
7.3 键控状态 Keyed State
-
键控状态是根据输入数据流中定义的键(key)来维护和访问的。
-
Flink 为每个key维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。
-
当任务处理一条数据时,他会自动将状态的访问范围限定为当前数据的key。
键控状态数据结构
-
值状态(value state)
- 将状态表示为单个的值
-
列表状态(List state)
- 将状态表示为一组数据的列表
-
映射状态(Map state)
- 将状态表示为一组key-value对
-
聚合状态(Reducing state & Aggregating State)
-
将状态表示为一个用于聚合操作的列表
7.4 状态后端 State Backends
7.4.1 概述
-
每传入一条数据,有状态的算子任务都会读取和更新状态。
-
由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问。
-
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端( state backend)
-
状态后端主要负责两件事:本地状态管理,以及将检查点(checkPoint)状态写入远程存储
7.4.2 选择一个状态后端
-
MemoryStateBackend
- 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM堆上,而将checkpoint存储在JobManager的内存中
- 特点:快速、低延迟,但不稳定
-
FsStateBackend(默认)
- 将checkpoint存到远程的持久化文件系统(FileSystem)上,而对于本地状态,跟MemoryStateBackend一样,也会存在TaskManager的JVM堆上
- 同时拥有内存级的本地访问速度,和更好的容错保证
-
RocksDBStateBackend
-
将所有状态序列化后,存入本地的RocksDB中存储
8. 容错机制
8.1 一致性检查点(checkpoint)
-
Flink 故障恢复机制的核心,就是应用状态的一致性检查点
-
有状态流应用的一致检查点,其实就是所有任务的状态,在某个时间点的一份拷贝(一份快照);这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时候
(5这个数据虽然进了奇数流但是偶数流也应该做快照,因为属于同一个相同数据,只是没有被他处理)
(这里根据奇偶性分流,偶数流求偶数和,奇数流求奇数和,5这里明显已经被sum_odd(1+3+5)处理了,且sum_even不需要处理该数据,因为前面已经判断该数据不需要到sum_even流,相当于所有任务都已经处理完source的数据5了。)
-
在JobManager中也有个Chechpoint的指针,指向了仓库的状态快照的一个拓扑图,为以后的数据故障恢复做准备
8.2 从检查点恢复状态
-
在执行流应用程序期间,Flink 会定期保存状态的一致检查点
-
如果发生故障, Flink 将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程
(如图中所示,7这个数据被source读到了,准备传给奇数流时,奇数流宕机了,数据传输发生中断)
-
遇到故障之后,第一步就是重启应用
(重启后,起初流都是空的)
-
第二步是从 checkpoint 中读取状态,将状态重置
(读取在远程仓库(Storage,这里的仓库指状态后端保存数据指定的三种方式之一)保存的状态)
-
从检查点重新启动应用程序后,其内部状态与检查点完成时的状态完全相同
-
第三步:开始消费并处理检查点到发生故障之间的所有数据
-
这种检查点的保存和恢复机制可以为应用程序状态提供“精确一次”(exactly-once)的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样一来所有的输入流就都会被重置到检查点完成时的位置
(这里要求source源也能记录状态,回退到读取数据7的状态,kafka有相应的偏移指针能完成该操作)
8.3 Flink检查点算法
概述
checkpoint和Watermark一样,都会以广播的形式告诉所有下游。
-
一种简单的想法
暂停应用,保存状态到检查点,再重新恢复应用(当然Flink 不是采用这种简单粗暴的方式)
-
Flink的改进实现
- 基于Chandy-Lamport算法的分布式快照
- 将检查点的保存和数据处理分离开,不暂停整个应用
(就是每个任务单独拍摄自己的快照到内存,之后再到jobManager整合)
-
检查点分界线(Checkpoint Barrier)
- Flink的检查点算法用到了一种称为分界线(barrier)的特殊数据形式,用来把一条流上数据按照不同的检查点分开
- 分界线之前到来的数据导致的状态更改,都会被包含在当前分界线所属的检查点中;而基于分界线之后的数据导致的所有更改,就会被包含在之后的检查点中
具体讲解
-
现在是一个有两个输入流的应用程序,用并行的两个 Source 任务来读取
-
两条自然数数据流,蓝色数据流已经输出完
蓝3
了,黄色数据流输出完黄4
了 -
在Souce端 Source1 接收到了数据
蓝3
正在往下游发向一个数据蓝2 和 蓝3
; Source2 接受到了数据黄4
,且往下游发送数据黄4
-
偶数流已经处理完
黄2
所以后面显示为2, 奇数流处理完蓝1 和 黄1 黄3
所以为5,并分别往下游发送每次聚合后的结果给Sink
-
JobManager 会向每个 source 任务发送一条带有新检查点 ID 的消息,通过这种方式来启动检查点
(这个带有新检查点ID的东西为barrier,由图中三角型表示,数值2只是ID)
- 数据源将它们的状态写入检查点,并发出一个检查点barrier
- 状态后端在状态存入检查点之后,会返回通知给source任务,source任务就会向JobManager确认检查点完成
上图,在Source端接受到barrier后,将自己此身的3 和 4 的数据的状态写入检查点,且向JobManager发送checkpoint成功的消息,然后向下游分别发出一个检查点 barrier
可以看出在Source接受barrier时,数据流也在不断的处理,不会进行中断
此时的偶数流已经处理完蓝2
变成了4,但是还没处理到黄4
,只是下游sink发送了一个数据4,而奇数流已经处理完蓝3
变成了8(黄1+蓝1+黄3+蓝3),并向下游sink发送了8
此时检查点barrier都还未到Sum_odd奇数流和Sum_even偶数流
- 分界线对齐:barrier向下游传递,sum任务会等待所有输入分区的barrier到达
- 对于barrier已经达到的分区,继续到达的数据会被缓存
- 而barrier尚未到达的分区,数据会被正常处理
此时蓝色流的barrier先一步抵达了偶数流,黄色的barrier还未到,但是因为数据的不中断一直处理,此时的先到的蓝色的barrier会将此时的偶数流的数据4进行缓存处理,流接着处理接下来的数据等待着黄色的barrier的到来,而黄色barrier之前的数据将会对缓存的数据相加
这次处理的总结:分界线对齐:barrier 向下游传递,sum 任务会等待所有输入分区的 barrier 到达,对于barrier已经到达的分区,继续到达的数据会被缓存。而barrier尚未到达的分区,数据会被正常处理
- 当收到所有输入分区的 barrier 时,任务就将其状态保存到状态后端的检查点中,然后将 barrier 继续向下游转发
当蓝色的barrier和黄色的barrier(所有分区的)都到达后,进行状态保存到远程仓库,然后对JobManager发送消息,说自己的检查点保存完毕了
此时的偶数流和奇数流都为8
- 向下游转发检查点 barrier 后,任务继续正常的数据处理
- Sink 任务向 JobManager 确认状态保存到 checkpoint 完毕
- 当所有任务都确认已成功将状态保存到检查点时,检查点就真正完成了
8.4 保存点(Savepoints)
CheckPoint为自动保存,SavePoint为手动保存
- Flink还提供了可以自定义的镜像保存功能,就是保存点(save points)
- 原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
- Flink不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作
- 保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份、更新应用程序、版本迁移、暂停和重启程序,等等
8.5状态一致性
8.5.1 概述
-
有状态的流处理,内部每个算子任务都可以有自己的状态
-
对于流处理器内部来说,所谓的状态一致性,其实就是我们所说的计算结果要保证准确。
-
一条数据不应该丢失,也不应该重复计算
-
在遇到故障时可以恢复状态,恢复以后的重新计算,结果应该也是完全正确的。
8.5.2 分类
Flink的一个重大价值在于,它既保证了exactly-once,也具有低延迟和高吞吐的处理能力。
-
AT-MOST-ONCE(最多一次) 当任务故障时,最简单的做法是什么都不干,既不恢复丢失的状态,也不重播丢失的数据。At-most-once 语义的含义是最多处理一次事件。
这其实是没有正确性保障的委婉说法——故障发生之后,计算结果可能丢失。类似的比如网络协议的udp。
-
AT-LEAST-ONCE(至少一次) 在大多数的真实应用场景,我们希望不丢失事件。这种类型的保障称为 at-least-once,意思是所有的事件都得到了处理,而一些事件还可能被处理多次。
这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
-
EXACTLY-ONCE(精确一次) 恰好处理一次是最严格的保证,也是最难实现的。恰好处理一次语义不仅仅意味着没有事件丢失,还意味着针对每一个数据,内部状态仅仅更新一次。
这指的是系统保证在发生故障后得到的计数结果与正确值一致。
8.5.3 一致性检查点(Checkpoints)
- Flink使用了一种轻量级快照机制——检查点(checkpoint)来保证exactly-once语义
- 有状态流应用的一致检查点,其实就是:所有任务的状态,在某个时间点的一份备份(一份快照)。而这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时间。
- 应用状态的一致检查点,是Flink故障恢复机制的核心
端到端(end-to-end)状态一致性
-
目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在Flink流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如Kafka)和输出到持久化系统
-
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性
-
整个端到端的一致性级别取决于所有组件中一致性最弱的组件
端到端 exactly-once
- 内部保证——checkpoint
- source端——可重设数据的读取位置
- sink端——从故障恢复时,数据不会重复写入外部系统
- 幂等写入
- 事务写入
幂等写入
-
所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。
(中间可能会存在不正确的情况,只能保证最后结果正确。比如5=>10=>15=>5=>10=>15,虽然最后是恢复到了15,但是中间有个恢复的过程,如果这个过程能够被读取,就会出问题。)
事务写入
-
事务(Transaction)
- 应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤销
- 具有原子性:一个事务中的一系列的操作要么全部成功,要么一个都不做
-
实现思想
构建的事务对应着checkpoint,等到checkpoint真正完成的时候,才把所有对应的结果写入sink系统中。
-
实现方式
- 预习日志
- 两阶段提交
预写日志(Write-Ahead-Log,WAL)
- 把结果数据先当成状态保存,然后在收到checkpoint完成的通知时,一次性写入sink系统
- 简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么sink系统,都能用这种方式一批搞定
- DataStream API提供了一个模版类:GenericWriteAheadSink,来实现这种事务性sink
两阶段提交(Two-Phase-Commit,2PC)
- 对于每个checkpoint,sink任务会启动一个事务,并将接下来所有接收到的数据添加到事务里
- 然后将这些数据写入外部sink系统,但不提交它们——这时只是"预提交"
- 这种方式真正实现了exactly-once,它需要一个提供事务支持的外部sink系统。Flink提供了TwoPhaseCommitSinkFunction接口
不同Source和Sink的一致性保证
8.6 Flink+Kafka 端到端状态一致性的保证
- 内部——利用checkpoint机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
- source——kafka consumer作为source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重制偏移量,重新消费数据,保证一致性
- sink——kafka producer作为sink,采用两阶段提交sink,需要实现一个TwoPhaseCommitSinkFunction
Exactly-once 两阶段提交
- JobManager 协调各个 TaskManager 进行 checkpoint 存储
- checkpoint保存在 StateBackend中,默认StateBackend是内存级的,也可以改为文件级的进行持久化保存
- 当 checkpoint 启动时,JobManager 会将检查点分界线(barrier)注入数据流
- barrier会在算子间传递下去
- 每个算子会对当前的状态做个快照,保存到状态后端
- checkpoint 机制可以保证内部的状态一致性
-
每个内部的 transform 任务遇到 barrier 时,都会把状态存到 checkpoint 里
-
sink 任务首先把数据写入外部 kafka,这些数据都属于预提交的事务;遇到 barrier 时,把状态保存到状态后端,并开启新的预提交事务
(barrier之前的数据还是在之前的事务中没关闭事务,遇到barrier后的数据另外新开启一个事务)
- 当所有算子任务的快照完成,也就是这次的 checkpoint 完成时,JobManager 会向所有任务发通知,确认这次 checkpoint 完成
- sink 任务收到确认通知,正式提交之前的事务,kafka 中未确认数据改为“已确认”
Exactly-once 两阶段提交步骤总结
-
第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
-
jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanager
-
sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据
-
jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
-
sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
-
外部kafka关闭事务,提交的数据可以正常消费了。