如果让你来做一个有状态流式应用的故障恢复,你会如何来做呢?
单机和多机会遇到什么不同的问题?
Flink Checkpoint 是做什么用的?原理是什么?
一、什么是 Checkpoint?
Checkpoint 是对当前运行状态的完整记录。程序重启后能从 Checkpoint 中恢复出输入数据读取到哪了,各个算子原来的状态是什么,并继续运行程序。
即用于 Flink 的故障恢复。
这种机制保证了实时程序运行时,即使突然遇到异常也能够进行自我恢复。
二、如何实现 Checkpoint 功能?
如果让你来设计,对于流式应用如何做到故障恢复?
我们从最简单的单机单线程看起。
一)单机情况
同步执行,每次只处理一条数据
很简单,这种情况下,整个流程一次只处理一条数据。
- 数据到 Write 阶段结束,各个算子记录一次各自状态信息(如读取的 offset、中间算子的状态)
- 遇到故障需要恢复的时候,从上一次保存的状态开始执行
- 当然为了降低记录带来的开销,可以攒一批之后再记录。
同时处理多条数据
每个计算节点还是只处理一条数据,但该节点空闲就可以处理下一条数据。
如果还按照一个数据 Write 阶段结束开始保存状态,就会出现问题:
- 前面节点的状态,在处理下一个数据时被改过了
- 从此时保存的记录恢复,前面的节点会出现重复处理的问题
- 此时被称为 - 确保数据不丢(At Least Once)
一种解决方式:
- 在输入数据中,定期插入一个 barrier。
- 各算子遇到 barrier 就开始做状态保留,并且不再接收新数据的计算。
- 当前算子状态保留后,将 barrier 传递给下一个算子,并重复上面的步骤。
- 当 barrier 传递到最后一个算子,并完成状态保留后,本次状态保留完成。
这样,各个节点保存的都是相同数据节点时的状态。
故障恢复时,能做到不重复处理数据,也就是精确一次(Exactly-once)。
但这里,你可能会发现一个问题:
- 数据已经写出了怎么办?在两个保存点之间,已经把结果写到外部了,重启后不是又把部分数据再写了一次?
这里实际是「程序内部精确一次」和「端到端精确一次」。
那么如何做到「端到端精确一次」?
- 方案一:最后一个 sink 算子不直接向外部写出,等到 barrier 来了,才把这一批数据批量写出去
- 方案二:两阶段提交。需要 sink 端支持(如 kafka)。
- 方式类似于 MySQL 的事务。
- sink 端正常向外部写出,不过输出端处于 pre-commit 状态,这些数据还不可读取
- 当 sink 端等到 barrier 时,将输出端数据变为 committed,下游输出端的数据才正式可读
不过以上方法为了做到端到端精确一次,会带来数据延迟问题。(因为要等 Checkpoint 做完,数据才实际可读)。
解决数据延迟有一种方案:
- 方案:幂等写入。同样一条数据,无论写入多少次对输出端看来都是一样的。(比如按照主键重复写这一条数据,并且数据本身没变化)
二)重要概念介绍
一致性级别
前面的例子中,我们提到了部分一致性级别,这里我们总结下。在流处理中,一致性可以分为 3 个级别:
- at-most-once(最多一次): 这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。
- at-least-once (至少一次): 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
- exactly-once (精确一次): 这指的是系统保证在发生故障后得到的计数结果与正确值一致。恰好处理一次是最严格的保证,也是最难实现的。
按区间分:
- 程序(Flink)内部精确一次
- 端到端精确一次