端到端的 Exactly-Once 处理概述 | 青训营笔记

156 阅读11分钟

这是我参与「第四届青训营 」笔记创作活动的第23天

Apache Flink 1.4.0 于 2017 年 12 月发布,它为使用 Flink 进行流处理引入了一个重要的里程碑:一个名为TwoPhaseCommitSinkFunction此处相关 Jira)的新功能,它提取了两阶段提交协议的通用逻辑,并使得一件事情成为可能:构建使用 Flink 和一系列数据源(sources)和接收器(sinks) (包括 Apache Kafka 版本 0.11 及更高版本)的端到端exactly-once应用程序成为可能。它提供了一个抽象层,并且只需要用户实现少数几种方法来实现端到端的精确一次语义。

如果这就是您需要听到的全部内容,请让我们将您指向Flink 文档中的相关位置,您可以在其中阅读有关如何使用TwoPhaseCommitSinkFunction的信息。

但是,如果您想了解更多信息,在这篇文章中,我们将分享对新功能的深入概述以及 Flink 幕后发生的事情。

在这篇文章的其余部分,我们将:

  • 描述 Flink 的检查点在 Flink 应用程序中保证精确一次结果的作用。

  • 展示 Flink 如何通过两阶段提交协议与数据源和数据接收器交互,以提供端到端的

    Exactly-once 保证。

  • 通过一个简单的示例了解如何使用TwoPhaseCommitSinkFunction

    来实现一次性文件接收器。

Apache Flink 应用程序中的 Exactly-once 语义

当我们说“exactly-once semantics”时,我们的意思是每个传入的事件都会影响最终结果一次。即使在机器或软件出现故障的情况下,也不会出现重复数据,也不会出现未经处理的数据。

Flink 长期以来一直Flink 应用程序中提供完全一次性的语义。在过去的几年里,我们深入地介绍了 Flink 的 checkpointing,这是 Flink 提供完全一次语义能力的核心。Flink 文档还提供了对该功能的全面概述

在我们继续之前,这里是检查点算法的快速总结,因为理解检查点对于理解这个更广泛的主题是必要的。

Flink 中的检查点是以下内容的一致快照:

  1. 应用程序的当前状态
  2. 输入流中的位置

Flink 以可配置的定期间隔生成检查点,然后将检查点写入持久存储系统,例如 S3 或 HDFS。将检查点数据写入持久化存储是异步发生的,这意味着 Flink 应用程序在检查点过程中继续处理数据。

如果机器或软件发生故障并在重新启动时,Flink 应用程序会从最近成功完成的检查点恢复处理;Flink 恢复应用程序状态并从检查点回滚到输入流中的正确位置,然后再次开始处理。这意味着 Flink 计算结果时就好像失败从未发生过一样。

在 Flink 1.4.0 之前,Exactly-once 语义仅限于Flink 应用程序的范围,并没有扩展到 Flink 处理后发送数据的大部分外部系统。

但是 Flink 应用程序与各种数据接收器一起运行,开发人员应该能够在一个组件的上下文之外维护完全一次性的语义。

为了提供端到端的Exactly-once语义——也就是说,除了Flink应用程序的状态之外,这些语义也适用于Flink写入的外部系统——这些外部系统必须提供一种提交或回滚写入的方法与 Flink 的检查点相协调。

在分布式系统中协调提交和回滚的一种常用方法是两阶段提交协议。在下一节中,我们将深入幕后讨论 Flink 如何TwoPhaseCommitSinkFunction 利用两阶段提交协议来提供端到端的完全一次性语义。

使用 Apache Flink 的端到端 Exactly Once 应用程序

我们将介绍两阶段提交协议,以及在一个简单的读取和写入 Kafka 的Flink应用实例中如何启用端到端的一次性语义Kafka 是一种与 Flink 一起使用的流行消息系统,Kafka 最近在 0.11 版本中增加了对事务的支持。这意味着 Flink 现在拥有必要的机制,可以在从 Kafka 接收数据和向 Kafka 写入数据时在应用程序中提供端到端的完全一次性语义。

