Exactly Once 语义在Flink中的实现 | 青训营笔记

107 阅读7分钟

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

一、数据流和动态表

1.1传统SQL和流处理

特征SQL流处理
处理数据的有界性处理的表是有界的流是一个无限元组序列
处理数据的完整性执行查询可以访问完整的数据执行查询无法访问所有的数据
执行时间批处理查询产生固定大小结果后终止查询不断更新结果,永不终止

1.2什么是动态表?

动态表:与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们。数据库表是INSERT、UPDATE 和DELETE DML语句的stream的结果,通常称为changelog stream.

动态表是 Flink 对流数据的 Table API 和 SQL 支持的核心概念。与表示批处理数据的静态表不同,动态表是随时间变化的。动态表可以像静态的批处理表一样进行查询,查询一个动态表会产生持续查询(Continuous Query)。连续查询永远不会终止,并会生成另一个动态表。查询(Query)会不断更新其动态结果表,以反映其动态输入表上的更改。

1.3流式持续查询的过程为:

  1. 流被转换为动态表。为了处理带有关系查询的流,必须先将其转换为表。本质上,我们其实是从一个、只有插入操作的 changelog(更新日志)流,来构建一个表。

  2. 对动态表计算连续查询,生成新的动态表。 持续查询,会在动态表上做计算处理,并作为结果生成新的动态表。与批处理查询不同,连续查询从不终止,并根据输入表上的更新更新其结果表。在任何时间点,连续查询的结果在语义上,等同于在输入表的快照上,以批处理模式执行的同一查询的结果。

  3. 生成的动态表被转换回流。 与常规的数据库表一样,动态表可以通过插入(Insert)、更新(Update)和删除(Delete)更改,进行持续的修改。将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。

1.4 Flink 的 Table API 和 SQL 支持三种方式对动态表的更改进行编码:

  • 仅追加(Append-only)流

仅通过插入(Insert)更改,来修改的动态表,可以直接转换为“仅追加”流。这个流 中发出的数据,就是动态表中新增的每一行。

  • 撤回(Retract)流

Retract 流是包含两类消息的流,添加(Add)消息和撤回(Retract)消息。动态表通过将 INSERT 编码为 add 消息、DELETE 编码为 retract 消息、UPDATE 编码为被更改行(前一行)的 retract 消息和更新后行(新行)的 add 消息,转换为 retract 流。

  • Upsert(更新插入)流

Upsert 流包含两种类型的消息:Upsert 消息和 delete 消息。转换为 upsert 流的动态表,需要有唯一的键(key)。通过将 INSERT 和 UPDATE 更改编码为 upsert 消息,将 DELETE 更改编码为 DELETE 消息, 就可以将具有唯一键(Unique Key)的动态表转换为流。

需要注意的是,在代码里将动态表转换为 DataStream时,仅支持 Append 和 Retract 流。而向外部系统输出动态表的 TableSink 接口,则可以有不同的实现,比如之前我们讲到的 ES,就可以有 Upsert 模式。

二、Exactly-Once和Checkpoint

2.1 不同数据处理保证的语义

  • At_Most_Once:

    至多一次,表示一条消息不管后续处理成功与否只会被消费处理一次

  • At_Least_Once:

    至少一次,表示一条消息从消费到后续的处理成功,可能会发生多次

  • Exactly_Once:

    精确一次,表示一条消息从其消费到后续的处理成功,只会发生一次

Flink声称实现了Exactly_Once语义,实际上在事件处理过程中,各种异常情况都有可能发生,所以根本不可能保证每条消息真的真会被处理一次。Flink的Exactly_Once真正的含义在于可以保证Flink状态的容灾和只向后端提交一次持久存储(要求后端支持事务,例如Kafka、MySQL)。

2.2 Flink如何实现Exactly_Once语义的?Flink通过以下特性实现Exactly_Once:

  1. Source支持数据重读

  2. Sink支持事务。可以是类似二阶段提交,如kafka,或者Sink支持幂等,可以覆盖之前写入的数据,如redis

  3. 基于Checkpoint保证状态的容灾及一致性

第一点和第三点需要外部系统配合实现,Flink内部主要通过Checkpoint实现Exactly_Once语义。

  • Flink的Checkpoint机制:

Checkpoint是Flink实现容错机制最核心的功能,它会根据用户的配置周期性地对流中各个算子(Operator)的状态生成快照,持久化到外部存储。Flink程序一旦意外崩溃时,重新运行程序时可以有选择地从这些快照进行恢复,从而修正因为故障带来的程序数据异常。Flink写入到外部存储是异步的,意味着Flink在这个阶段可以继续处理数据。

三、端到端Exactly-Once实现

3.1端到端Exactly-Once语义

image.png

  1. Checkpoint 能保证每条数据都对各个有状态的算子更新一次,sink 输出算子仍然可能下发重复的数据:

  2. 严格意义的端到端的 Exatty once语义需要特殊的sink算子实现。

3.2两阶段提交协议

在多个节点参与执行的分布式系统中,为了协调每个节点都能同时执行或者回滚某个事务性的操作, 引入了一个中心节点来统一处理所有节点的执行逻辑,这个中心节点叫做协作者(coordinator) 被中心节点调度的其他业务节点叫做参与者(participant)

image.png

  • 两阶段提交协议(一)- 预提交阶段
  1. 协作者向所有参与者发送一个commit消息:

  2. 每个参与的协作者收到消息后,执行事务,但是不真正提交:

  3. 若事务成功执行完成,发送一个成功的消息 (vote yes) ;执行失败,则发送一个失败的消息 (vote no)

image.png

  • 两阶段提交协议(二)- 提交阶段

若协作者成功接收到所有的参与者vote yes的消息:

  1. 协作者向所有参与者发送个 commit消息;
  2. 每个收到commit消息的参与者释放执行事务所需的资源,并结束这次事务的执行:
  3. 完成步骤2后, 参与者发送个ack消身给协作者:
  4. 协作者收到所有参与者的ack消息后,标识该事务执行完成。

若协作者有收到参与者vote no的消息(或者发生等待超时) :

  1. 协作者向所有参与者发送个 ollback消息;
  2. 每个收到rllback消息的参与者回滚事务的执行操作,并释放事务所占资源;
  3. 完成步骤2后, 参与者发送个ack消息给协作者:
  4. 协作者收到所有参与者的ack消息后,标识该事务成功完成回滚。

image.png

3.3 Flink两阶段提交总结

  1. 事务开启:在sink task向下游写数据之前,均会开启一个事务,后续所有写数据的操作均在这个事务中执行,事务未提交前,事务写入的数据下游不可读;

  2. 预提交阶段: JobManager 开始下发Checkpoint Barrier,当各个处理逻辑接收到barrier后停止处理后续数据,对当前状态制作快照,此时sink也不在当前事务下继续处理数据(处理后续的数据需要新打开下一个事务)。状态制作成功则向JM成功的消息,失败则发送失败的消息;

  3. 提交阶段: 若JM收到所有预提交成功的消息,则向所有处理逻辑(包括sink)发送可以提交此次事务的消息,sink 接收到此消息后,则完成此次事务的提交,此时下游可以读到这次事务写入的数据:若JM有收到预提交失败的消息,则通知所有处理逻辑回滚这次事务的操作,此时sink则丢弃这次事务提交的数据下。