Pulsar知识整理

关键特性

  • Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。

  • 极低的发布延迟和端到端延迟。

  • 可无缝扩展到超过一百万个 topic。
  • 简单的客户端 API,支持 Java、Go、Python 和 C++。
  • 支持多种 topic 订阅模式(独占订阅、共享订阅、故障转移订阅)。
  • 通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递 。
  • 基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。
  • 分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如S3、GCS)中。

消息

Pulsar采用发布订阅的设计模式(pub-sub),该模式下,producer发送消息到topic,consumer订阅topic,消费producer发布的消息,并在处理完成后向broker发送确认。

消息是pulsar的基础“单元”,消息的组成如下:

组件

描述

Value/data payload 值。消息携带的消息体,二进制数据。
Key 键。消息可以选择使用key进行标记,这个与消息的顺序,topic压缩等操作有关系
Properties 属性。用户可以自定义的一些键值对
Sequence ID 序号。所有保存在topic中的消息,都会有一个排序的序号。类比Kafka,就是消息的Offset
Publish time 发送时间。producer发送消息时的时间戳
EventTime 事件时间。一个可选的时间戳。
TypeMessageBuilder 这个是用来构造一个消息。

Producer生产者

发送模式

Producer 可以以同步(sync) 或 异步(async) 的方式发布消息到 broker。

  • 同步发送:producer会同步等待broker给它一个ack,如果收不到ack,就认为消息发送失败。
  • 异步发送:Producer 将把消息放到阻塞队列里,并立即返回。然后,客户端将在后台将消息异步发送给 broker。 如果队列已满,则调用API的时候,producer可能会立即被阻止或失败,具体取决于传递给 producer 的参数。

访问模式

Producer访问topic的模式有分享(Shared)、独占(Exclusive)、等待独占(waitForExclusive):

  • 分享模式:可以同时有多个producer,向一个topic发送消息。
  • 独占模式:只有一个producer可以向topic发送消息。如果已经有一个producer连接了该topic,其他试图在该topic上发布消息的producer会马上得到错误信息。
  • 等待独占模式:producer连接到topic上,如果有其他producer已经连接了该topic,则新的producer连接会被挂起,直到独占topic的连接结束(主-备,只有主能操作,主挂了,预备的连接升级为主)。

压缩

Producer支持在传输消息的时候,对消息进行压缩,支持的压缩算法有:

  • LZ4
  • ZLIB
  • ZSTD
  • SNAPPY

批量处理

Pulsar支持启用消息的批量处理,开启批量处理的时候,producer会积累请求的消息,并合并为一个批次,一次性发送。批量处理的量大小由“最大消息数”和“最大延迟时间”两个配置决定。达到“最大消息数”或者“最大延迟时间”就会发送。

在Pulsar中,批次被跟踪存储为单个单元,而不是每个消息独立存储为单个单元。

Consumer处理消息的时候,也是按整个批次接收,接收后,再将批次拆分成一个个独立的消息。

当Consumer确认了一个批次中的所有消息,这个批次才会被判定为已确认。这就有可能造成重复消费的问题,各种异常、nack,确认超时等情况发生,会导致broker会把批次消息重新推送给consumer,即使其中一部分consumer已经消费过。

为了解决这个问题,Pulsar在2.6.0之后,引入的批次ack下标的概念,broker会维护每个批次中的消息ack下标,避免向consumer推送重复的消息。

但是默认情况下,broker这个功能是关闭的(acknowledgmentAtBatchIndexLevelEnabled=false)。因为开启批次下标功能,会导致更多的内存开销(因为要维护每个批次的内部下标)。

分块

当启用分块的时候(chunkingEnabled=true),如果消息的大小超过配置的最大大小,则producer会将原始消息分隔成多个块,并将它们与块的元数据单独和按顺序地发送到broker。

在broker中,分块消息将和普通消息以相同的方式存储在Managed Ledger上。

consumer缓存的收到的块消息,直到收到消息的所有块。然后consumer将分块拼接成真正的消息,并将它放入到接收队列。这个时候,客户端才算真正接收到消息,从队列取出消息进行消费。如果consumer未能在过期时间内接受到消息的所有分块,则会放弃过期未完成的分块消息,默认1小时。

consumer一旦确认消费整个分块消息,则内部会将消息对应的所有分块都进行确认。

因为需要缓存分块消息,所以可能出现分区消息过多,缓冲区爆满的情况。所以有maxPendingChunkedMessage配置,当缓存的消息量达到这个值,新的分块消息,consumer就会通过静默ack或者nack,并让broker稍后把这些消息重发。