Flink 对端到端的exactly-once 语义的支持不仅限于Kafka,您可以将它与任何提供必要协调机制的源/接收器一起使用。例如,来自 Dell/EMC 的开源流式存储系统-Pravega同样也使用TwoPhaseCommitSinkFunction支持端到端的额一次性语义.

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bf2cc897fb3741ceaf26973afe0d67c7~tplv-k3u1fbpfcp-zoom-1.image

在我们今天将讨论的示例 Flink 应用程序中,我们有:

  • 从 Kafka 读取的数据源(在 Flink 中,一个KafkaConsumer
  • 窗口聚合
  • 将数据写回 Kafka 的数据接收器(在 Flink 中,一个KafkaProducer
  1. 数据接收器要提供exactly-once保证,它必须将事务范围内的所有数据写入Kafka。提交捆绑了两个检查点之间的所有写入。这可确保在发生故障时回滚写入

  2. 但是,在具有多个并发运行的接收器任务的分布式系统中,简单的提交或回滚是不够的,因为所有组件必须一起“同意”提交或回滚以确保一致的结果Flink 使用两阶段提交协议及其预提交阶段来应对这一挑战。

  3. 检查点的开始代表我们两阶段提交协议的“预提交”阶段当一个检查点开始时,Flink JobManager 会向数据流中注入一个检查点屏障(它将数据流中的记录分为进入当前检查点的集合与进入下一个检查点的集合)。

  4. 屏障从操作员传递到操作员。对于每个算子,它都会触发算子的状态后端来拍摄其状态的快照

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b889e3ea40f24890af979a2296270e71~tplv-k3u1fbpfcp-zoom-1.image

  5. 数据源存储其 Kafka 偏移量并在完成此操作后,将检查点屏障传递给下一个操作员

    • 如果操作员仅具有内部状态,则此方法有效内部状态是由 Flink 的状态后端存储和管理的所有内容 - 例如,第二个运算符中的窗口和。当一个进程只有内部状态时,除了在检查点之前更新状态后端中的数据之外,在预提交期间不需要执行任何其他操作。Flink 负责在检查点成功的情况下正确提交这些写入,或者在失败的情况下中止它们。

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efd2d9e6351544e99b2905166436ea62~tplv-k3u1fbpfcp-zoom-1.image

    • 但是,当进程具有外部状态时,该状态的处理方式必须稍有不同外部状态通常以写入外部系统(如 Kafka)的形式出现。在这种情况下,为了提供完全一次的保证,外部系统必须为与两阶段提交协议集成的事务提供支持。
  6. 我们知道我们示例中的数据接收器具有这样的外部状态,因为它正在将数据写入 Kafka。在这种情况下,在预提交阶段,数据接收器除了将其状态写入状态后端外,还必须预先提交其外部事务。

  7. 当检查点屏障通过所有操作符并且触发的快照回调完成时,预提交阶段结束。此时检查点成功完成并包含整个应用程序的状态,包括预先提交的外部状态。如果发生故障,我们将从该检查点重新初始化应用程序。

  8. 下一步是通知所有操作员检查点已成功这是两阶段提交协议的提交阶段,JobManager 为应用程序中的每个操作员发出检查点完成的回调。数据源和窗口算子没有外部状态,因此在提交阶段,这些算子不必采取任何行动。但是,数据接收器确实具有外部状态,并使用外部写入提交事务。

    https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c95301a0b1454c12be7bf1b4c5d7a467~tplv-k3u1fbpfcp-zoom-1.image

  9. 所以让我们把所有这些不同的部分放在一起:

    • 一旦所有操作员完成他们的预提交,他们就会发出一个提交。

    • 如果至少一个预提交失败,所有其他的都将被中止,我们回滚到上一个成功完成的检查点。

    • 在成功的预提交之后,必须

      保证提交最终成功——我们的操作员和我们的外部系统都需要做出这种保证。如果一次 commit 失败(例如,由于间歇性网络问题),整个 Flink 应用程序失败,根据用户的重启策略重启,并有另一次 commit 尝试。这个过程很关键,因为如果提交最终没有成功,就会发生数据丢失。

因此,我们可以确定所有操作员都同意检查点的最终结果:所有操作员都同意数据要么被提交,要么提交被中止并回滚。

在 Flink 中实现两阶段提交算子

将两阶段提交协议放在一起所需的所有逻辑可能有点复杂,这就是为什么 Flink 将两阶段提交协议的通用逻辑提取到抽象TwoPhaseCommitSinkFunction类中。

让我们讨论如何在一个简单的基于文件的示例上扩展TwoPhaseCommitSinkFunction。我们只需要实现四个方法,并展示它们对一次性文件接收器的实现:

  1. beginTransaction -

    为了开始事务,我们在目标文件系统的临时目录中创建一个临时文件。随后,我们可以在处理该文件时将数据写入该文件。

  2. preCommit -

    在预提交时,我们刷新文件,关闭它,并且不再写入它。我们还将为属于下一个检查点的任何后续写入启动一个新事务。

  3. commit -

    在提交时,我们以原子方式将预先提交的文件移动到实际的目标目录。请注意,这会增加输出数据可见性的延迟。

  4. abort -

    在中止时,我们删除临时文件。

众所周知,如果出现任何故障,Flink 会将应用程序的状态恢复到最新的成功检查点。在极少数情况下,一个潜在的问题是在成功的预提交之后但在该事实(提交)通知到达我们的操作员之前发生故障。在这种情况下,Flink 会将我们的 operator 恢复到已经预提交但尚未提交的状态。

我们必须保存足够的关于处于检查点状态的预提交事务的信息,以便能够在重新启动后abort进行commit事务处理。在我们的示例中,这将是临时文件和目标目录的路径。

TwoPhaseCommitSinkFunction考虑到这种情况,它总是在从检查点恢复状态时发出抢先提交。以幂等的方式实现提交是我们的责任。一般来说,这应该不是问题。在我们的例子中,我们可以识别出这样一种情况:临时文件不在临时目录中,而是已经被移动到了目标目录中。

还有一些其他边缘情况也TwoPhaseCommitSinkFunction考虑在内。在 Flink 文档中了解更多信息

打包

如果您已经做到了这一点,感谢您通过详细的帖子与我们在一起。以下是我们介绍的一些关键点:

  • Flink 的 checkpointing 系统是 Flink 支持两阶段提交协议和提供端到端完全一次语义的基础。

  • 这种方法的一个优点是 Flink 不会像其他一些系统那样具体化传输中的数据——不需要像大多数批处理那样将计算的每个阶段都写入磁盘。

  • Flink 的新TwoPhaseCommitSinkFunction

    版本提取了两阶段提交协议的通用逻辑,使得使用 Flink 和支持事务的外部系统构建端到端的 Exactly-once 应用程序成为可能

  • Flink 1.4.0

    开始,Pravega 和 Kafka 0.11 生产者都提供了exactly-once 语义;Kafka 在 Kafka 0.11 中首次引入了事务,这使得 Flink 中的 Kafka 一次性生产者成为可能。

  • Kafka 0.11 producerTwoPhaseCommitSinkFunction

    是在 之上实现的,

    与 at-least-once Kafka producer 相比,它提供的开销非常低。

我们对这项新功能所实现的功能感到非常兴奋,我们期待能够TwoPhaseCommitSinkFunction在未来支持更多的制作人。

这篇文章首次出现在 data Artisans 博客上,由原作者 Piotr Nowojski 和 Mike Winters 贡献给 Apache Flink 和 Flink 博客。