状态快照
快照 – 是 Flink 作业状态全局一致镜像的通用术语。快照包括指向每个数据源的指针(例如,到文件或 Kafka 分区的偏移量)以及每个作业的有状态运算符的状态副本,该状态副本是处理了 sources 偏移位置之前所有的事件后而生成的状态。
Checkpoint – 一种由 Flink 自动执行的快照,其目的是能够从故障中恢复。Checkpoints 可以是增量的,并为快速恢复进行了优化。
外部化的 Checkpoint – 通常 checkpoints 不会被用户操纵。Flink 只保留作业运行时的最近的 n 个 checkpoints(n 可配置),并在作业取消时删除它们。但你可以将它们配置为保留,在这种情况下,你可以手动从中恢复。
Savepoint – 用户出于某种操作目的(例如有状态的重新部署/升级/缩放操作)手动(或 API 调用)触发的快照。Savepoints 始终是完整的,并且已针对操作灵活性进行了优化。
状态快照如何工作
Flink 使用 Chandy-Lamport algorithm 算法的一种变体,称为异步 barrier 快照(asynchronous barrier snapshotting)。
每个checkpoint在启动的时候,flink的jobmanager会为其创建一个checkpoint coordinator(状态协调器),checkpoint coordinator全权负责本应用的快照制作。
当 checkpoint coordinator(job manager 的一部分)指示 task manager 开始 checkpoint 时,它会让所有 sources 记录它们的偏移量,并将编号的 checkpoint barriers 插入到它们的流中。这些 barriers 流经 job graph,标注每个 checkpoint 前后的流部分。
Checkpoint n 将包含每个 operator 的 state,这些 state 是对应的 operator 消费了严格在 checkpoint barrier n 之前的所有事件,并且不包含在此(checkpoint barrier n)后的任何事件后而生成的状态。
当 job graph 中的每个 operator 接收到 barriers 时,它就会记录下其状态。拥有两个输入流的 Operators(例如 CoProcessFunction
)会执行 barrier 对齐(barrier alignment) 以便当前快照能够包含消费两个输入流 barrier 之前(但不超过)的所有 events 而产生的状态。
Flink 的 state backends 利用写时复制(copy-on-write)机制允许当异步生成旧版本的状态快照时,能够不受影响地继续流处理。只有当快照被持久保存后,这些旧版本的状态才会被当做垃圾回收。
一致性的概念
一致性其实就是结果的正确性。对于分布式系统而言,就是不同节点中相同数据的副本应该时时刻刻保持一致。
对于 Flink 来说,多个节点并行处理不同的任务,要保证计算结果是正确的,就必须不漏掉任何一个数据,而且也不会重复处理同一个数据。
流式计算本身就是一个一个来的,所以正常处理的过程中结果肯定是正确的;但在发生故障、需要恢复状态进行回滚时就需要更多的保障机制了。我们通过检查点的保存来保证状态恢复后结果的正确。
一致性的级别
最多一次(AT-MOST-ONCE)
当任务发生故障时,直接进行重启,既不恢复丢失的状态, 也不重放丢失的数据。每个数据在正常情况下会被处理一次,遇到故障时就会丢掉,这就是 “最多处理一次”。
如果发生故障数据可以直接被丢掉,结果的准确性就无法保证,更适用于主要的需求是快,对结果的正确性存在稍许偏差的场景。
至少一次(AT-LEAST-ONCE)
正常的生产活动中,我们更希望保证数据不丢失,但是不能保证数据只处理一次,有些数据会被重复处理,这就是“至少一次” (at-least-once)。
为了保证达到至少一次的状态一致性,就需要在发生故障时能够重放数据。最常见是kafka架构,这时只要记录一个偏移量,当任务发生故障重启后,重置偏移量就可以重放检查点之后的数据了。
精确一次(EXACTLY-ONCE)
精确一次意味着所有数据不仅不会丢失,而且只被处理一次,不会重复处理。 精确一次真正意义上保证结果的绝对正确。
要保证精确一次,首先必须能达到至少一次的要求,就是数据不丢失。所以同样需要有数据重放机制来保证这一点。另外,还需要有专门的设计保证每个数据只被处理一 次。Flink 中使用的是一种轻量级快照机制——检查点(checkpoint)来保证 exactly-once 语义。
端到端精确一次
对于 Flink 内部来说,检查点机制可以保证故障恢复后数据不丢(在能够重放的前提下),并且只处理一次,所以已经可以做到 精确一次的一致性语义了。
由于检查点保存的是之前所有任务处理完某个数据后的状态快照,所以重放的数据引起的状态改变一定不会包含在里面,最终结果中只处理了一次。 所以,端到端一致性的关键点,就在于输入的数据源端和输出的外部存储端。
输入端
输入端主要指的就是 Flink 读取的外部数据源。
想要在故障恢复后不丢数据,外部数据源就必须拥有重放数据的能力。常见的做法就是对数据进行持久化保存,并且可以重设数据的读取位置,Kafka就是一个典型的例子。在 Flink 的 Source 任务中将数据读取的偏移量保存为状态,这样就可以在故障恢复时从检查点中读取出来,对数据源重置偏移量,重新获取数据。
数据源可重放数据,加上 Flink 的 Source 算子将偏移量作为状态保存进检查点,就可以保证数据不丢。这是达到至少一次的一致性语义的基本要求。
输出端
精确一次最大的困难在于数据有可能重复写入外部系统。
因为检查点保存之后,继续到来的数据也会一一处理,任务的状态也会更新,最终通过 Sink 任务将计算结果输出到外部系统;只是状态改变还没有存到下一个检查点中。这时如果出现故障,这些数据都会重新来一遍,就计算了两次。
但对于外部系统来说,再次执行写入就会把同一个数据写入两次。 为了实现端到端精确一次,我们还需要对外部存储系统、以及 Sink 连接器有额外的要求。
能够保证 exactly-once 一致性的写入方式有两种: 幂等写入和事务写入。
幂等写入
幂等就是说一个操作可以重复执行很多次,但只导致一次结果更改。这并没有真正解决数据重复计算、写入的问题,而是重复写入也没关系,结果不会改变。
事务写入
事务是指所有操作必须成功完成,否则在每个操作中所做的所有更改都会被撤消。事务有四个基本特性:原子性、 一致性、隔离性和持久性。
在 Flink 流处理的结果写入外部系统时,构建一个事务,让写入操作可以随着检查点来提交和回滚,就可以解决重复写入的问题了。所以事务写入的基本思想就是: 用一个事务来进行数据向外部系统的写入,这个事务是与检查点绑定在一起的。当 Sink 任务 遇到 barrier 时,开始保存状态的同时就开启一个事务,接下来所有数据的写入都在这个事务中;待到当前检查点保存完毕时,将事务提交,所有写入的数据就真正可用了。如果中间过程出现故障,状态会回退到上一个检查点,而当前事务没有正常关闭,所以也会回滚,写入到外部的数据就被撤销了。
事务写入有两种形式:预写日志(WAL)和两阶段提交(2PC)
(1)预写日志
事务提交是需要外部存储系统支持事务的,否则没有办法真正实现写入的回撤。
预写日志(WAL)就是一种非常简单的方式。具体步骤是:
①先把结果数据作为日志(log)状态保存起来
②进行检查点保存时,也会将这些结果数据一并做持久化存储
③在收到检查点完成的通知时,将所有结果一次性写入外部系统。
优点:数据提前在状态后端中做了缓存,理论上适合所有外部存储系统,比较简单
缺点:类似于检查点完成时做一个批处理,一次性的写入会带来一些性能上的问题,且有可能写入失败,必须等待发送成功的返回确认消息。在成功写入所有数据后,在内部再次确认相应的检 查点,这才代表着检查点的真正完成。
(2)两阶段提交
这种提交方式是完全基于事务的,它需要外部系统提供事务支持。 具体的实现步骤为:
①当第一条数据到来时,或者收到检查点的分界线时,Sink 任务都会启动一个事务。
②接下来接收到的所有数据,都通过这个事务写入外部系统;这时由于事务没有提交,所以数据尽管写入了外部系统,但是不可用,是“预提交”的状态。
③当 Sink 任务收到 JobManager 发来检查点完成的通知时,正式提交事务,写入的结果就真正可用了。 当中间发生故障时,当前未提交的事务就会回滚,于是所有写入外部系统的数据也就实现了撤回。
优点:充分利用了 Flink 现有的检查点机制,分界线的到来, 就标志着开始一个新事务;而收到来自 JobManager 的 checkpoint 成功的消息,就是提交事务的指令。
缺点:对外部系统有很高的要求