kafka exactly-once
2018年,Apache Kafka以一种特殊的设计和方法实现了强语义的exactly-once和事务性。
这篇文章将讲解kafka中exactly-once和事务操作的原理,具体为
(1)exactly-once在kafka中的定义。
(2)数据生产者“幂等操作”,kafka的事务性以及exactly-once实现原理。
(3)exactly-once的流处理。
1. 什么是恰好一次exactly-once
exactly-once定义为: 不管在处理的时候是否有错误发生,计算的结果(包括所有所改变的状态)都一样。
所以,在计算的时候如果发生了一个错误,系统重新计算,重新计算的结果和没有错误发生所得到的结果是一样的,因为这些计算操作是“恰好一次的”。这有另外一个专业术语:“幂等操作”。
为什么exactly-once那么重要呢?(1)在流处理操作中,很多应用场景必须需要“恰好一次”的支持。比如生活着有一个很重要的使用场景:在转账给朋友的时候,用户只希望一次转账,如果不支持“恰好一次”,那么就无法保障在违背用户本意的情况下重复转账。(2)对于kafka而言,其是流处理平台的核心部件,因为kafka通常作为公司内部的消息系统中间件,是其他系统的消息传输的桥梁。(3)支持exactly-once操作可以解锁更过的应用,比如金融行业应用。
使用Kafka进行流处理通常主要包含三个步骤:根据topic读取数据 - 流操作 - 将结果保存到指定的topic下。Kafka的流处理支持无状态的流操作(stateless)和有状态的流操作(stateful),无状态的意思是流处理的时候只需要针对某一条消息进行处理,结果只受到这条消息的影响,比如在每一条消息后面追加字符“a”;有状态指的是在消息处理的时候需要保存前后多条消息的相关信息,结果受到多条消息的影响,比如count,average操作。所以有状态操作更加强大但是实现起来更加困难,特别是当它也支持“恰好一次”的时候。
如果不支持exactly-once操作,那么可能出现下面的错误:
(1)重复写入。下图所示左边为输入数据,中间为数据处理,右边是结果写入。现在计算出了结果并且成功写入,但是由于某些原因,系统没有正确识别成功写入结果这个信号,所以系统重试了,这样就导致了下面第二张图所示的结果:也就是计算结果重复写入。
(2)计算状态被多次更新。
如下图所示,箭头所指的是一个有状态的操作(前面已经讲到无状态操作和有状态操作),第一次计算的时候更新了该处的状态。那么如果因为某些原因第一次的计算有问题需要重新计算,箭头所指的状态会被再次更新,从而导致最终的计算结果不正确。因为正确的计算是状态只被更新一次。这里所说的状态似乎有一点抽象,举一个例子,在统计操作中,count++,可以表示成一个状态,每次来一个数据,就增加1个量。
(3)重复读入
第一个数据已经顺利读取,处理和结果写入,但是由于数据读取的原因,系统没有正确识别到第一次数据的读取,所以再次读取了相同的数据,再次计算并输出结果。此时如下面第二张图所示。这样同样的输入数据就产生了两个结果写入,而且如果中间的流操作是有状态的,这两个结果很可能是不一样的。
上面所阐述的问题,进一步说明了exactly-once的重要性。kafka提供了自己的exactly-once保证。
2. 要么都做,要么都不做
要么都做,要么都不做。做什么呢?体现在:(1)写出所有的计算结果 (计算结果写入到kafka指定的topic中).(2)所有状态的更新。(3)把输入的消息标记为已消费(这里的输入数据理解为kafka的消费者从broker中pull数据)。对上面这三个,kafka使用另一种具有相同语义的方式表示,分别为:(1)将计算结果写入输出topic中(2)把更新操作写入“更新日志changelog”中(注意,操作的状态能够根据“更新日志”进行回滚,类似于MySQL的更新日志,这个有别于普通的系统操作日志)(3)把消费的消息偏移量写入相应的topic中。这也就是Apache Kafka实现“要么都做,要么都不做”和exactly-once的总体设计思路。
具体地,上面阐述关系到三个操作,分别为:
消息生产者提交数据到broker
broker进行消息处理,
消费者消费数据
对于第一点,需要实现幂等操作以及多分区地原子写入。这里的“写入”指的是消息的producer向broker传入消息。这里不多讲“幂等”操作,可以简单理解为同一个消息,producer一次或者多次重复向broker传输,对broker的影响是一样的。多分区原子写入指的是,producer将多条消息一次向broker中的多个partition传输,原子性体现在要么这些消息都成功传入了,要么都没有传入。
在下面第一张图片中展示了kafka实现消息传输的幂等操作的原理。每一条消息除了消息的key和消息的值,还增加了两个字段,分别是producer的ID和一个全局唯一的序列号。这个序列号由broker生成,类似于流水号。在图片中,闪电表示消息的ack失败,消息重传,kafka根据消息的pid和seq来判断这条消息是否已经传过。因为pid和seq也同消息一样存在kafka的patition中的,所以不需要当心丢失问题。要启动kafka的幂等性,无需修改代码,默认为关闭,需要修改配置文件:enable.idempotence=true 同时要求 ack=all 且 retries>1。
对于第二点,需要实现:将模式“消息读入->消息处理->结果写出”作为事务操作,并且整个操作满足exactly-once。所谓的事务操作,也就是这个操作需要满足原子性,完整性,一致性和持久性。kafka在支持事务性的同时也保证了系统性能,这体现在它简单但是高效的设计和实现上面。下面分析kafka实现事务的原理。在下面第一张图片中,左下角表示事务日志,系统存在一个事务锁,在某一个事务开始之前需获取这个锁。左上角的T1,T2表示两个topic,P1和P2表示两个partition,也就是这个事务往两个不同的topic和两个不同的partition上面存储数据。看图片的右上角,第一行代码,首先告诉系统要开始一个事务,接着发送消息到broker相应topic和partition中,所有都正常且完成之后,提交这个事务。所有的这个过程都另外有相应的log。只有成功完成了这个事务之后,消费者才能消费这个事务所提交的消息。
实现事务的回滚需要借助changelogs的帮助,如下面第一张图片所示。changelogs是存储在相应的topic中。
对于第三点,需要实现,kafka消费者只读取已经标记为“成功提交”的数据,这句话隐含了另外一层意思,消息提交的状态有多种,而成功提交只是其中之一。这里的“提交”指的是producer向broker提交的消息。那么什么才能算是成功提交了呢?消息被partition的leader和其所有的follower成功记录了,才能算是成功提交了。成功提交所带来的好处就是不怕断电不怕机器故障,也就是高容错性。下面图片展示了kafka如何解决消费者重复读的问题。(1)消息的消费,(2)消息的处理,(3)把消息的处理结果发送到某一个topic中和(4)把偏移量的发送某一个topic中,它们被放到一个事务中,当所有这些成功之后,才能算是成功。注意到,消费者的偏移量是使用一个producer发送的,也就是把偏移量当成了一种消息在kafka集群中保存起来。这样的话,只要这个事务完成了,那么偏移量也成功保存了。
所以,对应下面第一张图片,不仅仅有changelogs,还有__consumer_offsets
默认情况下kafka的事务是关闭的,通过配置文件开启,需要
transactional.id=“unique-id”, 要求enable.idempotence=true.
启动exactly-once需要配置:processing.guarantee="exactly-once ", 默认是最少一次。
3. 脏数据
脏数据指的是producer把消息数据提交到了broker中,但是它们没有成功,此时这些数据依然存在broker中。为了避免让消费者消费这些脏数据,kafka设置了消息的隔离等级,可以通过配置文件,指定只有成功提交的数据才能被消费。配置为isolation.level=“read_committed”。默认是read_uncommitted