Flink 任务正常启动,Checkpoint 在一两天之后超时,收不到最新的确认信息。然后以相同的包重新启动并正常运行几天,如此反复仍找不到原因,而我们的配置可能会像下面那样。本文将会给出一些思考。

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //Restart after setup failure
        env.setRestartStrategy(RestartStrategies.failureRateRestart(3, Time.milliseconds(1000), Time.minutes(5)));
        env.disableOperatorChaining();
        env.enableCheckpointing(1000 * 60 * 15, CheckpointingMode.AT_LEAST_ONCE);
        //Business complex settings timed out 1 hour.
        env.getCheckpointConfig().setCheckpointTimeout(1000 * 60 * 60);
        env.getCheckpointConfig().setMinPauseBetweenCheckpoints(1000  * 10);
        env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION);

1. 前言

众所周知,Flink 为高可用性实现了一个强大的检查点机制,并确保功能快速恢复。围绕 Checkpoint 流程本身已经做了很多工作。在官方文档中,还解释了实际生产中的 Checkpoint 和 Checkpoint 调优参数的一些原理(特别是大规模状态集)。笔者结合官方文档和工作实践做一个总结。

2. Checkpoint 速度的性能指标

如果我们想微调 Flink Checkpoint 操作,那么首先需要能够度量当前 Checkpoint 是快还是慢。有两个指标可以提供。

每个 Checkpoint 启动时间。观察每个 Checkpoint 的开始时间,以检测每个 Checkpoint 之前和之后是否存在空闲时间间隔。如果时间间隔存在,那么当前 Checkpoint 将会在合理的时间之内全部完成。

观察缓存的数据量。这个缓存旨在等待其他较慢的流中的 Barrier(即 Barrier Alignment)。这往往与 Checkpoint 的原理有关。

但是,一般情况下,使用者能够根据第一项内容监控一个应用程序检测速度。

3. 相邻 Checkpoint 的间隔设置

现在让我们假设一种使用场景,对于一个非常大的状态数据集,每个 Checkpoint 花费的时间超过了系统设置的最大时间(即,Checkpoint Interval),会发生什么呢。

答案是,应用程序一直在做 Checkpoint,因为应用程序正在完成一个 Checkpoint 时发现该执行下一个 Checkpoint 了,随之启动一个新的 Checkpoint。最终的结果是非常糟糕的:应用程序自身可能无法运行。

当然,我们可能会说,让我们设置并行 Checkpoint 的数量,或者用增量 Checkpoint 代替全量 Checkpoint。Checkpoint 仅对前一个 Checkpoint 的状态进行增量更改,然后在恢复时重放状态更改。

但是在这里,我们可以使用一种更直接和有效的方法来设置连续检查点的时间间隔。直观的解释是强制设置检查点之间的空闲时间,如下所示。

flink checkpoint调优 flink checkpoint慢_Checkpoint


配置的详细说明可以参考 Flink 调优:Checkpoint 配置。相关配置的设置如下:

StreamExecutionEnvironment.getCheckpointConfig().setMinPauseBetweenCheckpoints(milliseconds)

4. Checkpoint 的资源设置

当我们的 Checkpoint 包含越多的状态数据集,我们就会消耗越多的资源。Flink 首先在每个任务上建立 Checkpoint 数据,然后将 Checkpoint 数据持久化到外部存储。这里的一个优化是,当总状态数据固定时,每个任务的平均 Checkpoint 数据越少,相应地总 Checkpoint 时间就越短。所以,我们可以对每个任务设置更多的并发度(即,分配更多资源),以加速 Checkpoint 的执行。

5. Checkpoint 任务本地恢复

为了将来优化检查点,有必要在运行时执行 Checkpoint。首先,我们需要了解 Flink Checkpoint 不是完全在主节点中的进程,而是分布在每个任务中,然后作为摘要持久存在。这些任务生产的 Checkpoint 数据可以在随后的应用程序恢复中重新分配,包括在并行度增加或减少时。

为了快速状态恢复,每个任务都会将 Checkpoint 数据写到本地磁盘和远程分布式存储中,即双重副本。只要一个任务的本地 Checkpoint 数据没有损坏,当还原应用程序时系统将会首先加载本地 Checkpoint 数据,这会大幅度减少从远程拉取状态数据的过程。该过程如下图所示:

flink checkpoint调优 flink checkpoint慢_Flink_02

6. 外部状态存储的选择

上文中提及的方法本质上不是解决大规模状态集中 Checkpoint 慢的问题,而是降低 Checkpoint 慢的风险和影响。这里我们反复强调大规模状态,我们合理化思维,因为规模大,我们会很慢,所以如果我们找到具有更快存储状态的介质(或策略),那么过程也可能会更快。

因此,我们可以为状态存储选择一种更高效的外部存储介质(如,RocksDB),而不仅仅限于内存或完全在磁盘上。这是我们 StateBackend 做的选择之一。

你可以使用 RocksDB 作为增量 Checkpoint 的存储,其中 Checkpoint 不稳定地增长,并定期合并清楚的历史状态。

flink checkpoint调优 flink checkpoint慢_Flink_03


上图以一个有状态的算子为例,一个 Checkpoint 文件保存周期是可配置的,本例中是 retained 是2,上面展示了每次 Checkpoint 时 RocksDB 示例中存储的状态以及文件引用关系等。

  • 对于 Checkpoint CP1,本地 RocksDB 目录包含 2 个磁盘文件(sstable),它是基于 Checkpoint 的名字来创建的。当 Checkpoint 完成时,在共享状态注册表(Shared State Registry)中创建两个实体,并且将它们的引用计数置为 1。在共享注册表中存储的 Key 是由算子、Subtask 和原始 sstable 文件名构成,同时注册表维护了一个 Key 到实际文件存储路径的映射 Map。
  • 对于 Checkpoint CP2,RocksDB 创建了两个新的 sstable 文件,旧的两个文件也存在。在 CP2,新的两个生成新文件,老的两个引用原来的存储。当 Checkpoint 结束,所有引用文件的引用计数都加 1。
  • 对于 Checkpoint CP3,RocksDB 合并 sstable-(1)、sstable-(2) 和 sstable-(3) 为 sstable-(1,2,3),同时删除原始文件。合并的文件包含了原始文件中的所有信息,并删除重复的实体。除了合并的文件,sstable-(4) 依然存在,另外创建了一个 sstable-(5) 。Flink 将新的 sstable-(1,2,3) 和 sstable-(5) 存储到底层,sstable-(4) 引用 CP2 中的,并对相应的引用计数加 1,同时现在可以删除旧的 CP1 的 Checkpoint,由于 retained 已达到 2,作为删除的一部分,Flink 将所有 CP1 的引用文件的引用计数减 1。
  • 对于 Checkpoint CP4,RocksDB 合并 sstable-(4)、sstable-(5) 和新的 sstable-(6) 为 sstable-(4,5,6)。Flink 存储新的 sstable,并引用 sstable-(1,2,3),sstable-(1,2,3) 的引用计数加 1,同时删除 CP2 中 retained 是 2 的。由于 sstable-(1)、sstable-(2) 和 sstable-(3) 已降至 0,因此 Flink 从底部将其删除。