目录
十:容错机制
流式数据连续不断地到来,无休无止;所以流处理程序也是持续运行的,并没有一个明确 的结束退出时间。机器运行程序,996 起来当然比人要容易得多,不过希望“永远运行”也是 不切实际的。因为各种硬件软件的原因,运行一段时间后程序可能异常退出、机器可能宕机, 如果我们只依赖一台机器来运行,就会使得任务的处理被迫中断。
一个解决方案就是多台机器组成集群,以“分布式架构”来运行程序。这样不仅扩展了系 统的并行处理能力,而且可以解决单点故障的问题,从而大大提高系统的稳定性和可用性。
在分布式架构中,当某个节点出现故障,其他节点基本不受影响。这时只需要重启应用, 恢复之前某个时间点的状态继续处理就可以了。这一切看似简单,可是在实时流处理中,我们 不仅需要保证故障后能够重启继续运行,还要保证结果的正确性、故障恢复的速度、对处理性 能的影响,这就需要在架构上做出更加精巧的设计。
在 Flink 中,有一套完整的容错机制(fault tolerance)来保证故障后的恢复,其中最重要 的就是检查点(checkpoint)。在第九章中,我们已经介绍过检查点的基本概念和用途,接下来 我们就深入探讨一下检查点的原理和 Flink 的容错机制。
10.1 检查点(Checkpoint)
发生故障之后怎么办?最简单的想法当然是重启机器、重启应用。由于是分布式的集群, 即使一个节点无法恢复,也不会影响应用的重启执行。这里的问题在于,流处理应用中的任务 都是有状态的,而为了快速访问这些状态一般会直接放在堆内存里;现在重启应用,内存中的 状态已经丢失,就意味着之前的计算全部白费了,需要从头来过。就像编写文档或是玩 RPG游戏,因为宕机没保存而要重来一遍是一件令人崩溃的事情;这种惨痛的经历让我们养成了一 个好习惯——随时存档,这样即使遇到宕机也可以读档继续了。
在流处理中,我们同样可以用存档读档的思路,把之前的计算结果做个保存,这样重启之 后就可以继续处理新数据、而不需要重新计算了。进一步地,我们知道在有状态的流处理中, 任务继续处理新数据,并不需要“之前的计算结果”,而是需要任务“之前的状态”。所以我们 最终的选择,就是将之前某个时间点所有的状态保存下来,这份“存档”就是所谓的“检查点” (checkpoint)。
遇到故障重启的时候,我们可以从检查点中“读档”,恢复出之前的状态,这样就可以回 到当时保存的一刻接着处理数据了。
检查点是 Flink 容错机制的核心。这里所谓的“检查”,其实是针对故障恢复的结果而言 的:故障恢复之后继续处理的结果,应该与发生故障前完全一致,我们需要“检查”结果的正 确性。所以,有时又会把 checkpoint 叫作“一致性检查点”。
10.1.1 检查点的保存
1. 周期性的触发保存
“随时存档”确实恢复起来方便,可是需要我们不停地做存档操作。如果每处理一条数据 就进行检查点的保存,当大量数据同时到来时,就会耗费很多资源来频繁做检查点,数据处理 的速度就会受到影响。
所以更好的方式是,每隔一段时间去做一次存档,这样既不会影响数据 的正常处理,也不会有太大的延迟——毕竟故障恢复的情况不是随时发生的。在 Flink 中,检查点的保存是周期性触发的,间隔时间可以进行设置。
所以检查点作为应用状态的一份“存档”,其实就是所有任务状态在同一时间点的一个“快照”(snapshot),它的触发是周期性的。具体来说,当每隔一段时间检查点保存操作被触发时, 就把每个任务当前的状态复制一份,按照一定的逻辑结构放在一起持久化保存起来,就构成了检查点。
2. 保存的时间点
这里有一个关键问题:当检查点的保存被触发时,任务有可能正在处理某个数据,这时该怎么办呢?
方案:(如果一个数据被所有任务处理完,则保存状态,否则,恢复到之前保存的状态,然后故障恢复后,重新提交任务)
当所有任务都恰好处理完一个相同的输入数据的时候,将它们的状态保存下来。首先,这样避免了除状态之外其他额外信息的存储,提高了检查点保存的效率。 其次,一个数据要么就是被所有任务完整地处理完,状态得到了保存;要么就是没处理完,状态全部没保存:这就相当于构建了一个“事务”(transaction)。如果出现故障,我们恢复到之前保存的状态,故障时正在处理的所有数据都需要重新处理;所以我们只需要让源(source) 任务向数据源重新提交偏移量、请求重放数据就可以了。这需要源任务可以把偏移量作为算子状态保存下来,而且外部数据源能够重置偏移量;Kafka 就是满足这些要求的一个最好的例子。
3. 保存的具体流程
检查点的保存,最关键的就是要等所有任务将“同一个数据”处理完毕。下面我们通过一 个具体的例子,来详细描述一下检查点具体的保存过程。
回忆一下我们最初实现的统计词频的程序——WordCount。这里为了方便,我们直接从数 据源读入已经分开的一个个单词,例如这里输入的就是:
“hello”“world”“hello”“flink”“hello”“world”“hello”“flink”……
对应的代码就可以简化为:
SingleOutputStreamOperator<Tuple2<String, Long>> wordCountStream =
env.addSource(...)
.map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));
.keyBy(t -> t.f0);
.sum(1);
源(Source)任务从外部数据源读取数据,并记录当前的偏移量,作为算子状态(Operator State)保存下来。然后将数据发给下游的 Map 任务,它会将一个单词转换成(word, count)二元 组,初始 count 都是 1,也就是(“hello”, 1)、(“world”, 1)这样的形式;这是一个无状态的算子任 务。进而以 word 作为键(key)进行分区,调用.sum()方法就可以对 count 值进行求和统计了;Sum 算子会把当前求和的结果作为按键分区状态(Keyed State)保存下来。最后得到的就是当 前单词的频次统计(word, count)

当我们需要保存检查点(checkpoint)时,就是在所有任务处理完同一条数据后,对状态做个快照保存下来。例如上图中,已经处理了 3 条数据:“hello”“world”“hello”,所以我们 会看到 Source 算子的偏移量为 3;后面的 Sum 算子处理完第三条数据“hello”之后,此时已 经有 2 个“hello”和 1 个“world”,所以对应的状态为“hello”-> 2,“world”-> 1(这里 KeyedState底层会以 key-value 形式存储)。
此时所有任务都已经处理完了前三个数据,所以我们可以把当前的状态保存成一个检查点,写入外部存储中。
至于具体保存到哪里,这是由状态后端的配置项 “ 检查点存储 ”( CheckpointStorage )来决定的,可以有作业管理器的堆内存 (JobManagerCheckpointStorage)和文件系统(FileSystemCheckpointStorage)两种选择。一般 情况下,我们会将检查点写入持久化的分布式文件系统。

&spm=1001.2101.3001.5002&articleId=127049941&d=1&t=3&u=2e78764158e6448abbb7e173e81180a1)
764

被折叠的 条评论
为什么被折叠?



