Flink 中的容错机制

在 Flink 中,有一套完整的容错机制来保证故障后的恢复,其中最重要的就是检查点。

1.检查点(Checkpoint)

在流处理中,我们可以用存档读档的思路,就是将之前某个时间点所有的状态保存下来,这份“存档”就是所谓的“检查点”(checkpoint)。

遇到故障重启的时候,我们可以从检查点中“读档”,恢复出之前的状态,这样就可以回到当时保存的一刻接着处理数据了。

这里所谓的“检查”,其实是针对故障恢复的结果而言的:故障恢复之后继续处理的结果,应该与发生故障前完全一致,我们需要“检查”结果的正确性。所以,有时又会把checkpoint叫做“一致性检查点”。

1.检查点的保存

1.周期性的触发保存

“随时存档”确实恢复起来方便,可是需要我们不停地做存档操作。如果每处理一条数据就进行检查点的保存,当大量数据同时到来时,就会耗费很多资源来频繁做检查点,数据处理的速度就会受到影响。所以在 Flink中,检查点的保存是周期性触发的,间隔时间可以进行设置。

2.保存的时间点

我们应该在所有任务(算子)都恰好处理完一个相同的输入数据的时候,将它们的状态保存下来。

这样做可以实现一个数据被所有任务(算子)完整地处理完,状态得到了保存。

如果出现故障,我们恢复到之前保存的状态,故障时正在处理的所有数据都需要重新处理;我们只需要让源(source)任务向数据源重新提交偏移量、请求重放数据就可以了。当然这需要源任务可以把偏移量作为算子状态保存下来,而且外部数据源能够重置偏移量;kafka 就是满足这些要求的一个最好的例子。

3.保存的具体流程

检查点的保存,最关键的就是要等所有任务将“同一个数据”处理完毕。下面我们通过一个具体的例子,来详细描述一下检查点具体的保存过程。

回忆一下我们最初实现的统计词频的程序——word count。这里为了方便,我们直接从数据源读入已经分开的一个个单词,例如这里输入的是:“hello”,“world”,“hello”,“flink”,“hello”,“world”,“hello”,“flink”…

我们所需要的就是每个任务都处理完“hello”之后保存自己的状态。

2.从检查点恢复状态

当我们需要保存检查点时,就是在所有任务处理完同一条数据后,对状态做个快照保存下来。例如我们输入数据为:“hello”,“world”,“hello”,“flink”,“hello”,“world”,“hello”,“flink”…

我们所需要的就是每个任务都处理完“hello”之后保存自己的状态。

3.检查点算法

在 Flink 中,采用了基于 Chandy-Lamport 算法的分布式快照,可以在不暂停整体流处理的前提下,将状态备份保存到检查点。

1.检查点分界线(Barrier)

借鉴水位线的设计,在数据流中插入一个特殊的数据结构,专门用来表示触发检查点保存的时间点。收到保存检查点的指令后,Source 任务可以在当前数据流中插入这个结构;之后的所有任务只要遇到它就开始对状态做持久化快照保存。由于数据流是保持顺序依次处理的,因此遇到这个标识就代表之前的数据都处理完了,可以保存一个检查点;而在它之后的数据,引起的状态改变就不会体现在这个检查点中,而需要保存到下一个检查点。

这种特殊的数据形式,把一条流上的数据按照不同的检查点分隔开,所以就叫做检查点的“分界线”(Checkpoint Barrier)。

在JobManager中有一个“检查点协调器”,专门用来协调处理检查点的相关工作。检查点协调器会定期向TaskManager发出指令,要求保存检查点(带着检查点ID);TaskManager会让所有的Source任务把自己的偏移量(算子状态)保存起来,并将带有检查点ID的分界线插入到当前的数据流中,然后像正常的数据一样像下游传递;之后Source任务就可以继续读入新的数据了。

2.分布式快照算法(Barrier 对齐的精准一次)

watermark 指示的是“之前的数据全部到齐了”,而 barrier 指示的是“之前所有数据的状态更改保存入当前检查点”:它们都是一个“截止时间”的标志。所以在处理多个分区的传递时,也要以是否还会有数据到来作为一个判断标准。

具体实现上,Flink使用了 Chandy-Lamport算法的一种变体,被称为“异步分界线快照”算法。算法的核心就是两个原则

当上游任务向多个并行下游任务发送 barrier 时,需要广播出去;

而当多个上游任务向同一个下游任务传递分界线时,需要在下游任务执行“分界线对齐”操作,也就是需要等到所有并行分区的 barrier 都到齐,才可以开始状态的保存。

1.场景说明

为了详细解释检查点算法的原理,我们对之前的word count程序进行扩展,考虑所有算子并行度为2的场景。

我们有两个并行的Source任务,会分别读取两个数据流(或者是一个源的不同分区)。这里每条流中的数据都是一个个的单词:“hello”“world”“hello” “flink”交 替出 现。此时第一条流的Source任务(为了方便,下文中我们直接叫它“Source 1”,其它任务类似)读取了3个数据,偏移量为3;而第二条流的Source任务(Source 2)只读取了一个“hello”数据,偏移量为1。

2.检查点保存算法具体过程为

1)JobManager发送指令,触发检查点的保存;Source 任务中插入一个分界线,并将偏移量保存到远程的持久化存储中。

