感谢英文原文:https://flink.apache.org/features/2018/03/01/end-to-end-exactly-once-apache-flink.html
Apache Flink中的端到端精确一次处理概述(和Apache Kafka一样)
2018年3月1日Piotr Nowojski(@PiotrNowojski)和Mike Winters(@wints)
本文改编自2010年Flink Forward Berlin的Piotr Nowojski的演讲。你可以在Flink Forward Berlin网站上找到幻灯片和演示文稿。
2017年12月发布的Apache Flink 1.4.0为Flink引入了一个重要的流程处理里程碑:一个名为TwoPhaseCommitSinkFunction
(相关的Jira 在这里)的新功能,它提取了两阶段提交协议的通用逻辑,并且可以构建结束使用Flink和一系列数据源和接收器(包括Apache Kafka版本0.11及更高版本)完成一次应用程序。它提供了一个抽象层,并要求用户只实现少数几个方法来实现端到端的一次性语义。
如果需要了解所有内容,请告诉我们Flink文档中的相关位置,您可以在其中阅读有关如何投入TwoPhaseCommitSinkFunction
使用的信息。
如果你想了解更多信息,我们将在这篇文章中分享对新功能的深入概述以及Flink幕后发生的事情。
在本文的其它部分,我们将:
- 描述Flink检查点在Flink应用程序中保证一次性结果的作用。
- 显示Flink如何通过两阶段提交协议与数据源和数据接收器交互,以提供 end-to-end 的一次性保证。
- 详细介绍如何使用
TwoPhaseCommitSinkFunction
实现一次性文件接收器的简单示例。
Apache Flink 应用程序中的 精确一次语义
当我们说 “exactly-once semantics” 时,我们的意思是每个传入事件只影响最终结果一次。即使机器或软件出现故障,也没有重复数据,也没有未经处理的数据。
Flink长期以来在 Flink应用程序中提供了一次性语义。在过去几年中,已经深入探讨了Flink的检查点,这是Flink提供精确一次语义的能力的核心。Flink文档还提供了该功能的全面概述。
在继续之前,这里是检查点算法的快速摘要,因为理解检查点对于理解这个更广泛的主题是必要的。
Flink中的检查点是一致性的快照:
- 应用程序的当前状态
- 输入流中的位置
Flink以固定的可配置间隔生成检查点,然后将检查点写入持久存储系统,例如S3或HDFS。将检查点数据写入持久存储是异步发生的,这意味着Flink应用程序在检查点过程中继续处理数据。
如果发生机器或软件故障,重新启动后,Flink应用程序将从最近成功完成的检查点恢复处理; Flink恢复应用程序状态,并在再次开始处理之前从检查点回滚到输入流中的正确位置。这意味着Flink计算结果,就好像从未发生过失败一样。
在Flink 1.4.0之前,一次性语义仅限于Flink应用程序的范围,并且没有扩展到Flink在处理后发送数据的大多数外部系统。
但是Flink应用程序与各种数据接收器一起运行,并且开发人员应该能够在一个组件的上下文之外保持一次性语义。
提供 end-to-end 的一次性语义 - 即除了Flink应用程序的状态之外,还应用于Flink写入的外部系统的语义 - 这些外部系统必须提供提交或回滚写入的方法与Flink的检查点协调。
协调分布式系统中的提交和回滚的一种常用方法是两阶段提交协议。在下一节中,我们将进入幕后讨论Flink如何TwoPhaseCommitSinkFunction
利用两阶段提交协议提供端到端的一次性语义。
使用Apache Flink精确一次端到端应用程序
我们将介绍两阶段提交协议以及它如何在一个读取和写入Kafka的示例Flink应用程序中实现端到端的一次性语义。Kafka是一个与Flink一起使用的流行消息传递系统,Kafka最近通过其0.11版本添加了对事务的支持。这意味着Flink现在拥有必要的机制,可以在从Kafka接收数据和向Kafka写入数据时在应用程序中提供端到端的一次性语义。
Flink对端到端一次性语义的支持不仅限于Kafka,您可以将它与任何提供必要协调机制的源/接收器一起使用。例如,来自Dell / EMC的开源流媒体存储系统Pravega也支持Flink通过端口进行端到端的一次性语义 TwoPhaseCommitSinkFunction
。
现在我们要讨论的示例Flink应用程序有:
- 从Kafka读取的数据源(在Flink,KafkaConsumer)
- 窗口聚合
- 将数据写回Kafka的数据接收器(在Flink,KafkaProducer中)
要使数据接收器提供一次性保证,它必须在事务范围内将所有数据写入Kafka。提交捆绑了两个检查点之间的所有写入。
这可确保在发生故障时回滚写入。
但是,在具有多个并发运行的接收器任务的分布式系统中,简单的提交或回滚是不够的,因为所有组件必须在提交或回滚时“一致”以确保一致的结果。Flink使用两阶段提交协议及其预提交阶段来解决这一挑战。
检查点的启动表示我们的两阶段提交协议的“预提交”阶段。当检查点启动时,Flink JobManager会将检查点屏障(将数据流中的记录分隔为进入当前检查点的集合与进入下一个检查点的集合)注入数据流。
barrier 从 operator 传递给 operator 。对于每个operator ,它会触发 operator 的状态后端以获取其状态的快照。
数据源存储其Kafka偏移量,在完成此操作后,它将检查点屏障传递给下一个 operator 。
如果 operator 仅具有内部状态,则此方法有效。内部状态是由Flink的状态后端存储和管理的所有内容 - 例如,第二个 operator 中的窗口总和。当进程只有内部状态时,除了在检查点之前更新状态后端中的数据之外,不需要在预提交期间执行任何其他操作。Flink负责在检查点成功的情况下正确提交这些写入,或者在发生故障时中止它们。
但是,当进程具有外部状态时,必须稍微处理此状态。外部状态通常以写入外部系统(如Kafka)的形式出现。在这种情况下,为了提供一次性保证,外部系统必须为与两阶段提交协议集成的事务提供支持。
我们知道我们示例中的数据接收器具有这样的外部状态,因为它正在向Kafka写入数据。在这种情况下,在预提交阶段,除了将其状态写入状态后端之外,数据接收器还必须预先提交其外部事务。
当检查点 barrier 通过所有 operator 并且触发的快照回调完成时,预提交阶段结束。此时检查点已成功完成,并且包含整个应用程序的状态,包括预先提交的外部状态。如果发生故障,我们将从此检查点重新初始化应用程序。
下一步是通知所有 operator 检查点已成功。这是两阶段提交协议的提交阶段,JobManager为应用程序中的每个 operator 发出检查点完成的回调。数据源和窗口 operator 没有外部状态,因此在提交阶段,这些 operator 不必执行任何操作。但是,数据接收器确实具有外部状态,并使用外部写入提交事务。
所以让我们把所有这些不同的部分组合在一起:
- 一旦所有 operator 完成预提交,他们就会发出提交。
- 如果至少一个预提交失败,则所有其他提交都将中止,然后我们回滚到上一个成功完成的检查点。
- 在成功预先提交之后,必须保证提交最终成功 - 我们的 operators 和我们的外部系统都需要做出这种保证。如果提交失败(例如,由于间歇性网络问题),整个Flink应用程序将失败,根据用户的重新启动策略重新启动,并且还有另一次提交尝试。此过程至关重要,因为如果提交最终未成功,则会发生数据丢失。
因此,我们可以确定所有 operators 都同意检查点的最终结果:所有 operators 都同意数据已提交或提交被中止并回滚。
在Flink中实现两阶段提交 operators
将两阶段提交协议放在一起所需的所有逻辑可能有点复杂,这就是为什么Flink将两阶段提交协议的通用逻辑提取到抽象TwoPhaseCommitSinkFunction
类中的原因.
我们将讨论如何扩展 TwoPhaseCommitSinkFunction
基于文件的简单示例。我们只需要实现四种方法,并为完全一次的文件接收器提供它们的实现:
-
beginTransaction -
为了开始事务,我们在目标文件系统的临时目录中创建一个临时文件。随后,我们可以在处理数据时将数据写入此文件。 -
preCommit -
在预提交时,我们刷新文件,关闭它,再也不要写入它。我们还将为属于下一个检查点的任何后续写入启动新事务。 -
commit -
在提交时,我们将预先提交的文件原子地移动到实际的目标目录。请注意,这会增加输出数据可见性的延迟。 -
abort -
在中止时,我们删除临时文件。
我们知道,如果发生任何故障,Flink会将应用程序的状态恢复到最新的成功检查点。一个潜在的问题是在极少数情况下,在成功预先提交之后但在通知该事实(提交)到达 operators 之前发生故障。在这种情况下,Flink将 operators 恢复到已经预先提交但尚未提交的状态。
我们必须在检查点状态下保存有关预提交事务的足够信息,以便能够重启abort
或commit
重启后的事务。在我们的示例中,这将是临时文件和目标目录的路径。
在TwoPhaseCommitSinkFunction
采用此方案考虑在内,它总是发出从检查点恢复状态时先发制人的承诺。我们有责任以幂等的方式实现提交。一般来说,这应该不是问题。在我们的示例中,我们可以识别出这样的情况:临时文件不在临时目录中,但已经移动到目标目录。
还有一些其他边缘情况TwoPhaseCommitSinkFunction
也会考虑在内。在Flink文档中了解更多信息。
Wrapping Up
如果已经做到这一点,感谢通过详细的帖子与我们在一起。以下是我们涉及的一些要点:
- Flink的检查点系统作为Flink的基础,支持两阶段提交协议并提供端到端的一次性语义。
- 这种方法的一个优点是Flink不像其他一些系统那样实现传输中的数据 - 不需要像大多数批处理那样将计算的每个阶段写入磁盘。
- Flink的新功能
TwoPhaseCommitSinkFunction
提取了两阶段提交协议的通用逻辑,并使用Flink和支持事务的外部系统构建端到端的一次性应用程序成为可能 - 从Flink 1.4.0开始,Pravega和Kafka 0.11生成器都提供了一次性语义; Kafka首次在Kafka 0.11中引入了事务,这使得Kafka在Flink中成为可能的 producer 。
-
kafka 0.11 producer 是在顶部实现
TwoPhaseCommitSinkFunction
,它比一次在-至少 kafka producer operator 提供了非常低的开销。
我们对这个新功能的实现感到非常兴奋,我们期待能够TwoPhaseCommitSinkFunction
在未来为其他 producer 提供支持。
这篇文章首次出现在Artisans数据博客上,并由原作者Piotr Nowojski和Mike Winters贡献给Apache Flink和Flink博客。
参考
https://www.ververica.com/blog/end-to-end-exactly-once-processing-apache-flink-apache-kafka