如何打造可以无限扩展的分布式消息队列?
几十年前,消息队列开始兴起,它用于连接大型机和服务器应用程序,并逐渐在企业的服务总线与事件总线设计模式、应用间的路由和数据迁移中发挥至关重要的作用。自此,应用程序架构和数据角色经历了重大变化:例如,面向服务的架构、流处理、微服务、容器化、云服务和边缘计算,这些只是诸多变化中的冰山一角。这些变化创造了大量的新需求,这些新需求远远超出了原有消息队列的技术能力。
为了满足这些需求,处理消息队列的全新方法应运而生。现代应用对消息解决方案的要求不仅仅是主动连接、移动数据,而是要在持续增长的服务和应用中智能处理、分析和传输数据,并且在规模持续扩大的情况下不增加运营负担。
为了满足上述要求,新一代的消息传递和数据处理解决方案 Apache Pulsar 应运而生。Apache Pulsar 起初作为消息整合平台在 Yahoo 内部开发、部署,为 Yahoo Finance、Yahoo Mail 和 Flickr 等雅虎内部关键应用连接数据。2016 年 Yahoo 把 Pulsar 开源并捐给 Apache 软件基金会(ASF),2018 年 9 月 Pulsar 毕业成为 ASF 的顶级项目,逐渐从单一的消息系统演化成集消息、存储和函数式轻量化计算的流数据平台。
Pulsar 的设计是为了方便和现有的 Kafka 部署集成,同时也方便开发人员将其连接到应用程序。Pulsar 最初就是为连接 Kafka 构建的。Pulsar 提供和 Kafka 兼容的 API,无需更改代码,只要使用 Pulsar 客户端库重新编译,现有应用程序即可连接到 Kafka。 Pulsar 还提供内置的 Kafka 连接器,可以消费 Kafka topic 的数据或将数据发布到 Kafka topic。
系统架构是软件最底层的设计决策,一旦实施,就很难改变。架构决定了软件特性和根本不同。Apache Pulsar 在功能上有很多优势,例如统一的消费模型,多租户,高可用性等等,但最本质、最重要的区别还是 Apache Pulsar 的系统架构。Apache Pulsar 的设计架构与其他消息传递解决方案(包括 Apache Kafka)的架构有着本质不同,Pulsar 从设计时就采用了分层分片式的架构,以提供更好的性能、可扩展性和灵活性。
现实生活中,存在的消息系统有很多,Yahoo 为什么研发自己的消息系统呢?因为已有的消息系统无法解决 Yahoo 遇到的问题和规模,Yahoo 需要多租户,能够支撑上百万的 topics,同时满足低延迟、持久化和跨地域复制要求。而现有的消息系统,存在如下诸多问题:
- 分区模型紧耦合存储和计算,不是云原生(Cloud Native)的设计。
- 存储模型过于简单,对文件系统依赖太强。
- IO 不隔离,消费者在清除 Backlog 时会影响其他生产者和消费者。
- 运维复杂,替换机器、服务扩容需重新均衡数据。
于是,我们决定开始研发 Pulsar 来解决消息队列的扩展性问题。解决扩展性问题的核心思路是数据分片,Pulsar 从设计时就采用了分层分片式的架构,以提供更好的性能、可扩展性和灵活性。
下面我们从技术角度来详细解析 Apache Pulsar 的架构。
Pulsar 的分层架构
从数据库到消息系统,大多数分布式系统采用了数据处理和数据存储共存于同一节点的方法。这种设计减少了网络上的数据传输,可以提供更简单的基础架构和性能优势,但其在系统可扩展性和高可用性上会大打折扣。
Pulsar 架构中数据服务和数据存储是单独的两层:数据服务层由无状态的 “Broker” 节点组成,而数据存储层则由 “Bookie” 节点组成。
图 1. 传统单体架构 vs. Pulsar 存储计算分层架构
这种存储和计算分离的架构给 Pulsar 带来了很多优势。首先,在 Pulsar 这种分层架构中,服务层和存储层都能够独立扩展,可以提供灵活的弹性扩容。特别是在弹性环境(例如云和容器)中能够自动扩容缩容,并动态适应流量的峰值。并且, Pulsar 这种分层架构显著降低了集群扩展和升级的复杂性,提高了系统可用性和可管理性。此外,这种设计对容器是非常友好的,这使 得 Pulsar 也成为了流原生平台的理想选择。
Pulsar 系统架构的优势也包括 Pulsar 分片存储数据的方式。Pulsar 将主题分区按照更小的分片粒度来存储,然后将这些分片均匀打散分布在存储层的 “bookie” 节点上。这种以分片为中心的数据存储方式,将主题分区作为一个逻辑概念,分为多个较小的分片,并均匀分布和存储在存储层中。这种架构设计为 Pulsar 带来了更好的性能,更灵活的扩展性和更高的可用性。
Pulsar 架构中的每层都可以单独设置大小,进行扩展和配置。根据其在不同服务中的作用不同,可灵活配置集群。对于需要长时间保留的用户数据,无需重新配置 broker,只要调整存储层的大小。如果要增加处理资源,不用重新强制配置存储层,只需扩展处理层。此外,可根据每层的需求优化硬件或容器配置选择,根据存储优化存储节点,根据内存优化服务节点,根据计算资源优化处理节点。
图 2. Apache Pulsar 分层分片的架构
而大多数消息队列技术(包括 Apache Kafka)都采用单体架构,其消息处理和消息持久化(如果提供了的话)都在集群内的同一个节点上。这种体系结构在大多数传统的数据库平台以及 Hadoop 等大数据系统中也较为常见,与昂贵的外部存储阵列的常见替代方案相比,其设计目的在于将数据的计算与存储放到同一台机器上来处理,以减少网络流量和访问延迟,同时降低存储成本。这种方法在小型环境中很容易部署,但在性能、可伸缩性和灵活性方面存在明显问题。随着固态磁盘的广泛使用,网络带宽的迅速提升以及存储延迟的显著降低,已经没有必要采用单体架构进行这种权衡处理了。
接下来,我们结合数据处理中各种不同的 IO 访问模式来深入了解 Pulsar 系统架构的优势。
IO 访问模式的优势
流系统中通常有三种 IO 访问模式:
1.写(Writes):将新数据写入系统中;
2.追尾读(Tailing Reads):读取最近写入的数据;
3.追赶读(Catch-up Reads):读取历史的数据。例如当一个新消费者想要从较早的时间点开始访问数据,或者当旧消费者长时间离线后又恢复时。
和大多数其他消息系统不同,Pulsar 中这些 IO 访问模式中的每一种都与其他模式隔离。在同样 IO 访问模式下,我们来对比下 Pulsar 和其他传统消息系统(存储和服务绑定在单个节点上,如 Apache Kafka)的不同。
传统消息系统(图 3 左侧图)中,每个 Broker 只能利用本地磁盘提供的存储容量,这会给系统带来一些限制:
1.Broker 可以存储和服务的数据量受限于单个节点的存储容量。因此,一旦 Broker 节点的存储容量耗尽,它就不能再提供写请求,除非在写入前先清除现有的部分数据。
2. 对于单个分区,如果需要在多个节点中存储多个备份,容量最小的节点将决定分区的最终大小。
相比之下,在 Apache Pulsar(图 3 右侧图)中,数据服务和数据存储是分离的,Pulsar 服务层的任意 Broker 都可以访问存储层的所有存储节点,并利用所有节点的整体存储容量。在服务层,从系统可用性的角度来看,这也有着深远的影响,只要任一个 Pulsar 的 Broker 还在运行,用户就可以通过这个 Broker 读取先前存储在集群中的任何数据,并且还能够继续写入数据。
下面我们来详细看一下在每种 IO 访问模式下的架构优势。
写
在传统消息系统架构中,一个分区的所有权会分配给 Leader Broker。对于写请求,该 Leader Broker 接受写入并将数据复制到其他 Broker。如图 3 左侧所示,数据首先写入 Leader Broker 并复制给其他 followers。 数据的一次持久化写入的过程需要两次网络往返。
在 Pulsar 系统架构中,数据服务由无状态 Broker 完成,而数据存储在持久存储中。数据会发送给服务该分区的 Broker,该 Broker 并行写入数据到存储层的多个节点中。一旦存储层成功写入数据并确认写入,Broker 会将数据缓存在本地内存中以提供追尾读(Tailing Reads)。
图 4. Writes 访问模式对比
如图 4 所示,和传统的系统架构相比,Pulsar 的系统架构并不会在写入的 IO 路径上引入额外的网络往返或带宽开销。而存储和服务的分离则会显著提高系统的灵活性和可用性。
追尾读
对于读取最近写入的数据场景,在传统消息系统架构中,消费者从 Leader Broker 的本地存储中读取数据;在 Pulsar 的分层架构中,消费者从 Broker 就可以读取数据,由于 Broker 已经将数据缓存在内存中,并不需要去访问存储层。
图 5. Tailing Read 访问模式对比
这两种架构只需要一次网络往返就可以读取到数据。由于 Pulsar 在系统中自己管理缓存中的数据,没有依赖文件系统缓存,这样 Tailing Reads 很容易在缓存中命中,而无需从磁盘读取。传统的系统架构一般依赖于文件系统的缓存,读写操作不仅会相互竞争资源(包括内存),还会与代理上发生的其他处理任务竞争。因此,在传统的单片架构中实现缓存并扩展非常困难。
追赶读
追赶读(Catch-up Reads)非常有趣。传统的系统架构对 Tailing reads 和 Catch-up reads 两种访问模式进行了同样的处理。即使一份数据存在多个 Broker 中,所有的 Catch-up reads 仍然只能发送给 Leader Broker。
Pulsar 的分层架构中历史(旧)数据存储在存储层中。Catch-up 读可以通过存储层并行读取数据,而不会与 Write 和 Tailing Reads 两种 IO 模式竞争或干扰。
三种 IO 模式放在一起看
最有趣的是当你把这些不同的模式放在一起时,也就是实际发生的情况。这也正是单体架构的局限性最令人痛苦的地方。传统的消息系统架构中,所有不同的工作负载都被发送到一个中心(Leader Broker)位置,几乎不可能在工作负载之间提供任何隔离。
然而,Pulsar 的分层架构可以很容易地隔离这些 IO 模式:服务层的内存缓存为 Tailing Reads 这种消费者提供最新的数据;而存储层则为历史处理和数据分析型的消费者提供数据读取服务。
图 6. 三种 IO 模式对比
这种 IO 隔离是 Pulsar 和传统消息系统的根本差异之一,也是 Pulsar 可用于替换多个孤立系统的关键原因之一。Apache Pulsar 的存储架构读、写分离,能保证性能的一致性,不会引起数据发布和数据消费间的资源竞争。已发布数据的写入传递到存储层进行处理,而当前数据直接从 broker 内存缓存中读取,旧数据直接从存储层读取。
超越传统消息系统
上面讨论了 Pulsar 的分层架构如何为不同类型的工作负载提供高性能和可扩展性。Pulsar 分层架构带来的好处远远不止这些。我举几个例子。
无限的流存储
并行访问流式计算中的最新数据和批量计算中的历史数据,是业界一个普遍的需求。
由于 Pulsar 基于分片的架构,Pulsar 的一个主题在理论上可以达到无限大小。当容量不足时,用户只需要添加容器或存储节点即可轻松扩展存储层,而无需重新平衡数据;新添加的存储节点会被立即用于新的分片或者分片副本的存储。
Pulsar 将无界的数据看作是分片的流,分片分散存储在分层存储(tiered storage)、BookKeeper 集群和 Broker 节点上,而对外提供一个统一的、无界数据的视图。其次,不需要用户显式迁移数据,减少存储成本并保持近似无限的存储。因此,Pulsar 不仅可以存储当前数据,还可以存储完整的历史数据。
图 7. 无限的流存储
数据查询和数据分析
Pulsar 有能力存储数据流的完整历史记录,因此用户可以在其数据上使用各种数据工具。Pulsar 使用 Pulsar SQL 查询历史消息,使用 Presto 引擎高效查询 BookKeeper 中的数据。Presto 是用于大数据解决方案的高性能分布式 SQL 查询引擎,可以在单个查询中查询多个数据源的数据。Pulsar SQL 允许 Presto SQL 引擎直接访问存储层中的数据,从而实现交互式 SQL 查询数据,而不会干扰 Pulsar 的其他工作负载。Pulsar 与 Presto 的集成就是一个很好的例子,如下是使用 Pulsar SQL 查询的示例。
图 8. Presto 与 Apache Pulsar 的集成
Apache Pulsar 的周边生态
批处理是对有界的数据进行处理,通常数据以文件的形式存储在 HDFS 等分布式文件系统中。流处理将数据看作是源源不断的流,流处理系统以发布 / 订阅方式消费流数据。当前的大数据处理框架,例如 Spark、Flink 在 API 层和执行层正在逐步融合批、流作业的提交与执行,而 Pulsar 由于可以存储无限的流数据,是极佳的统一数据存储平台。Pulsar 还可以与其他数据处理引擎(例如 Apache Spark 或 Apache Flink)进行类似集成,作为批流一体的数据存储平台,这进一步扩展了 Pulsar 消息系统之外的角色。下图展示了 Pulsar 的周边生态。
图 9. Apache Pulsar 周边生态
总结
Apache Pulsar 是云原生的分布式消息流系统,采用了计算和存储分层的架构和以 Segment 为中心的分片存储,因此 Apache Pulsar 具有更好的性能、可扩展性和灵活性,是一款可以无限扩展的分布式消息队列。
Apache Pulsar 是一个年轻的开源项目,拥有非常多吸引人的特性。Pulsar 社区的发展迅猛,在不同的应用场景下不断有新的案例落地。期待大家能和 Apache Pulsar 社区深入合作,一起进一步完善、优化 Pulsar 的特性和功能。
作者介绍:
Sijie Guo, StreamNative 联合创始人,Apache BookKeeper 和 Apache Pulsar PMC 成员和 Committer。之前是 Twitter 消息组的技术负责人,与他人共同创建了 Apache DistributedLog。加入 Twitter 之前,他曾在 Yahoo!从事推送通知基础架构工作。