目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在
Flink
流
处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据
源(例如
Kafka
)和输出到持久化系统。
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每
一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一
致性最弱的组件。具体可以划分如下:
⚫
内部保证 —— 依赖
checkpoint
⚫
source
端 —— 需要外部源可重设数据的读取位置
⚫
sink
端 —— 需要保证从故障恢复时,数据不会重复写入外部系统
而对于
sink
端,又有两种具体的实现方式:幂等(
Idempotent
)写入和事务性
(
Transactional
)写入。
⚫
幂等写入
所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,
也就是说,后面再重复执行就不起作用了。
⚫
事务写入
需要构建事务来写入外部系统,构建的事务对应着
checkpoint
,等到
checkpoint
真正完成的时候,才把所有对应的结果写入
sink
系统中。
对于事务性写入,具体又有两种实现方式:预写日志(
WAL
)和两阶段提交
(
2PC
)。
DataStream API
提供了
GenericWriteAheadSink
模板类和
TwoPhaseCommitSinkFunction
接口,可以方便地实现这两种方式的事务性写入。
不同
Source
和
Sink
的一致性保证可以用下表说明:
![](https://i-blog.csdnimg.cn/blog_migrate/440976b449a69889d7334e8a66b13905.png)
3 检查点(checkpoint)
Flink
具体如何保证
exactly-once
呢
?
它使用一种被称为
"
检查点
"
(
checkpoint
)
的特性,在出现故障时将系统重置回正确状态。下面通过简单的类比来解释检查点
的作用。
假设你和两位朋友正在数项链上有多少颗珠子,如下图所示。你捏住珠子,边
数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手中的珠子。当你
分神忘记数到哪里时,怎么办呢
?
如果项链上有很多珠子,你显然不想从头再数一
遍,尤其是当三人的速度不一样却又试图合作的时候,更是如此
(
比如想记录前一分
钟三人一共数了多少颗珠子,回想一下一分钟滚动窗口
)
。
于是,你想了一个更好的办法
:
在项链上每隔一段就松松地系上一根有色皮筋,
将珠子分隔开
;
当珠子被拨动的时候,皮筋也可以被拨动
;
然后,你安排一个助手,
让他在你和朋友拨到皮筋时记录总数。用这种方法,当有人数错时,就不必从头开
始数。相反,你向其他人发出错误警示,然后你们都从上一根皮筋处开始重数,助
手则会告诉每个人重数时的起始数值,例如在粉色皮筋处的数值是多少。
Flink
检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是
:
对于指定
的皮筋而言,珠子的相对位置是确定的
;
这让皮筋成为重新计数的参考点。总状态
(
珠子的总数
)
在每颗珠子被拨动之后更新一次,助手则会保存与每根皮筋对应的检
查点状态,如当遇到粉色皮筋时一共数了多少珠子,当遇到橙色皮筋时又是多少。
当问题出现时,这种方法使得重新计数变得简单。
1 Flink
的检查点算法
Flink
检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。记住
这一基本点之后,我们用一个例子来看检查点是如何运行的。
Flink
为用户提供了用
来定义状态的工具。例如,以下这个
Scala
程序按照输入记录的第一个字段
(
一个字
符串
)
进行分组并维护第二个字段的计数状态。
![](https://i-blog.csdnimg.cn/blog_migrate/95d7876504e349f878bbba24c0baefa9.png)
该程序有两个算子
: keyBy
算子用来将记录按照第一个元素
(
一个字符串
)
进行分
组,根据该
key
将数据进行重新分区,然后将记录再发送给下一个算子
:
有状态的
map
算子
(mapWithState)
。
map
算子在接收到每个元素后,将输入记录的第二个字段
的数据加到现有总数中,再将更新过的元素发射出去。下图表示程序的初始状态
:
输
入流中的
6
条记录被检查点分割线
(checkpoint barrier)
隔开,所有的
map
算子状态均
为
0(
计数还未开始
)
。所有
key
为
a
的记录将被顶层的
map
算子处理,所有
key
为
b
的记录将被中间层的
map
算子处理,所有
key
为
c
的记录则将被底层的
map
算子处
理。
![](https://i-blog.csdnimg.cn/blog_migrate/f7428d44ba00dff2cf387eca84355abe.png)
上图是程序的初始状态。注意,
a
、
b
、
c
三组的初始计数状态都是
0
,即三个圆
柱上的值。
ckpt
表示检查点分割线(
checkpoint barriers
)。每条记录在处理顺序上
严格地遵守在检查点之前或之后的规定,例如
["b",2]
在检查点之前被处理,
["a",2]
则在检查点之后被处理。
当该程序处理输入流中的
6
条记录时,涉及的操作遍布
3
个并行实例
(
节点、
CPU
内核等
)
。那么,检查点该如何保证
exactly-once
呢
?
检查点分割线和普通数据记录类似。它们由算子处理,但并不参与计算,而是
会触发与检查点相关的行为。当读取输入流的数据源
(
在本例中与
keyBy
算子内联
)
遇到检查点屏障时,它将其在输入流中的位置保存到持久化存储中。如果输入流来
自消息传输系统
(Kafka)
,这个位置就是偏移量。
Flink
的存储机制是插件化的,持久
化存储可以是分布式文件系统,如
HDFS
。下图展示了这个过程。
![](https://i-blog.csdnimg.cn/blog_migrate/9a1d94d438076807831772e40e1ec35f.png)
当
Flink
数据源
(
在本例中与
keyBy
算子内联
)
遇到检查点分界线(
barrier
)时,
它会将其在输入流中的位置保存到持久化存储中。这让
Flink
可以根据该位置重启。
检查点像普通数据记录一样在算子之间流动。当
map
算子处理完前
3
条数据并
收到检查点分界线时,它们会将状态以异步的方式写入持久化存储,如下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/7f7a6215179d4c6bbe92c901beb38525.png)
位于检查点之前的所有记录
(["b",2]
、
["b",3]
和
["c",1])
被
map
算子处理之后的情
况。此时,持久化存储已经备份了检查点分界线在输入流中的位置
(
备份操作发生在
barrier
被输入算子处理的时候
)
。
map
算子接着开始处理检查点分界线,并触发将状
态异步备份到稳定存储中这个动作。
当
map
算子的状态备份和检查点分界线的位置备份被确认之后,该检查点操作
就可以被标记为完成,如下图所示。我们在无须停止或者阻断计算的条件下,在一
个逻辑时间点
(
对应检查点屏障在输入流中的位置
)
为计算状态拍了快照。通过确保
备份的状态和位置指向同一个逻辑时间点,后文将解释如何基于备份恢复计算,从
而保证
exactly-once
。值得注意的是,当没有出现故障时,
Flink
检查点的开销极小,
检查点操作的速度由持久化存储的可用带宽决定。回顾数珠子的例子
:
除了因为数
错而需要用到皮筋之外,皮筋会被很快地拨过。
![](https://i-blog.csdnimg.cn/blog_migrate/04d1a4c9c0d023f9db52e9adca08ccf4.png)
检查点操作完成,状态和位置均已备份到稳定存储中。输入流中的所有数据记
录都已处理完成。值得注意的是,备份的状态值与实际的状态值是不同的。备份反
映的是检查点的状态。
如果检查点操作失败,
Flink
可以丢弃该检查点并继续正常执行,因为之后的某
一个检查点可能会成功。虽然恢复时间可能更长,但是对于状态的保证依旧很有力。
只有在一系列连续的检查点操作失败之后,
Flink
才会抛出错误,因为这通常预示着
发生了严重且持久的错误。
现在来看看下图所示的情况
:
检查点操作已经完成,但故障紧随其后。
![](https://i-blog.csdnimg.cn/blog_migrate/98fdf2bf0ff35351ca43ed64d5750905.png)
在这种情况下,
Flink
会重新拓扑
(
可能会获取新的执行资源
)
,将输入流倒回到
上一个检查点,然后恢复状态值并从该处开始继续计算。在本例中,
["a",2]
、
["a",2]
和
["c",2]
这几条记录将被重播。
下图展示了这一重新处理过程。从上一个检查点开始重新计算,可以保证在剩
下的记录被处理之后,得到的
map
算子的状态值与没有发生故障时的状态值一致。
![](https://i-blog.csdnimg.cn/blog_migrate/89530780cb2ff033f95a8fd5808293b1.png)
Flink
将输入流倒回到上一个检查点屏障的位置,同时恢复
map
算子的状态值。
然后,
Flink
从此处开始重新处理。这样做保证了在记录被处理之后,
map
算子的状
态值与没有发生故障时的一致。
Flink
检查点算法的正式名称是异步分界线快照
(asynchronous barrier
snapshotting)
。该算法大致基于
Chandy-Lamport
分布式快照算法。
检查点是
Flink
最有价值的创新之一,因为
它使
Flink
可以保证
exactly-once
,
并且不需要牺牲性能
。
(讲的很抽象,我的理解就是存储一个数据状态,等故障时,从故障点重算,而不是整体重算)