什么是数据的一致性
这所说的数据一致性指,在一个 Flink 任务遇到不可坑因素整体死掉或者部分死掉,已经外部存储介质死掉后,将死掉的部分重写启动后,计算结果和出现故障之前一致,不会产生任何的影响。
如果要实现这种效果,无论发生什么,所有算子做到如下要求:
- source 算子中,一条记录只向下游发送一次。
- 在聚合算子、合集算子、转换算子中一条数据只处理一次。
- 在 sink 算子中,一条数据只向外部存储介质中写入一次。
只要做到这些,我们就可以说,我们整个任务的数据是一致的。
今天就把 Flink 自己封装的 Kafka 的生产这和消费者拿出来看看,source 和 sink 是如何做数据一致性的。
刻舟求剑
大家都知道刻舟求剑的寓言故事。这个故事告诉做记号得重要性。同理,当我们每处理一条记录,我们就记录下来。
例如,类似如下格式的日志。
2021年6月16日10点32分 primary_key1 of a record
每当算子处理完一条消息,就记录下如上所示的日志。加入这个算子的实例失败了,当我们重新启动的时候吧,我们可以查找此日志,查到这个日志,我们就可以找到我们在失败之前处理到哪里了,然后从哪里开发处理数据。
这是一个非常简单的情况。我们知道 Flink 是一个分布式系统,任务上的多个算子会被安排到不同的服务器上跑。一个分布式系统要想做“记号”那可不是简单。我们就来看看 Flink 是如何做的。下面的图片展示了 Flink 做“记号”的过程。这个过程就叫“二次提交”。
二次提交的流程:
- JM 发送 checkpoint trigger
- 执行 snapshotState 方法,这里会执行 producer 的 flush 方法。这个方法会将 producer 缓存里面的数据发送完。
- TM 将 barrier 的快照保存到 backend 里面,包括 kafka transaction id 和 checkpoint id 的对应关系。
- 发送 checkpoint ACK 给 JM。
- 当所有的算子实例执行完对应的 checkpoint 以后 JM 调用算子的 notifyCheckpointComplete() 方法,进行后续对状态的处理。如果有一个 TM 没有发送对应的 ACK ,那么,这次 checkpoint 都不能算执行完成,如果超过了时间,JM 会调用各个算子的 notifyCheckpointAborted 方法,取消这次 chekcpoint。
Flink 在类 TwoPhaseCommitFunction 中实现了二次提交的逻辑, TwoPhaseCommitFunction 是一个抽象类,所以只说它还是不给你完全的解释 FlinkKafkaProducer 的数据一致性。
第一阶段-准备阶段:
当一个 barrier 过来后,一个 producer 实例调用 snapshotSate() ,将transactionId、checkpointId 保存在本地的 pendingCommitTransaction 里面,并且开启 kafka 的一个事务,最后将 transactionID 放到 backend。这一步的目的是将单个算子里面记录一下,写到那个 transacton id 了,
第二阶段-提交:
当所有的 producer 实例都完成了在同一个 barrier 里面的 snaptshot ,JobMannager 会收到各个 producer 实例的 ack ,然后JobMannager 回调 producer.notifyCompleteCheckpoint() , 这个函数的处理逻辑是将 pendingCommitTransactiond 中等待提交的事务ID删除掉,并将在本次 barrier 中的 transactionId 提交给 Kafka Coordinator。这样就完成了一次 checkpoint 和 kafka 一次事务。这就保证了当一个完整的 checkpoint 执行结束后,Flink kafka Producer 才会提交 tranaction id ,如果发生什么异常(例如宕机),checkpoint 无法完成,可能会重新启动任务,只要 transaction 没有提交,即使我们使用同一个 transaction 提交了重复的数据,也不会造成 kafka 里面数据重复,因为 Kafka coodinator 会悄悄的为我们去重的。
优点:模型简单,容易实现。
缺点:严重依赖 JobManager ,存在单节点故障隐患。