1.2 Pulsar的优势
开源领域中有诸多优秀的开源消息队列,如RabbitMQ、RocketMQ和Kafka。在它们的基础上,Pulsar实现了很多上一代消息系统或者上一代流数据系统没有实现的功能和特性,比如云原生、多租户、存储与计算分离、分层存储等。针对之前消息队列系统的诸多痛点,Pulsar予以解决。
本节将从多个角度介绍Pulsar,让读者对Pulsar的优势有一个整体认识。为了帮助大家深刻理解这些优势,本节和1.3节都会对Pulsar、RocketMQ、Kafka进行对比。
1.2.1 Pulsar不只是消息队列
消息队列是在消息的传输过程中保存消息的容器。消息队列是分布式系统中重要的中间件,在高性能、高可用、低耦合等系统架构中扮演着重要的角色。在业务系统中,通过消息队列可以实现异步通信并解耦业务系统;在访问量剧增的情况下,通过消息队列可以实现流量的削峰填谷。
在大数据场景下,大数据系统需要处理流式数据,如业务日志数据、监控数据、用户行为数据等。通过消息队列可对这些数据进行采集和汇总,然后导入大数据实时计算引擎,或利用消息对接功能写入存储之中。使用消息队列可解决异构系统的数据对接问题。
基于上述背景和问题,Pulsar诞生了。
1)Pulsar是一个分布式消息平台,可以同时处理流式数据和异构系统数据对接这两类问题。这是因为Pulsar具有非常灵活的消息传递模型。
2)为了实现更加丰富的消费模式,Pulsar提出了订阅的概念。订阅是一种数据消费的规则,它决定了如何将消息传递给消费者。不同的订阅模式可决定多个消费者在消费时的不同行为。Pulsar提供了多种订阅模式—独占模式(Exclusive)、故障切换模式(Failover)、共享模式(Shared)、键共享模式(Key_Shared),可以根据不同的业务场景选择不同的订阅模式。Pulsar还提供了数据连接器的功能,Pulsar I/O是Pulsar提供的将数据灵活地导入其他系统或从其他系统中导出的工具。
3)Pulsar是一个集消息传递、消息存储、轻量化函数式计算于一体的流数据平台。Pulsar不仅提供了数据的存储与消费能力,还提供了一定的流处理能力。Pulsar Function是Pulsar自带的一个轻量级计算引擎,可以对Pulsar内的数据进行简单统计、过滤、汇总。
4)Pulsar是一个分布式可扩展的流式存储系统,并在数据存储的基础上构建了消息队列和流服务的统一模型。其中,Pulsar具有的消息队列功能类似于RabbitMQ和RocketMQ在业务系统中的功能,数据流的模型类似于大数据系统中的Kafka。注意,消息队列和流服务这两个概念并不是完全无关的,它们只是在逻辑上侧重点不同而已。
1.2.2 存储与计算分离
很多人都知道存储和计算分离,那么为什么需要存储和计算分离呢?Pulsar为什么可以实现存储和计算分离呢?
1. 为什么需要存储和计算分离
在分布式系统中,有许多存储与计算混合部署的产品,如Hadoop与Kafka。在架构设计时选择计算和存储混合部署,可以将计算移动到数据所在的地方,充分发挥数据本地读写的优势,加快计算速度并降低网络带宽的影响。Kafka是将计算和存储混合部署的优势发挥到极致的一个项目。
Kafka最初由领英开发,并于2011年年初开源,于2012年成为Apache顶级项目。Kafka项目的目标是为实时数据处理提供一个统一、高吞吐、低延迟的平台。
在Kafka中,每个分区的管理与存储职能都依赖其中一个服务端节点(承担分区Leader角色的Broker节点)。该节点在处理数据写入请求的同时,会将数据写到本机的存储路径下,并负责向其他副本写入数据。在数据的读取过程中,该节点负责读取磁盘数据并发送回客户端。在Kafka的架构中,存储与计算功能都由一个Kafka Broker节点负责,这样可简化服务端处理流程,增大单机的处理吞吐量。RabbitMQ和RocketMQ与Kafka类似,采用的也是存储与计算相结合的设计方式。
随着技术的进步,网络速度越来越快,不再是分布式系统的瓶颈,而计算机磁盘I/O速度增长相对缓慢,计算和存储混合的架构缺点随之逐渐显现[8]。
计算与存储混合的设计可能会造成机器资源的浪费。当系统的计算资源或存储资源不足时,需要对系统进行扩容。此时无论计算资源和存储资源谁先达到瓶颈,都需要增加机器,所以可能会浪费很多机器资源。当对存储资源进行扩容时,在计算和存储混合的模式下可能需要迁移大量数据,进而大大增加扩容成本。
由于计算和存储混合暴露出来的缺点越来越多,加之网络速度越来越快,架构设计又重新回到计算和存储分离这一方向上[9]。
2. Pulsar存储与计算分离的原理分析
Pulsar是一个存储与计算分离的消息队列,其中提供计算服务的角色被称为Broker,提供存储服务的角色被称为Bookie。Broker是服务端的一个无状态组件,主要负责两类职能—数据的生产消费与Pulsar管理。真正扛起存储重任的是BookKeeper。
Broker提供的是无状态的计算服务,在计算资源不足时可独立扩容。Bookie提供的是有状态的存储服务,Pulsar中的数据会以数据块的形式分配到不同的Bookie节点。当存储资源不够时,可通过增加Bookie节点进行扩容。Pulsar会感知Bookie集群的变化,并在合适的时机使用新增加的Bookie节点进行存储,这就避免了人为迁移数据的操作。Broker与Bookie的扩容相互独立,避免了资源浪费,提高了Pulsar的可维护性。
Pulsar是一个复杂的系统,由多个组件构成,第2章会对Pulsar的基本结构进行更详细的介绍。
1.2.3 云原生架构
云原生是一种在云计算时代构建和运行应用的方法,可以充分利用和发挥云平台的弹性、自动化优势。云原生应用在云上可以最佳方式运行,让业务系统的可用性、敏捷性和可扩展性得到大幅提升。
云原生的代表技术包括容器、服务网格(Service Mesh)、微服务、不可变基础设施和声明式API等。利用这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统做出频繁和可预测的重大变更。
云原生应用能够做到容器化,容器可以让应用以一种标准化的方式进行交付,以一种敏捷、可扩展和可复制的方式部署到云上,从而充分发挥云的能力。容器消除了线上与线下的环境差异,保证了应用在生命周期内环境的一致性和标准化。云原生基础设施能够提供动态编排调度功能。应用开发者可以借助Docker将应用和依赖打包到一个可移植的容器中,并通过Kubernetes进行容器化管理,实现集群的自动化部署、扩缩容、维护等功能。
Pulsar是一个云原生应用,拥有诸多云原生应用的特性,如无状态计算层、计算与存储分离,可以很好地利用云的弹性(伸缩能力),从而具有足够高的扩容性和容错性。Pulsar是消息队列领域基于云原生基础架构设计的产品,它能够很好地在容器化环境中运行。
Pulsar采用的存储与计算分离架构在云原生环境中有着更大的价值。Pulsar实例中的存储节点可以由一组Bookie容器负责,计算节点由一组Broker容器负责。存储与计算节点可以完全独立扩缩容,通过Kubernetes这样的容器编排工具,业务方可以快速构建可弹性扩缩容的云原生消息队列。
此外,Pulsar对Kubernetes有良好的支持。在开源项目Apache Pulsar Helm Chart[10]的支持下,Pulsar可以保障业务轻松迁移到Kubernetes环境中。在Apache Pulsar Helm Chart项目中,Kubernetes可以单独管理各类组件,如Zookeeper、Bookie、Broker、Function、Proxies。除此之外,Apache Pulsar Helm Chart项目中还集成了Pulsar的管理与监控工具,如Pulsar Manager、Prometheus与Grafana。
社区还提供了Pulsar Operator。Pulsar Operator是一个在Kubernetes中管理Pulsar集群的控制器[11],它为Pulsar提供了完整生命周期的管理功能,包括部署、升级、扩展和配置更改。借助Pulsar Operator,Pulsar可以在部署于公共云或私有云上的Kubernetes集群中无缝运行。
1.2.4 Pulsar的存储特性
Pulsar依赖BookKeeper构建存储能力,并因此具备了分块存储、分层存储、存储与计算分离的特性。本节将深入讨论Pulsar的存储特性。
在大数据系统中,使用分块存储的系统有着悠久的历史,从GFS(Google File System,Google文件系统)到开源的HDFS(Hadoop File System)。这种面向大规模数据密集型应用的、可伸缩的分布式文件系统在大数据存储领域引起巨大反响。基于分块存储的文件系统可以运行在廉价的普通硬件设备上,并提供灾难冗余的能力[12]。
在这类分布式文件系统中,所有的文件被抽象成数据块进行存储,因此我们可以方便地对这类文件系统进行扩展。
众多的消息队列所采用的数据存储结构各异,我们将通过对比Kafka与RocketMQ来介绍Pulsar的分块存储结构。
1. Kafka的存储结构
Kafka基于只追加日志文件策略构建了核心存储结构,消息队列中的数据以日志的方式进行组织,对日志的所有写操作都提交在日志的最末端,而对日志的读取也只能按顺序进行。每个主题的每个分区都对应着一个独立文件,Kafka的分区对应着文件系统的物理分区。该设计让Kafka具有了高吞吐量与低成本的优势。机械硬盘的连续读写性能较好,但随机读写性能很差,这是因为磁头移动到正确的磁道上需要时间。随机读写时,磁头需要不停地移动来进行寻址,这浪费了很多时间,因此整体性能不高。顺序写入文件的存储结构使Kafka即使使用机械硬盘也可以拥有高性能与高吞吐量。
Kafka可以借助多分区来实现主题级别的扩展,也可以通过增加物理机的方式实现一定程度的横向扩容。但是,也正是因为这种存储结构,在大规模集群中使用Kafka会引入了新的问题。
❑ 会带来额外的运维成本。在Kafka中每一个分区都只能归属于一台机器,即ISR(In-Sync Replicas,同步的副本)集合中的一个主节点。Kafka的多个数据副本只能保证高可用性,每个分区的容量大小受限于主节点的磁盘大小。如果在单个磁盘即将被占满时进行扩容,需要运维人员进行扩展分区和迁移数据等操作。
❑ 有单机分区上限问题。每个Kafka分区都会使用一个顺序写入的文件进行数据存储。但是在单个Kafka节点上有成百上千个分区时,从磁盘角度看,若顺序写入过多则其会退化为随机写入,此时磁盘读写性能会随着分区数量的增加而降低。因此Kafka单节点可以支持的分区数量是有限制的。
2. RocketMQ的存储结构
在RocketMQ消息队列中,消息的存储是由内部的消费队列(ConsumeQueue)和提交日志(CommitLog)配合完成的。消息的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,在消息队列中起索引的作用。在RocketMQ中每个实际的主题都对应着一个ConsumeQueue,其中存放着CommitLog中所有消息的存放位置。CommitLog以物理文件的形式存放在服务器中,并被当前服务器中所有ConsumeQueue共享。不同于Kafka使每个逻辑队列都对应着一个物理分区,RocketMQ采用物理存储与逻辑队列相互分离的分区方式,这种分区方式可以称为逻辑分区。
在逻辑分区方式下,多个分区的数据会写入同一个数据文件中,这使RocketMQ可以支持大量的分区数据量,但是在消费数据时却会因存在较多的随机读操作而降低数据读取的效率。针对逻辑分区的架构,RocketMQ做了一些优化,例如CommitLog以顺序写的形式来提高写入性能,在随机读取时充分利用操作系统的页缓存机制来提升读取性能。但是理论上RocketMQ的吞吐量依然比Kafka低。
3. Pulsar的存储结构
Pulsar利用BookKeeper实现了分块存储的能力,它在一定程度上兼具Kafka物理分区与RocketMQ逻辑分区的优点。
在BookKeeper中,Ledger代表一个独立日志块或一段数据流,是持久化存储的单元。记录会被有序地写入Ledger中。数据一经写入就不允许进行修改了。Pulsar能够将每个主题映射为多个独立的数据段,每个数据段对应一个Ledger。随着时间的推移,可以为主题创建多个数据段,为Pulsar主题提供几乎无限的存储能力。
每个Ledger都拥有独立的I/O能力,Pulsar可以将Broker上的网络I/O均匀分布在不同的Bookie节点上,又可以充分利用Bookie节点的I/O能力。Pulsar通过BookKeeper获得了容量和吞吐量方面的水平可扩展能力,通过向集群添加更多Bookie节点,可以立即增加容量与吞吐量。
Pulsar的存储原理将在第5章详细介绍,这里暂不展开。
1.2.5 消息传输协议
Pulsar支持可插拔的协议处理机制,可以在运行时动态加载额外的协议处理程序。基于消息队列协议层,目前Pulsar已经支持Kafka、RocketMQ、AMQP和MQTT等多种协议。并将自身云原生、分层存储、自动负载管理等诸多特性推广至更多的消息队列系统[13]。
Pulsar协议层支持的Kafka项目为Kafka on Pulsar(KoP)协议。通过将KoP协议部署在现有的Pulsar集群中,用户可以在Pulsar集群中继续使用原生的Kafka协议,同时能够利用Pulsar的强大功能,完善存量Kafka应用的使用体验。
在使用原生Kafka客户端的情况下,通过Pulsar可构建Kafka服务端功能,从而以低成本的方式解决Kafka在多租户支持、负载均衡、海量主题支持等方面的痛点。更多关于Pulsar消息传输协议的内容会在6.2节介绍。
1.2.6 消费方式
从消息队列中读取数据的角色称为消费者。消费者从消息队列中读取数据有两种方法—主动拉取(Pull模式)与被动接收(Push模式)。在Pull模式下,消费者会不断轮询消息队列,判断是否有新的数据,如果有就读取该数据。在Push模式下,一旦生产者有新数据放入消息队列中,系统就会推送给消费者。
Push模式具有更好的实时性,由服务端发送消息,消息在客户端缓冲队列等待客户端的实际读取。该模式需要有一定的反压机制,以避免因消息堆积导致内存溢出。Pull模式能够更好地控制数据流,数据拉取的操作由客户端控制,客户端在需要读取数据时会主动发送数据拉取请求,服务端根据该请求将数据发送到客户端。
RocketMQ与Kafka都基于Pull模式进行数据读取。Pull模式的优势在于可以控制数据的消费速度和消费数量,保证消费者不会达到饱和状态。但是在没有数据时,会出现多次空轮询,浪费计算资源。
Pulsar中的消费者在读取数据时采用以Push模式为主、Pull模式为辅的同步模式。Pulsar中的客户端有一个缓冲队列。客户端会向服务端发送流量配额请求,服务端会主动向客户端推送配额允许范围内的数据。消费者连接建立后,服务端通过配额向消费者推送数据。与此同时,消费者每消费一条数据都会增加客户端流量配额计数,在配额计数达到队列的一半时,客户端会再次发送流量配额请求,请求服务端推送数据。数据同步的原理会在5.4.3节进行更加详细的介绍。
1.2.7 丰富的功能与生态
用户在使用消息队列或者流式服务时,有时遇到的应用场景仅是对消息进行搬运,或者进行一些简单的统计、过滤、汇总等操作,Pulsar通过Pulsar Function就可以原生支持这些功能。Pulsar官方提供了多种导入与导出数据的连接器。通过简单地配置Pulsar I/O,可灵活地将Pulsar与关系型数据库、非关系型数据库(如MongoDB)、数据湖、Hadoop生态等外部系统相结合。Pulsar Function相关内容会在第7章进行更加详细的介绍。
在大数据应用中,Pulsar可以用于存储结构化数据。结构化数据由预定义的字段构成,Pulsar提供了Schema功能来进行结构化定义。通过Pulsar SQL功能,Trino Pulsar连接器使Trino集群内的Trino Worker能够通过SQL语句查询数据。Pulsar SQL相关内容会在第8章进行更详细的介绍。
在Pulsar中使用运维管理与监控工具,如Prometheus、Grafana、Pulsar Manager等,能够减少在运维、优化、排错方面的投入。第9章将会介绍如何使用这些工具。
经过社区的努力,现在Pulsar可以与多种大数据生态结合,如Kafka、HDFS、HBase、Flink、Spark、Trino等。Pulsar与Flink结合的内容将会在第10章进行介绍。Pulsar在大数据生态中的应用场景与使用方法会在第11章进行介绍。