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

117 阅读3分钟

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

Flink 分布式快照的核心元素

  • Barrier(数据栅栏) :可以把 Barrier 简单地理解成一个标记,该标记是严格有序的,并且随着数据流往下流动。每个 Barrier 都带有自己的 ID,Barrier 极其轻量,并不会干扰正常的数据处理。
  • 异步:每次在把快照存储到我们的状态后端时,如果是同步进行就会阻塞正常任务,从而引入延迟。因此Flink 在做快照存储时,采用异步方式
  • 增量:由于 checkpoint 是一个全局状态,用户保存的状态可能非常大,多数达 G 或者 T 级别,checkpoint 的创建会非常慢,而且执行时占用的资源也比较多,因此 Flink 提出了增量快照的概念。也就是说,每次进行的全量 checkpoint,是基于上次进行更新的。

End-to-End Exactly Once

  • Source端: 可重设数据的读取位置。即source端会将数据的读取位置作为状态进行存储,当发生故障恢复时,依靠记录的读取位置,重新从数据源读取数据进行计算。
  • Flink内部: checkpoint机制
  • Sink端: 要求从故障恢复时,数据不会重复地写入外部系统。具体实现方式有两种(幂等写入、事务写入)。
  • 幂等写入: 即多次写入的结果是一样的。这种一般针对NoSQL数据库,如 redis、HBase等,只要key-value一样,多次写入的结果是一样的。(注意这里只是结果一致,但不保证过程也是一致的)
  • 事务写入: 即事务中的操作要么全部成功,要么全部失败。实现思想是构建的事务对应 Flink 中的 checkpoint,只有等到 checkpoint 真正完成的时候,才把对应的结果写入到 sink 系统中(或者是执行真正的提交)。

事务写入

实现思想:构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中

实现方式: 两阶段提交(2PC)

  1. 对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里
  2. 然后将这些数据写入外部 sink 系统,但不提交它们,这时只是“预提交”
  3. 当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入
  4. 这种方式真正实现了 exactly-once,它需要一个提供事务支持的外部 sink 系统。

Flink 中两阶段提交的实现方法被封装到了 TwoPhaseCommitSinkFunction 这个抽象类中,我们只需要实现其中的beginTransaction、preCommit、commit、abort 四个方法就可以实现“精确一次”的处理语义。

  • beginTransaction,在开启事务之前,会在目标文件系统的临时目录中创建一个临时文件,在处理数据时将数据写入这个文件里面。
  • preCommit,在预提交阶段,将内存中缓存的数据刷写(flush)到文件,然后关闭文件。还将为属于下一个检查点的任何后续写入启动新事物
  • commit,在提交阶段,将预提交写入的临时文件移动到真正的目标目录中,这代表着最终的数据会有一些延迟;
  • abort,在中止阶段,我们删除临时文件。