从12个方面详细解读Pulsar的主题与订阅

xbkong
发布于 2023-6-7 16:02
浏览
0收藏

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。四种订阅模式的图解如下所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

在Pulsar中,订阅模式并不是创建消费者时指定的,而是在消费者启动时指定的,并可以通过重启改变订阅类型,当一个消费组(订阅)并没有启动真实的消费者,这个消费组的订阅类型是未定义。

接下来对上述四种订阅模式展开介绍。

1.2.1 独占模式(Exclusive)

Exclusive独占模式,一个订阅(对标Kafka/RocketMQ消费组)只允许一个消费者订阅,如果多个消费者尝试使用该订阅去消费消息会抛出异常,也就是说一个消费者处理主题的所有分区,如下图所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

这个是Consumer B启动时会报错,只有ConsumerA能收到消息。

注意:Pulsar默认的订阅类型为 Exclusive。

1.2.2 主备/故障转移模式(Failover)

Failover订阅模式,可以允许多个消费者附加到一个订阅上。消费者的队列分配策略根据主题类型有所区别:

  • 如果是分区主题(一个主题拥有多个队列的主题),broker服务端会根据消费者优先级和消费者名称字典顺序进行排序,然后Broker会将主题中的分区平均分配给高优先级的消费者,低优先级消费者会成为分区的备消费者。
  • 如果是非分区主题,Broker按照消费者订阅主题的顺序选择主消费者,其他的成为备消费者。

非分区主题的订阅示例说明如下:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

对于分区主题的订阅图解说明如下:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

温馨提示:Pulsar的Failover订阅模式可以类比RocketMQ中的集群消费模式下的平均分配算法。

1.2.3 共享/轮询模式(Shared)

在Shared订阅模式下,多个消费者可以附加到同一个订阅,消息以循环分发的方式轮流发送给各个消费者,并且任何给定的消息只会传递给一个消费者,当一个消费者断开连接时,所有发送给它并未被确认的消息将重新调度,再发送给其他消费者。

Shared模式的订阅图解如下所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

上图中的ConsumerA、ConsumerB、ConsumerC都会参与消息消费。

Shared模式与Failover模式的主要差别是Shared模式并不和消费者绑定队列,即Shared模式将所有分区的消息当成一个整体来看,

使用Shared模式需要的几个注意事项:

  • 无法保证消息的顺序性
  • Shared模式不能使用累积确认机制。

这种模式的最大缺点是不能启用累积确认机制,消息确认效率会降低,但其优势也比较明显,在解决单个队列积压方面,能充分所有消费者的处理能力。

1.2.4 基于Key的共享模式(Key_Shared)

在Key_Shared模式中,多个消费者可以附加到同一个订阅。具有相同Key的消息会分发给同一个消费者。

Key_Shared模式的消息分发机制如下所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

Pulsar提供了Sticky(粘性)、Auto-split Hash Range(自动分割哈希范围)、Auto-split Consistent Hashing(自动分割一致性哈希)这三种选择算法。

选择消费者的基本过程如下所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

  • 将分片Key传递到一个哈希函数,生成一个哈希值
  • 将Key的哈希值传入到对应的分片算法中,从而选择出一个消费者。

当一个新的消费者加入或者一个消费者退出时,分配算法都将会重新计算消息到消费者的映射(选择)。

接下来分别介绍这三种分配算法底层的工作机制。

1.2.4.1 Auto-split Hash Range

分段哈希取模算法,设定的总区间为0~65536,再根据消费者个数来分配段,然后用消费者的key进行哈希算法得出hash值,再用这个hash值域65535取模得到最终的区间值,然后看该值落在哪个区间,就由哪个消费者来消费,示例如下:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

下面用图解的方式说明当新增消费者或减少消费者,分段哈希取模算法是如何进行重新映射的:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

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的区别是每一个消费者对应的区间是固定的,并且由使用者来保证每一个消费者的区间不会重叠。

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

对应消费者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的消息在被统一时刻只会传递给单一的消费者处理,但当一个消费者新加入时,一些键的映射会发生改变,说明如下所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

为了避免相同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共同处理的特殊类型的主题,分区的主题的示意图如下所示:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

正如上图中那样,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中的系统主题如下图所示:

从12个方面详细解读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)

如果没有启用消息重复删除机制,下图展示了消息被持久化多次的情况:

从12个方面详细解读Pulsar的主题与订阅-鸿蒙开发者社区

也就是当消息发送者由于超时进行重试时,Broker收到了同一个客户端多条内容相同的消息,Broker会将多条内容相同的消息存储多次,造成消息在服务端的重复存储。

Pulsar支持消息重复删除机制,如果开启了消息重试机制,其工作示意如下所示:

从12个方面详细解读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,表示无超时时间



文章转载自公众号:中间件兴趣圈

分类
标签
已于2023-6-7 16:02:37修改
收藏
回复
举报
回复
    相关推荐