从12个方面详细解读Pulsar的主题与订阅
1、主题(Topics)
Pulsar中主题类似一个URL,格式如下所示:
{persistent|non-persistent}://tenant/namespace/topic
主题的每一个部分说明如下:
- persistent|non-persistent 表示持久化或非持久化
- tenant 表示租户
- namespace 表示命名空间
- topic 主题的名称
在Pulsar中你不需要显示的创建主题,如果当客户端向一个不存在的主题发送消息或订阅消息时,Pulsar会自动创建主题。
1.1 命名空间(Namespaces)
命名空间是一个租户下的逻辑概念,命名空间可以为应用程序管理主题的目录层次结构。
1.2 订阅类型(Subscription Types)
在Pulsar中,有四种订阅类型,它们分别是exclusive、shared、failover和key_shared。四种订阅模式的图解如下所示:
在Pulsar中,订阅模式并不是创建消费者时指定的,而是在消费者启动时指定的,并可以通过重启改变订阅类型,当一个消费组(订阅)并没有启动真实的消费者,这个消费组的订阅类型是未定义。
接下来对上述四种订阅模式展开介绍。
1.2.1 独占模式(Exclusive)
Exclusive独占模式,一个订阅(对标Kafka/RocketMQ消费组)只允许一个消费者订阅,如果多个消费者尝试使用该订阅去消费消息会抛出异常,也就是说一个消费者处理主题的所有分区,如下图所示:
这个是Consumer B启动时会报错,只有ConsumerA能收到消息。
注意:Pulsar默认的订阅类型为 Exclusive。
1.2.2 主备/故障转移模式(Failover)
Failover订阅模式,可以允许多个消费者附加到一个订阅上。消费者的队列分配策略根据主题类型有所区别:
- 如果是分区主题(一个主题拥有多个队列的主题),broker服务端会根据消费者优先级和消费者名称字典顺序进行排序,然后Broker会将主题中的分区平均分配给高优先级的消费者,低优先级消费者会成为分区的备消费者。
- 如果是非分区主题,Broker按照消费者订阅主题的顺序选择主消费者,其他的成为备消费者。
非分区主题的订阅示例说明如下:
对于分区主题的订阅图解说明如下:
温馨提示:Pulsar的Failover订阅模式可以类比RocketMQ中的集群消费模式下的平均分配算法。
1.2.3 共享/轮询模式(Shared)
在Shared订阅模式下,多个消费者可以附加到同一个订阅,消息以循环分发的方式轮流发送给各个消费者,并且任何给定的消息只会传递给一个消费者,当一个消费者断开连接时,所有发送给它并未被确认的消息将重新调度,再发送给其他消费者。
Shared模式的订阅图解如下所示:
上图中的ConsumerA、ConsumerB、ConsumerC都会参与消息消费。
Shared模式与Failover模式的主要差别是Shared模式并不和消费者绑定队列,即Shared模式将所有分区的消息当成一个整体来看,
使用Shared模式需要的几个注意事项:
- 无法保证消息的顺序性
- Shared模式不能使用累积确认机制。
这种模式的最大缺点是不能启用累积确认机制,消息确认效率会降低,但其优势也比较明显,在解决单个队列积压方面,能充分所有消费者的处理能力。
1.2.4 基于Key的共享模式(Key_Shared)
在Key_Shared模式中,多个消费者可以附加到同一个订阅。具有相同Key的消息会分发给同一个消费者。
Key_Shared模式的消息分发机制如下所示:
Pulsar提供了Sticky(粘性)、Auto-split Hash Range(自动分割哈希范围)、Auto-split Consistent Hashing(自动分割一致性哈希)这三种选择算法。
选择消费者的基本过程如下所示:
- 将分片Key传递到一个哈希函数,生成一个哈希值
- 将Key的哈希值传入到对应的分片算法中,从而选择出一个消费者。
当一个新的消费者加入或者一个消费者退出时,分配算法都将会重新计算消息到消费者的映射(选择)。
接下来分别介绍这三种分配算法底层的工作机制。
1.2.4.1 Auto-split Hash Range
分段哈希取模算法,设定的总区间为0~65536,再根据消费者个数来分配段,然后用消费者的key进行哈希算法得出hash值,再用这个hash值域65535取模得到最终的区间值,然后看该值落在哪个区间,就由哪个消费者来消费,示例如下:
下面用图解的方式说明当新增消费者或减少消费者,分段哈希取模算法是如何进行重新映射的:
1.2.4.2 Auto-split Consistent Hashing
一致性哈希算法。如果要开启一致性hash算法,需要在broker端subscriptionKeySharedUseConsistentHashing设置为true。
1.2.4.3 Sticky
这个类似Auto-split Hash Range,总的区间也是0~65536,与Auto-split Hash Range的区别是每一个消费者对应的区间是固定的,并且由使用者来保证每一个消费者的区间不会重叠。
对应消费者C1的初始化代码:
KeySharedPolicySticky keySharedPolicy = KeySharedPolicy.stickyHashRange();
keySharedPolicy.ranges(new Range(0,16384), new Range(32768, 49152));
try (Consumer<String> consumer = client.newConsumer(Schema.STRING)
.topic(Constants.testTopic)
.subscriptionName(Constants.subscriptionName)
.subscriptionType(Constants.subscriptionType)
.subscriptionInitialPosition(Constants.subscriptionInitialPosition)
.keySharedPolicy(keySharedPolicy)
.subscribe()) {
... 省略 ....
Key_Shared订阅模式要确保同一个Key的消息在被统一时刻只会传递给单一的消费者处理,但当一个消费者新加入时,一些键的映射会发生改变,说明如下所示:
为了避免相同key的消费被转发给不同的消费者,Plusar的处理方法是新消费者创建时与Broker建立连接后,会将新消费者与当前的读位置【可以理解已经下发给消费者的最大消息偏移量】关联起来,只有在读位置之前的消息全部确认后,后续消息才会继续推送到新的消费者。
但这样做也会引发一个新的问题:那就是如果一个现有消费者堵塞了,并且没有定义消费超时,那么新的消费者将收不到任何消息,直到被阻塞的消费者恢复或断开连接。
当然我们也可以在消费端设置 allowOutOfOrderDelivery 为true,放松上面的限制,即新的消费者连接后里面可以接受消息,这样会短暂的破坏这一原则,在严格顺序消费场景,会破坏顺序语义。
值得注意的是当消费者使用Key_Shared订阅类型时,需要在消息发送端禁用批处理或者启用积压Key的批处理机制。
在消息发送端启用基于Key的批处理机制,Java版本的示例代码如下:
Producer<byte[]> producer = client.newProducer()
.topic("my-topic")
.batcherBuilder(BatcherBuilder.KEY_BASED)
.create();
在使用Key_Shared订阅模式时需注意如下几点:
- 在消息发送时必须为消息指定一个Key
- 无法使用累积确认
- 当主题中最新消息的位置为X时,默认新的消费者一上线后不会立马消费消息,必须等待X之前的消息全部确认。
1.3 订阅模式(Subscription modes)
订阅模式主要是指的游标类型。Pulsar在创建一个订阅时,将创建一个游标来记录最后消费的位置。当消费者重新启动时可以继续从上一次消费位置继续消费。
Pulsar中定义了Durable、NonDurable两种订阅模式:
- Durable 游标持久,如果Broker由于某一个错误重启后,它可以从持久化存储组件(BookKeeper)中恢复游标,这样消息会从上一次持久的位置开始消费。Durable为默认方式。
- NonDurable 游标非持久化,一旦Broker停止,游标就会丢失,并且永远无法再恢复。
客户端在构建消费者时可以通过如下代码改变订阅模式:
Consumer<byte[]> consumer = pulsarClient.newConsumer()
.topic("my-topic")
.subscriptionName("my-sub")
.subscriptionMode(SubscriptionMode.Durable)
.subscribe();
1.4 多主题订阅(Multi-topic subscriptions)
当消费者订阅Pulsar主题时,默认情况下它订阅一个特定的主题,例如persistent://public/default/my-topic。然而,从Pulsar 1.23.0-incubating版本开始,Pulsar消费者可以同时订阅多个主题。你可以用两种方式定义主题列表:
- 主题名称可以使用正则表达式,例如persistent://public/default/finance-.*。
- 可以显示指定多个主题
温馨提示:当使用正则表达式订阅多个主题时,这些主题会限制在同一个命名空间(Namespace)中。
当符合正则表达式的主题创建后,消费者能够自动订阅。当生产者向多个主题发送消息时,不能保证消息在不同主题直接的顺序性。
多主题订阅的使用示例代码如下:
PulsarClient pulsarClient = // Instantiate Pulsar client object
// Subscribe to all topics in a namespace
Pattern allTopicsInNamespace = Pattern.compile("persistent://public/default/.*");
Consumer<byte[]> allTopicsConsumer = pulsarClient.newConsumer()
.topicsPattern(allTopicsInNamespace)
.subscriptionName("subscription-1")
.subscribe();
1.5 分区主题(Partitioned topics)
普通主题仅仅由一个Broker提供服务,这限制了主题的最大吞吐量。分区主题是由多个Broker共同处理的特殊类型的主题,分区的主题的示意图如下所示:
正如上图中那样,Topic1拥有5个分区(P0,p1,p2,p3,p4),分别分在3个Broker中,其中broker1,broker2上分别创建了2个,而Broker3中创建了3个,关于分区在Broker的分布情况由Pulsar内部根据负载决定其分布。
消息发送者在消息发送时如何选择分区由**(Routing mode)路由模式**来决定,Broker如何将消息推送给消费者(订阅者)则有订阅类型来决定。
分区主题需要通过管理API显示创建,分区的数量可以在创建主题时指定,具体命令如下:
./pulsar-admin topics create-partitioned-topic persistent://codingw/codingw00/dw_test_03_05_000 --partitions 4
1.6 路由模式(Routing mode)
需要将消息发送到分区主题时必须指定路由模式(负载均衡算法),Pulsar中的路由模式由MessageRoutingMode枚举类型定义,其选项说明如下:
- RoundRobinPartition 轮询模式,如果消息不包含Key,则按批次进行轮询所有分区,如果设置了key,则按Key的哈希值与分区数取模。这个是Pulsar的默认行为。
- SinglePartition 如果消息没有指定Key,生产者将随机选择一个分区,一旦分区被选择后,后续所有消息都将发送该分区;如果指定了Key,则按key的哈希进行散列。
- CustomPartition 用户自定义算法,需要实现MessageRouter接口。
1.7 顺序性保证(ordering guarantee)
消息的顺序性主要取决于消息的路由模型(MessageRoutingMode)与消息的key。如果消息指定了Key,则无论是RoundRobinPartition还是SinglePartition,相同的key的消息都会发送到相同的分区。
在Pulsar中提供了两种顺序级保证:
- Per-key-partition 分区级 同一个Key的消息分布在一个分区中,实现消息在分区级顺序,使用技巧:消息附加Key并采取RoundRobinPartition、SinglePartition路由算法。
- Per-producer 来自同一个消息发送者的所有消息保持顺序,使用技巧:每条消息不设置key并且采用SinglePartition路由算法。
在Pulsar中提供了JavaStringHash、Murmur3_32Hash两种Hash算法,默认为JavaStringHash,但如果客户端存在多种语言,则推荐使用Murmur3_32Hash。可以通过如下代码指定Hash算法:
Producer<String> producer = pulsarClient.newProducer(Schema.STRING)
.enableBatching(false)
.topic(Constants.testTopic)
.hashingScheme(HashingScheme.JavaStringHash)
.create()
1.8 非持久化主题(Non-persistent topics)
默认情况下Pulsar会将所有未确认的消息存储在BookKeeper集群中。因此Broker发生故障,未确认的消息可以进行故障转移。与之对应的是非持久化主题,Pulsar将这部分消息只保存在Broker的内存中,一旦Broker发生故障而重启,这块消息会丢失。
非持久化主题的前缀为:non-persistent,如下所示:
non-persistent://tenant/namespace/topic
对于非持久化主题,消息只存在Broker的内存中,没有额外的缓存区,这意外着Broker接受到消息生产者消息后,会立即传递给所有连接的消费者,一旦Broker出现异常或者无法从内存中检索消息数据,则可能会导致消息丢失,因此需要谨慎使用。
默认情况下非持久化主题在Broker上时开启的,可以在Pulsar Broker配置文件中通过修改enableNonPersistentTopics的值为fasle禁用该机制。
non-persistent的主题元信息不会持久化到Zookeeper,这就意味着如果拥有主题的Broker崩溃,这些非持久化主题的主题无法自动转移到其他Broker。解决的办法:Broker的配置文件中将allowAutoTopicCreation设置为true,并将allowAutoTopicCreationType设置为non-partitioned。
1.9 系统主题(System topic)
系统主题是一个预定义的主题,供Pulsar内部使用。目前Pulsar中的系统主题如下图所示:
我们可以通过pulsar运维命令 pulsar-admin topics list 命令中增加 -ist或者--include-system-topic选项,用于显示系统主题。
1.10 消息重新投递(Message redelivery)
Apache Pulsar使用至少消费一次的语义。也就是确保消息至少被消费一次,要想激活Broker的消息重新投递机制,在消费端可以通过如下机制:
- Negative Acknowledgment 主动取消确认。
- Acknowledgment Timeout 确认超时机制。
- Retry letter topic 重试主题。
1.11 消息保留与过期机制
默认情况下,Pulsar对消息采取如下机制:
- 立即删除已被消费者确认的所有消息
- 将所有未确认的消费持久化存储在backlog中
但我们可以覆盖上述默认行为:
- 消息保留机制(Message retention)使您能够存储已被使用者确认的消息
- 消息过期(Message expiry)使您能够为尚未被确认的消息设置生存时间(TTL)
关于消息保留与过期删除机制,将在后续文章中以专题方式详细介绍其实现原理,对应官方文档:Message retention and expiry | Apache Pulsar
1.12 消息重复删除(Message deduplication)
如果没有启用消息重复删除机制,下图展示了消息被持久化多次的情况:
也就是当消息发送者由于超时进行重试时,Broker收到了同一个客户端多条内容相同的消息,Broker会将多条内容相同的消息存储多次,造成消息在服务端的重复存储。
Pulsar支持消息重复删除机制,如果开启了消息重试机制,其工作示意如下所示:
在Pulsar中,消息重复删除机制可以在namespace或主题级别开启。
默认情况下在Broker、Namespace、Topic都是禁用消息重复删除机制的。
我们可以通过如下三种方式开启消息重复删除机制:
- 在Broker端进行全局配置
- 通过pulsar-admin namespaces命令在namespace级别设置
- 通过pulsar-admin topics 命令在topic级别设置
在broker的配置文件中我们可以配置如下参数:
- brokerDeduplicationEnabled 在Broker是否开启消息重复删除机制,默认为false。如果设置为true,则默认在namespace,topic级别开启,如果设置为false,则可以单独在namespace、topic级别启用或禁用。
- brokerDeduplicationMaxNumberOfProducers 设置识别消息重复删除所涉及到的最大生产者数量,默认为10000。
- brokerDeduplicationEntriesInterval 重复消息快照中条目数量,默认为1000。
- brokerDeduplicationSnapshotIntervalSeconds 生成消息快照的间隔周期,默认为120s。
- brokerDeduplicationProducerInactivityTimeoutMinutes Broker丢弃不活动生产者快照的等待时间,如果一个生产者在指定时间内不与Broker保持心跳,超过挂职后会删除对应的快照,单位为分钟,默认为360,表示6小时。
默认情况下,在所有Pulsar名称空间/主题上禁用消息重复删除。要在所有名称空间/主题上启用它,请将brokerDeduplicationEnabled参数设置为true并重新启动Broker。
如果Broker端将brokerDeduplicationEnabled设置为false,我们也可以pulsar-admin namespaces set-deduplication或者pulsar-admin topics set-deduplication命令在namespace或者主题级别开启消息重复删除。示例如下:
bin/pulsar-admin namespaces set-deduplication public/default --enable
如果要禁用,命令如下:
bin/pulsar-admin namespaces set-deduplication public/default --disable
如果在Pulsar broker、命名空间或主题中启用了消息去重功能,建议客户端无限次地重试消息,直到成功为止,否则可能会破坏顺序保证,因为一些请求可能会超时,并且应用程序不知道请求是否成功添加到主题。
如果开启消息重复删除机制,建议客户端配合做如下两件事情:
- 为生产者制定一个全局唯一的名称
- 将消息发送超时设置为0,表示无超时时间
文章转载自公众号:中间件兴趣圈