Kafka丢消息与重复消费场景

0 简介

Kafka 最初的设计目的是用于处理海量的日志。

早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如不保证消息的可靠性,可能会丢失消息,也不支持集群,功能上也比较简陋,这些牺牲对于处理海量日志这个特定的场景都是可以接受的。

随后的几年 Kafka 逐步补齐了这些短板 ,当下的 Kafka 已经发展为一个非常成熟的消息队列产品 ,无论在数据可靠性、稳定性和功能特性等方面都可以满足绝大多数场景的需求。

但是,如果使用不当 ,还是有可能发生丢消息重复消费等情况。

Kafka 为了实现超高的并发性能,大量使用了批量和异步 的设计,也因此,反而导致它同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka 并不会立即发送出去,而是要等一会儿攒一批再发送,在它的 Broker 中,很多地方都会使用这种"先攒一波再一起处理"的设计。所以,Kafka 其实不太适合实时的在线业务场景。

golang 一般使用 c.Consumer.Offsets.AutoCommit.Interval的库连接 Kafka,默认配置可见:github.com/Shopify/sar...

框架配参数时是通过:

ini 复制代码
cli, err = skafka.NewClient(cfg.KafkaAddr, func(config *sarama.Config) {
    config.Version = sarama.V2_3_0_0
    //config.Producer.Flush.Frequency = time.Second
})

1 生产者阶段

1.1 生产者丢消息

生产者丢消息一般分为:

  1. producer 把消息发送给 broker,因为网络抖动,消息没有到达 broker;

  2. producer 把消息发送给 broker-leader,leader 接收到消息,在未将消息同步给 follower 之前,挂掉了;

  3. producer 把消息发送给 broker-leader,leader 接收到消息,leader 未成功将消息同步给每个 follower,有消息丢失风险;

  4. 某个 broker 消息尚未从内存缓冲区持久化到磁盘,就挂掉了,这种情况无法通过ack机制感知。

其中,前三种都是可以通过 ack 配置来规避的,生产者客户端可配置:

arduino 复制代码
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll

ack参数取值见:github.com/Shopify/sar...

ini 复制代码
const (
    // 不等待ack
    NoResponse RequiredAcks = 0
    // 本地提交成功
    WaitForLocal RequiredAcks = 1
    // 等待所有同步副本提交成功,这副本的数量看服务端的`min.insync.replicas`配置
    WaitForAll RequiredAcks = -1
)

遇到提交消息失败时,生产者可以进行重试。

此时要关注 c.Producer.Retry.Max 最大重试次数、c.Producer.Retry.Backoff 重试时间间隔、c.Producer.Flush.Frequency flush发送频率、c.Producer.Flush.Messages 触发flush的消息量 等等。

不过并不是所有的异常都是可以通过重试来解决的,比如消息太大,超过 max.request.size 参数配置的值时,这种方式就不可行了。

Kafka 为了提升性能,使用页缓存机制,将消息写入页缓存而非直接持久化至磁盘,采用了异步批量刷盘机制,也就是说,按照一定的消息量和时间间隔去刷盘,刷盘的动作由操作系统来调度的,如果刷盘之前,Broker 宕机了,重启后在页缓存的这部分消息则会丢失。

1.2 消息重复生产

先来看一个定义:

类型 消息是否会重复 消息是否会丢失 优势 劣势 适用场景
最多一次 生产端发送消息后不用等待和处理服务端响应,消息发送速度会很快。 网络或服务端有问题会造成消息的丢失。 消息系统吞吐量大且对消息的丢失不敏感。例如:日志收集、用户行为等场景。
最少一次 生产端发送消息后需要等待和处理服务端响应,如果失败会重试。 吞吐量较低,有重复发送的消息。 消息系统吞吐量一般,但是绝不能丢消息,对于重复消息不敏感。
有且仅有一次 消息不重复,消息不丢失,消息可靠性很好。 吞吐量较低。 对消息的可靠性要求很高,同时可以容忍较小的吞吐量。

生产者阶段消息重复根本原因其实就是:生产发送的消息没有收到正确的 ack 响应,导致 producer 重试。

比如 broker 落盘成功,但 ack 响应由于网络原因丢失了,生产者进行重试,broker 再次落盘成功。

实际上,就是为了降低消息丢失的概率,反而 增加了重试概率,从而导致消息重复生产。

解决方案是启用 Kafka 的幂等性:enable.idempotence=true 同时要求 ack=all 且 retries>1。

