这篇文章改编自2017年Flink Forward柏林的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的检查点,这是Flink提供精确一次语义的能力的核心。 Flink文档还提供了该功能的全面概述。
在继续之前,这里是检查点算法的快速摘要,因为理解检查点对于理解这个更广泛的主题是必要的。
Flink中的检查点是一致性快照:
Flink以固定的可配置间隔生成检查点,然后将检查点写入持久存储系统,例如S3或HDFS。将检查点数据写入持久存储是异步发生的,这意味着Flink应用程序在检查点过程中继续处理数据。
如果发生机器或软件故障,重新启动后,Flink应用程序将从最近成功完成的检查点恢复处理; Flink恢复应用程序状态,并在再次开始处理之前从检查点回滚到输入流中的正确位置。这意味着Flink计算结果,就好像从未发生过失败一样。
在Flink 1.4.0之前,一次性语义仅限于Flink应用程序的范围,并且没有扩展到Flink在处理后发送数据的大多数外部系统。
但是Flink应用程序与各种数据接收器一起运行,并且开发人员应该能够在一个组件的上下文之外保持一次性语义。
提供端到端的一次性语义 - 即除了Flink应用程序的状态之外,还应用于Flink写入的外部系统的语义 - 这些外部系统必须提供提交或回滚写入的方法与Flink的检查点协调。
协调分布式系统中的提交和回滚的一种常用方法是两阶段提交协议。在下一节中,我们将进入幕后讨论Flink的TwoPhaseCommitSinkFunction如何利用两阶段提交协议提供端到端的一次性语义。
我们将介绍两阶段提交协议以及它如何在一个读取和写入Kafka的示例Flink应用程序中实现端到端的一次性语义。 Kafka是一个与Flink一起使用的流行消息传递系统,Kafka最近通过其0.11版本添加了对事务的支持。 这意味着Flink现在拥有必要的机制,可以在从Kafka接收数据和向Kafka写入数据时在应用程序中提供端到端的一次性语义。
Flink对端到端一次性语义的支持不仅限于Kafka,您可以将它与任何提供必要协调机制的源/接收器一起使用。 例如,来自Dell / EMC的开源流媒体存储系统Pravega也通过TwoPhaseCommitSinkFunction与Flink支持端到端的一次性语义。
在我们今天要讨论的示例Flink应用程序中,我们有:
要使数据接收器提供一次性保证,它必须在事务范围内将所有数据写入Kafka。提交捆绑了两个检查点之间的所有写入。
这可确保在发生故障时回滚写入。
但是,在具有多个并发运行的接收器任务的分布式系统中,简单的提交或回滚是不够的,因为所有组件必须在提交或回滚时“一致”以确保一致的结果。 Flink使用两阶段提交协议及其预提交阶段来解决这一挑战。
检查点的启动表示我们的两阶段提交协议的“预提交”阶段。当检查点启动时,Flink JobManager会将检查点屏障(将数据流中的记录分为进入当前检查点的集合与进入下一个检查点的集合)注入数据流。
屏障从算子传递给算子。对于每个算子,它会触发算子的状态后端以获取其状态的快照。
数据源存储其Kafka偏移量,在完成此操作后,它将检查点屏障传递给下一个算子。
如果算子仅具有内部状态,则此方法有效。 内部状态是由Flink的状态后端存储和管理的所有内容 - 例如,第二个运算符中的窗口总和。 当进程只有内部状态时,除了在检查点之前更新状态后端中的数据之外,不需要在预提交期间执行任何其他操作。 Flink负责在检查点成功的情况下正确提交这些写入,或者在发生故障时中止它们。
但是,当进程具有外部状态时,必须稍微处理此状态。 外部状态通常以写入外部系统(如Kafka)的形式出现。 在这种情况下,为了提供一次性保证,外部系统必须为与两阶段提交协议集成的事务提供支持。
我们知道我们示例中的数据接收器具有这样的外部状态,因为它正在向Kafka写入数据。 在这种情况下,在预提交阶段,除了将其状态写入状态后端之外,数据接收器还必须预先提交其外部事务。
当检查点屏障通过所有算子并且触发的快照回调完成时,预提交阶段结束。 此时检查点已成功完成,并且包含整个应用程序的状态,包括预先提交的外部状态。 如果发生故障,我们将从此检查点重新初始化应用程序。
下一步是通知所有算子检查点已成功。 这是两阶段提交协议的提交阶段,JobManager为应用程序中的每个算子发出检查点完成的回调。 数据源和窗口运算符没有外部状态,因此在提交阶段,这些运算符不必执行任何操作。 但是,数据接收器确实具有外部状态,并使用外部写入提交事务。
所以让我们把所有这些不同的部分组合在一起:
因此,我们可以确定所有算子都同意检查点的最终结果:所有算子都同意数据已提交或提交被中止并回滚。
将两阶段提交协议放在一起所需的所有逻辑可能有点复杂,这就是为什么Flink将两阶段提交协议的通用逻辑提取到抽象的TwoPhaseCommitSinkFunction类中。
让我们讨论如何在一个简单的基于文件的示例上扩展TwoPhaseCommitSinkFunction。我们只需要实现四种方法,并为完全一次的文件接收器提供它们的实现:
我们知道,如果发生任何故障,Flink会将应用程序的状态恢复到最新的成功检查点。一个潜在的问题是在极少数情况下,在成功预先提交之后但在通知该事实(提交)到达我们的算子之前发生故障。在这种情况下,Flink将我们的算子恢复到已经预先提交但尚未提交的状态。
我们必须在检查点状态下保存有关预提交事务的足够信息,以便能够在重新启动后中止或提交事务。在我们的示例中,这将是临时文件和目标目录的路径。
TwoPhaseCommitSinkFunction将此方案考虑在内,并且在从检查点恢复状态时始终发出抢先提交。我们有责任以幂等的方式实现提交。一般来说,这应该不是问题。在我们的示例中,我们可以识别出这样的情况:临时文件不在临时目录中,但已经移动到目标目录。
还有一些其他边缘情况,TwoPhaseCommitSinkFunction也考虑在内。在Flink文档中了解更多信息。
如果您已经做到这一点,感谢您通过详细的帖子与我们在一起。以下是我们涉及的一些要点:
我们对这个新功能的实现感到非常兴奋,我们期待能够在将来使用TwoPhaseCommitSinkFunction支持其他制作人。
这篇文章首次出现在Artisans数据博客上,并由原作者Piotr Nowojski和Mike Winters贡献给Apache Flink和Flink博客。
https://flink.apache.org/features/2018/03/01/end-to-end-exactly-once-apache-flink.html