目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在 Flink 流


处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据


源(例如 Kafka )和输出到持久化系统。


端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每


一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一


致性最弱的组件。具体可以划分如下:


⚫ 内部保证 —— 依赖 checkpoint


⚫ source 端 —— 需要外部源可重设数据的读取位置


⚫ sink 端 —— 需要保证从故障恢复时,数据不会重复写入外部系统


而对于 sink 端,又有两种具体的实现方式:幂等( Idempotent )写入和事务性


( Transactional )写入。


⚫ 幂等写入


所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,


也就是说,后面再重复执行就不起作用了。


⚫ 事务写入


需要构建事务来写入外部系统,构建的事务对应着 checkpoint ,等到 checkpoint


真正完成的时候,才把所有对应的结果写入 sink 系统中。


对于事务性写入,具体又有两种实现方式:预写日志( WAL )和两阶段提交



( 2PC )。 DataStream API 提供了 GenericWriteAheadSink 模板类和



TwoPhaseCommitSinkFunction 接口,可以方便地实现这两种方式的事务性写入。



不同 Source 和 Sink 的一致性保证可以用下表说明:




flink checkpoint为什么保存了sink的数据 flink 保存中间状态_big data


3 检查点(checkpoint)


Flink 具体如何保证 exactly-once 呢 ? 它使用一种被称为 " 检查点 " ( checkpoint )


的特性,在出现故障时将系统重置回正确状态。下面通过简单的类比来解释检查点


的作用。


假设你和两位朋友正在数项链上有多少颗珠子,如下图所示。你捏住珠子,边


数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手中的珠子。当你


分神忘记数到哪里时,怎么办呢 ? 如果项链上有很多珠子,你显然不想从头再数一


遍,尤其是当三人的速度不一样却又试图合作的时候,更是如此 ( 比如想记录前一分


钟三人一共数了多少颗珠子,回想一下一分钟滚动窗口 ) 。


于是,你想了一个更好的办法 : 在项链上每隔一段就松松地系上一根有色皮筋,


将珠子分隔开 ; 当珠子被拨动的时候,皮筋也可以被拨动 ; 然后,你安排一个助手,


让他在你和朋友拨到皮筋时记录总数。用这种方法,当有人数错时,就不必从头开


始数。相反,你向其他人发出错误警示,然后你们都从上一根皮筋处开始重数,助


手则会告诉每个人重数时的起始数值,例如在粉色皮筋处的数值是多少。


Flink 检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是 : 对于指定


的皮筋而言,珠子的相对位置是确定的 ; 这让皮筋成为重新计数的参考点。总状态


( 珠子的总数 ) 在每颗珠子被拨动之后更新一次,助手则会保存与每根皮筋对应的检


查点状态,如当遇到粉色皮筋时一共数了多少珠子,当遇到橙色皮筋时又是多少。


当问题出现时,这种方法使得重新计数变得简单。


1 Flink 的检查点算法


Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。记住


这一基本点之后,我们用一个例子来看检查点是如何运行的。 Flink 为用户提供了用


来定义状态的工具。例如,以下这个 Scala 程序按照输入记录的第一个字段 ( 一个字


符串 ) 进行分组并维护第二个字段的计数状态。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_flink_02

 

flink checkpoint为什么保存了sink的数据 flink 保存中间状态_大数据_03

 


该程序有两个算子 : keyBy 算子用来将记录按照第一个元素 ( 一个字符串 ) 进行分


组,根据该 key 将数据进行重新分区,然后将记录再发送给下一个算子 : 有状态的


map 算子 (mapWithState) 。 map 算子在接收到每个元素后,将输入记录的第二个字段


的数据加到现有总数中,再将更新过的元素发射出去。下图表示程序的初始状态 : 输


入流中的 6 条记录被检查点分割线 (checkpoint barrier) 隔开,所有的 map 算子状态均


为 0( 计数还未开始 ) 。所有 key 为 a 的记录将被顶层的 map 算子处理,所有 key 为 b


的记录将被中间层的 map 算子处理,所有 key 为 c 的记录则将被底层的 map 算子处


理。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_输入流_04


 


上图是程序的初始状态。注意, a 、 b 、 c 三组的初始计数状态都是 0 ,即三个圆


柱上的值。 ckpt 表示检查点分割线( checkpoint barriers )。每条记录在处理顺序上


