Pulsar简介
Pulsar是云原生分布式消息流平台(即可作为消息中间件),最初源于Yahoo,支持Yahoo应用服务140万个主题,日处理超过1000亿条消息。Pulsar于2016年开源并捐赠给Apache软件基金会,现为Apache软件基金会顶级项目。
Pulsar 自从出身就不断的再和其他的消息队列(Kafka,RocketMQ 等等)做比较,但是 Pulsar 的设计思想和大多数的消息队列中间件都不同,具备了高吞吐,低延迟,计算存储分离,多租户,异地复制等功能,所以 Pulsar 也被誉为下一代消息队列中间件。
Pulsar的特性如下:
- 支持多租户,通过多租户可为每个租户单独设置认证机制、存储配额、隔离策略等。
- 具有高吞吐、低延时、强容错等特性
- 原生支持多集群部署,集群间支持无缝的数据复制(Geo-replication),也就是说可以实现在新加坡集群上生产消息,在美国集群上进行消费。
- 高扩展性,能够支撑上百万个topic
- 支持多语言客户端,如Java、Go、Python和C++
- 支持多种消息订阅模式(exclusive, shared, failover,下文会有介绍)
- 高可靠的消息持久化存储
- 支持数据分层式存储,可将冷数据保存到S3、GCS等低成本的存储系统中
为什么使用Pulsar
pulsar样例
生产者
消费者
Pulsar基础知识
Topic组成部分
在Pulsar中topic的格式为:{persistent|non-persistent}://tenant/namespace/topic
这里会涉及到几个概念
topic名称组成 | 描述 |
持久化/非持久化 | pulsar支持两种主题类型:持久化和非持久化,默认是持久化。对于持久化的主题,消息会持久化到磁盘中,而非持久化则不会。 |
租户(tenant) | 租户是topic最基本单位,租户可以跨集群分布,每个租户都可以有单独的认证和授权机制,租户也是存储配额、消息TTL(即消息自动确认时间)和隔离策略的管理单元。 |
命名空间(namespace) | 将相关联的topic作为一个组来管理,是管理topic的基本单元。大多数对topic的管理都是以命名空间为粒度,比如GEO-replication是以namespace为单元。 |
主题(topic) | 即主题名 |
通俗理解可以把这样的层级看作一个公司由不同的部门(tenant),每个部门有不同的组(Namespace),每个组有不同的业务(topic)
下图以一个直观的视角来看一个topic的组成:
Topic
和其他消息队列类似,Pulsar中也有Topic。Topic即在生产者与消费者中传输消息的通道。消息可以以Topic为单位进行归类,生产者负责将消息发送到特定的Topic,而消费者指定特定的Topic进行消费。
分区Topic(Topic-Partition)
Pulsar的Topic可以分为非分区Topic和分区Topic。
普通的Topic仅仅被保存在单个Broker中,这限制了Topic的最大吞吐量。分区Topic是一种特殊类型的主题,支持被多个Broker处理,从而实现更高的吞吐量。
针对一个Topic,可以设置多个Topic分区来提高Topic的吞吐量。每个Topic Partition由Pulsar分配给某个Broker,该Broker称为该Topic Partition的所有者。生成者和消费者会与每个Topic分区的Broker创建链接,发送消息并消费消息。
如下图所示,Topic1有Partition1、Partition2、Partition3、Partition4、Partition5五个分区,Partition1和Partition4由Broker1处理,Partition2和Partition5由Broker2处理,Partition3由Broker3处理。
从Pulsar社区版的golang-sdk可以看出,客户端的Producer和Consumer在初始化的时候,都会与每一个Topic-Partition创建链接,并且会监听是否有新的Partition,以创建新的链接。
非持久Topic
默认情况下,Pulsar会保存所有没确认的消息到BookKeeper中。持久Topic的消息在Broker重启或者Consumer出现问题时保存下来。
除了持久Topic,Pulsar也支持非持久Topic。这些Topic的消息只存在于内存中,不会存储到磁盘。
因为Broker不会对消息进行持久化存储,当Producer将消息发送到Broker时,Broker可以立即将ack返回给Producer,所以非持久Topic的消息传递会比持久Topic的消息传递更快一些。相对的,当Broker因为一些原因宕机、重启后,非持久Topic的消息都会消失,订阅者将无法收到这些消息。
重试Topic
由于业务逻辑处理出现异常,消息一般需要被重新消费。Pulsar支持生产者同时将消息发送到普通的Topic和重试Topic,并指定允许延时和最大重试次数。当配置了允许消费者自动重试时,如果消息没有被消费成功,会被保存到重试Topic中,并在指定延时时间后,重新被消费。
死信Topic
当Consumer消费消息出错时,可以通过配置重试Topic对消息进行重试,但是,如果当消息超过了最大的重试次数仍处理失败时,该怎么办呢?Pulsar提供了死信Topic,通过配置deadLetterTopic,当消息达到最大重试次数的时候,Pulsar会将消息推送到死信Topic中进行保存。
主题订阅模式
pulsar的主题订阅模式包括四种:独占(exclusive)、共享(shared)、灾备(failover)、key共享(key_shared)。
订阅模式
- 独占:一个订阅只与一个消费者可以关联,只有这个消费者接收到topic的全部消息,如果这个消费者故障了就会停止消费。该模式适用于全局有序的消息消费。
- 灾备:一个订阅可以与多个消费者关联,但只有一个消费者会消费到数据,当该消费者故障时,由另一个消费者来继续消费。该模式适用于全局有序的消息消费。
- 共享:一个订阅可以与多个消费者关联,消息通过轮询机制发送给不同的消费者。该模式适用于对消费顺序无要求的消息消费,YPulsar默认使用共享模式。
- key共享:一个订阅可以与多个消费者关联,消息根据给定的映射规则,相同key的消息由同一个消费者消费。该模式适用于按key有序的消息消费。类似于shared模式,但是相同键(key)的消息会传递给同一个消费者。
-
- 注意:需要为消息指定一个 key 或 orderingKey
- Key_Shared 模式不能使用累积确认(cumulative acknowledgment)
二、Pulsar生产者(Producer)
Producer是连接topic的程序,它将消息发布到一个Pulsar broker上。
(一)访问模式
消息生成者有多种模式访问Topic ,可以使用以下几种方式将消息发送到Topic。
Shared:默认情况下,多个生成者可以将消息发送到同一个Topic。
Exclusive:在这种模式下,只有一个生产者可以将消息发送到Topic ,当其他生产者尝试发送消息到这个Topic时,会发生错误。只有独占Topic的生产者发生宕机时(Network Partition)该生产者会被驱逐,新的生产者才能产生并向Topic发送消息。
WaitForExclusive:在这种模式下,只有一个生产者可以将消息发送到Topic。当已有生成者和Topic建立连接时,其他生产者的创建会被挂起而不会产生错误。如果想要采用领导者选举机制来选择消费者的话,可以采用这种模式。
(二)路由模式
当将消息发送到分区Topic时,需要指定消息的路由模式,这决定了消息将会被发送到哪个分区Topic。Pulsar有以下三种消息路由模式,RoundRobinPartition为默认路由模式。
RoundRobinPartition:如果消息没有指定key,为了达到最大吞吐量,生产者会以round-robin (轮询)方式将消息发布到所有分区。请注意round-robin并不是作用于每条单独的消息,而是作用于延迟处理的批次边界,以确保批处理有效。如果消息指定了key,分区生产者会根据key的hash值将该消息分配到对应的分区。这是默认的模式。
SinglePartition:如果消息没有指定key,生产者将会随机选择一个分区,并发布所有消息到这个分区。如果消息指定了key,分区生产者会根据key的hash值将该消息分配到对应的分区。
CustomPartition:自定义模式,用户可以创建自定义路由模式,通过实现MessageRouter接口实现自定义路由规则。
(三)批量处理
Pulsar支持对消息进行批量处理。批量处理启用后,Producer会在一次请求中累积并发送一批消息。批量处理时的消息数量取决于最大消息数(单次批量处理请求可以发送的最大消息数)和最大发布延迟(单个请求的最大发布延迟时间)决定。开启批量处理后,积压的数量是批量处理的请求总数,而不是消息总数。
索引确认机制
通常情况下,只有Consumer确认了批量请求中的所有消息,这个批量请求才会被认定为已处理。当这批消息没有全部被确认的情况下,发生故障时,会导致一些已确认的消息被重复确认。
为了避免Consumer重复消费已确认的消息,Pulsar从Pulsar 2.6.0开始采用批量索引确认机制。如果启用批量索引确认机制,Consumer将筛选出已被确认的批量索引,并将批量索引确认请求发送给Broker。Broker维护批量索引的确认状态并跟踪每批索引的确认状态,以避免向Consumer发送已确认的消息。当该批信息的所有索引都被确认后,该批信息将被删除。
默认情况下,索引确认机制处于关闭状态。开启索引确认机制将产生导致更多内存开销。
key-based batching
key_shared模式下,Broker会根据消息的key来分发消息,但默认的批量处理模式,无法保证将所有的相同的key都打包到同一批中,而且Consumer在接收到批数据时,会默认把第一个消息的key当作这批消息的key,这会导致消息的错乱。因此key_shared模式下,不支持默认的批量处理。
key-based batching能够确保Producer在打包消息时,将相同key的消息打包到同一批中,从而consumer在消费的时候,也能够消费到指定key的批数据。
没有指定key的消息在打包成批后,这一批数据也是没有key的,Broker在分发这批消息时,会使用NON_KEY作为这批消息的key。
(四)消息分块
启用分块后,如果消息大小超过允许发送的最大消息大小时,Producer会将原始消息分割成多个分块消息,并将分块消息与消息的元数据按顺序发送到Broker。
在Broker中,分块消息会和普通消息以相同的方式存储在Ledger中。唯一的区别是,Consumer需要缓存分块消息,并在接收到所有的分块消息后将其合并成真正的消息。如果Producer不能及时发布消息的所有分块,Consumer不能在消息的过期时间内接收到所有的分块,那么Consumer已接收到的分块消息就会过期。
Consumer会将分块的消息拼接在一起,并将它们放入接收器队列中。客户端从接收器队列中消费消息。当Consumer消费到原始的大消息并确认后,Consumer就会发送与该大消息关联的所有分块消息的确认。
处理一个producer和一个订阅consumer的分块消息
如下图所示,当生产者向主题发送一批大的分块消息和普通的非分块消息时。假设生产者发送的消息为M1,M1有三个分块M1-C1,M1-C2和M1-C3。这个Broker在其管理的Ledger里面保存所有的三个块消息,然后以相同的顺序分发给消费者(独占/灾备模式)。消费者将在内存缓存所有的块消息,直到收到所有的消息块。将这些消息合并成为原始的消息M1,发送给处理进程。
多个生产者和一个生产者处理块消息
当多个生产者发布块消息到单个主题,这个Broker在同一个Ledger里面保存来自不同生产者的所有块消息。如下所示,生产者1发布的消息M1,M1 由M1-C1,M1-C2和M1-C3三个块组成。生产者2发布的消息M2,M2由M2-C1,M2-C2和M2-C3三个块组成。这些特定消息的所有分块是顺序排列的,但是其在Ledger里面可能不是连续的。这种方式会给消费者带来一定的内存负担。因为消费者会为每个大消息在内存开辟一块缓冲区,以便将所有的块消息合并为原始的大消息。
三、Pulsar消费者(Consumer)
Consumer是通过订阅关系连接Topic,接收消息的程序。
Consumer向Broker发送flow permit request以获取消息。在 Consumer端有一个队列,用于接收从Broker推送来的消息。
(一)消息确认
Pulsar提供两种确认模式:
累积确认:消费者只需要确认最后一条收到的消息,在此之前的消息,都不会被再次发送给消费者。
单条确认:消费者需要确认每条消息并发送ack给Broker。
如图,上方为累积确认模式,当消费者发送M12的确认消息给Broker后,Broker会把M12之前的消息和M12一样都标记为已确认。下方为单条确认模式,当消费者发送M7的确认消息给Broker后,Broker会把M7这条消息标记为已确认。当消费者发送M12的确认消息给Broker后,Broker会把M12这条消息标记为已确认。
需要注意的是,订阅模式中的shared模式是不支持累积确认的。因为该订阅模式下的每个消费者都能消费数据,无法保证单个消费者的消费消息的时序和顺序。
AcknowledgmentsGroupingTracker
消息的单条确认和累积确认并不是直接发送确认请求给Broker,而是把请求转交给AcknowledgmentsGroupingTracker处理。
为了保证消息确认的性能,并避免Broker接收到非常高并发的ack请求,Tracker默认支持批量确认,即使是单条消息的确认,也会先进入队列,然后再一批发往Broker。在创建consumer的时候,可以设置acknowledgementGroupTimeMicros,默认情况下,每100ms或者堆积超过1000时,AcknowledgmentsGroupingTracker会发送一批确认请求。如果设置为0,则每次确认消息后,Consumer都会立即发送确认请求。
(二)取消确认
当Consumer无法处理一条消息并想重新消费时,Consumer可以发送一个取消确认的消息给Broker,Broker会重新将这条消息发送给Consumer。
如果启用了批量处理,那这一批中的所有消息都会重新发送给消费者。
消息取消确认也有单条取消模式和累积取消模式,取决于消费者使用的订阅模式。
在Exclusive模式和Failover订阅模式中,消费者仅仅只能对收到的最后一条消息进行取消确认。
在Shared和Key_Shared的订阅类型中,消费者可以单独否定确认消息。
如果启用了批量处理,那这一批中的所有消息都会重新发送给消费者。
NegativeAcksTracker
取消确认和其他消息确认一样,不会立即请求Broker,而是把请求转交NegativeAcksTracker进行处理。Tracker中记录着每条消息以及需要延迟的时间。Tracker默认是33ms左右一个时间刻度进行检查,默认延迟时间是1分钟,抽取出已经到期的消息并触发重新投递。Tracker存在的意义是为了合并请求。另外如果延迟时间还没到,消息会暂存在内存,如果业务侧有大量的消息需要延迟消费,还是建议使用reconsumeLater接口。NegativeAck唯一的好处是不需要每条消息都指定时间,可以全局设置延迟时间。
(三)redelivery backoff机制
通常情况下可以使用取消确认来达到处理消息失败后重新处理消息的目的,但通过redelivery backoff可以更好的实现这种目的。可以通过指定消息重试的次数、消息重发的延迟来重新消费处理失败的消息。
(四)确认超时
除了取消确认和redelivery backoff机制外,还可以通过开启自动重传递机制来处理未确认的消息。启用自动重传递后,client会在ackTimeout时间内跟踪未确认的消息,并在消息确认超时后自动向代理重新发送未确认的消息请求。
如果开启了批量处理,那这批消息都会重新发送给Consumer。
与确认超时相比,取消确认会更合适。因为取消确认能更精确地控制单个消息的再交付,并避免在消息处理时引起的超过确认超时而导致无效的再重传。
(五)消息预拉取
Consumer客户端SDK会默认预先拉取消息到Consumer本地,Broker侧会把这些已经推送到Consumer本地的消息记录为pendingAck,这些消息既不会再投递给别的消费者,也不会ack超时,除非当前Consumer被关闭,消息才会被重新投递。Broker侧有一个RedeliveryTracker接口,这个Tracker会记录消息到底被重新投递了多少次,每条消息推送给消费者时,会先从Tracker的哈希表中查询一下重投递的次数,和消息一并推送给消费者。
(六)未确认的消息处理
如果消息被消费者消费后一直没有确认怎么办?
unAckedMessageTracker中维护了一个时间轮,时间轮的刻度根据ackTimeout、tickDurationInMs这两个参数生成,每个刻度时间=ackTimeout/tickDurationInMs。新追踪的消息会放入最后一个刻度,每次调度都会移除队列头第一个刻度,并新增一个刻度放入队列尾,保证刻度总数不变。每次调度,队列头刻度里的消息将会被清理,unAckedMessageTracker会自动把这些消息做重投递。
重投递就是客户端发送一个redeliverUnacknowledgedMessages命令给Broker。每一条推送给消费者但是未ack的消息,在Broker侧都会有一个集合来记录(pengdingAck),这是用来避免重复投递的。触发重投递后,Broker会把对应的消息从这个集合里移除,然后这些消息就可以再次被消费了。
-
Pulsar架构设计
-
分层架构
-
Pulsar各组件交互示意图
- 一个pulsar实例可以由多个集群组成,集群间的消息数据可以进行复制。单个集群由以下三部分组成:
- 一个或者多个broker:负责处理producer发出的消息,并将消息给consumer消费;
- 一个或多个BookKeeper(又称bookies):bookies提供消息的持久化存储能力,broker将消息存储在bookies中。
- 一个ZooKeeper集群:ZooKeeper提供分布式配置和协调能力,存储归属信息、broker负载报告、bookies ledger信息等。
- 可以看出,Pulsar采用了分层架构,由两层组成:
- 无状态服务层:由一组接收和传递消息的Broker组成
- 有状态持久层:由一组 bookies存储节点组成,可持久化地存储消息
- 这样的好处是可以独立进行扩展,即
- 当需要支持更多的消费者或生产者时,由于Broker是无状态的,可以简单地添加更多的 Broker。触发负载均衡条件后,主题分区将在Brokers中做Load Balance,一些主题分区的所有权会转移到新的Broker。
- 当需要更多存储空间来将消息保存更长时间时,只需添加更多 Bookie,流量将自动切换到新的 Bookie 中。Pulsar中不会涉及到不必要的数据搬迁,不会将旧数据从现有存储节点重新复制到新存储节点。
- 一条消息的旅程
- 在讲Pulsar各组件的设计之前,我们可以从一个整体的视角,去看一条消息是如何从生产走到消费的。
- 消息“Hello”生产与消费的执行链路
- 注:1)红色线代表生产消息的链路,蓝色线代表消息被消费时数据流动链路;2)为了简化流程,上图不代表只有这种执行链路,消息到达bookie实际上是先到达journal文件,再异步刷到EntryLogFile
- 发送消息:我们在使用pulsar时会指定pulsar broker的地址,生产者会发起一个lookup请求,找到topic所在的broker,再与这个broker建立tcp长连接,进行后续的消息发送
- broker处理消息:broker启动时会通过ZooKeeper找到BookKeeper,然后通过BK Client与bookkeeper建立连接。在收到生产者的消息后,会以Entry的形式通过Ledger API发送到BookKeeper当中。当收到存储成功的ack后,先将消息保存到Cache中,再返回给生产者消息已发送成功。
- BookKeeper处理消息:收到消息后,以Entry形式存储在Fragment中。
- 消费消息:先与生产者类似,消费者与对应的broker建立连接。当正常订阅消息时,消费者只需要从Cache中读取数据。当需要读某一些历史数据时,Cache中不存在话需要从Bookie中获取。
-
Broker核心设计
- 基本概念
- Broker组成部分
- 在Broker中包含多个组件:
- Dispatcher:调度分发器,通过自定义的二进制协议,与生产者、消费者进行数据传输
- Load Balancer:用于做负载均衡,即分配合适的topic数量到自身
- Managed Ledger:Ledger是一个只追加的数据结构,其中存储着一条条消息。而Managed Ledger作为Ledger的上一层抽象,负责管理消息流,作为添加消息和消费消息的入口,有一个写入器进程添加消息,并且有多个cursor消费消息,每个cursor有自己的消费位置。
- BK Client:请求Bookie的客户端
- Cache:为了提升性能,broker会将消费者要消费的消息提前放到Cache中。如果积压的消息超过了缓存大小,Broker就直接从BooKeeper读取消息(存在的场景如消息未确认ack,但消费者继续请求数据,这时积压的消息就可能超过缓存大小)
- Global replicators:用于做Geo复制
- Pulsar是一个横向可伸缩的消息系统,对于服务层即Broker来说,可伸缩意味着一个集群中的流程需尽可能均匀地分布在所有可用的Brokers上。可伸缩的场景包括:
- Broker遇到负载高时需要做负载均衡
- 扩充Broker以支撑集群流量增加
- 缩减Broker时需要平摊该broker的流量到其它broker
- Broker故障处理
- 流量建立在topic之上,因此要理解Broker如何做到可伸缩,需要先了解topic是如何被分配到不同的broker。
- Topic分配到Broker的粒度
- 向broker分配topic(后续讨论假定一个topic只有一个分区)不是在topic级别完成的,而是在Namespace Bundle级别(更高级别)完成的。所谓bundle列表,是指对一个namespace中的topic进行切分得到的列表,一个bundle包含多个topic,如图:
- Namespace Bundles
- topic通过一致性哈希得到自己处于哪个分组。如图6个topic,bundle数是4,这样每个bundle分配到1到2个topic。假设有3个broker,那bundle就均匀分配到broker上,即topic均匀分配到了broker上。
- bundle分配到broker
- 客户端在指定 topic 时,需要查找 topic 所属哪个 broker。这时候通过 topic 全限定名相关的 namespace 来确定落在哪个namespace bundle 上,然后通过查找 namespace bundle 所属 broker 来确定分配情况。这里相关的元数据都在zk上。
- 通过bundle,可以减少pulsar在处理topic与broker分配关系时需要保存的元数据大小。每个Borke拥有一个或多个bundle,也就是拥有一个namespace下所有topic的一个子集的所有权。
- broker负载均衡
- Bundle3分配到Broker2
- Pulsar的Load Balancer支持自动的负载均衡,在broker集群内部,会通过 zookeeper 选举一个Master,对Broker的负载进行监控。
- 当检测到某个broker过载时,会强制将一些流量分配到低负载的broker,也就是说,broker会强制“卸载”bundle的一些流量较大的子集,以降低broker的负载。
- 例如,默认卸载阈值是85%,如果broker资源使用率(基于cpu、网络、内存等指标计算)使用超过了95%,那么需要卸载的bundle的资源使用率为95% - 85% + 5%(百分比差) = 15%。
- 再重分配topic后,生产者和消费者再与新的broker建立连接。
- 扩充/缩减Broker
- 对于扩充Broker来说,Pulsar不会直接进行topic的重分配,而是把broker放入可用broker集合中,当触发负载均衡时,再用到新的broker。
- 对于缩减Broker来说,与负载均衡类似,即将Broker负责的topic分配给其它的Broker,生产者和消费者再进行重连接。
- Broker故障处理
- Broker3宕机时的容灾
- 当broker故障时,对于procuder和consumer来说,会出现连接超时的情况,这时会进入重试。
- 在broker集群内部,同样是通过 zookeeper 选举一个Master,对Broker是否宕机做判断。当发现故障后,会触发故障broker负责的topic的重分配,分配到其它可用的broker。这样producer和consumer会与重分配后的broker建立连接。
- 由于Broker是无状态的,重分配后相关元数据都是记录在ZK上,因此不会进行数据复制,且单个Broker的故障不会影响数据的处理。
-
BookKeeper整体架构
- BookKeeper整体由三个部分组成:客户端 (Client)、数据存储节点 (Bookie集群) 和元数据注册发现服务 (ZooKeeper),Bookies 在启动的时候向 ZooKeeper 注册节点,Client 通过 ZooKeeper 发现可用的 Bookie。在讲pulsar架构中的BK Client可以与这里的Client对应上。
- 基本概念
- BookKeeper核心设计
- Pulsar使用BookKeeper作为消息持久化层实现。Apache BookKeeper是一个高扩展、强容错、低延迟的分布式预写日志(WAL)系统。它相当于把底层的存储系统服务化,这样可以使得依赖BookKeeper的分布式系统(如分布式消息队列)在设计时可以只关注应用层和功能层的内容,而存储层比较难解决问题,如一致性、容错能力,BookKeeper已经实现了。
-
bookie的存储设计
- Ledger:它是 bookie逻辑上的一个基本存储单元,是对存储消息的log文件的抽象,BK Client 的读写操作也都是以 Ledger 为粒度的;
- Fragment:bookie的最小分布单元(实际上也是物理上的最小存储单元),也是 Ledger 的组成单位
- Entry:每条数据都是一个 Entry,它代表一个 record,每条 record 都会有一个对应的 entry id。这里数据的内容对应到消息上的话,如果是单条消息发送,每条消息对应一个entry,如果是批量消息发送的话,批量消息对应一个entry。
- 当broker与bookie建立连接后,会通过BK Client新建一个Ledger,这个Ledger包含一些元数据,包括:
- 状态,是open还是close
- Last Entry ID:关闭状态时,最后一个EntryID是多少
- Ensemble Size, Write Quorum Size, Ack Quorum Size,即ledger可操作的bookie数、写入bookie数和收到ack的bookie数,下文会细讲
- Ensembles:Ledger所在的bookies列表
- 后续Entry的发送,会挂在这个Ledger当中。当entry数达到Ledger配置的存储空间大小、最大entry数阈值等条件时,会关闭这个Ledger,重新新建一个Ledger。
- 多副本存储
- Pulsar的消息以日志的形式存储在bookie中,日志又被分为多个Segment(Segment与Ledger概念类似,Segment是Pulsar的概念,Ledger是BookKeeper的概念。Pulsar可将Segment的数据脱离BookKeeper,存放在S3等存储介质中,但本文不讨论这种场景,因此可认为本文说的Segment和Ledger是同一个东西),均匀地分布在bookie集群中的多个bookie中。
- 如图所示为segment分段存储到bookie的示意图
- 在这里,再看Ledger中的元数据Ensemble Size, Write Quorum Size, Ack Quorum Size:
- Ensemble Size:可供Segment存储选择的Bookie数。如图的Ensemble Size是4,即有4个bookie可以选择存储。Ensemble Size可以控制Ledger的读写带宽,如增加Ensemble Size,就是增加了可写的机器数
- Write Quorum Size:Segment存储的bookie数,如图,每个Segment存储在3个bookie上。Write Quorum Size可以控制数据的副本数,即与可用性相关。
- Ack Quorum Size:需要等待的Bookie Ack 数。如可以配置为2,对于Segment1来说,只要bookie1、bookie2、bookie4当中两个返回ack,就认为Entry存储成功。它的作用是可以减少尾延效应。
-
Bookie扩容
- 如图所示,增加了BookieX和BookieY,Topci1-Part2在写到SegmentX+1、SegmentX+2的时候,会将Segment放入新的bookie中,新的bookie也就被利用起来。这里没有任何的数据复制,没有像kafka的rebalance过程。
-
Bookie故障处理
- 如图有一个磁盘故障导致Bookie2上的Segment4被破坏,BooKeeper后台会检测到这个错误并进行复制修复。
- 副本修复是Segment(甚至是Entry级别)的修复,并且只复制必须的数据,如图BookKeeper从Bookie3和Bookie4读取Segment4中的数据,并修复到Bookie1中。
- 对于整个Bookie节点出错的情况,首先由于有副本的存在,Broker可以继续进行接入和读取消息。其次在后台会有一个AutoRecoveryMain线程,将数据复制到Bookie1上,这样就完成了故障修复。
消息读写流程
消息的写入
将Entry追加写入Ledger中。
将这次Entry的更新操作写入Journal日志中,当由多个数据写入时,可以批量提交,将数据刷到Journal磁盘中。
将Entry数据写入写缓存中。
返回写入成功响应。
到这里,消息写入的同步流程已经完成。
3-A. 内存中的Entry数据会根据Ledger和写入Ledger的时间顺序进行排序,批量写入Entry Log中。
3-B. Entry在Entry log中的偏移量以Index Page的方式写入Ledger Cache中,即iIdex Files。
Entry Log和Ledger Cache中的Index File会Flush到磁盘中。
消息的读取
A.先从写缓存中以尾部读的方式读取。
B.如果写缓存未命中,则从读缓存中读取。
C.如果读缓存未命中,则从磁盘中读取。磁盘读取有三步:
C-1.读取Index Disk,获取Entry的偏移量。
C-2.根据Entry的偏移量,在Entry Disk中快速找到Entry。
C-3.将Entry数据写入读缓存中。
-
对比kafka的异同
异步跨地域复制
Pulsar 目前支持以下三种异步跨地域复制的方案:
- 全连通
- 单向复制
- Failover 模式
从是否具有 configurationStoreServers (global zookeeper)的角度可以分为以下两种异步跨地域复制方案:
1. 有 configurationStoreServers
- 全连通
2. 没有 configurationStoreServers
- 单向复制
- Failover 模式
在整个跨地域复制中的一个核心理念在于,各个集群之间的数据是否能够互通,它们之间的交互主要依靠如下配置信息:
- cluster (cluster name)
- zookeeper (local cluster zk servers)
- configuration-store (global zk servers)
- web-service-url
- web-service-url-tls
- broker-service-url
- broker-service-url-tls
在初始化 pulsar cluster 时,用户可以指定上述对应的信息,示例如下:
bin/pulsar initialize-cluster-metadata \
--cluster pulsar-cluster-1 \
--zookeeper zk1.us-west.example.com:2181 \
--configuration-store zk1.us-west.example.com:2181 \
--web-service-url http://pulsar.us-west.example.com:8080 \
--web-service-url-tls https://pulsar.us-west.example.com:8443 \
--broker-service-url pulsar://pulsar.us-west.example.com:6650 \
--broker-service-url-tls pulsar+ssl://pulsar.us-west.example.com:6651
Full-mesh(全连通)
Full-mesh 的形式允许数据在多个集群中共享,如下图:
概念解析
- configurationStoreServers: 存储的是各个集群的配置信息,也就是让集群之间能够互相感知到对方的地址信息。除此之外还会存储 tenant 和 namespace 的信息,主要目的在于简化操作流程,当更新其中一个集群的信息,其它集群都可以通过 global zookeeper 获取到这次信息的更改。
- tenant: 当前创建的 tenant 允许哪些集群进行操作(–allowed-clusters)
- namespace: 当前创建的 namespace 允许在哪几个集群之间进行数据的复制 (–clusters)
原理
对于多个集群之间的数据复制,我们均可以简化到两个集群之间的数据复制,基于这个理念,Geo-Replication 的原理如下图所示:
当前拥有两个集群,分别部署在北京和上海,当用户在北京的集群中使用 producer 发送数据时,首先会发送到北京机房的本地集群中(topic1)与此同时会去创建一个 replication cursor,用于专门复制数据的一个游标,通过这个 cursor 信息,你可以判断当前数据究竟复制到哪一个阶段。同时会去创建 replication producer,它会把数据从北京机房的 topic1 中读取数据,然后将数据写到上海机房的 topic1 中,上海机房的 broker 收到 producer 的请求之后,会写到本地相同的 topic 中来(topic1)。此时如果上海机房的用户开启 consumer 去消费数据的话,会接收到由北京机房 producer 生产的数据信息。反之亦然。
在这里需要说明如下问题:
- 在全连通的场景下,北京机房的数据会复制给上海机房的集群,上海机房的数据也会复制给北京的机房,那么是否会出现北京机房的数据复制给上海机房之后,上海机房反向再把该条数据复制回到北京,形成数据的死循环?因为当 producer 在发送消息时,它是知道自己当前所在的集群是属于哪一个的,当生产的消息经过 replication producer 的复制时,会在该消息标记一个 label:replication_from,代表这条消息从哪里来,可以解决反向复制的问题。
- 在 Geo-Replication 的场景下,同样可以保证消息的 exactly-once 的语义(at-least-once + broker 端的去重(producer-name + sequence ID))
- 复制的延迟取决于两个机房之间网络的时延,如果时延比较大,需要考虑两个机房之间的网络情况。
一旦配置了 global zookeeper 之后,数据之间的复制都是双向复制的,所有 global zookeeper 下面挂载的集群之间的数据都是互通的。
单向复制
上面我们提到,在配置了 global zookeeper 的情况下,是没有办法做数据的单向复制的,但是很多场景下,我们并不需要所有的集群之间的数据都是全连通的,这种场景下,我们就可以考虑使用单向复制的功能,需要强调的是,单向复制并不需要用户单独配置或指定 configurationStoreServers,配置时只需要将 configurationStoreServers 的值配置为本地集群的 zookeeper 地址(zookeeperServers)即可。
那么在不配置 global zookeeper 的情况下,如何去做跨集群复制的场景呢?
在上面我们提到,global zookeeper 的作用主要是用来存储多个集群的地址信息以及相应的 namespace 信息,并没有额外的元数据信息。所以在单向复制的场景下,你需要告诉其它机房的集群,你需要读到不同集群之间的 namespace 信息。
Failover 模式
Failover 模式是单向复制的特例。
Failover 模式下,远端机房的集群只是用来做数据的备份,并不会有 producer 和 consumer 的存在,只有当当前处于 active 的集群宕机之后,才会把对应的 producer 和 consumer 切换到对应的 standby 集群中来继续消费。因为有 replication sub 的存在,所以会一同将订阅的状态也复制到备份机房。
目前 pulsar Geo-Replication
存在的问题
- Pulsar 只能保证单机房生产的消息顺序,在多机房的场景下没办法保证多个机房的消息全局有序
- 由于 cursor snapshot 是定期进行的 (即定期查看一下多个集群中 Message ID 的对应关系),在时间上精确度不会太高,多少有些偏差。
- 目前只会同步 “Mark delete position” 的位置(ack确认之后Mark delete position标志可以删除的数据),对于单独签收的消息暂时无法同步。
- 只有在所有相关集群都处于「可用」状态时,才可以进行 cursor snapshot。
- 当使用 cursor snapshot 后,会产生一些缓存,影响到后续涉及 backlog 的计算结果。
参考链接