Kafka的三种消息投递语义

Kafka的三种消息投递语义以及如何保证幂等

Kafka的三种消息投递语义

Kafka支持的三种消息投递语义:

  • at most once:至多一次,消息可能会丢,但不会重复
  • at least once:至少一次,消息肯定不会丢失,但可能重复
  • exactly once:有且只有一次,消息不丢失不重复,且只消费一次。

at least once 和 at most once

消息的投递和消费分为两端:producer-broker,broker-consumer。

producer-broker: 当producer向broker发送消息时,一旦这条消息commit了,由于broker有replication的存在,这条消息就不会丢失,这样就可以在producer-broker端保证at least once消息语义。当然,也可以通过设置Producer异步发送来实现at most once语义。

broker-consumer: consumer在消费消息时,每个consumer都会在保存offset来记录自己消费的位置(从__consumer_offsets这个topic中取,老版本存储在zk中)。当consumer挂了的时候,就会发生负载均衡,需要consumer group中另外的consumer来接管并继续消费。consumer在处理消息和修改offset时也有两种处理方式:

  • consumer读取消息后,先修改offset,然后处理消息。
  • consumer读取消息后,先处理消息,然后修改offset。

如果consumer在处理消息的过程中挂掉了,我们无法确认consumer是否已经处理完了消息。所以处理方式一,虽然消息会丢,但消息不会被重复消费,确保了at most once语义。处理方式二,因为consumer还没有修改offset就挂了,所以consumer group中接管的consumer会从上次消费的offset处接手继续消费,虽然消息会重复,但消息肯定不会丢,保证了at least once语义。

exactly once

想要做到exactly once,就需要producer-broker、broker-consumer端两端同时保证。

幂等

对于producer,如果broker配置了enable.idempotence = true,每个producer在初始化的时候都会被分配一个唯一的Producer ID,producer向指定topic的partition发送消息时,携带一个自己维护的自增的Sequence Number。broker会维护一个<pid,topic,partition>对应的seqNum。 每次broker接收到producer发来的消息,会和之前的seqNum做比对,如果刚好大一,则接受;如果相等,说明消息重复;如果相差大于一,则说明中间存在丢消息,拒绝接受。

这个设计解决了两个问题:

  • broker保存消息后,发送ACK前宕机,producer认为没有发送成功并重试,造成消息重复
  • 前一条消息发送失败,后一条成功,前一条消息重试后成功,造成消息乱序

事务性保证

上述的幂等操作,只能保证单个producer对于同一个<topic,partition>的exactly once,并不能保证向多个topic partitions写操作时的原子性。更不能保证多个读写操作时的原子性。例如某个场景是,从某个topic消费消息,处理转换后回写到另一个topic中。

事务性保证可以使应用程序将生产数据和消费数据当作一个原子单元来处理,即使该生产或消费跨多个<Topic, Partition>。应用程序也可以在重启后,从上一个事物点恢复,也即事物恢复。

因为消息可以是跨topic和partition的,所以为实现这一效果,必须是应用程序提供一个稳定的(重启后不变)唯一的ID Transaction ID,使得PID 和 Transaction ID 一一对应起来。

conusmer端

以上事务性保证只是针对producer端的,对consumer端依然无法保证。

如果是消费kafka中的topic,并将结果回写到另一个topic,那么可以将消费消息和发送消息绑定为一个事务。 如果要将处理消息后的结果保存到外部系统,就要用到两阶段提交了。