Apache Pulsar 消息确认机制原理与源码分析


前言

分布式消息系统的可靠性保证机制通常有如下三种:

  • At Most Once:指消息最多只被传递一次,但有可能出现消息丢失的情况。这种保证方式适用于对数据可靠性和一致性要求不高的场景。
  • At Least Once:消息确保至少被处理一次,但由于系统设计或不可抗力导致的处理失败,可能会出现重复处理的情况。大多数常用的消息队列,如RocketMQ、RabbitMQ和Kafka,都默认提供“至少一次”的保证。Pulsar默认也是。
  • Exactly Once:指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息在消费端也只被消费一次。Exactly-Once语义是消息系统和流式计算系统中消息流转的最理想状态,但是在业界并没有太多理想的实现。因为真正意义上的Exactly-Once依赖消息系统的服务端、消息系统的客户端和用户消费逻辑这三者状态的协调。需要三者参与实现分布式事务。Pulsar也能保证这种,设计原理见:Apache Pulsar 技术系列 - Pulsar事务实现原理

在Pulsar中保证消息至少被消费者消费一次是通过Ack机制实现的,Pulsar有四种Ack机制,即单条消息确认机制、累积消息确认机制、批消息中的单个消息确认、否定应答Nack。在了解这四种Ack机制前需要先了解Pulsar的订阅类型。订阅决定了一组消费者(一个或多个)消费 Topic 中消息的方式,同一个 Topic 可以同时支持使用多个不同的订阅类型。在实际使用中,这种设计理念也方便我们根据需要为不同的应用程序配置不同的消费模式。目前 Pulsar 支持以下四种订阅类型:
在这里插入图片描述

  • Exclusive:Pulsar 默认的订阅类型,每次仅允许一个消费者消费订阅(即订阅的topic中的消息)中的消息。如果有其他消费者想要接入该订阅,请求将会被 Broker 拒绝。也就是说只有一个消费者处理topic的所有分区消息。该订阅类型能够确保消息的顺序性。
  • Failover:可以允许多个消费者附加到一个订阅上。消费者的队列分配策略根据Topic类型有所区别。如果是分区Topic,broker服务端会根据消费者优先级和消费者名称字典顺序进行排序,然后Broker会将Topic中的分区平均分配给高优先级的消费者,低优先级消费者会成为分区的备消费者,类似于 Apache Kafka 中的消费分区rebalance机制。 如果是非分区Topic,Broker按照消费者订阅Topic的顺序选择主消费者,其他的成为备消费者。
  • Shared:允许多个消费者共同消费同一订阅的消息。在此模式下,多个消费实例会以 Round-robin 的方式接收 Topic 中的消息。因此,这种类型并不能保证顺序性。虽然每个消费者都能接收到消息,但正常情况下并不会出现消息重复消费的情况。如果某个消费者突然宕机或断开连接,所有发送给该消费者且未确认的消息会被重新发送给其他消费者确认。
  • Key_Shared:与 Shared 订阅类型相似,Key_Shared 也支持多个消费者共同消费同一订阅的消息,但发送给不同消费者的消息根据 Key 决定,即具有相同 Key 的消息会发送给相同的消费者。

在Pulsar中消息Ack机制是通过Cursor实现的,每个订阅会和一个Cursor绑定。根据订阅类型的不同,Cursor可能会记录一个或多个消费者的消费位置信息。例如,使用 Shared 或 Key_Shared 订阅时,消费者会共享同一订阅的Cursor。如果与 Kafka 类比,Pulsar 中的订阅类似于 Kafka 中的消费组,Cursor有点像偏移量(Offset)的概念。

需要注意的是,由于每个 Topic 不一定只有一个订阅,你可以针对单个 Topic 创建多个类型不同的订阅,这意味着多个消费组可以从同一个 Topic 中读取数据。在这种情况下,每个订阅的Cursor针对各自的消费组记录组内消费者的消费位置信息,互不影响。

上述说到Pulsar四种Ack机制,需要详细介绍一下:

  • 单条消息确认机制:指每条消息都需要客户端单独ack确认,客户端宕机或ack超时只会重新推送未ack的消息。假设消息1、2、3发送给了Consumer-A,消息4、5、6发送给了Consumer-B,而Consumer-B又消费的比较快,先ack了消息4,此时Cursor中会单独记录消息4为已ack状态。如果其他消息都被消费,但没有被ack,并且两个消费者都下线或ack超时,则Broker会只推送消息1、2、3、5、6,已经被ack的消息4不会被再次推送。
  • 累积消息确认机制:指ack消息N表示N之前的消息也被成功消费。如Consumer接受到了消息1、2、3、4、5,为了提升ack的性能,Consumer可以不分别ack5条消息,只需要调用acknowledgeCumulative,然后把消息5传入,Broker会把消息5以及之前的消息全部标记为已ack。
  • 批消息中的单个消息确认:这种消息确认模式,调用的接口和单条消息的确认一样,但是这个能力需要Broker开启配置项acknowledgmentAtBatchIndexLevelEnabled。当开启后,Pulsar可以支持只ack一个batch里面的某些消息。假设Consumer拿到了一个批消息,里面有消息1、2、3,如果不开启这个选项,我们只能消费整个batch再ack,否则Broker会以批为单位重新全部投递一次。前面介绍的选项开启之后,我们可以通过acknowledge方法来确认批消息中的单条消息。该机制可以避免每次消费重试时都重复推送整批的消息。
  • 否定应答Nack:客户端发送一个redeliverUnacknowledgedMessages命令给Broker,明确告知Broker,当前Consumer无法消费这条消息,消息将会被重新投递。

注意并不是所有的订阅类型都能使用上述Ack类型,如shared或者key_shared订阅类型就不支持累积消息确认机制,原因是在shared或者key_shared订阅类型下前面的消息不一定是被当前consumer消费的,如果使用累积消息确认会把别人的消息也一起ack。订阅类型与Ack类型之间的适配关系如下:
在这里插入图片描述
本文将主要介绍单条消息确认机制和累积消息确认机制的原理与源码分析。

单条消息确认机制

单条消息确认机制下的Ack请求会携带多个MessageIdData,每个MessageIdData表示确认一个entry。Ack请求数据结构如下:
在这里插入图片描述
MessageIdData数据结构如下:
在这里插入图片描述
采用区间的形式记录全部消息的ack情况,假设在Ledger-1中我们的区间信息如下:
在这里插入图片描述
则我们存储的区间信息如下,即会用区间来表示已经连续Ack的范围:
[ (1:-1, 1:2] , (1:3, 1:6] ]
如果旧的MarkDeletePosition存在已ack的区间,则将MarkDeletePosition更新为最新已ack区间的右边界,如示例中新的MarkDeletePosition=1:2。然后将新的MarkDeletePosition和ack区间存储到cursor ledger或zk中。
zk中存储的数据结构如下:
在这里插入图片描述
在这里插入图片描述
cursor ledger中存储的数据结构如下:
在这里插入图片描述
接下来进行源码分析,Broker ServerCnx在handleSubscribe中会触发topic加载(若已加载则无需重复加载)接着创建consumer并将consumer添加到subscription中,创建成功的con

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值