Pulsar架构
Apache Pulsar采用分层架构。Apache Pulsar 集群由两层组成:无状态服务层,由一组接收和传递消息的代理组成;有状态持久层,由一组称为bookie的Apache BookKeeper存储节点组成 ,用于持久存储消息。
- Broker
- Broker层无状态服务,主要负责和客户端之间消息传递
- BookKeeper
- BookKeeper节点(Bookie)存储消息日志和消费游标,内部采用RocksDB作为内嵌数据库存储索
- ZooKeeper:
- ZooKeeper主要为Broker和Bookie存储元数据
Bookkeeper架构及原理
Bookkeeper是Pulsar中的核心组件,它作为Pulsar消息日志存储和消费游标的中间件,支撑Pulsar数据存储、数据一致性、容错等核心功能
Bookkeeper架构
- 多副本:将数据持久存在多台机器上,或存储在多个数据中心
- 持久性:可以持久化存储日志数据
- 一致性:通过DL方案来保证不同读者之间的一致性,类Raft方案
- 可用性:通过ensemble更改和推测读取提高可用性,同时增强一致性和持久性
- 低延迟:通过I/O隔离来保护读写延迟,同时保持一致性和持久性
Bookie多副本机制
概念
- Ensemble :用哪几台 bookie 去存储 ledger 对应的 entry,控制一个ledger的读写带宽
- Write Quorum (WQ):对于一条 entry,需要存多少副本
- Ack Quorum (AQ): 在写 entry 时,要等几个成功回执,目的是减少长尾时延
高可用特点
- 写入高可用:利用Ensemble变化机制,当在写入数据的bookie发生磁盘故障时,客户端会重新选择数据的放置,这样可以确保,在集群中剩余的bookie数量足够充足时,写入操作总可以实现。
- 读取高可用:利用随机读机制,在Kafka中仅从一个指定为leader的存储节点读取数据,Bookkeeper的不同之处在于可以使客户端从Ensemble中的任意bookie读取记录,这样有助于将读取流量分散到各个bookie,同时还能减少追尾读的延迟。
Pulsar中消息日志在Bookkeeper读写流程
Pulsar中Ledger维护
Message Log Ledger
在Pulsar中,一个Topic的partiion写入消息日志时,会在Bookkeeper中创建个Ledger并不断追加写入,Ledger是在Bookkeeper中的概念,其实它就是一个Segement(下文中提到的Segment其实就是Ledger),如果Ledger写满,就再创建个Ledger继续写入。
Cursor Ledger
在 Pulsar 中,每个订阅主题都有一个cursor。每当订阅者确认有关该主题的消息时,都会更新此cursor。更新cursor可确保订阅者不会再次收到该消息。当订阅者读取并处理了消息后,它会向代理发送确认消息。然后,代理将更新写入该订阅的cursor ledger。如果Broker随后失败,它会通过从 BookKeeper 读取cursor ledger的最后一个记录来恢复订阅的游标。
Ledger的元数据
Ledger Metadata
- State: Open/Closed
- Last Add Confirmed(LAC) Entry Id: 最近全部写入的Entry Id
- Ensemble Size: 一个Ledger最多几个Bookie
- WriteQuorum Size: 一个Entry最多写几个Bookie
- AckQuorum Size: 一个Entry写几个Bookie成功后,返回Ack
- Ensembles: Fragement的维护
模拟Ledger Metadata数据流转过程
Ledger Metadata 初始设置如下
- State: Open
- Last Add Confirmed(LAC) Entry Id: -1L
- Ensemble Size: 3
- WriteQuorum Size: 2
- AckQuorum Size: 2
- Ensembles: []
Last Add Confirmed用途
在上图中,LAC EntryId维护在Ledger中,每个Ledger在有消息日志写入时(Entry),会并发写到WriteQuorum Size个Bookie中,并等待AckQuorum Size个Bookie节点全部返回成功回执才会更新LAC EntryId,这样读取的时候可以保证一致性
Bookkeeper内部读写原理
- 写过程:在Bookkeeper中,会先随机写入到Journal(WAL)如果底层Journal Disk是SSD,这块可以开启强制fsync,然后排序后写Memtable(Write Cache)后就可以返回ACK给客户端。
- 读过程:
- Tailing Reads:如果Memtable中存在,则直接读取到Entry返回给客户端
- Catch-up Reads:如果Memtable不存在,会从索引中根据<LedgerId,EntryId>去Ledger Disk中的找到对应的日志数据
Pulsar核心特性
统一的消息消费模型
Streaming模型
独占订阅
顾名思义,订阅A中只能有一个消费者使用主题分区。下面说明了独占订阅的示例。有一个 订阅 A的活跃消费者A-0。消息 m0 到 m4 按顺序传递并由 A-0使用。如果另一个消费者 A-1 想要附加到订阅 A,则不允许这样做
故障切换
使用故障转移订阅,多个消费者可以附加到同一个订阅。然而,对于给定的主题分区,一个消费者将被选为该主题分区的主消费者。其他消费者将被指定为故障转移消费者。当主消费者断开连接时,分区将重新分配给故障转移消费者之一进行消费,而新分配的消费者将成为新的主消费者。发生这种情况时,所有未确认的消息都将传递给新的主消费者。下图说明了故障转移订阅。消费者 B-0 和 B-1 通过订阅 B订阅消费消息。 B-0 是主消费者并接收所有消息。 B-1是故障转移消费者,如果消费者B-0 发生故障,它将接管消费
Queuing模型
使用共享订阅,可以将任意数量的消费者附加到同一个订阅。消息在多个消费者之间以循环分发的方式传递,任何给定的消息都只传递给一个消费者。当消费者断开连接时,所有传递给它但未确认的消息将被重新发送给在该订阅上存活的剩余消费者。下图说明了共享订阅。消费者 C-1、 C-2和 C-3 都在同一个主题分区上消费消息。每个消费者都会收到大约 1/3 的消息。如果你想提高消费率,你可以在不增加分区数量的情况下,简单地让更多消费者共同消费
共享订阅
按key共享订阅
消息保留
在Pulsar中,消息在被确认后不会立即被删除。消费者在收到消息确认时更新Cursor,只有在所有订阅都消费此消息后才能删除消息(消息在其Cursor中标记为已确认),Pulsar 还可以配置消息保留更长时间,在所有订阅都消费完成后未达到该时间也不会删除。下图说明了消息保留在具有 2 个订阅的主题分区中的样子。订阅 A 已经消费了 M6 之前的所有消息,订阅 B 已经消费了 M10 之前的所有消息。如果没配置消息保留,M6 之前的所有消息(在灰色框中)都可以安全删除。
消息TTL
除了消息保留,Pulsar 还支持消息生存时间(TTL)。如果在配置的 TTL 时间内没有任何消费者消费消息,则消息将自动标记为已确认。**消息保留和消息 TTL 之间的区别在于,消息保留适用于标记为已确认并设置为删除的消息。保留是应用于主题的时间限制,而 TTL 适用于未使用的消息。**因此,TTL 是订阅消费的时间限制。上面说明了 Pulsar 中的 TTL。如果订阅B没有存活的消费者 ,例如消息 M10 在配置的 TTL 时间段过去后,即使没有消费者消费该消息,将自动标记为已确认。
多租户
Pulsar 支持多租户。主题被组织在两个特定于多租户的资源下:属性和命名空间。属性代表系统中的租户。租户可以在其属性中配置多个命名空间。然后每个命名空间可以包含任意数量的主题。命名空间是 Pulsar 中租户的基本管理单元,可以设置 ACL(访问控制列表)、跨地域复制等。
身份验证与授权
在 Pulsar 中,当客户端连接到Broker时,Broker使用身份验证插件来建立该客户端的身份,验证成功后为该客户端分配** Role Token**。此Role Token是一个字符串,例如 property-1-admin 或 property-2**-admin**,可以表示单个客户端或多个客户端。Role Token用于控制Namespace或Topic上,生产、消费消息或function的权限(ACL),无论是授权还是鉴权都支持自己定义的实现。
可扩展性
在 Pulsar 中,一个 Topic(非 partitioned topic)只能在一个 Broker 中,所以客户端往一个 Topic 生产和消费数据会受限于单个 Broker 的带宽和性能。Pulsar 提供的 Partitioned Topic 功能能够将一个 Topic 被分成多个子 topic 并可被多个 Brokers 服务。下图是4个Broker的 Pulsar 集群,其中Topic1和Topic2的 topic partiotion数量为2。
Broker1和Broker3处理Topic1的一个 partition的流量,Broker是 Apache Pulsar 中的无状态服务层,Broker实际上并不在本地存储任何消息数据,Pulsar Topic的消息日志存储在分布式日志存储系统BookKeeper中,下面会具体讨论 BookKeeper上线或下线问题。每个Topic partition都由 Pulsar 分配给一个Broker。将Topic partition分配给的Broker称为该topic partition的Owner Broker 。Pulsar 生产者和消费者连接到topic partition的Owner Broker,以向Owner Broker生成消息,并使用来自Owner Broker的消息。
Broker故障恢复
下图是 Pulsar 如何处理Broker故障的示例。Broker 2 由于某种原因(例如停电)而停机。Pulsar 检测到 Broker 2 宕机(ZK心跳),立即将 Topic1-Part2的所有权 从 Broker 2 转移到 Broker 3。当 Broker 3 接管 Topic1-Part2的ownership时,不需要将数据从Segement1 到SegementX全部重新复制。新数据会立即附加并存储为 Topic1-Part2中的Segement X+1 。Segement X+1分布并存储在 Bookie 1、2 和 4 上。因为它不必重新复制数据,所以所有权转移会立即发生,而不会牺牲Topic partition的可用性。
Bookie集群扩容
下图说明了 Pulsar 如何处理集群扩展。当Broker2 将消息写入 Topic1-Part2的Segement X 时,Bookies X 和 Y 被添加到集群中。Bookies X 和 Y 立即被 Broker2 发现,然后 Broker 会尝试将 Segment X+1 和 X+2 的消息存储到新添加的 Bookie 中。新添加的 Bookie 的流量立即增加,无需重新复制任何数据。Apache BookKeeper 提供多种感知放置策略,以确保集群中所有存储节点之间的流量平衡,防止数据严重倾斜。
Bookie故障恢复
下图说明了 Pulsar(通过 Apache BookKeeper)如何处理 Bookie磁盘写入故障。这里有一个磁盘故障导致 Bookie2 上的Segement 4 被破坏。Apache BookKeeper 将检测到这一点并安排副本修复。Apache BookKeeper 中的副本修复是在Segement(甚至是入口)级别的多对多快速修复,比重新复制整个主题分区的粒度要细得多。Apache BookKeeper 可以从 Bookie 3 和 Bookie 4 中的任何一个读取Segement 4 的消息,并在 Bookie1 处修复Segement 4。所有副本修复都在后台进行。所有的 Broker 都可以立即继续写入,而不会牺牲主题分区的可用性,方法是交换一个工作的 Bookie 来替换该段的失败Bookie。
与Kafka的区别
Kafka 和 Pulsar 都有类似的消息传递概念。客户端通过Topic与两个系统进行交互。每个主题被划分为多个分区。但是Pulsar 和 Kafka 的根本区别在于 Kafka 是一个以分区为中心的 发布/订阅系统,而Pulsar 是一个以 分段为中心的 发布/订阅系统。
- 上图说明了以分区为中心和以段为中心的系统之间的区别。在 Kafka 中,一个分区只能存储在单个节点上并复制到其他节点,其容量受到最小节点容量的限制。扩容时(或副本故障、磁盘故障、机器故障)需要重新平衡分区,为了防止数据倾斜,需复制整个分区以平衡新添加的代理的数据和流量。重新复制过程中数据容易出错,且会消耗网络带宽和 I/O。在执行此操作时需要非常小心,否则很容易影响生产环境。
- 相比之下,在 Apache Pulsar 中,分区会以分段分布在集群中,以段为中心,在上面可扩展性中已经说到,扩展容量时不需要重新平衡数据,BookKeeper 中使用可扩展的以段为中心的分布式日志存储系统。新节点或新分区上的流量将立即自动增加,并且不会重新复制旧数据。通过利用分布式日志存储,Pulsar 可以最大化段放置选项,实现高写入和读取可用性。例如,对于 BookKeeper副本设置等于 2,只要任何 2 个 Bookie 启动,Topic Partition就可用于写入。对于读取可用性,该分段只要一个Bookie能够提供服务,就可以读取它。
跨地域复制
在Pulsar中,跨地域复制不仅可以应用在多数据中心的异地灾备场景,也可以构建集群的读写分离方案,可以参考腾讯的计费平台场景
Pulsar中异步异地复制过程
在异步异地复制中,当在 Pulsar 主题上生成消息时,它们首先被持久化到本地集群,然后异步复制到远程集群。在正常情况下,当没有连接问题时,会立即复制消息,同时将它们分派给本地消费者。集群与集群之间同步延迟由数据中心之间的网络往返时间 (RTT) 决定。
Pulsar中异步异地复制原理
在此图中,每当生产者 P1、 P2分别向集群 A、 集群 B中 的主题 T1发布消息时,这些消息都会立即跨集群复制到C集群,集群A和集群B复制到集群C的过程是通过Producer异步发给集群C的,Cursor和订阅消费同理,在这个过程用于记录复制到哪个消息。
在 Pulsar 中,按租户启用异地复制 。只有当租户可以访问两个集群时,才能在两个特定集群之间启用异地复制。复制在Namespace级别进行管理的,可以创建和配置命名空间在租户可以访问的两个或多个集群之间进行复制。在该命名空间中的任何主题上发布的任何消息都将被复制到指定集中的所有集群。
层级存储
- 第一级缓存(L1)是 Pulsar Broker 本身,用于提供Tailing Read。当消息可以被提交时,它可以直接发送给附加到该主题的所有订阅者,而根本不需要涉及磁盘。
- 下一级缓存(L2)是 BookKeeper 节点上的Bookie存储磁盘。当一条消息被写入 BookKeeper 节点上的日志时,它也会被写入一个内存缓冲区(Memtable),该缓冲区会定期flush到Bookie的Ledger Storage。BookKeeper 节点使用此磁盘来提供读取服务。在 Pulsar 中,很少会从一个内存缓冲区(Memtable)读取消息。Tailing Reads消费者通常直接从 Pulsar 的缓存中读取消息。Catch-up Reads消费者通常会请求太旧而无法在Memtable的消息。Bookie存储磁盘为这些Catch-up Reads提供服务。Bookie存储磁盘以一种格式存储消息,该格式优化了在同一磁盘上存储许多不同主题的能力,同时尽可能地在同一主题内提供顺序读取。
- 最后一级缓存(L3)是长期存储,如果为 Pulsar 配置了“分层存储”,则使用该存储。分层存储允许用户将主题积压的旧部分移动到更便宜的存储形式(如S3、GCS、HDFS)。分层存储利用了消息的不变性,并将单个消息长期存储在Bookie会很浪费资源。Pulsar 主题日志由Ledger(Segement)组成,每个Ledger默认对应一个 50000 条消息的序列。一次只有一个Ledger处于活动状态。之前的所有Ledger都已关闭。当Ledger关闭时,不能向其中添加新消息。鉴于Ledger中的各个消息是不可变的,并且它们的偏移量是不可变的,因此整个段因此是不可变的。所以可以复制到更廉价的存储中。
总结
本文基础篇整理了Pulsar核心特性及其相关原理,限于篇幅,有些地方未做深层次探究,后续会对zab/raft/dl 共识算法的相似和区别、Pulsar中如何保证数据不丢不重,还有一些事务消息和延时队列等扩展特性继续进行探究。