Flink中的状态一致性

1.概念

一致性其实就是结果的正确性。对于分布式系统而言,从不同节点读取时总能得到相同的值;而对于事务而言,是要求提交更新操作后,能够读取到新的数据。

  • 有状态的流处理,内部每个算子任务都可以有自己的状态。
  • 对于流处理内部来说,所谓状态一致性,其实就是所说的计算结果要保证准确。
  • 一条数据不应该丢失,也不应该重复计算。
  • 在遇到故障时可以状态恢复,恢复以后重新计算,计算结果也是完全正确的。

2.一致性级别

  • AT-MOST-ONCE(最多一次)
  • 当任务发生故障时,直接重启,别的什么都不干;既不恢复丢失的状态,也不重放丢失的数据。每条数据在正常情况下会被处理一次,遇到故障时就会丢掉。即为每条消息就只消费一次。
  • AT-LEAST-ONCE(至少一次)
    • 所有的数据都不会丢,肯定被处理了;不过不能保证只处理一次,有些数据会被重复处理。在某些场景下,重复处理数据是不会影响结果的正确性。比如:统计某电商网站的UV。
  • EXACTLY-ONCE(精确一次
    • 所有数据不仅不会丢失,而且只会被处理一次,不会重复处理。Flink中使用的是一种轻量级快照机制---检查点(checkpoint)来保证exactly-once语义。

3.端到端精确一次(end-to-end exactly-once)

对于Flink内部来说,检查点机制可以保证故障恢复之后数据不丢(在能够重复放的情况下),并且只处理一次,所以已经可以做到exactly-once的一致性语义了。故端到端一致性的关键点,就在于输入的数据源端和输出的外部存储端。

3.1 输入端保证

输入端主要指的就是Flink读取的外部数据源。想要在故障恢复后不丢数据,外部数据源就必须拥有重放数据的能力。常见做法就是对数据进行持久化保存,并且可以重设数据的读取位置。一个最经典的应用就是Kafka。在Flink的Source任务中将数据读取的偏移量保存为状态,这样就可以在故障恢复时从检查点中读取出来,对数据源重置偏移量,重新获取数据。

3.2 输出端保证

输出端主要指的就是Flink处理完数据,通过sink任务输出到外部系统。对于输出端,需要保证在故障恢复时,数据不会重复写入外部系统。对于输出端,有两种 具体的实现方式:幂(Idempotent)写入和事务性(Transactional)写入。

3.2.1 幂等(idempotent)写入

所谓"幂等"操作,就是说一个操作可以重复执行很多次,但只导致一次结果更改。这种方式主要限制在于外部存储系统必须支持这样的幂等写入。

对于幂等写入,在遇到故障恢复时,可能会出现短暂的不一致。因为保存点完成之后到发生故障之间的数据,其实已经写入了一遍,回滚的时候并不能消除它们。不过当数据的重放逐渐超过发生故障的点的时候,最终的结果还是一致的。

3.2.2 事务(transaction)写入

输出端最大的问题,就是写入到外部系统的数据难以撤回。而利用事务就可以实现对已写入数据的撤回。在Flink流处理的结果写入外部系统时,如果能够构建一个事务,让写入操作可以随着检查点来提交和回滚,这样就解决了重复写入的问题。 事务与检查点是绑定在一起的,当Sink任务遇到barrier时,开始保存状态的同时就开启一个事务,后续所有数据的写入都在该事务中;待到当前检查点保存完毕时,将事务提交,所有写入的数据就真正可用了。如果中间过程出现故障,状态会回退到上一个检查点,而当前事务没有正常关闭(因为当前检查点没有保存完),所以也会回滚,写入到外部的数据就被撤销了。对于事务写入有两种实现方式:预写日志(WAL)和两阶段提交(2PC)。

预写日志(write-ahead-log,WAL)

实现步骤:

  1. 先把结果数据作为日志(log)状态保存起来;
  2. 进行检查点保存时,同时将这些结果数据做持久化存储;
  3. 在收到检查点完成的通知时,将所有结果一次性写入外部系统。

预写日志这种批写入的方式,有可能会写入失败;所以在写入动作之后,必须等待发送成功的返回确认消息。在成功写入所有数据后,内部再次确认相应的检查点,此时代表着检查点的真正完成。同时也需要确认信息也进行持久化保存,在故障恢复时,只有存在对应的确认信息,才能保证这批数据已经写入,可以恢复到对应的检查点位置。

两阶段提交(two-phase-commit,2PC)

两阶段提交,顾名思义,先做"预提交",等检查点完成之后再正式提交。这种提交方式是真正基于事务的,它需要外部系统提供事务支持。

实现步骤:

  1. 当第一条数据到来时,或者收到检查点的分界线时,Sink任务都会启动一个事务。
  2. 接下来接收到的所有数据,都通过这个事务写入外部系统;这时由于事务没有提交,数据虽然写入了外部系统,但是不可用,是"预提交"的状态。
  3. 当Sink任务收到JobManager发来检查点完成的通知时,正式提交事务,写入的结果就真正可用了。

当中间发生故障时,当前未提交的事务就会回滚,于是所有写入外部系统的数据也就实现了撤回。两阶段提交的方式充分利用了Flink现有的检查点机制:分界线的到来,就标志着开始一个新事务;而收到来自JobManager的checkpoint成功的消息,就是提交事务的指令。

两阶段提交对外部系统有要求:

  • 外部系统必须提供事务支持,或者Sink任务必须能够模拟外部系统上的事务。
  • 在检查点的间隔期间里,必须能够开启一个事务并接受数据写入。
  • 在收到checkpoint完成的通知之前,事务必须是"等待提交"的状态。在故障恢复的情况下,这可能需要-些时间。如果这个时候sink系统关闭事务(例如超时了) ,那么未提交的数据就会丢失
  • sink任务必须能够在进程失败后恢复事务 。
  • 提交事务必须是幂等操作。

3.3 总结

对于端到端的精准一次而言:

系统 要求
source端 可重设数据的读取位置
Flink内部 依赖checkpoint
sink端 从故障恢复时,数据不会重复写入外部系统

4.Flink和Kafka连接时的精确一次保证

4.1 概述

Flink与Kafka连接时属于端到端精准一次,可以从三个组件的角度进行分析:

  • Flink内部
    • 通过checkpoint机制, 把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性。
  • 输入端
    • 输入数据源端的Kafka可以对数据进行持久化保存,并可以重置偏移量(offset)。在Source任务(FlinkKafkaConsumer)中将当前读取的偏移量保存为算子状态,写入到检查点中;当发生故障时,从检查点中读取恢复状态,并由连接器FlinkKafkaConsumer向Kafka重新提交偏移量,就可以重新消费数据、保证结果的一致性了。
  • 输出端
    • 输出端保证exactly-once的最佳实现,当然就是两阶段提交(2PC)。Flink官方实现的Kafka连接器中,提供了写入到Kafka的FlinkKafkaProducer,它就实现TwoPhaseCommitSinkFunction接口。
    • 两阶段提交:处理完毕得到结果,写入Kafka时是基于事务的"预提交";等到检查点保存完毕,才会提交事务进行"正式提交"。

4.2 2PC具体步骤

情景: 由Flink从Kafka读取数据、并将处理结果写入Kafka,如图所示

Flink与Kafka连接的两阶段提交,离不开检查点的配合,这个过程需要JobManager协调各个TaskManager进行状态快照,而检查点具体存储位置则是由状态后端(State Backend)来配置管理的。一般情况,我们会将检查点存储到分布式文件系统上。

(1) 启动检查点保存

检查点保存的启动,标志着进入了两阶段提交协议的"预提交"阶段。当然,现在还没有具体提交的数据。

JobManager通知各个TaskManager启动检查点保存,Source任务会将检查点分界线(barrier)注入数据流。这个barrier可以将数据流中的数据,分为进入当前检查点的集合和进入下一个检查点的集合。

(2) 算子任务对状态做快照

分界线(barrier)会在算子间传递下去。每个算子收到barrier时,会将当前的状态做个快照,保存到状态后端。

Source任务将barrier插入数据流后,也会将当前读取数据的偏移量作为状态写入检查点,存入状态后端;然后把barrier向下游传递,自己就可以继续读取数据了。接下来barrier传递到了内部的Window算子,它同样会对自己的状态进行快照保存,写入远程的持久化存储。

(3) Sink任务开启事务,进行预提交

分界线(barrier)终于传到了Sink任务,这时Sink任务会开启一个事务。后续到来的所有数据,Sink任务都会通过该事务来写入Kafka。这里barrier是检查点的分界线,也是事务的分界线。由于之前的检查点可能尚未完成,因此上一个事务也可能尚未提交;此时barrier的到来开启了新的事务,上一个事务尽管可能没有被提交,但也不再接收新的数据了。

对于Kafka而言,提交的数据会被标记为"未确认"(uncommitted)。这个过程就是所谓的"预提交"(pre-commit)。

(4) 检查点保存完成,提交事务

当所有算子的快照都完成,也就是这次的检查点保存最终完成时,JobManager会向所有任务发确认通知,告诉TaskManager当前检查点已成功保存。

当Sink任务收到确认通知后,就会正式提交之前的事务,把之前"未确认"的数据标为"已确认",接下来就可以正常消费了。

4.3 需要的配置

  • 必须启用checkpoint。
  • 在FlinkKafkaProducer的构造函数中传入参数Semantic.EXACTLY_ONCE。
  • 配置Kafka(写入的外部系统)读取数据的消费者的隔离级别:isolation.level=read_commited。
  • 事务超时配置:Flink的Kafka连接器中配置的事务超时时间transaction.timeout.ms默认是1小时,而Kafka集群配置的事务最大超时时间transaction.max.timeout.ms默认是15分钟。实际配置应该是:transaction.timeout.ms≤transaction.max.timeout.ms。
相关推荐
Desmend__13 分钟前
正则表达式那些事儿
数据库·mysql·正则表达式
袁庭新29 分钟前
LuaRocks如何安装数据库驱动?
java·数据库·redis·lua·luarocks·袁庭新
hummhumm38 分钟前
第 10 章 - Go语言字符串操作
java·后端·python·sql·算法·golang·database
Narutolxy41 分钟前
从 MySQL 5.7 到 8.0:理解 GROUP BY 的新规则与实战优化20241112
数据库
chusheng184042 分钟前
Python 正则表达式进阶用法:分组与引用详解
数据库·python·正则表达式
喵叔哟2 小时前
重构代码之移动字段
java·数据库·重构
念白4432 小时前
智能病历xml提取
数据库·sql·oracle
qingy_20462 小时前
【JavaWeb】JavaWeb入门之XML详解
数据库·oracle
大数据面试宝典2 小时前
用AI来写SQL:让ChatGPT成为你的数据库助手
数据库·人工智能·chatgpt
努力的小雨2 小时前
快速上手 KSQL:轻松与数据库交互的利器
数据库·经验分享