Consumer消费者

consumer向topic发起订阅,处理producer发送到topic的消息。

consumer向broker发送消息流获取申请,以获取信息。在consumer端有一个队列,用于接收broker推送来的消息。每当consumer.receive()调用一次,就从缓冲区获取一条信息。

接收模式

consumer可以通过同步(Sync)或(异步)的方式从broker接收数据。

发送模式

说明

同步接收 同步模式,在收到消息之前都是被阻塞的。
异步接收 异步接收模式会立即返回一个future,一旦收到新的消息,就立即完成。

确认

producer发送消息到broker之后,就会被永久保存起来。consumer成功消费了一条消息后,会向broker发送一个确认消息,broker在收到消费者消费成功的消息后,才有可能会被删除。如果希望消息被consumer消费后,还能被保留,可以配置“消息保留策略”。

消息有两种方式去确认:

  • 分开确认:消费者对每个消息独立确认。例如broker向consumer发送了1,2,3,4,四个消息,consumer可以直接确认2、4。但是1、3没确认。
  • 累计确认:消费者只确认最后一条消息,该消息之前的所有消息也都会被确认。同样以1、2、3、4为例,确认4,则4个消息都会确认被消费了。这个就与kafka的提交offset类似了。

注意:累计确认,不能在“共享订阅模式”下使用!!!因为该模式涉及到多个访问相同订阅的消费者,只能按单条消费确认。

取消确认

当consumer没有成功消费某条消息,希望后面重新消费该消息,则可以给broker发送一个nack请求。

注意:

  • 在独占消费模式和灾备订阅模式中,消费者仅仅只能对收到的最后一条消息进行nack。
  • 在共享订阅和KEY共享模式中,你可以对单个消息进行nack。
  • 独占、灾备和共享模式,本来是有序的,但是使用nack,可能会导致消息乱序。
  • 如果开启批量处理,nack一个消息时,跟这个消息同个批次的其他消息也会在后续重新投递给consumer。

确认超时

如果consumer在指定的时间内没有给broker发送ack或nack消息,客户端会跟踪“超时未确认”的消息。并在指定超时时间后,给broker发送一个“重发未确认消息”的请求。

注意:尽量优先使用nack~

死信主题

当consumer没有办法成功消费某个消息的时候,可以将该消息暂存到另外一个特殊的topic,然后去消费该消息之后的其他消息,这个特殊的topic,就是“死信主题”。

java的实例代码:

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)

              .topic(topic)

              .subscriptionName("my-subscription")

              .subscriptionType(SubscriptionType.Shared)

              .deadLetterPolicy(DeadLetterPolicy.builder()

                    .maxRedeliverCount(maxRedeliveryCount)

                    .deadLetterTopic("your-topic-name")

                    .build())

              .subscribe();

deadLetterTopic并不是必须的,没有指定私信主题名称的时候,默认主题为:--DLQ。

当前死信主题能在共享和key共享两种模式下使用。

重试主题

死信主题,能在我们消费某些消息失败的时候,将这些消息暂存起来,待后续处理。但是我们现实的业务场景,大多是希望自动重试,例如消息消费失败,1分钟之后重新消费该信息,所以就有了“重试主题”。

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)

                .topic(topic)

                .subscriptionName("my-subscription")

                .subscriptionType(SubscriptionType.Shared)

                .enableRetry(true)

                .receiverQueueSize(100)

                .deadLetterPolicy(DeadLetterPolicy.builder()

                        .maxRedeliverCount(maxRedeliveryCount)

                        .retryLetterTopic("persistent://my-property/my-ns/my-subscription-custom-Retry")

                        .build())

                .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)

                .subscribe();

Topic主题

Topic的结构如下:

{persistent|non-persistent}://tenant/namespace/topic

Topic名称组成

说明

persistent/non-persistent

topic的类型。Pulsar支持“持久化”和“非持久化”两种topic类型。默认是持久化类型。对于持久化类型的topic,所有消息都会被持久化到磁盘中,而非持久化的topic,数据只会保存在内存中。

当broker接收到非持久化topic的消息,会马上投递给当前或者的consumer;如果当前没有活着的consumer,则消息丢失。因为没有经过持久化,所以会比普通的持久化topic要快,但是会丢信息。

租户(tenant) Pulsar用于支持多租户的,数据基于租户隔离。
命名空间(namespace) 用于将相关联的topic作为一个组来管理。每个租户下一个配置多个命名空间。
主题名 主题的名字。

注意:不需要特意去Pulsar上创建topic,当使用一个不存在的topic时,Pulsar会自动创建这个topic。

