这是我参与「第四届青训营 -大数据场」笔记创作活动的的第3篇笔记
Flink容错机制
为什么要容错机制?我们Flink是每天24小时不间断运行的,中间机器寄了网络断了都有可能发生吧,我们得从这个故障中恢复吧。
在学之前,这里有三个故障发生后数据处理方法的语义要清楚:
- At-most-once:故障了啥也不管,所以数据消费最多一次,甚至可能数据根本没有往下游发
- At-least-once:保证每条数据至少处理一次,可能会重复发给下游,消费多次
- Exactly-Once:每条数据都会被处理且只处理一次,最严格的语义
Flink容错机制就是要保证故障发生后,数据处理是Exactly-Once的
所以我们要学两个东西:
- checkpoint,保证可以从故障中恢复原来的状态
- 端到端的Exactly Once机制,保证即使故障产生,端与端之间的数据只消费一次
checkpoint
什么是checkpoint?
这个概念在spark中也有,就是当前这个时刻对整个处理流程的状态进行备份,当故障发生时可以快速恢复到原先的状态。
怎么实现checkpoint?
这里举个例子,假设我任意时刻都做一个快照进行备份,假设是下图这种情况,7要发送给sum_odd进行聚合,我对这个状态进行备份,等恢复的时候这个7我认为是处理过了还是没处理过?这就需要思考一个合适的方法来处理,具体是对什么时刻进行备份,以及我怎么让恢复时刻的语义能保证数据处理并只处理一次呢?
开动脑筋,思考一下:
假如说上游的数据下发给下游,并且下游全部计算完毕后这个状态是不是就可以备份了。这样到时候恢复的时候,我们就可以默认当前上游的数据一定是已经被消费过的,不需要再发了。
那么我们是不是可以得出一个简单的算法来做checkpoint了:比如这个时刻我要做checkpoint,上游往下游发完数据就停住,下游处理完数据也停住,整个状态暂停,等备份完以后,再继续接收上游的数据。假如说是这样一个算法,那是不是被checkpoint浪费了很多性能和资源呀
神奇的Chandy-Lamport算法(分布式快照算法)
下面我们根据图示介绍一下这个分布式快照算法
-
JM给每个Source发一个checkpoint barrier,现在开始备份了
- Source收到barrier以后,先暂停处理数据,把自己的状态保存(state backend),往他所有的下游发checkpoint barrier,发完告诉JM自己的状态保存好了,然后就可以继续处理数据了
-
中间算子要等所有上游的barrier都到了才可以开始制作快照,制作快照的方式和之前一样,暂停→保存→下发barrier→制作完毕继续处理数据
需要注意的是:上游发完barrier会继续处理数据,然后往下游发数据的。假如说source1把barrier发过来了,在等Source2的barrier到达,但是Source1比较快,又把数据发过来了,这个时候Source1发过来的这些数据是不会被处理的,不能算做在这个状态里,会先缓存下来,等状态制作好再处理
- 当整个处理流程都告诉JM制作完成了,那么checkpoint就完成了
思考一下,这个算法跟原先我们思考的算法有什么不同?
- 原先我们设想的是把整个处理流程都停掉作快照,但这里我们只需要暂停当前制作快照的算子,做好以后可以继续处理数据
再总结一下,checkpoint对于每个算子来说,都是等上游barrier到达,然后保存自己的状态,然后下发barrier,然后告诉JM保存好了,然后继续处理手头数据。
端到端的Exactly-Once机制
??Flink有checkpoint不就好了吗?能够从故障中恢复过来了呀?
- 思考一下,我们在处理流程内部确实保证了能够故障恢复和Exactly-Once语义,但是sink往外部系统写的数据能保证Exactly-Once吗?
- 从故障发生到故障恢复这段时间里,sink是会往外部写数据的,经过状态恢复以后,我们还得保证sink往外写的Exactly-Once
那么怎么保证端到端的Exactly-Once呢,我们需要学一个协议,叫做两阶段提交协议
两阶段提交协议
首先有几个概念:
- 协调者:调度其他执行节点的中心节点,比如说JM
- 参与者:被协调者调度的节点,比如说Source、window、sink等算子
- 事务:可以理解成一系列操作,但是要保证原子性(要么全部操作,要么全部都不操作)。比如说a给b转账100:a-100和b+100这个事务的两个操作要么全执行,要么不执行
看名字就知道这个协议有预提交和提交两个阶段:
-
预提交阶段:
- 首先协调者给所有参与者发一条预提交消息,告诉参与者要工作了
- 每个参与者收到预提交消息以后,执行事务,但是不真正提交
- 如果事务完成了就给协调者发vote yes,否则就vote no,至此预提交阶段结束
-
提交阶段:
- 如果协调者收到的全是yes,给所有参与者发commit消息,这个就是真正可以执行事务了。那所有参与者都执行完以后给协调者发个ACK消息,协调者标记好事务完成
- 如果协调者收到no,那么给所有参与者发rollback消息,回滚之前的事务。所有参与者回滚完给协调者发个ACK消息,协调者标记事务回滚完成
至此,我们学会了两阶段提交协议,现在看看Flink中是怎么应用的
在Flink中JM作为协调者,Source到sink这条链路上的节点作为参与者
结合我们刚刚学的checkpoint:
- JM给Source发一个barrier就类似于预提交消息
- barrier不断向下传递相当于这个消息传给了所有的参与者
- 每个算子制作自己的状态后,给JM发个制作完毕的消息就相当于vote yes/no
- 当JM收到所有的算子的状态都制作完毕了,都是voteyes,那么JM给所有的算子发一个commit消息,就是可以真正执行事务了。如果是voteno,则发rollback消息
- 在收到这个消息之前,sink对下游开启的是一个事务,虽然一直往kafka里写了,但是kafka并不可见。收到JM的commit消息以后可以真正执行了,可以往kafka里写数据了。当然如果rollback消息,就回滚这个事务
总结:
Flink通过内部链路checkpoint和Sink节点两阶段提交协议相结合的方式实现了整个逻辑内部以及端到端的Exactly-Once语义
具体实现方法是这样:
- JM给Source发checkpoint barrier,然后Source制作自己状态的快照,然后把barrier下发,告诉JM制作完毕
- 后续所有算子都是如此:等待上游所有的barrier→制作快照→barrier下发→告诉JM→继续处理数据
- Sink往外部写数据都是开启事物,当JM收到所有的状态都制作完毕后,会下发提交或回滚消息,Sink根据消息执行或回滚事务