Kafka丢消息与重复消费场景

559 阅读8分钟

0 简介

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

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

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

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

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

v2-017c102d378756ab2bbba699b1b5f7ea_1440w.jpg

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

框架配参数时是通过:


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 配置来规避的,生产者客户端可配置:

config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll

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

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) 的不恰当提交。

image2022-9-18_18-24-17.png 参考上图,当前一次 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秒。

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

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

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

iShot2024-12-02 12.14.44.png 在 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 设计,在高可用之余,也会导致消息时序无法保证,消费者要注意不能依赖消息时序进行业务处理。