订阅模式

Pulsar中有四种订阅模式:独占、灾备、共享和key共享:

Pulsar知识整理_第1张图片

注意:当一个subscription没有任何consumer的时候,它的订阅模式是undefined。当有一个consumer连接上subscription时,该consumer使用的模式,就决定了整个subscription的模式。所有,可以通过修改配置,然后重启所有的consumer来改变subscription的模式

独享模式(Exclusive):

该模式下,只允许一个consumer连接到subscription,如果有第一个consumer连接到subscription,则会报错。

Pulsar知识整理_第2张图片

灾备模式(Failover):

在灾备模式下,允许多个consumer同时连接到相同的subscription,但是只要一个master的consumer能接收到“分分区topic”或“分区topic的分区”的消息。当作为master的consumer断开连接了,就会把所有未ack的消息投递给下一个或者的consumer(故障转移)。

对于分区的topic,broker会把所有consumer的优先级和consumer的名称的字典顺序来排序,然后broker尽量将分区均匀地分配给优先级高的consumer。

对于非分区的topic,第一个订阅的consumer就是master。

Pulsar知识整理_第3张图片

共享模式(Shared):

在共享模式下,允许多个consumer同事连接到相同的subscription,消息会被轮询的投递到这些consumer。如果有一个consumer挂了,那些已经发送给它,但是未得到ack的消息,会被重新分发给其他或者的consumer。

注意:

  • 消息的顺序没有保障。因为消息被轮询投递给各个consumer,多个consumer之间,是并发处理消息
  • 无法使用“累计确认”,因为同一个topic的消息,分别发送给了不同的consumer,如果允许使用“累计确认”,会将其他consumer接收到的消息也确认了

Pulsar知识整理_第4张图片

key共享模式(Key_Shared):

该模式跟普通共享模式差不多,也允许多个consumer同时消费信息,但是会把key相同的消息投递给同一个consumer,大致能保证相同key的消息的顺序。

当有一个consumer新增或者挂掉的时候,会改变消息的分配逻辑。例如,是key%活着的consumer,得到消息需要投递给哪一个consumer,如果新增或者减少consumer数量,就会导致分配的consumer不一样。

注意:

  • consumer使用key共享模式,对应的producer发送消息的时候,就必须设置key或者排序key。producer没设置key,broker咋用key类分配消息给consumer呢~
  • 不能使用“累计确认”。跟共享模式道理一样,会造成消息误提交。
  • producer不能使用批量处理,或者使用就key的批量处理。普通的批处理,同一个批次里的消息key不一样,可能需要分配给不同的consumer,但实际上,一个批次是真正的一个单元,只能给一个consumer

Pulsar知识整理_第5张图片

多主题订阅

从Pulsar1.23.0-incubating版本之后,Pulsar的consumer端就支持同时订阅多个topic。可以有两种方式:

  • 正则匹配,像persistent://public/default/finance-*,public租户的默认命名空间下,所有以finance-开头的topic,都订阅了。
  • 明确指定多个topic。

注意:订阅多个topic的时候,多个topic之间消息的顺序性没有保证

分区topic

普通的topic,只被保存在单个broker上,限制了主题的吞吐量。分区topic是一种特殊的topic,可以分布在多个broker上,提高topic的吞吐量。

分区topic底层实际上是N个内部的普通topic,N就是分区数,这些内部的普通topic可以被分配到不同的broker上。

当向分区topic发送消息的时候,每条消息会被路由到其中的一个broker。Pulsar自动处理跨broker的分区分布。

Pulsar知识整理_第6张图片

路由模式:

前面我们提到过“订阅模式”,订阅模式是消息怎么投递给consumer;现在讲的是“路由模式”,producer怎么把消息发给broker。

Pulsar有三种路由模式:

  • 轮询分区(RoundRobinPartition):如果消息没有指定key,为了达到最大吞吐量的目的,producer会将消息轮询投递给各个分区(批量处理的情况下,轮询处理的不是消息,而是批次)。如果指定了key,producer会根据key的hash值将该消息分配到对应的分区。默认模式。
  • 单个分区(SinglePartition):如果没有指定key,producer会随机选择一个分区,并且发布的所有消息都分配到该分区。如果消息指定了key,producer会根据key的hash值将该消息分配到对应的分区。
  • 自定义分区(CustomPartition):使用自定义的消息路由器来实现分区路由。

顺序保证:

当使用“轮询分区”、“单个分区”的路由模式,producer只要指定key,则相同key的消息会被投递到同一个分区,key相同的消息,顺序有保证。

