From f07db479b0a951833a51cb5cb33d17830dd9421c Mon Sep 17 00:00:00 2001 From: asahi Date: Thu, 13 Nov 2025 19:23:19 +0800 Subject: [PATCH] =?UTF-8?q?doc:=20=E9=98=85=E8=AF=BBkafka=20exactly-once?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8C=E5=B9=B6=E5=81=9A=E5=87=BA=E8=A1=A5?= =?UTF-8?q?=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mq/kafka/kafka.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/mq/kafka/kafka.md b/mq/kafka/kafka.md index a7063ae..d44826f 100644 --- a/mq/kafka/kafka.md +++ b/mq/kafka/kafka.md @@ -38,6 +38,7 @@ - [消费者](#消费者) - [push vs pull](#push-vs-pull) - [consumer position](#consumer-position) + - [writing to an external system](#writing-to-an-external-system) - [静态成员](#静态成员) - [消息传递语义](#消息传递语义) - [复制](#复制) @@ -347,8 +348,29 @@ Kafka消费者会向其消费分区的leader broker发送fetch请求。消费者 #### push vs pull 在Kafka的实现中,和大多数消息系统一样,data从生产者push到broker,消费者从broker中pull data。 #### consumer position -其他消息系统消费者通常使用ack来告知broker该消息已经被正确处理,但是这样会存在如下问题:如果被consumer拉取的消息被处理,但是在发送ack之前消费者失败,那么该消息会被重复消费。 -相比于通过ack来保证消息被消费者正确处理,kafka通过其他方式来对其进行处理。kafka topic由一系列分区组成,在一个时间点,每个分区都只会由一个consumer group中的一个消费者实例消费。每个消费者实例只需要维护一个integer,作为ack确认的等价物,维护position的开销相当小。 +大多数消息系统中,会在broker端通过metadata来追踪哪些消息已经被消费,当消息被实际传送给consumer或被consumer所ack时,会记录消息此时的状态。使用该方式会存在如下问题: +- 如果在消息被传递给consumer后,立马将该消息标记为已消费: + - 若consumer接收到消息后,如果处理该消息失败,那么该消息会丢失 +- 如果在消息被consumer成功处理,且consumer向broker发送ack后,broker才将消息标记为已消费: + - 如果consumer成功处理该消息,但是向broker发送ack失败,那么该消息会被重复传递并消费,这会导致消息的重复消费 + - broker需要为消息维护多个状态(已发送未消费/已消费/未发送),这将会带来性能问题 + +相比于通过ack来保证消息被消费者正确处理,kafka通过其他方式来对其进行处理。kafka topic由一系列分区组成,在一个时间点,每个分区都只会由一个consumer group中的一个消费者实例消费。每个消费者实例只需要维护一个integer,作为ack确认的等价物,维护position的开销相当小。 + +并且,通过offset方案,还支持通过rewind对旧消息进行重复消费,在一些场景下该特性十分有用。 + + +##### writing to an external system +当消息处理的逻辑中存在`将output向外部系统写入时`,需要协调consumer position和`向外部系统写入output`两个操作。通常,在实现该协调过程时,会引入两阶段提交,保证`consumer position的存储`和`consumer output`的存储是一致、原子的,要么都发生,要么都不发生。 + +但是,通常情况下,会`在相同的位置存储offset和output`,这样可以无需引入两阶段提交,尤其在外部系统并不支持两阶段提交协议的情况下。 + +> 例如,在消息消费时,如果要将消息消费结果写入到mysql数据库中时,也可以同时在数据库中记录`(topic, partition, consumer group, last consumed offset)`的信息,此时,可以通过数据库事务来保证offset的更新和消息结果的写入同时成功/失败。 +> +> 并且,在consumer实例启动、rebalance分配到分区时,都会从数据库中读取offset,这样能够保证消息的exactly-once消费。 + + + #### 静态成员 静态成员用于提升基于group rebalance协议应用的性能。rebalance协议依赖于组协调器,组协调器用于为组成员分配实体id。组协调器产生的实体id是短暂的,并且当成员重启并重新加入到组之后,实体id会发生变化。对于基于consumer的app,上述“动态成员”可能会造成在应用周期性重新启动时大量任务会被新分配给其他消费者实例。 由于上述缺点,Kafka group管理协议允许组成员提供持久的实体id,基于这些id,组成员身份保持不变,故而rebalance不会被触发。