kafka如何实现精准一次性消费?

4,800 阅读11分钟

前言

在开始这篇文章的时候,我仔细回想了一下上一次写文章的时候应该还是上一次写文章的时候,回想上一次写文章的时候,仿佛还在昨天。那么今天要聊的是什么呢?没错,是大家都非常熟悉的kafka,在消息队列中,或多或少都会考虑到消息丢失,重复消费等等之类的问题。那么接下来就由这篇文章来介绍kafka中所遇到问题的解决方案。

1.jpg

基础架构

为了以防有的同学对kafka的组成已经记不得了,让我们来再看看kafka的基础架构

2.png

主要组件为:

  • Producer:消息生产者,就是向kafka broker发消息的客户端。
  • Consumer:消息消费者,向kafka broker取消息的客户端。
  • Consumer Group(CG):消费者组,由多个consumer组成。消费者组内每个消费者负 责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
  • Broker:一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个 topic。
  • Topic:可以理解为一个队列,生产者和消费者面向的都是一个topic。
  • Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上, 一个 topic可以分为多个partition,每个partition是一个有序的队列。
  • Replica:副本,为保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower。
  • leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。
  • follower:每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个follower会成为新的follower。

可以看出大致的工作流程为:

  • producer 生产数据后根据topic发送到对应kafka中topic的分区中
  • 分区在主分区保存数据后会同步到自己的其他副本之中
  • 消费者组中的消费者从kafka中对应的主分区中拉取数据进行消费,消费完成之后会记录消费到哪一个 offset(偏移量)

搞明白 kafka主要架构和流程之后,来到今天的主角,可靠性保证上面

数据可靠性保证

在消息队列中,为保证producer发送的数据,能可靠的发送到指定的topic,都会有一个应答机制ack,kafka也是一样,不过不同的是在kafka中默认配置情况下在topic的partition 收到producer发送的数据后,其中的leader要向所有follower同步数据,同步完成之后向producer发送ack,如果producer收到 ack,就会进行下一轮的发送,否则重新发送数据。

3.png

这样的话请思考一个问题,leader收到数据,所有follower都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。这个问题怎么解决呢?

ISR

就像追女孩子一样,我们把所有有机会追到的对象都放在同一个列表里,经常同她们发消息,其中不免会有对你爱答不理的,那还要继续追吗?肯定是要及时止损的啦,只留下成功率大的岂不美哉。kafka也一样,它的Leader 维护了一个动态的 in-sync replica set,简称ISR,意为和leader保持同步的follower集 合。当ISR中的follower完成数据的同步之后,leader就会给follower发送ack。如果follower长时间 未 向leader同步数据 ,则该follower将被踢出ISR ,该时间阈值由配置文件中replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader。

可能有些同学就会说了,那要是假设运行时间无限长的话,是不是所有的follower都会被踢出的啊?这个问题以及leader挂了如何选举新的,我后面会说明。

ack配置参数

有了ISR集合,就避免了被一棵树吊死的情况发生,但是对于我们来说,如果有些消息我们是可以容忍丢失的,我只希望kafka以最快的速度接收消息,回复ack,甚至同步给follower都不是必要的。kafka对此提供了3种关于ack的配置。 参数为配置文件中的acks

  • 0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据
  • 1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据
  • -1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复

故障处理细节

前面说到的leader选举以及ISR集合维护的问题,我们首先应该明白,在kafka存放数据的log文件中,有两个很重要的概念,分别为LEO(Log End Offset)和HW(High Watermark)

  • LEO( Log End Offset):指的是每个副本最大的offset;
  • HW(High Watermark):字面意思高水位,指的是消费者能见到的最大的offset,ISR队列中最小的LEO。

类似于木桶效应, 一只水桶能装多少水取决于它最短的那块木板,对比到kafka中来,partition就是水桶,offset就是水位。为了方便理解,让我来画一个桶

4.png

数字表示的是offset,leo和hw我也都做了标明再说回故障处理,其中分为follower故障和leader故障

follower故障处理

follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。

leader故障处理

leader发生故障之后,会从ISR中选出一个新的leader,之后为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

精准一次性消费