当使用“单个分区”的路由模式,producer没有指定key,则所有消息都会被投递到同一个分区,顺序有保证,但是吞吐量就低了,降级为普通topic。

消息保留策略

Pulsar默认的消息保留策略为:

  • 马上删除所有consumer ack的消息。
  • 以backlog的形式,持久保存未被ack的消息。

有两个特性,可以覆盖上面的行为:

消息保留。允许保存已经被consumer ack了的消息。

Pulsar知识整理_第7张图片

消息过期 Time-To-Live(TTL)。允许给消息设置一个TTL,消息过期了,即使消息还没被消费,也会被清除。

Pulsar知识整理_第8张图片

消息去重

Pulsar支持对消息去重,同一条消息即使被重复投递多次,在broker端只会被保存一次。

消息去重,实际上就是“生产者幂等”。

使用其他消息中间件,都没有提供该特性的支持,当要保证消息发送的可靠性,就会引入失败重试逻辑。而这往往就会导致消息的重复发送,消费者端就被迫需要去做消息幂等处理,实现“消费者幂等”。

所以Pulsar对于那些需要实现“仅一次”的场景,会更友好。

要启用消息去重,需要同时对broker和client两端做配置。

有三种级别来启用broker端的消息去重:

1、broker级别启用,所有的namespace和topics都会去重。相关配置如下:

参数名

说明

默认值

brokerDeduplicationEnabled 整个broker级别的去重配置开关,true为开启,fasle为关闭。 false
brokerDeduplicationMaxNumberOfProducers 为了去重目的而存储信息的最大producer数量。 10000
brokerDeduplicationEntriesInterval

一个快照文件能保存的最大消息数。

配置较大的值,产生的快照文件数量会比较少;但是这样会导致tpoics的恢复变慢(要重放快照文件)。

1000
brokerDeduplicationProducerInactivityTimeoutMinutes producer掉线多长时间后会,broker会把快照文件清除。 360(单位:分钟)

2、即使brokerDeduplicationEnabled=false,没有开启去重功能。也可以单独在namespace下开启:

pulsar-admin namespaces set-deduplication \

public/default \
--enable

3、也可以只开启单独的topics:

pulsar-admin topics set-deduplication

client端启用去重,需要做两件事:

  • 指定producer名称。
  • 设置消息超时时间为0,不超时。

为什么做这两件事情,就是启动客户端去重呢?

producer发送消息的时候有SequenceNumber的概念,每发一个消息,就会+1。

在broker端,会根据producer的名称来保存producer发送的消息对应的SequenceNumber。

至于为什么要设置消息超时时间为0,就不大懂了,可能是消息超时被删会影响去重判断?

消息延迟投递

我们很多场景,需要在一段时间后,触发一个事情。例如开会15分钟之前发送一个提醒~Pulsar的延迟消息功能就符合这种场景。

发送延迟消息,消息会被存储在BookKeeper,broker保存消息的时候,并不会做任何检查的(不会判断消息是延迟消息,还是正常消息)。

而是当consumer消费一个消息的时候,判断消息是否是延迟消息,如果是延迟消息,会将消息加入到DelayedDeliveryTracker。

DelayedDeliveryTracker会在内存中保存一个时间索引(时间→消息id),DelayedDeliveryTracker检查延迟时间到了之后,才把消息重新投递consumer。

注意:目前延迟消息只能作用于“共享订阅”模式。在“独占”或者“灾备”模式下,消息都是直接投递

Pulsar知识整理_第9张图片

Broker

Pulsar的broker是一个无状态的组件,主要负责运行两个组件:

  • 作为一个http服务,暴露restful接口,供管理任务和producer/consumer发现topic使用。producer连接到broker来发消息,consumer连接到broker来消费消息。
  • 作为分发器,使用自定义的协议,采用异步TCP发送数据。为了让效率更高,消息的分发,一般是通过managed leger的缓存来实现。但是当消息堆积,缓存超过的最大限制,broker就会开始从BookKeeper里读取数据。

注意:Pulsar的集群是支持跨集群复制数据的。为了支持Topic的异地复制,Broker会使用Relicators追踪本地发布的数据,并把这些数据用JAVA 客户端重新发布到其他地方。

集群

一个Pulsar实例可以加入多个集群,在集群之前实现数据同步。

单个Pulsar集群由三部分组成:

  • 一个或多个broker。broker与Pulsar的配置存储层交互,处理各种协作任务,并且使用BookKeeper来存储消息;依赖Zookeeper来处理特定的任务等。
  • 包含一个或多个bookie的BookKeeper集群负责消息的存储。
  • 一个Zookeeper,用来处理Pulsar集群之间的协调任务。

