Flink容错与状态一致性保证
CheckPoint机制
为了进行容错,我们在进行Flink的处理的时候,需要进行容错管理,难免我们的应用会发生故障的。而Flink提供了这样的一种容错机制CheckPoint,它能够保证Flink内部的一致性,实现内部Exact Once语义。首先看看什么是CheckPoint机制
它是受Chandy-Lamport算法的启发,形成的一种轻量级的分布式快照,它的意思是每个操作(具体到是每一个并行操作)的状态进行持久化或者保存在内存中,或者说进行一次快照,这样子就能够保存了这一瞬间的状态和数据,”这一瞬间“并不是指时间的一瞬间,使我们要进行CheckPoint的那个关键点的一瞬间,等会儿就明白了。
Flink中的checkpoint模式
在Flink中,是有JobManager按照一定的周期提供CheckPoint,它有点儿类似于watermark,也是丢一个barrier放在数据流里面,它也在数据流里面流动,但是丝毫不影响算子对真实数据的操作,task的执行者遇到这个barrier不会当做数据处理,反之停止处理barrier后面的数据流,将后面的数据流缓存。
串行的barrier
可以看到,黄色的方块是数据流,而绿色的条形是barrier,一个barrier携带的就是一个checkpoint的ID,这是一个数据通道的barrier。
并行的barrier
并行的很奇妙,当我们的一个operator接受了多个并行的输入流时,每个并行的输入流都会有barrier。假设当前有输入流A和输入流B,就像上面的图,那么A的barrier来了的时候,就会停止处理barrier之后的数据,而只接受他们缓存在本地,转而等待其他数据流的barrier到来,这时候B的barrier如果没有到来,他还是会处理来自B的数据,也就是说B这条输入流并不受A的影响,因为属于B的barrier还没有来。当B的barrier来的时候,也和A一样,将后面的数据先缓存好,而不去处理它。这时候所有的并行的输入流的barrier都来了,那么就可以当前这个operator的状态和一些数据进行快照checkpoint了,这样这个operator的状态就被存储了。
Flink的checkpoint要控制合理的间隔,保证多个输入流的checkpointID是一样的,不然会混乱。保证了checkpointID是一致的话,当我们checkpoint后,就将这个operator的barrier广播到operator的所有输出流,因为所有与这个operator相连的其他operator都在等待这个通道的barrier,这样后面的operator才可以进行checkpoint
下面就要回到上面的”一瞬间“的问题了,这里为什么说不是真正意义上的一瞬间,因为我们不同operator实际上进行checkpoint的时间是不同的,所以保存的不是checkpoint的一瞬间,而是这个barrier相对于这些数据流的位置语义的一瞬间,这样就能够保证在一个barrier:checkpointID的时候,所有属于这个checkpointID之前的数据都能够存储起来。
Flink的CheckPoint具体流程
- ①
上面已经说过了,checkpoint是在JobManager发起的,其实是在JobManager的Checkpoint Coordinator(Checkpoint协调者)发起了CheckPoint,而且是向所有的source发起。这个sink你可以暂时想象成真正的Flink外部的Sink,也可以想象成某一个算子,比如map
- ②
当source接受到了checkpoint的触发,也就是barrier,将自己的状态异步写入到持久化存储中,这样source的就能够保证持久化了,然后将barrier广播到任何下游输出流通道去。
- ③
当task的状态备份完成后,就可以对CheckPoint Coordinator返回一个消息,告诉他已经备份完成了。即state handle:备份数据的地址告诉了JM
- ④
当下游的节点受到了上游的所有通道输入流的barrier后,就会执行它的checkpoint,将数据持久化。但是特别情况就是最后的Sink, sink 节点收集齐上游两个 input 的 barrier 之后,会执行本地快照,这里特地展示了 RocksDB incremental Checkpoint 的流程,首先 RocksDB 会全量刷数据到磁盘上(红色大三角表示),然后 Flink 框架会从中选择没有上传的文件进行持久化备份(紫色小三角)。
- ⑤
同样,sink的checkpoint完成之后,也会返回一个消息给Checkpoint Coordinator,告诉它随后一个已经完成了。
- ⑥
最后,JobManager的CheckPoint Coordinator接受到了最后一个state handle,就认为这一个CheckPointID已经全局完成了。向持久化存储中再备份一个 Checkpoint meta 文件。
- ④
状态后端
前面讲了那么多的checkpoint,到底checkpoint文件存到哪里去了,这就是所谓的状态后端要做的了。这里只是稍微看看,以后再深入看看。
MemoryStateBackend
这个是存储在内存中的,即在MemoryStateBackend内部,数据以java对象的方式存储在堆内存中,在 CheckPoint 时,State Backend 对状态进行快照,并将快照信息作为 CheckPoint 应答消息的一部分发送给 JobManager(master),同时 JobManager 也将快照信息存储在堆内存中。
设置为false就可以关闭异步快照
new MemoryStateBackend(MAX_MEM_STATE_SIZE, false);
这种方法很明显,只适合本地测试,生产环境明显不实用
FsStateBackend
FsStateBackend 需要配置一个文件系统的 URL(类型、地址、路径),例如:”hdfs://namenode:40010/flink/checkpoints” 或 “file:///data/flink/checkpoints”。
FsStateBackend 将正在运行中的状态数据保存在 TaskManager 的内存中。CheckPoint 时,将状态快照写入到配置的文件系统目录中。 少量的元数据信息存储到 JobManager 的内存中(高可用模式下,将其写入到 CheckPoint 的元数据文件中)。其实就和上面的图差不多,只不过在高可用模式下元数据文件存在了也持久化了。
false也可以关闭异步快照
new FsStateBackend(path, false);
使用场景L:
- 状态比较大、窗口比较长、key/value 状态比较大的 Job。
- 所有高可用的场景。
RocksDBStateBackend
RocksDBStateBackend 需要配置一个文件系统的 URL(类型、地址、路径),例如:”hdfs://namenode:40010/flink/checkpoints” 或
“file:///data/flink/checkpoints”。
感觉有点儿像上面的方式
但是RocksDBStateBackend 将正在运行中的状态数据保存在 RocksDB 数据库中,RocksDB 数据库默认将数据存储在 TaskManager 的数据目录。 CheckPoint 时,整个 RocksDB 数据库被 checkpoint 到配置的文件系统目录中。 少量的元数据信息存储到 JobManager 的内存中(高可用模式下,将其存储到 CheckPoint 的元数据文件中)。
使用场景:
- 状态非常大、窗口非常长、key/value 状态非常大的 Job。
- 所有高可用的场景。
一致性保证
数据处理语义
最多一次(At-most-once):
**它表现的是一种【尽力而为】的感觉,就是不管我的operator执行的怎么样,有没有失败,我都不会让我的上游去重新发数据过来,就让数据最多被处理一次,从而导致下游的operator可能根本处理不到这个数据。可见可靠性非常不好,但是性能又非常好对吧。
至少一次(At-least-once)
这个和上面的情况就是相反的,它特别犟,它一定让这个数据处理,如果处理过程中出现了差错,那么就会让这个operator的上游重新传过来,一直到成功为止。但是你可以看到,下面这种情况,有可能operator2在失败后,让operator重传,有可能第一次重传出现了延迟,而operator2这边可能有什么超时机制,导致再让operator1去重传,就有了第二次重传,结果第一次重传的数据过来了,进行处理,然后第二次数据也过来,又进行了处理,导致了处理了多次。
精确一次(Exactly-once)
这个就是最理想的状态,它能保证所有的数据恰好只处理一次。实现这种Exactly-once有两种方式:
- 分布式快照+状态检查点checkpoint
- 上面的At-least-once+对重复数据去重
分布式快照+状态检查点checkpoint
这个想必已经不用说了,很明显前面已经说的非常的清楚了。
**我们已经将s=4的情况持久化了,然后在s=12的时候出现了差错,这时候就将持久化存储的状态s=4恢复过来,然后恢复。即回滚到上一次完整的checkpoint
对At-least-once数据进行去重
这个方式,就是和At-least-once是一样的,就是对重复发过来的数据进行去重。
精确一次真的是精确一次吗???????
我们先来看个场景:分别对source来的数据进行偶数相加和奇数相加
- 1⃣️:这个图表现的是一个正常的数据流,偶数流入到偶数的分区,奇数流入到奇数的分区。
- 2⃣️:这个JobManager发起了一个CheckPoint,发入所有的source。
- 3⃣️:这时候也就正常的对source的3和4进行了持久化。然后每个checkpoint[2]就广播出去了,假如现在Sum odd完成了3+5=8,然后收入到两个checkpoint(分别来自两个上游),这时候进行checkpoint,但是呢我们的另一个分区Sum even突然崩了,可是还没有进行checkpoint。这时候恢复到之前最完整的一次checkpoint,不是本次checkpoint,这里应该是回到了最初。然后又要从2开始传给下游。
有人就有疑问了,这里明明数据重新传了,然后数据也重新处理了,这还是精确一次吗
其实这里不是用户逻辑上的处理一次,也就是说,我们这种流处理系统,尤其是真正的生产环境,不可能做到所有的机器都完好,总会要出现失败的情况,所以所谓的用户逻辑处理不可能只处理一次。
这里的精确一次,是指有效一次,也可以是时间上的一次,有点儿像时间倒流的感觉,就好像什么都没有发生一样,比如我们的At-least-once,就没有“时光倒流”的感觉,它整个时间驱动是往前的,而Exactly-once通过分布式快照checkpoint,将整个流处理系统的状态恢复到最完整的一次,这样就像是时间倒流,时间重写,感觉这段时间什么都没有发生,然后从最完整的哪个状态数据继续发,继续处理,这样就当作的数据只处理一次。
另一种理解方式就是有效一次,而且是持久化到状态后端的有效一次,这样就容易理解了。事件的处理可以发生多次,但是该处理的效果只在持久后端状态存储中反映一次。
Flink实现Exactly-once
Flink只支持内部Exactly-once
Flink有两种Exactly-once,一种是内部Exactly-once,另一种是source和sink的Exactly-once。为什么要这样分,因为前面所说的Checkpoint和分布式快照都只能实现内部的Exactly-once,并不能对外部的source和sink提供Exactly-once。比如,我们的数据写出到sink了,然后还没有做持久化,就突然出错,恢复时从上一次完整的checkpoint来恢复,这时候可见某些数据肯定又会要第二次写出到sink,而第一次写出到sink的数据可能被其他用户或者应用消费了,第二次又来重复数据,导致sink又重复消费这些数据,可见Flink只支持内部Exactly-once,外部的Exactly-once还是需要外部source和sink的支持。