明白如何保证数据可靠性之后,发现剩下的问题是kafka在消费过程中,要么会丢失部分数据,要么会造成数据的重复,即最多一次消费(at most once),主要是保证数据不会重复,但有可能存在数据丢失问题,和至少一次消费(at least once), 主要是保证数据不会丢失,但有可能存在数据重复问题。 聪明的小伙伴们肯定已经想到了,有重复数据的情况下,我进行去重或者保证消费幂等性,不就可以了。没错,那就是我们的目标精确一次消费(Exactly-once),也就是

At Least Once + 幂等性 = Exactly Once

要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number。而Broker端会对做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。 但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once。

我们不妨来思考下什么业务场景下会有数据丢失和数据重复的场景?我来举两个栗子

数据丢失

比如实时计算任务进行计算,到数据结果存盘之前,进程崩溃,假设在进程崩溃前kafka调整了偏移量,那么kafka 就会认为数据已经被处理过,即使进程重启,kafka也会从新的偏移量开始,所以之前没有保存的数据就被丢失掉了。

5.png

数据何时会重复

如果数据计算结果已经存盘了,在kafka调整偏移量之前,进程崩溃,那么kafka会认为数据没有被消费,进程重启,会重新从旧的偏移量开始,那么数据就会被2次消费,又会被存盘,数据就被存了2遍,造成数据重复。

6.png

而且kafka默认情况下是每5秒钟做一次自动提交偏移量,这样并不能保证精准一次消费 。配置文件中参数为: enable.auto.commit:是否自动提交,默认值是true,就是默认采用自动提交的机制。 auto.commit.interval.ms:每多少秒提交一次。默认值是 5000,单位是毫秒。

问题解决

策略一:利用关系型数据库的事务进行处理

出现丢失或者重复的问题,核心就是偏移量的提交与数据的保存,不是原子性的。如果能做成要么数据保存和偏移量都成功,要么两个失败,那么就不会出现丢失或者重复了。 这样的话可以把存数据和修改偏移量放到一个事务里。这样就做到前面的成功,如果后面做失败了,就回滚前面那么就达成了原子性,这种情况先存数据还是先修改偏移量没影响。

比如说把存数据和偏移量都保存在mysql中,就可以使用这种方式。 7.png

优点:

  • 事务方式能够保证精准一次性消费

不足:

  • 数据必须都要放在某一个关系型数据库中,无法使用其他功能强大的nosql数据库。
  • 事务本身性能不好。
  • 如果保存的数据量较大一个数据库节点不够,多个节点的话,还要考虑分布式事务的问题。分布式事务会带来管理的复杂性,一般企业不选择使用。

使用场景:

  • 数据足够少(通常经过聚合后的数据量都比较小,明细数据一般数据量都比较大),并且支持事务的数据库。
策略二:手动提交偏移量+幂等性处理

我们知道如果能够同时解决数据丢失和数据重复问题,就等于做到了精确一次消费。那就各个击破。首先解决数据丢失问题,办法就是要等数据保存成功后再提交偏移量,所以就必须手工来控制偏移量的提交时机。 但是如果数据保存了,没等偏移量提交进程挂了,数据会被重复消费。怎么办?那就要把数据的保存做成幂等性保存。即同一批数据反复保存多次,数据不会翻倍,保存一次和保 存一百次的效果是一样的。如果能做到这个,就达到了幂等性保存,就不用担心数据会重复了。

比如说将数据用put带id的方式保存进ES,天然保证幂等性,或是使用别的方案来保证幂等性,关于幂等性,我以后会专门来写一篇文章来专门细说,关注哦。

8.png

优点:

  • 可以支持数据量大的场景。

难点:

  • 话虽如此,在实际的开发中手动提交偏移量其实不难,难的是幂等性的保存,有的时候 并不一定能保证,这个需要看使用的数据库,如果数据库本身不支持幂等性操作,那只能优 先保证的数据不丢失,数据重复难以避免,即只保证了至少一次消费的语义。

使用场景:

  • 处理数据较多,或者数据保存在不支持事务的数据库上。

小结

以上就是kafka如何来实现精准一次性消费的方案,没有什么方案是完美的方案,在实际的业务场景中,也是要根据具体的情况来选择最合适的方案来使用的。还是那句话,偏离业务谈技术就是耍流氓。

好啦,都看到这里了,点点赞点点关注再走呗~

完!

关注微信公众号:最锋利的矛,最快更新哦