严格地遵守在检查点之前或之后的规定,例如 ["b",2] 在检查点之前被处理, ["a",2]


则在检查点之后被处理。


当该程序处理输入流中的 6 条记录时,涉及的操作遍布 3 个并行实例 ( 节点、 CPU


内核等 ) 。那么,检查点该如何保证 exactly-once 呢 ?


检查点分割线和普通数据记录类似。它们由算子处理,但并不参与计算,而是


会触发与检查点相关的行为。当读取输入流的数据源 ( 在本例中与 keyBy 算子内联 )


遇到检查点屏障时,它将其在输入流中的位置保存到持久化存储中。如果输入流来


自消息传输系统 (Kafka) ,这个位置就是偏移量。 Flink 的存储机制是插件化的,持久


化存储可以是分布式文件系统,如 HDFS 。下图展示了这个过程。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_flink_05


 


当 Flink 数据源 ( 在本例中与 keyBy 算子内联 ) 遇到检查点分界线( barrier )时,


它会将其在输入流中的位置保存到持久化存储中。这让 Flink 可以根据该位置重启。


检查点像普通数据记录一样在算子之间流动。当 map 算子处理完前 3 条数据并


收到检查点分界线时,它们会将状态以异步的方式写入持久化存储,如下图所示。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_big data_06


 


位于检查点之前的所有记录 (["b",2] 、 ["b",3] 和 ["c",1]) 被 map 算子处理之后的情


况。此时,持久化存储已经备份了检查点分界线在输入流中的位置 ( 备份操作发生在


barrier 被输入算子处理的时候 ) 。 map 算子接着开始处理检查点分界线,并触发将状


态异步备份到稳定存储中这个动作。


当 map 算子的状态备份和检查点分界线的位置备份被确认之后,该检查点操作


就可以被标记为完成,如下图所示。我们在无须停止或者阻断计算的条件下,在一


个逻辑时间点 ( 对应检查点屏障在输入流中的位置 ) 为计算状态拍了快照。通过确保


备份的状态和位置指向同一个逻辑时间点,后文将解释如何基于备份恢复计算,从


而保证 exactly-once 。值得注意的是,当没有出现故障时, Flink 检查点的开销极小,


检查点操作的速度由持久化存储的可用带宽决定。回顾数珠子的例子 : 除了因为数


错而需要用到皮筋之外,皮筋会被很快地拨过。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_大数据_07


 


检查点操作完成,状态和位置均已备份到稳定存储中。输入流中的所有数据记


录都已处理完成。值得注意的是,备份的状态值与实际的状态值是不同的。备份反


映的是检查点的状态。


如果检查点操作失败, Flink 可以丢弃该检查点并继续正常执行,因为之后的某


一个检查点可能会成功。虽然恢复时间可能更长,但是对于状态的保证依旧很有力。


只有在一系列连续的检查点操作失败之后, Flink 才会抛出错误,因为这通常预示着


发生了严重且持久的错误。


现在来看看下图所示的情况 : 检查点操作已经完成,但故障紧随其后。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_检查点_08


 


在这种情况下, Flink 会重新拓扑 ( 可能会获取新的执行资源 ) ,将输入流倒回到


上一个检查点,然后恢复状态值并从该处开始继续计算。在本例中, ["a",2] 、 ["a",2]


和 ["c",2] 这几条记录将被重播。


下图展示了这一重新处理过程。从上一个检查点开始重新计算,可以保证在剩


下的记录被处理之后,得到的 map 算子的状态值与没有发生故障时的状态值一致。


flink checkpoint为什么保存了sink的数据 flink 保存中间状态_检查点_09


 


Flink 将输入流倒回到上一个检查点屏障的位置,同时恢复 map 算子的状态值。


然后, Flink 从此处开始重新处理。这样做保证了在记录被处理之后, map 算子的状


态值与没有发生故障时的一致。


Flink 检查点算法的正式名称是异步分界线快照 (asynchronous barrier


snapshotting) 。该算法大致基于 Chandy-Lamport 分布式快照算法。


检查点是 Flink 最有价值的创新之一,因为 它使 Flink 可以保证 exactly-once


并且不需要牺牲性能


(讲的很抽象,我的理解就是存储一个数据状态,等故障时,从故障点重算,而不是整体重算)