Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。
极低的发布延迟和端到端延迟。
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 可以以同步(sync) 或 异步(async) 的方式发布消息到 broker。
Producer访问topic的模式有分享(Shared)、独占(Exclusive)、等待独占(waitForExclusive):
Producer支持在传输消息的时候,对消息进行压缩,支持的压缩算法有:
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向topic发起订阅,处理producer发送到topic的消息。
consumer向broker发送消息流获取申请,以获取信息。在consumer端有一个队列,用于接收broker推送来的消息。每当consumer.receive()调用一次,就从缓冲区获取一条信息。
consumer可以通过同步(Sync)或(异步)的方式从broker接收数据。
发送模式 |
说明 |
---|---|
同步接收 | 同步模式,在收到消息之前都是被阻塞的。 |
异步接收 | 异步接收模式会立即返回一个future,一旦收到新的消息,就立即完成。 |
producer发送消息到broker之后,就会被永久保存起来。consumer成功消费了一条消息后,会向broker发送一个确认消息,broker在收到消费者消费成功的消息后,才有可能会被删除。如果希望消息被consumer消费后,还能被保留,可以配置“消息保留策略”。
消息有两种方式去确认:
注意:累计确认,不能在“共享订阅模式”下使用!!!因为该模式涉及到多个访问相同订阅的消费者,只能按单条消费确认。
当consumer没有成功消费某条消息,希望后面重新消费该消息,则可以给broker发送一个nack请求。
注意:
如果consumer在指定的时间内没有给broker发送ack或nack消息,客户端会跟踪“超时未确认”的消息。并在指定超时时间后,给broker发送一个“重发未确认消息”的请求。
注意:尽量优先使用nack~
当consumer没有办法成功消费某个消息的时候,可以将该消息暂存到另外一个特殊的topic,然后去消费该消息之后的其他消息,这个特殊的topic,就是“死信主题”。
java的实例代码:
|
deadLetterTopic并不是必须的,没有指定私信主题名称的时候,默认主题为:- -DLQ。
当前死信主题能在共享和key共享两种模式下使用。
死信主题,能在我们消费某些消息失败的时候,将这些消息暂存起来,待后续处理。但是我们现实的业务场景,大多是希望自动重试,例如消息消费失败,1分钟之后重新消费该信息,所以就有了“重试主题”。
|
|
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共享:
注意:当一个subscription没有任何consumer的时候,它的订阅模式是undefined。当有一个consumer连接上subscription时,该consumer使用的模式,就决定了整个subscription的模式。所有,可以通过修改配置,然后重启所有的consumer来改变subscription的模式。
独享模式(Exclusive):
该模式下,只允许一个consumer连接到subscription,如果有第一个consumer连接到subscription,则会报错。
灾备模式(Failover):
在灾备模式下,允许多个consumer同时连接到相同的subscription,但是只要一个master的consumer能接收到“分分区topic”或“分区topic的分区”的消息。当作为master的consumer断开连接了,就会把所有未ack的消息投递给下一个或者的consumer(故障转移)。
对于分区的topic,broker会把所有consumer的优先级和consumer的名称的字典顺序来排序,然后broker尽量将分区均匀地分配给优先级高的consumer。
对于非分区的topic,第一个订阅的consumer就是master。
共享模式(Shared):
在共享模式下,允许多个consumer同事连接到相同的subscription,消息会被轮询的投递到这些consumer。如果有一个consumer挂了,那些已经发送给它,但是未得到ack的消息,会被重新分发给其他或者的consumer。
注意:
key共享模式(Key_Shared):
该模式跟普通共享模式差不多,也允许多个consumer同时消费信息,但是会把key相同的消息投递给同一个consumer,大致能保证相同key的消息的顺序。
当有一个consumer新增或者挂掉的时候,会改变消息的分配逻辑。例如,是key%活着的consumer,得到消息需要投递给哪一个consumer,如果新增或者减少consumer数量,就会导致分配的consumer不一样。
注意:
从Pulsar1.23.0-incubating版本之后,Pulsar的consumer端就支持同时订阅多个topic。可以有两种方式:
注意:订阅多个topic的时候,多个topic之间消息的顺序性没有保证。
普通的topic,只被保存在单个broker上,限制了主题的吞吐量。分区topic是一种特殊的topic,可以分布在多个broker上,提高topic的吞吐量。
分区topic底层实际上是N个内部的普通topic,N就是分区数,这些内部的普通topic可以被分配到不同的broker上。
当向分区topic发送消息的时候,每条消息会被路由到其中的一个broker。Pulsar自动处理跨broker的分区分布。
路由模式:
前面我们提到过“订阅模式”,订阅模式是消息怎么投递给consumer;现在讲的是“路由模式”,producer怎么把消息发给broker。
Pulsar有三种路由模式:
顺序保证:
当使用“轮询分区”、“单个分区”的路由模式,producer只要指定key,则相同key的消息会被投递到同一个分区,key相同的消息,顺序有保证。
当使用“单个分区”的路由模式,producer没有指定key,则所有消息都会被投递到同一个分区,顺序有保证,但是吞吐量就低了,降级为普通topic。
Pulsar默认的消息保留策略为:
有两个特性,可以覆盖上面的行为:
消息保留。允许保存已经被consumer ack了的消息。
消息过期 Time-To-Live(TTL)。允许给消息设置一个TTL,消息过期了,即使消息还没被消费,也会被清除。
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发送消息的时候有SequenceNumber的概念,每发一个消息,就会+1。
在broker端,会根据producer的名称来保存producer发送的消息对应的SequenceNumber。
至于为什么要设置消息超时时间为0,就不大懂了,可能是消息超时被删会影响去重判断?
我们很多场景,需要在一段时间后,触发一个事情。例如开会15分钟之前发送一个提醒~Pulsar的延迟消息功能就符合这种场景。
发送延迟消息,消息会被存储在BookKeeper,broker保存消息的时候,并不会做任何检查的(不会判断消息是延迟消息,还是正常消息)。
而是当consumer消费一个消息的时候,判断消息是否是延迟消息,如果是延迟消息,会将消息加入到DelayedDeliveryTracker。
DelayedDeliveryTracker会在内存中保存一个时间索引(时间→消息id),DelayedDeliveryTracker检查延迟时间到了之后,才把消息重新投递consumer。
注意:目前延迟消息只能作用于“共享订阅”模式。在“独占”或者“灾备”模式下,消息都是直接投递。
Pulsar的broker是一个无状态的组件,主要负责运行两个组件:
注意:Pulsar的集群是支持跨集群复制数据的。为了支持Topic的异地复制,Broker会使用Relicators追踪本地发布的数据,并把这些数据用JAVA 客户端重新发布到其他地方。
一个Pulsar实例可以加入多个集群,在集群之前实现数据同步。
单个Pulsar集群由三部分组成:
注意:集群内部需要一个Zookeeper集群,保存元数据;集群之间还有一个Zookeeper集群,用来处理Pulsar集群之间的协调任务。
Pulsar集群使用Zookeeper来存储元数据,集群配置和协调任务。
在一个Pulsar实例中:
Pulsar提供的消息投递的可靠性的保障。即一个消息被成功投递到broker,它就一定会被投递到它的目标。
为了提供这种保证,未被consumer ack的消息,会一直被保存到consumer ack。
Pulsar用Apache BookKeeper作为持久化存储,这是一个分布式的预写日志(WAL)系统,以下特性很适合用于Pulsar:
另外,consumer订阅的消费位置cursors,也是存储在BookKeeper。
Ledger是一个只追加的数据结构,并且只有一个写入器,这个写入器负责多个BookKeeper存储节点(就是Bookies)的写入。 Ledger的条目会被复制到多个bookies。Ledgers本身有着非常简单的语义:
Ledger读一致性
BookKeeper的主要优势在于它能在有系统故障时保证读的一致性。由于Ledger只能被一个进程写入(之前提的写入器进程),这样这个进程在写入时不会有冲突,从而写入会非常高效。
在一次故障之后,ledger会启动一个恢复进程来确定ledger的最终状态并确认最后提交到日志的是哪一个条目。 在这之后,能保证所有的ledger读进程读取到相同的内容。
managed ledgers是一个在ledgers之上构造的概念,是BookKeeper ledgers提供的一个单一的日志抽象,作为一个topic的存储层。
也就是消息流的抽象,有一个写入器进程,不断的在消息流的尾部添加消息(producer发送消息),并且有多个cursor消费这个流(consumer订阅消息),每个cursor都有自己的消费位置。
一个managed ledgers底层使用多个BookKeeper ledgers来保存数据。使用多个ledgers的原因有:
在BookKeeper中,最重要的日志,就是事务日志。在写数据到ledger之前,bookie会确保这个更新的事务日志,已经持久化到存储上。
当Bookie启动,或者旧的日志文件的大小达到最大限制时,会创建新的日志文件。
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_网易订阅