原理:每个 producer 有一个 producer id,服务端会通过这个id关联记录每个producer的状态,每个producer的每条消息会带上一个递增的sequence,服务端会记录每个producer对应的当前最大sequence(producerId + sequence),如果新的消息带上的sequence不大于当前的最大sequence就拒绝这条消息,消息落盘会同时更新最大sequence,这个时候重发的消息会被服务端拒掉从而避免消息重复。

2 消费者阶段

2.1 原理

消费者阶段的异常本质上是对位移(offset) 的不恰当提交。

参考上图,当前一次 poll() 操作所拉取的消息集为 [x+2, x+7],x+2 代表上一次提交的消费位移,说明已经完成了 x+1 之前(包括 x+1 在内)的所有消息的消费,x+5 表示当前正在处理的位置。如果拉取到消息之后就进行了位移提交,即提交了 x+8,那么当前消费 x+5 的时候遇到了异常,在故障恢复之后,我们重新拉取的消息是从 x+8 开始的。也就是说,x+5 至 x+7 之间的消息并未能被消费,如此便发生了消息丢失的现象。

再考虑另外一种情形,位移提交的动作是在消费完所有拉取到的消息之后才执行的,那么当消费 x+5 的时候遇到了异常,在故障恢复之后,我们重新拉取的消息是从 x+2 开始的。也就是说,x+2 至 x+4 之间的消息又重新消费了一遍,故而又发生了重复消费的现象。

2.2 自动提交位移

在 Kafka 中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数 c.Consumer.Offsets.AutoCommit.Enable 配置,sarama 默认值为 true。当然这个默认的自动提交不是每消费一条消息就提交一次,而是定期提交,这个定期的周期时间由客户端参数 c.Consumer.Offsets.AutoCommit.Interval 配置,sarama 默认值为1秒。

假设刚刚提交完一次消费位移,然后拉取一批消息进行消费,在下一次自动提交消费位移之前,消费者崩溃了,那么又得从上一次位移提交的地方重新开始消费,这样便发生了重复消费的现象。

我们可以通过减小位移提交的时间间隔来减小重复消息的窗口大小,但这样并不能避免重复消费的发生,而且也会使位移提交更加频繁。

而当消息拉取和业务处理是分开线程的,则可能会导致消息丢失:

在 sarama 库中的自动提交,是基于被标记过的消息 session.MarkMessage(msg, ""),见:github.com/Shopify/sar...

如果不调用 session.MarkMessage(msg, ""),即使启用了自动提交也没有效果,下次启动消费者会从上一次的 offset 重新消费。

这样的处理,我们可以在业务处理完成后再调用 session.MarkMessage(msg, "") 来最大限度保证不会出现消息丢失场景。

2.3 手动提交位移

Kafka 当然也提供了手动提交位移的方法,并且分为同步提交和异步提交。但这里不打算细讲,理由很简单,会大大增加代码复杂度(对提交失败的重试处理),还无法完全避免重复消费

异步提交不用多说,当异步提交操作未完成时,程序崩溃,自然导致重复消费;

异步提交失败时,也可能导致重复消费:

  • 异步提交 x+1 时失败,走进重试逻辑;
  • 消费者继续处理 x+2,异步提交成功;
  • x+1的提交重试成功,覆盖掉 x+2 的位移,下次将重复拉取 x+2。

而同步提交,会大大降低消费者处理效率,基本可以说是放弃了队列的所有优势。

3 总结

消息丢失的场景,基本可以通过 Kafka服务端配置、生产者配置、消费者消息标记 来最大限度 的保证绝大多数情况 下不丢失消息。但建议设计之初考虑补消息的逻辑。

而消息重复场景,实际上可以说很多是为了对抗消息丢失而导致的,可以说很难完全避免强烈建议 消费者做好幂等性处理。

题外话,Kafka 的这些 broker、topic、partition 设计,在高可用之余,也会导致消息时序无法保证,消费者要注意不能依赖消息时序进行业务处理。

相关推荐
bing_1582 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
天上掉下来个程小白3 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
Asthenia04123 小时前
基于Jackson注解的JSON工具封装与Redis集成实战
后端
编程星空4 小时前
css主题色修改后会多出一个css吗?css怎么定义变量?
开发语言·后端·rust
程序员侠客行4 小时前
Spring事务原理 二
java·后端·spring
Nicole Potter4 小时前
请说明C#中的List是如何扩容的?
开发语言·面试·c#
dmy5 小时前
docker 快速构建开发环境
后端·docker·容器
sjsjsbbsbsn5 小时前
Spring Boot定时任务原理
java·spring boot·后端
计算机毕设指导65 小时前
基于Springboot学生宿舍水电信息管理系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·maven
计算机-秋大田6 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计