Pulsar知识整理_第10张图片

注意:集群内部需要一个Zookeeper集群,保存元数据;集群之间还有一个Zookeeper集群,用来处理Pulsar集群之间的协调任务。

元数据存储

Pulsar集群使用Zookeeper来存储元数据,集群配置和协调任务。

在一个Pulsar实例中:

  • 保存租户、命名空间和其他实体,并且保证数据的全局唯一性。
  • 每个集群都有自己的本地Zookeeper,用来存储集群特定的配置和任务协调,例如:哪些broker负责哪些topic的元数据、broker的负载报告、BookKeeper leger的元数据等。

持久存储

Pulsar提供的消息投递的可靠性的保障。即一个消息被成功投递到broker,它就一定会被投递到它的目标。

为了提供这种保证,未被consumer ack的消息,会一直被保存到consumer ack。

Apache BookKeeper

Pulsar用Apache BookKeeper作为持久化存储,这是一个分布式的预写日志(WAL)系统,以下特性很适合用于Pulsar:

  • 能让Pulsar去利用许多独立的日志(ledgers),这些ledgers会随便时间,由topic创建。
  • 提供了高效的顺序存储,可以用于数据的复制。
  • 保证了在发生系统故障时的读一致性。
  • 保证了在bookies之间均匀的I/O分布。
  • 容量和吞吐量有良好的水平拓展性。给BookKeeper集群添加新的节点,即可提高容量。
  • Bookies被设计成支持上千ledgers的并发读写。它使用多个存储设备,一个用于存储日志,一个用于普通的存储。这样Bookis可以将读写操作的影响隔离开。

另外,consumer订阅的消费位置cursors,也是存储在BookKeeper。

Pulsar知识整理_第11张图片

Ledgers

Ledger是一个只追加的数据结构,并且只有一个写入器,这个写入器负责多个BookKeeper存储节点(就是Bookies)的写入。 Ledger的条目会被复制到多个bookies。Ledgers本身有着非常简单的语义:

  • Pulsar Broker可以创建ledeger,添加内容到ledger和关闭ledger。
  • 当一个ledger被关闭后,除非明确的要写数据或者是因为写入器挂掉导致ledger关闭,否则这个ledger只会以只读模式打开。
  • 当ledger中的条目不再有用的时候,整个legder可以被删除(ledger分布是跨Bookies的)。

Ledger读一致性

BookKeeper的主要优势在于它能在有系统故障时保证读的一致性。由于Ledger只能被一个进程写入(之前提的写入器进程),这样这个进程在写入时不会有冲突,从而写入会非常高效。

在一次故障之后,ledger会启动一个恢复进程来确定ledger的最终状态并确认最后提交到日志的是哪一个条目。 在这之后,能保证所有的ledger读进程读取到相同的内容。

Managed ledgers

managed ledgers是一个在ledgers之上构造的概念,是BookKeeper ledgers提供的一个单一的日志抽象,作为一个topic的存储层。

也就是消息流的抽象,有一个写入器进程,不断的在消息流的尾部添加消息(producer发送消息),并且有多个cursor消费这个流(consumer订阅消息),每个cursor都有自己的消费位置。

一个managed ledgers底层使用多个BookKeeper ledgers来保存数据。使用多个ledgers的原因有:

  • 故障之后,如果原来的ledgers不能写了,就可以开一个新的ledgers
  • 当一个ledgers里的消息,已经被所有订阅的cursor消费了,则这个ledgers可以被删除。

日志存储

在BookKeeper中,最重要的日志,就是事务日志。在写数据到ledger之前,bookie会确保这个更新的事务日志,已经持久化到存储上。

当Bookie启动,或者旧的日志文件的大小达到最大限制时,会创建新的日志文件。

Pulsar proxy

Pulsar客户端和Pulsar集群的交互,可以直接连接Pulsar的brokers。但是某些情况下,这种直连无法做到,例如pulsar部署在云环境或者K8S中,ip并不固定。

Pulsar proxy就是为了解决这种问题,它扮演网关的角色,客户端与它通讯,而不是直接与broker。

Pulsar proxy是直接从Zookeeper上读取它需要的信息,例如broker的ip和端口(broker启动后,会被自己的信息,注册到Zookeeper中)。

相关资料

Messaging · Apache Pulsar

看这篇就够了!RocketMQ、Kafka、Pulsar事务消息|原子性|kafka|回滚|事务型|rocketmq_网易订阅

你可能感兴趣的:(pulsar,知识整理,pulsar)