1 架构
以下官网给的架构图,包含以下几部分:
Producer,生产者,封装消息,并将消息以同步或者异步的方式发送到Broker。
Broker,Broker负责消息的传输,topic的管理以及负载均衡,与其他消息队列组件不同,该Broker不负责消息的存储,是个无状态组件。
Bookie,负责消息的的持久化,采用Apache BookKeeper组件,BookKeeper是一个分布式的WAL系统。
Consumer:消费者,以订阅主题的方式消费消息,并确认。Pulsar中还定义了Reader角色,也是一种消费者,区别在于,它可以从指定置位获取消息,且不需要确认。
ZK,负责集群的配置管理,包括租户,命名空间等,并进行一致性协调。
从架构上看,与Kafka最大的不同点在于计算和存储分离。这种设计的好处是服务层和存储层可以独立扩展,提升了弹性的扩容。
2 生产消息
2.1 生产者创建
创建连接和生产者,需要指定以下必要的属性信息:
(1)Broker地址,一般是指定Broker集群的域名地址,由集群重定向到某台具体Broker上提供服务
(2)Topic信息,明确消息发送到哪个topic,topic由以下几个部分组成
{persistent|non-persistent}://tenant/namespace/topic
Topic名称组成 |
Description |
persistent / non-persistent |
用来标识 topic 的类型。 Pulsar 支持两种不同 topic:持久化和 非持久化型(如果你没有明确指定,topic 将会是默认的持久化类型)。 持久化 topic 的所有消息都会存储到硬盘上(除非是单机模式的Broker,否则都是会在多块磁盘上)。非持久化 topic 的数据将不会存储到硬盘上。 |
tenant |
租户,Pulsa支持多租户,该topic所属的租户名 |
namspace |
将相关联的 topic 作为一个组来管理,是管理 Topic 的基本单元。, 每个租户可以有多个命名空间。 |
topic |
主题名称 |
2.2 分区选择
除了普通的topic,Pulsar也支持分区topic,一个topic可以设置多个分区,那么在消息发送时如何路由到不同的分区,Pulsar支持三种路由模式。
(1)RoundRobinPartition,如果没有指定key,则以round-robin的方式路由到所有分区;如果指定key,则根据key做hash,散列到对应的分区上。
(2)SinglePartition,如果没有指定key,则随机选择一个分区,并发送所有消息;如果指定key,则根据key做hash,散列到对应的分区上。
(3)CustomPartition,使用自定义消息路由,可以定制消息进入特定的分区的策略。
2.3 消息发送
生产者在创建的时候,配置的是集群的请求地址(比如pulsar://pulsar-cluster.acme.com:6650),最终重定向到一个具体的Broker地址上,这就涉及到寻址的过程(可以对比下Kafka的元数据更新机制)。具体的步骤如下:
(1)客户端将尝试通过向服务器(Broker)发送 HTTP 查找请求,来确定主题(Topic)所在的服务器(Broker)。 通过查询Zookeeper中(缓存)的元数据,来确定这条消息的topic在哪个Broker上,如果该topic不在任何一个Broker上,则把这个topic分配在负载最少的Broker上。
(2)当客户端获取了Broker的地址之后,将会创建一个TCP连接(或复用连接池中的连接)并且进行鉴权。 客户端和Broker通过该连接交换基于自定义协议的二进制命令。 同时,客户端会向Broker发送一条命令用以在Broker上创建生产者/消费者,该命令将会在验证授权策略后生效。
连接建立后,就可以开始发送消息。发送的模式有同步和异步两种:
- 同步发送,生产者发送消息后,等待Broker的ack,如果没有接受到ack,则认为发送失败。
- 异步发送,生产者将消息发送到本地的阻塞队列,就立即返回,客户端将在后台将消息发送达到Broker,队列的大小可以配置(配置MaxPendingMessages大小)。发送完成后,将调用回调方法进行通知。
与Kafka类似,生产者支持消息的批量发送。producer将会累积一批消息,然后通过一次请求发送出去。批处理的大小取决于设置的最大的消息数量及最大的发布延迟。
3 消息存储
Pulsar采用的是计算存储分离架构,Broker并不持久化消息内容,可以认为是一个proxy层,实现类似Kafka的client的一部分功能,Broker是个无状态组件,消息内容实际存储在bookie中。Broker实现topic的负载均衡,以及对外对提供读写接口,实现消息传输。
3.1 负载均衡
生产者在发送前,需要拿到Topic归属的Broker,才能将消息发送到正确的Broker上。那topic和Broker的对应关系是如何维护和均衡的呢?
每个topic(分区)归属一个Broker,此Broker为该topic(分区)的所有者(ower),负责topic读写服务。topic(分区)发送变化,基于当前Broker的负载状况,将topic(分区)动态分配到适合的Broker。实际实现中,Pulsar并不是以topic的粒度实现负载均衡的,而是bundle。几者之间的关系如下:
bundle是每个命名空间中topic的集合,在NameSpace创建的时候可以指定bundle个数(配置defaultNumberOfNamespaceBundles),命名空间下的topic按照名称hash到对应的bundle,以bundle为粒度单位负载到各个Broker。我们看下负载均衡的几个场景:
- 当增加或者删除一个topic时,仅是打开或者关闭该topic,不会引起重新分配。
- 当卸载namespace下所有的topic,则会发生再均衡,会有10ms左右的抖动。
- 当Broker超载(基于 CPU 、网络和内存指标阈值,其中某个超过85%),会引起减负动作,将一部分bundle重新分配到其他Broker上。
- 当某个bundle的topic数量达到一定阀值,会引起bundle拆分动作(分为2个),将新的较小的 Bundle 重新分配给不同的 Broker。
均衡后的相关的数据保存到zk中。
3.2 非持久化与持久化消息存储
在topic的定义时,可以指定该topic消息的非持久化和持久化类型。
- 非持久化消息,仅会保存到Broker的缓存中,然后转发到消息端,不会持久化到磁盘。这就意味着,当某个 Pulsar Broker宕机,或断开订阅者与某个主题(非持久性)的连接,意味着所有正在传输的消息都会丢失,客户端也可能会看到消息的丢失。
- 持久化消息,pulsar采用Bookkeeper作为持久化存储。 BookKeeper是一个分布式的预写日志(WAL)系统。我们先认识下这个组件系统,然后再来看下pulsar是如何使用它的。
3.3 BookKeeper
1) 相关的概念
- Entry,Entry是存储到bookkeeper中的一条记录,其中包含Entry ID,记录实体等。
- Ledger,可以认为ledger是用来存储Entry的,多个Entry序列组成一个ledger。
- Journal,其实就是bookkeeper的WAL(write ahead log),用于存bookkeeper的事务日志,journal文件有一个最大大小,达到这个大小后会新起一个journal文件。
- Entry log,存储Entry的文件,ledger是一个逻辑上的概念,entry会先按ledger聚合,然后写入entry log文件中。同样,entry log会有一个最大值,达到最大值后会新起一个新的entry log文件
- Index file,ledger的索引文件,ledger中的entry被写入到了entry log文件中,索引文件用于entry log文件中每一个ledger做索引,记录每个ledger在entry log中的存储位置以及数据在entry log文件中的长度。
- MetaData Storage,元数据存储,是用于存储bookie相关的元数据,比如bookie上有哪些ledger,bookkeeper目前使用的是zk存储,所以在部署bookkeeper前,要先有zk集群。
2) 读写流程
结合下面的图,我们再看下读写流程
- 写入流程:
1、Ledger先写入Journal,并同步落盘持久化。
2、写入index以及Entry log,这一步都是先写入page cache,再落盘。
- 读取流程:
1、先读取index文件,获取entry log的索引
2、从对应的entry log中获取数据。
3)复制机制
Bookkeeper对所有的数据拷贝多个副本,一般是三份或是五份——到不同的机器上,可以是同一数据中心,也可以是跨数据中心。如图所示:
- Ensemble Size (E),表示将要写入的实际的Bookies数量。通过机架感知策略,从不同的机架选择bookie,组成bookie池。本例中bookie1-5为bookkeeper的ensemble,数量为5。
- Write Quorum Size (Qw),表示每条Entry实际需要写入的最小bookie数量,即Entry的副本数。本例中的bookie2-4,数量为3
- Ack Quorum Size (Qa),表示完成bookie数量的写入,即可向客户端返回ack。本例中的bookie3-4,数量为2。
三者之间的关系如下:
QW<=E,即Entry的副本数,不能超过bookie池的大小。QW越大,副本冗余越多,可靠性越高。
QA<=QW,确认bookie数据,不能超过实际的Entry的副本数,一般QA=(QW+1)/2。不像其他使用主/从或是管道复制算法在副本之间复制数据的分布式系统(例如Apache HDFS、Ceph、Kafka),Apache BookKeeper使用一种多数投票并行复制算法在确保可预测的低延时的基础上复制数据。
3.4 Broker与bookkeeper的交互
对于pulsar,Broker的Managed Ledger负责维护相关的bookie(即bookkeeper)。
如下图所示,通过分层+分片,Topic(分区)的所属的消息数据,分成多个Segment(即bookkeeper的ledeger),每个Segment有多个副本,存储在多个 BookKeeper 上。Pulsar Broker负责Segment(ledeger)开启和关闭。
当消息数据到达Broker后,写入器并行写入多个BookKeeper存储节点(就是Bookies)。
Broker 持久化完成后,会将数据缓存在本地内存中以提供追尾读(Tailing Reads),提高最新数据读取速度(后面重点介绍)。
3.5 消息保留和到期
默认的情况下,消息保留有以下两种:
- 对于没有订阅者的topic消息,将不会保留。
- 对于有多个订阅者的topic消息,所有的订阅者都已经确认完毕后,立即进行删除;如果未被确认,被一直持久化。
在有些场景中,默认的策略是不满足,比如即使消息都被消费并确认,但也有可能需要进行消息回溯。
Pulsar提供了保留和过期策略,覆盖默认策略。注意:以下的策略针对namespace所有topic。
(1)消息保留,对于已经消费完成的消息,可以通过指定保留大小(defaultRetentionTimeInMinutes)和保留时间(defaultRetentionSizeInMB),当某个阀值达到后,则达到进行删除标识,等待删除。
(2)消息过期,未被确认的消息设置存活时长(TTL)。当时间过期后,即使消息没有确认,也会被删除掉。
Kafka的消息清理,主要基于时间,日志大小,日志偏移量三种模式。
4 消息消费
4.1 主题订阅模式
在pulsar中,有4种可用的订阅,分别是 独占(exclusive),共享(shared),灾备(failover)和 键共享(key_shared)。
- 独占(exclusive),只能有一个消费者绑定到订阅上(topic/partition)。 如果多于一个消费者尝试以同样方式去订阅主题,消费者将会收到错误。独占是pulsar的默认模式。
- 灾备(failover),多个消费者绑定到订阅上(topic/partition),根据消费者的 优先级,确定master(优先级高的消费者)消费者,正常情况下,master消费者生效;当master消费者出现故障,断开连接后,那么其他的消费者按照优先级重新选定master继续消费未确认的消息。
- 共享(shared),多个消费者可以绑定到同一个订阅上(topic/partition),消息通过round robin轮询机制分发给不同的消费者,并且每个消息仅会被分发给一个消费者。 当消费者断开连接,所有被发送给他,但没有被确认的消息将被重新安排,分发给其它存活的消费者。
- 键共享(key_shared),多个消费者可以绑定到同一个订阅上(topic/partition),相同key或者orderingkey分发到同一个消费者,所以键共享模式的消息需要指定key或者orderingkey。
Kafka的多分区,同一个消费组中消费者的订阅模式与pulsar的独占模式类似。这种模式下,消费者的个数不能大于partition的个数,限制了消费能力,而pulsar的共享模式有效的解决了这个问题。
4.2 消费模式
Kafka是采用的拉模式,而pulsar是推模式。消费端订阅主题后,监听从Broker的推送的消息,当Broker有新消息,推送到consumer端的缓存队列(其大小可以通过recevieQueueSize设置),通过调用receive方法进行处理。消息处理完成后,发送确认信息,Broker会记录下当前消费的cursor(类似于Kafka的offset)。
pular支持批量发送以及累积确认,即确认最后一条消息,那么默认前面的信息都已经消费完成。pulsar也有订阅组(类似Kafka的消费组),但是对于共享与键共享的订阅类似,对于partition,没有消费者个数的限制。
4.3 Broker的读取流程
当某个消费者订阅主题(分区)后,与生产者类似,会与该主题(分区)的所有者(ower)建立连接,后续消息的推送都由该Broker负责。
根据订阅的策略模式,首先从Broker的缓存中读取最新的数据,如果读取到,则立即返回,不需要从磁盘读取,这种方式(Tailing Reads)满足大部分的场景。 如果要读取历史数据(Catch-up Reads),那么就从bookie中读取。
Broker 会向所有 Bookie 发送获取 LAC(LastAddConfirmed) 请求,得到大多数回复后即可计算出一个安全的 LAC 值,这个流程就是采用了 Quorum Read 的方式。Pulsar Broker 获取可靠的 LAC 之后,其读取可以从任一 Bookie 开始,如果在限定时间内没有响应则给第二个 Bookie 发送读取请求,然后同时等待这两个 Bookie,谁先响应就意味着读取成功,这个流程称之为 Speculative Read。由于Fragment分布在不同的bookie上,所以读取的过程会在bookie间漂移。
4.4 消费确认
当一条消息消费完成后,会向Broker发送一条确认消息,Broker将保存消费游标(类似Kafka的offset)。消费者也可以进行批量的确认,Broker记录下最后一条消息的游标。
在异常情况下,比如消费失败(如入库失败),需要重新消费,取消确认,Broker会重新加入队列,将会重新分配。通过设置消息确认的超时时间,如果超时,也将触发重新投递。
1 可靠性设计
以下从生产,存储,消费三个阶段来了解下可靠性设计。
1.1 生产阶段
通过同步或者异步方式将消息发送到Broker节点,并根据Broker的ack进行处理,如果发送失败,可以进行重试。这方面与Kafka的处理类似,确保了生产阶段消息的可靠性。
1.2 延迟投递
对于有些消息,需要延迟一段时间后投递(而不是立即投递),生产者定义消息的延迟时间,并发送到Broker,Broker将消息存储,并通过DelayedDeliveryTracker来管理定时的内存索引(time->messageid),当达到时间后,投递给消费者。
1.3 存储阶段
pulsar采用计算和存储分离的架构,每个topic都存在一个owner Broker,负责该topic的读写请求,采用分层存储,将消息保存到bookie上,下面分析Broker,bookie的可靠性。
1.4 Broker故障恢复
Broker由于某种故障(比如宕机,停电),导致不可用。如下图所示,Broker2是Topic1-Part2的owner,当Broker出现故障时(通过zk探测),Broker3要立即接管Topic1-Part2的owner角色。
由于计算和存储分离的架构,存储层是不需要重新复制的。如果有新数据到来,它立即附加并存储为Topic1-Part2中的Segment x + 1。 Segment x + 1被分发并存储在Bookie1, 2和4上,因为它不需要重新复制数据,所以所有权转移立即发生而不会牺牲主题分区的可用性。
1.5 bookie故障恢复
如图所示,bookie2上的Segment3由于磁盘损坏,导致不可用。
Apache BookKeeper中的副本修复是Segment(甚至是Entry)级别的多对多快速修复,这比重新复制整个主题分区要精细,只会复制必须的数据。 这意味着Apache BookKeeper可以从bookie 1和bookie 3读取Segment 3中的消息,并在bookie 1处修复Segment 3。所有的副本修复都在后台进行,对Broker和应用透明。
即使有Bookie节点出错的情况发生时,通过添加新的可用的Bookie来替换失败的Bookie,所有Broker都可以继续接受写入,而不会牺牲主题分区的可用性。
1.6 集群扩容
对于Broker的扩容,主题分区将立即在Brokers中做平衡迁移,一些主题分区的所有权立即转移到新的Broker。与Kafka不同,其rebanance几乎是零时间。
对于bookie的扩容,如下图所示。
当Broker 2将消息写入Topic1-Part2的Segment X时,将Bookie X和Bookie Y添加到集群中。 Broker 2立即发现新加入的Bookies X和Y。然后Broker将尝试将Segment X + 1和X + 2的消息存储到新添加的Bookie中。 新增加的Bookie立刻被使用起来,流量立即增加,而不会重新复制任何数据。 除了机架感知和区域感知策略之外,Apache BookKeeper还提供资源感知的放置策略,以确保流量在群集中的所有存储节点之间保持平衡。
1.7 消费阶段
在消息消费阶段,接受消息后,需要进行确认。如果确认超时,或者取消确认,那么消息将重新投递。如果消费失败或者异常(比如消息解析失败,或者下游系统故障等)。Pulsar提供了两种处理模式。
1.8 死信主题
与Kafka类似,如果消费不成功,并确认不可恢复(如消息体异常),那么就投递到死信主题。监控该主题的消息,就可以知道消息的异常情况。当然死信主题其性质也是主题,其消息也可以被消费。
1.9 重试主题
1.10 生产者可将消息发送到正常的主题和重试主题,并允许消费者重试。当消费失败后,延迟一段时间从重试主题从获取消息进行处理。
1.11 读取模式(reader)
对于消费模式,消费者监听所订阅的topic的信息,一旦有新的消息,Broker将推送消息到consumer,consumer处理处理完这些消息并最终确认它们。 每当消费者者连接到某个主题时, 它就会自动开始从最早的没被确认(unacked)的消息处读取, 因为该主题的游标是由Pulsar自动管理的。
对于读取模式,可以指定消费的位置,包含:
- 主题中的最早的 可用消息
- 主题中的最新 可用消息
- 除最早的和最新的之外的可用消息位点。
两者模式的比较如下图所示:
读取模式应用的场景是消息的重放。
2 高吞吐设计
支持高吞吐是pulsar重要特性,根据性能测试,单台Broker(4C8G),2KB大小消息体,10WTPS,端到端延迟控制在10ms以内。我们也从生产,存储,消费三个阶段分析下高并发的设计和措施。
2.1 生产阶段
生产阶段影响并发和吞吐量的因素主要有发送模式,压缩机制,批量发送。发送模式前面分析过,在同步模式的场景下,需要等待Broker的ack,会降低并发。对于一些可靠性要求不高的消息,比如日志,用户行为数据等,可以采用异步模式。以提升吞吐量。
pulsar支持LZ4,ZLIB,ZSTD,SNAPPY四种压缩算法,消息压缩能大大节省带宽,提高吞吐率,从实测结果看,LZ4的压缩效率要高;与Kafka类似,pulsar也支持批量发送消息,可以配置一次发送消息的大小(batchingMaxMessages),对于高并发的场景,一般选择批量发送,并按照实际发送量设置该值。
2.2 存储阶段
Kafka中,先写入到leader partition,然后复制到其他的partition分区。对于pulsar系统,Broker是无状态的,数据会发送给服务该分区的 Broker,该 Broker 并行写入数据到存储层的多个节点中。一旦存储层成功写入数据并确认写入,Broker 会将数据缓存在本地内存中以提供追尾读(Tailing Reads)。
和传统的架构相比,pulsar减少了相互复制引起的I/O和带宽消耗,通过并行提升了写入了速度。另外QW,QA的大小也影响并发量,数值越小,速度也快,吞吐量也越大,但是可靠性降低。
2.3 消费阶段
从Broker读取,消费端处理两个方面分析下:
(1)追尾读(Tailing Reads)
Broker将消息数据持久化到存储层后,会将最新的数据放入到本地cache中。消费者读取最近的数据,直接从缓存中获取即可,无需访问存储层。
与传统的从文件系统读取相比,减少了磁盘的I/O以及拷贝。pulsar对于最近的数据,缓存的命中率很高,大大加快了读取的速度。
(2)追赶读(Catch-up Reads)
对于历史数据,会保存到存储层,Catch-up读可以通过存储层来并行读取数据,加快读取的速度。我们把I/O读写放在一起比较:
传统的模式下,读写以及复制都是通过lead Broker,Broker的承担所有I/O负载,且无法分担。对于pulsar分层式架构,I/O隔离,不会出现读写单个节点资源争抢的情况,且负载到多个bookie并行读写,大大缓解了磁盘的负载,提高了吞吐能力。
(3)消费端处理
对于Kafka,每个消费组中的消费者个数不能超过其订阅的topic下partition的个数,限制了消费能力。在pulsar中,共享订阅模式对于每个partition可以有多个消费者,增加消费者的数量可以提升消费能力。pulsar支持累计ack(无法在共享订阅模式下使用),也能提升其消费能力。
1 跨地域复制
与Kafka类似,Pulsar也提供了跨地域复制的解决方案。在多个数据中心间,按命名空间级别进行配置(该命名空间下所有topic),在任意个集群间进行复制。如下图:
Topic T1可在三个集群生产,消息先存储到本地集群,然后立即异步复制到其他集群,复制的延迟依赖数据中心间的RTT。每个集群拥有该Topic的所有消息,所以对每个消费端来说,相当于消费本地集群的消息。由于cluster-C没有消费端,所有的消息仅在cluster-A,cluster-B两个集群消费。它的工作机制是在 Broker 内部,为跨地域的数据复制启动了一组内嵌的额外生产者和消费者(Replicator)。当外部消息产生后,内嵌的消费者会读取消息;读取完成后,调用内嵌的生产者将消息立即发送到远端的数据中心,远端的数据中心消费完成后,会ack该消息,通过cursor保存复制位置,确保复制中断后,会继续从断点处复制(原理和Kafka类似)。
需要注意的时,pulsar会标记数据是否是复制过来的,避免多机房间循环复制。
2 多租户
对于Kafka,不同的业务系统之间,可能需要搭建多套集群,这会增加运维的难度,另外Kafka的集群支持的主题数有限(几千个左右)。pulsar在设计之初就考虑到多租户的特性,一个集群可以运行上百万个topic,通过租户进行隔离。
在一个 Pulsar 集群中,有三个概念:tenant(图中的 property 是之前的一种叫法,现在更习惯将其称为:tenant)、namespace、topic。前面介绍过,一个topic的完整的命名包含这三个要素:{persistent|non-persistent}://tenant/namespace/topic
多租户满足企业级特性要求,设计上主要有以下特点:
- 使用身份验证、授权和 ACL(访问控制列表)确保其安全性
- 为每个租户强制执行存储配额
- 支持在运行时更改隔离机制,从而实现操作成本低和管理简单。