说明:并行的Source任务保存的状态为3和1,表示当前的1号检查点应该包含:第一条流中截至第三个数据、第二条流中截至第一个数据的所有状态更改。可以发现Source任务做这些的时候并不影响后面任务的处理,Sum任务已经处理完了第一条流中传来的(world, 1),对应的状态也有了更改。

  1. 触发检查点:JobManager 向 Source 发送 Barrier;
  2. Barrier 发送:向下游广播发送;
  3. Barrier 对齐:下游需要收到上游所有并行度传递过来的 Barrier 才做自身状态的保存;
  4. 状态保存:有状态的算子将状态保存至持久化。
  5. 先处理缓存数据,然后正常继续处理

完成检查点保存之后,任务就可以继续正常处理数据了。这时如果有等待分界线对齐时缓存的数据,需要先做处理;然后再按照顺序依次处理新到的数据。当 JobManager 收到所有任务成功保存状态的信息,就可以确认当前检查点成功保存。之后遇到故障就可以从这里恢复了。

(补充)由于分界线对齐要求先到达的分区做缓存等待,一定程度上会影响处理的速度;当出现背压时,下游任务会堆积大量的缓冲数据,检查点可能需要很久才可以保存完毕。

为了应对这种场景,Barrier 对齐中提供了至少一次语义以及 Flink 1.11 之后提供了不对齐的检查点保存方式,可以将未处理的缓冲数据也保存进检查点。这样,当我们遇到一个分区barrier 时就不需等待对齐,而是可以直接启动状态的保存了。

3.分布式快照算法(Barrier 对齐的至少一次)

(1)JobManager发送指令,触发检查点的保存;Source 任务中插入一个分界线,并将偏移量保存到远程的持久化存储中。

说明:并行的Source任务保存的状态为3和1,表示当前的1号检查点应该包含:第一条流中截至第三个数据、第二条流中截至第一个数据的所有状态更改。可以发现Source任务做这些的时候并不影响后面任务的处理,Sum任务已经处理完了第一条流中传来的(world, 1),对应的状态也有了更改。

4.分布式快照算法(非 Barrier 对齐的精准一次)

(1)JobManager发送指令,触发检查点的保存;Source 任务中插入一个分界线,并将偏移量保存到远程的持久化存储中。

说明:并行的Source任务保存的状态为3和1,表示当前的1号检查点应该包含:第一条流中截至第三个数据、第二条流中截至第一个数据的所有状态更改。可以发现Source任务做这些的时候并不影响后面任务的处理,Sum 2已经处理完了第一条流中传来(world, 1)对应的状态也有了更改。

4.检查点配置

检查点的作用是为了故障恢复,我们不能因为保存检查点占据了大量时间、导致数据处理性能明显降低。为了兼顾容错性和处理性能,我们可以在代码中对检查点进行各种配置。

1.启用检查点

默认情况下,Flink 程序是禁用检查点的。如果想要为 Flink 应用开启自动保存快照的功能,需要在代码中显式地调用执行环境的.enableCheckpointing()方法:

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 每隔 1 秒启动一次检查点保存
        env.enableCheckpointing(1000);

这里需要传入一个长整型的毫秒数,表示周期性保存检查点的间隔时间。如果不传参数直接启用检查点,默认的间隔周期为 500 毫秒,这种方式已经被弃用。

检查点的间隔时间是对处理性能和故障恢复速度的一个权衡。如果我们希望对性能的影响更小,可以调大间隔时间;而如果希望故障重启后迅速赶上实时的数据处理,就需要将间隔时间设小一些。

2.检查点存储

检查点具体的持久化存储位置,取决于“检查点存储”的设置。默认情况下,检查点存储在 JobManager的堆内存中。而对于大状态的持久化保存,Flink也提供了在其他存储位置进行保存的接口。

具体可以通过调用检查点配置的.setCheckpointStorage()来配置,需要传入一个 CheckpointStorage 的实现类。Flink 主要提供了两种CheckpointStorage:作业管理器的堆内存和文件系统。

        // 配置存储检查点到 JobManager 堆内存
        env.getCheckpointConfig().setCheckpointStorage(new JobManagerCheckpointStorage());
        // 配置存储检查点到文件系统
        env.getCheckpointConfig().setCheckpointStorage(new FileSystemCheckpointStorage("hdfs://namenode:40010/flink/checkpoints"));

对于实际生产应用,我们一般会将 CheckpointStorage 配置为高可用的分布式文件系统(HDFS,S3 等)。

3.其它高级配置

检查点还有很多可以配置的选项,可以通过获取检查点配置(CheckpointConfig)来进行设置。

CheckpointConfig checkpointConfig = env.getCheckpointConfig();
1.常用高级配置
  • 检查点模式(CheckpointingMode)
    设置检查点一致性的保证级别,有“精确一次”(exactly-once)和“至少一次”(at-least-once)两个选项。默认级别为 exactly-once,而对于大多数低延迟的流处理程序,at-least-once就够用了,而且处理效率会更高。

  • 超时时间(checkpointTimeout)
    用于指定检查点保存的超时时间,超时没完成就会被丢弃掉。传入一个长整型毫秒数作为参数,表示超时时间。

  • 最小间隔时间(minPauseBetweenCheckpoints)
    用于指定在上一个检查点完成之后,检查点协调器最快等多久可以出发保存下一个检查点的指令。这就意味着即使已经达到了周期触发的时间点,只要距离上一个检查点完成的间隔不够,就依然不能开启下一次检查点的保存。这就为正常处理数据留下了充足的间隙。当指定这个参数时,实际并发为 1。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章