导语 | 消息队列是分布式系统中重要的中间件,在高性能、高可用、低耦合等系统架构中扮演着重要作用。本文对Kafka、Pulsar、RocketMQ、RabbitMQ、NSQ这几个消息队列组件进行了一些调研,并整理了相关资料,为业务对MQ中间件选型提供参考。
一、概述
消息队列是分布式系统中重要的中间件,在高性能、高可用、低耦合等系统架构中扮演着重要作用。分布式系统可以借助消息队列的能力,轻松实现以下功能:
近几年出现了一些关注度较高的消息队列中间件选型,如Kafka、Pulsar、RocketMQ等,首先从宏观上做一些对比:
结论:
二、架构简介
(来源:https://zhuanlan.zhihu.com/p/38269875)
一个Kafka集群由多个Broker和一个ZooKeeper集群组成,Broker作为Kafka节点的服务器。同一个消息主题Topic可以由多个分区Partition组成,分区物理存储在Broker上。负载均衡考虑,同一个Topic的多个分区存储在多个不同的Broker上,为了提高可靠性,每个分区在不同的Broker会存在副本。
ZooKeeper是一个分布式开源的应用程序协调服务,可以实现统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等工作。Kafka里的ZooKeeper主要有一下几个作用:
(来源:https://cloud.tencent.com/developer/article/1845616)
Pulsar有三个重要的组件,Broker、BookKeeper和ZooKeeper,Broker是无状态服务,客户端需要连接到Broker上进行消息的传递。BookKeeper与ZooKeeper是有状态服务。BookKeeper的节点叫Bookie,负责存储消息和游标,ZooKeeper存储Broker和Bookie的元数据。Pulsar以这种架构,实现存储和计算分离,Broker负责计算,Bookie负责有状态存储。
Pulsar的多层架构影响了存储数据的方式。Pulsar将Topic分区划分为分片(Segment),然后将这些分片存储在Apache BookKeeper的存储节点上,以提高性能、可伸缩性和可用性。Pulsar的分布式日志以分片为中心,借助扩展日志存储(通过Apache BookKeeper)实现,内置分层存储支持,因此分片可以均匀地分布在存储节点上。由于与任一给定Topic相关的数据都不会与特定存储节点进行捆绑,因此很容易替换存储节点或缩扩容。另外,集群中最小或最慢的节点也不会成为存储或带宽的短板。
(来源:https://rocketmq.apache.org/docs/rmq-arc/)
RocketMQ是阿里开源的消息中间件,它是一个开源的分布式消息传递和流式数据平台。总共有四大部分:NameServer,Broker,Producer,Consumer。
NameServer主要用来管理brokers以及路由信息。broker服务器启动时会注册到NameServer上,并且两者之间保持心跳监测机制,以此来保证NameServer知道broker的存活状态。而且,每一台NameServer都存有全部的broker集群信息和生产者/消费者客户端的请求信息。
Broker负责管理消息存储分发,主从数据同步,为消息建立索引,提供消息查询等能力。
(来源:https://www.cxymm.net/article/Super_RD/70238869)
RabbitMQ基于AMQP协议来实现,主要由Exchange和Queue两部分组成,然后通过RoutingKey关联起来,消息投递到Exchange然后通过Queue接收。
(来源:https://zhuanlan.zhihu.com/p/37081073)
NSQ主要有nsqlookup、nsqd两部分组成:
二、选型要点
先来个汇总,接下来会对消息队列中间件的各项功能进行逐个分析。
客户端消费者获取消息的方式,Kafka和RocketMQ是通过长轮询Pull的方式拉取消息,RabbitMQ、Pulsar、NSQ都是通过Push的方式。
pull类型的消息队列更适合高吞吐量的场景,允许消费者自己进行流量控制,根据消费者实际的消费能力去获取消息。而push类型的消息队列,实时性更好,但需要有一套良好的流控策略(backpressure)当消费者消费能力不足时,减少push的消费数量,避免压垮消费端。
消息延迟投递,当消息产生送达消息队列时,有些业务场景并不希望消费者立刻收到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。延迟队列一般分为两种,基于消息的延迟和基于队列的延迟。基于消息的延迟指为每条消息设置不同的延迟时间,当队列有新消息进入的时候根据延迟时间排序,当然这样会对性能造成较大影响。另一种基于队列的延迟指的是设置不同延迟级别的队列,队列中每个消息的延迟时间都是相同的,这样免去了基于延迟时间排序对性能带来的损耗,通过一定的扫描策略即可投递超时的消息。
延迟消息的使用场景比如异常检测重试,订单超时取消等,例如:
Kafka不支持延迟消息。Pulsar支持秒级的延迟消息,所有延迟投递的消息会被Delayed Message Tracker记录对应的index,consumer在消费时,会先去Delayed Message Tracker检查,是否有到期需要投递的消息,如果有到期的消息,则从Tracker中拿出对应的index,找到对应的消息进行消费,如果没有到期的消息,则直接消费正常的消息。对于长时间的延迟消息,会被存储在磁盘中,当快到延迟间隔时才被加载到内存里。
RocketMQ开源版本延迟消息临时存储在一个内部主题中,不支持任意时间精度,支持特定的level,例如定时5s,10s,1m等。
RabbitMQ需要安装一个rabbitmq_delayed_message_exchange插件。
NSQ通过内存中的优先级队列来保存延迟消息,支持秒级精度,最多2个小时延迟。
由于某些原因消息无法被正确的投递,为了确保消息不会被无故的丢弃,一般将其置于一个特殊角色的队列,这个队列一般称之为死信队列。与此对应的还有一个“回退队列”的概念,试想如果消费者在消费时发生了异常,那么就不会对这一次消费进行确认(Ack), 进而发生回滚消息的操作之后消息始终会放在队列的顶部,然后不断被处理和回滚,导致队列陷入死循环。为了解决这个问题,可以为每个队列设置一个回退队列,它和死信队列都是为异常的处理提供的一种机制保障。实际情况下,回退队列的角色可以由死信队列和重试队列来扮演。
Kafka没有死信队列,通过Offset的方式记录当前消费的偏移量。
Pulsar有重试机制,当某些消息第一次被消费者消费后,没有得到正常的回应,则会进入重试Topic中,当重试达到一定次数后,停止重试,投递到死信Topic中。
RocketMQ通过DLQ来记录所有消费失败的消息。
RabbitMQ是利用类似于延迟队列的形式实现死信队列。
NSQ没有死信队列。
优先级队列不同于先进先出队列,优先级高的消息具备优先被消费的特权,这样可以为下游提供不同消息级别的保证。不过这个优先级也是需要有一个前提的:如果消费者的消费速度大于生产者的速度,并且消息中间件服务器(一般简单的称之为Broker)中没有消息堆积,那么对于发送的消息设置优先级也就没有什么实质性的意义了,因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker中至多只有一条消息,对于单条消息来说优先级是没有什么意义的。
Kafka、RocketMQ、Pulsar、NSQ不支持优先级队列,可以通过不同的队列来实现消息优先级。
RabbitMQ支持优先级消息。
一般消息在消费完成之后就被处理了,之后再也不能消费到该条消息。消息回溯正好相反,是指消息在消费完成之后,还能消费到之前被消费掉的消息。对于消息而言,经常面临的问题是“消息丢失”,至于是真正由于消息中间件的缺陷丢失还是由于使用方的误用而丢失一般很难追查,如果消息中间件本身具备消息回溯功能的话,可以通过回溯消费复现“丢失的”消息进而查出问题的源头之所在。消息回溯的作用远不止与此,比如还有索引恢复、本地缓存重建,有些业务补偿方案也可以采用回溯的方式来实现。
Kafka支持消息回溯,可以根据时间戳或指定Offset,重置Consumer的Offset使其可以重复消费。
Pulsar支持按时间对消息进行回溯。
RocketMQ支持按时间回溯,实现的原理跟Kafka一致。
RabbitMQ不支持回溯,消息一旦标记确认就会被标记删除。
NSQ一般消息是不可回溯的,但可以通过nsq_to_file工具,将消息写入到文件,然后从文件里重放消息。
流量削峰是消息中间件的一个非常重要的功能,而这个功能其实得益于其消息堆积能力。从某种意义上来讲,如果一个消息中间件不具备消息堆积的能力,那么就不能把它看做是一个合格的消息中间件。消息堆积分内存式堆积和磁盘式堆积。一般来说,磁盘的容量会比内存的容量要大得多,对于磁盘式的堆积其堆积能力就是整个磁盘的大小。从另外一个角度讲,消息堆积也为消息中间件提供了冗余存储的功能。
Kafka和RocketMQ直接将消息刷入磁盘文件中进行持久化,所有的消息都存储在磁盘中。只要磁盘容量够,可以做到无限消息堆积。
RabbitMQ 是典型的内存式堆积,但这并非绝对,在某些条件触发后会有换页动作来将内存中的消息换页到磁盘(换页动作会影响吞吐),或者直接使用惰性队列来将消息直接持久化至磁盘中。
Pulsar消息是存储在BookKeeper存储集群上,也是磁盘文件。
NSQ通过nsq_to_file工具,将消息写入到文件。
消息队列需要管理消费进度,确认消费者是否成功处理消息,使用push的方式的消息队列组件往往是对单条消息进行确认,对于未确认的消息,进行延迟重新投递或者进入死信队列。
Kafka通过Offset的方式确认消息。
RocketMQ与Kafka类似也会提交Offset,区别在于消费者对于消费失败的消息,可以标记为消息消费失败,Broker会重试投递,如果累计多次消费失败,会投递到死信队列。
RabbitMQ和NSQ类似,消费者确认单条消息,否则会重新放回队列中等待下次投递。
Pulsar使用专门的Cursor管理。累积确认和Kafka效果一样;提供单条或选择性确认。
消息TTL表示一条消息的生存时间,如果消息发出来后,在TTL的时间内没有消费者进行消费,消息队列会将消息删除或者放入死信队列中。
Kafka根据设置的保留期来删除消息。有可能消息没被消费,过期后被删除。不支持TTL。
Pulsar支持TTL,如果消息未在配置的TTL时间段内被任何消费者使用,则消息将自动标记为已确认。消息保留期与消息TTL之间的区别在于:消息保留期作用于标记为已确认并设置为已删除的消息,而TTL作用于未ack的消息。上面的图例中说明了Pulsar中的TTL。例如,如果订阅B没有活动消费者,则在配置的TTL时间段过后,消息M10将自动标记为已确认,即使没有消费者实际读取该消息。
RocketMQ提及到消息TTL的资料比较少,不过看接口似乎是支持的。
RabbitMQ有两种方式,一个是声明队列的时候在队列属性中设置,整个队列中的消息都有相同的有效期。还可以发送消息的时候给消息设置属性,可以位每条消息都设置不同的TTL。
NSQ似乎还没支持,有一个Feature Request的Issue处于Open状态。
多租户是指通过一个软件实例为多个租户提供服务的能力。租户是指对系统有着相同“视图”的一组用户。不支持多租户的系统里边,往往要为不同用户或者不同集群创建多个消息队列实例实现物理隔离,这样会带来较高的运维成本。作为一种企业级的消息系统,Pulsar的多租户能力按照设计可满足下列需求:
Pulsar通过下列方式满足了上述需求:
以策略的方式定义所有隔离机制,策略可在运行过程中更改,借此降低运维成本并简化管理工作。
消息顺序性是指保证消息有序。消息消费顺序跟生产的顺序保持一致。
Kafka保证了分区内的消息有序。
Pulsar支持两种消费模式,独占订阅的流模式只保证了消息的顺序性,共享订阅队列模型不保证有序性。
RocketMQ需要用到锁来保证一个队列同时只有一个消费者线程进行消费,保证消息的有序性。
RabbitMQ顺序性的条件比较苛刻,需要单线程发送、单线程消费,并且不采用延迟队列、优先级队列等高级功能。
NSQ是利用了golang自身的case/select实现的消息分发,本身不提供有序性保障,不能够把特性消息和消费者对应起来,无法实现消息的有序性。
在实际开发中,经常要查看MQ中消息的内容,比如通过某个MessageKey/ID,查询到MQ的具体消息。或者是对消息进行链路追踪,知道消息从哪里来,发送到哪里去,进而快速对问题进行排查定位。
Kafka存储层是以分布式提交日志的形式实现,每次写操作都顺序追加到日志的末尾。读也是顺序读。不支持检索功能。
Pulsar可以通过消息ID,查询到具体某条消息的消息内容、消息参数和消息轨迹。
RocketMQ支持按Message Key、Unique Key、Message Id对消息进行查询。
RabbitMQ使用基于索引的存储系统。这些将数据保存在树结构中,以提供确认单个消息所需的快速访问。由于RabbitMQ的消息在确认后会被删除,因此只能查询未确认的消息。
NSQ自身不支持消息持久化和消息检索,不过可以使用nsq_to_http等工具将消息写入可支持索引的存储里。
Kafka有两种消费模式,最终都会保证一个分区只有1个消费者在消费:
Pulsar有以下四种消费模式,其中独占模式和灾备模式跟Kafka类似,为流模型,每个分区只有1个消费者消费,能保证消息有序性。共享模式和Key共享模式为队列模型,多个消费者能提高消费速度,但不能保证有序性。
RocketMQ有两种消费模式,BROADCASTING广播模式,CLUSTERING集群模式。
广播消费指的是:一条消息被多个consumer消费,即使这些consumer属于同一个ConsumerGroup,消息也会被ConsumerGroup中的每个Consumer都消费一次,广播消费中ConsumerGroup概念可以认为在消息划分方面无意义。
集群消费模式:一个ConsumerGroup中的Consumer实例平均分摊消费消息。例如某个Topic有9条消息,其中一个ConsumerGroup有3个实例(可能是3个进程,或者3台机器),那么每个实例只消费其中部分,消费完的消息不能被其他实例消费。
RabbitMQ和NSQ的消费比较类似,都是跟Pulsar共享模式类似的,队列的形式,增加一个消费者组里的消费者数量能提高消费速度。
消息丢失是使用消息中间件时所不得不面对的一个同点,其背后消息可靠性也是衡量消息中间件好坏的一个关键因素。尤其是在金融支付领域,消息可靠性尤为重要。比如当服务出现故障时,一些对于生产者来说已经生产成功的消息,是否会在高可用切换时丢失。同步刷盘是增强一个组件可靠性的有效方式,消息中间件也不例外,Kafka和RabbitMQ都可以支持同步刷盘,但绝大多数情景下,一个组件的可靠性不应该由同步刷盘这种极其损耗性能的操作来保障,而是采用多副本的机制来保证。
Kafka可以通过配置request.required.acks参数设置可靠级别,表示一条消息有多少个副本确认接收成功后,才被任务发送成功。
Pulsar有跟Kafka类似的概念,叫Ack Quorum Size(Qa),Qa是每次写请求发送完毕后需要回复确认的Bookie的个数,其数值越大则需要确认写成功的时间越长,其值上限是副本数Qw。为了一致性,Qa应该是:(Qw+1)/2或者更,即为了确保数据安全性,Qa下限是 (Qw+1)/2。
RocketMQ与Kafka类似。
RabbitMQ是主从架构,通过镜像环形队列实现多副本及强一致性语义的。多副本可以保证在master节点宕机异常之后可以提升slave作为新的master而继续提供服务来保障可用性。
NSQ会通过go-diskqueue组件将消息落盘到本地文件中,通过mem-queue-size参数控制内存中队列大小,如果mem-queue-size=0每条消息都会存储到磁盘里,不用担心节点重启引起的消息丢失。但由于是存储在本地磁盘中,如果节点离线,堆积在节点磁盘里的消息会丢失。
Kafka的公司Confluent在2020年8月发了一篇Benchmarking Apache Kafka, Apache Pulsar, and RabbitMQ: Which is the Fastest?文章,并且提出了一个开源的MQ Benchmark框架THE OPENMESSAGING BENCHMARK FRAMEWORK,在这个文档里,对比了Kafka、Pulsar、RabbitMQ的吞吐量、端到端延迟等性能数据。最后得出结论Kafka相对来说性能最好。
但接下来StreamNative在2020年12月指出了Confluence的基准测试的一些问题,并对Pulsar进行了参数调优之后重新执行了一遍结果,测试报告展示Pulsar能达到跟Kafka同样的吞吐量,在某些场景下,Pulsar的延迟显著低于Kafka。
而且在性能测试上,有很多客户端、服务端参数设置、机器性能配置等影响,比如消息可靠性级别,压缩算法等,很难做到“完全”控制变量公平的测试。而且OpenMessaging Benchmark的开源Github的Readme上也提到了。
不过有几个关注点:
Pulsar和Kafka都被广泛用于各个企业,也各有优势,都能通过数量基本相同的硬件处理大流量。部分用户误以为Pulsar使用了很多组件,因此需要很多服务器来实现与Kafka相匹敌的性能。这种想法适用于一些特定硬件配置,但在多数资源配置相同的情况中,Pulsar的优势更加明显,可以用相同的资源实现更好的性能。举例来说,Splunk最近分享了他们选择Pulsar放弃Kafka的原因,其中提到“由于分层架构,Pulsar帮助他们将成本降低了30%-50%,延迟降低了80%-98%,运营成本降低了33%-50%”。Splunk 团队发现Pulsar可以更好地利用磁盘IO,降低CPU利用率,同时更好地控制内存。
在分布式系统里,单机性能指标虽然也很重要,分布式系统整体的性能以及灵活扩缩容、高可用容灾等能力也会是评估的一个重要参考。MQ中间件具体的性能指标,也需要我们自己根据实际的情况,根据实际购买的集群配置和客户端参数,进行压测调优来评估。
在使用过程中难免会出现各种异常情况,比如宕机、网络抖动、扩容等。消息队列具备异地容灾,高可用架构等能力,能避免一些计算节点、网络等基础设施不可用导致的故障。
Kafka通过分区多副本的方式解决高可用问题。
Pulsar的计算集群Broker是无状态的,可以灵活扩缩容,存储节点Bookie上通过消息分区分片副本的方式,每个分片都有一个或多个副本,保证在某一个Bookie挂掉后,有其他分片可以提供服务。
RocketMQ和RabbitMQ都是主从架构,当master挂掉后,由原来的从节点继续提供服务。备机提供消费服务,保证消息不丢,但不提供写服务。
NSQ是类似分布式架构,不过由于消息存储是在节点本地磁盘上,如果一个节点离线,堆积在节点磁盘上的消息会丢失。
Pulsar原生支持跨地域容灾功能,在这个图中,每当P1、P2和P3的生产者分别向Cluster-A、Cluster-B和Cluster-C中的T1 topic发送消息时,这些消息很快在不同的集群中复制。一旦消息完成复制,消费者C1和C2会从各自的集群消费到这个消息。
在这个跨地域容灾的设计支撑下,其一,我们可以比较容易的将服务分散到多个机房;其二,可以应对机房级别的故障,即在一个机房不可用的情况下,服务可以转接到其它的机房来继续对外提供服务。
一句话概括,Pulsar的跨地域复制,其实就是在一个本地集群中创建一个 Producer,把异地的集群作为这个Producer的发送地址,将本地集群的消息发送过去,并且在本地维护一个Cusor来保证消息可靠性和幂等性。
当消息量突然上涨,消息队列集群到达瓶颈的时候,需要对集群进行扩容,扩容一般分为水平扩容和垂直扩容两种方式,水平扩容指的是往往集群中增加节点,垂直扩容指的是把集群中部分节点的配置调高,增加处理能力。
Kafka集群由于主题分区是物理存储在Broker节点上的,新加入的集群的节点并没有存储分区分片,也就无法提供马上提供服务,因此需要把一些Topic的分区分配到新加入的节点里,这里会涉及到一个分区数据均衡的过程,将某些分区的数据复制到新节点上。这个过程跟分区当前堆积的数据量、Broker性能有关,有可能会出现由于源Broker负载过高,堆积数据过大,导致数据均衡的时间变长。
Pulsar的无限分布式日志以分片为中心,借助扩展日志存储(通过Apache BookKeeper)实现,内置分层存储支持,因此分片可以均匀地分布在存储节点上。由于与任一给定topic相关的数据都不会与特定存储节点进行捆绑,因此很容易替换存储节点或缩扩容。另外,集群中最小或最慢的节点也不会成为存储或带宽的短板。
RocketMQ新节点直接加入到集群中,在新的broker创建新topic并且分配队列,或者在已有topic基础上分配队列。与Kafka的区别是,Kafka的分区是在不同的物理机器上,而Rocketmq是逻辑分区,用的队列形式,因此不存在出现数据不均衡的情况。
RabbitMQ和NSQ类似,由于不涉及过多的消息持久化,直接往集群中增加节点。
Kafka/Pulsar/RocketMQ/RabbitMQ在腾讯云上都上线了标准产品,可以直接购买创建实例,能大大降低部署运维成本。而NSQ目前暂时还没有上线,需要自行部署。
CKafka在腾讯云上是以实例的形式售卖,专业版最低配1494元/月,500G SSD,40MB/s,TDMQ Pulsar是以类似无服务的方式按量计费,按调用次数/消息大小/存储大小等计费,调用次数2.00元/百万次。在用量较少的情况下,比如一些小型快速上线的业务,TDMQ Pulsar的成本会比CKafka低很多。
RocketMQ和RabbitMQ都是最近推出的产品,目前仍在公测阶段,暂时还没有定价。
三、总结
Kafka与Pulsar都是腾讯云主打的消息队列中间件,都具有高性能,高可靠,支持多种场景。Kafka推出的时间较早,各种场景比如日志、大数据处理等都有较成熟的解决方案。而Pulsar作为一个新秀,支持的功能比CKafka更丰富,而且跨地域容灾,多租户等功能,解决了很多Kafka设计缺陷和运维成本问题,整体稳定性更强。很多国内外大公司也有很多Pulsar的实践案例。因此,一些传统的日志、大数据处理等场景,对高吞吐量有要求的,对消息可靠性的要求没那么高的,可以选用Kafka,有很多优秀的文档说明怎么参数调优提高性能。而一些对消息可靠性、容灾要求更好,或者有高分区、延迟队列等需求的场景,可以选用Pulsar。
我们后台的技术栈是基于Golang的,在上文的对比中,还挑了一个基于Golang开发的消息队列NSQ,如果有一些定制化需求或者需要二次开发的,可以选用NSQ。也可以通过阅读NSQ的源码,学习一些优秀高性能消息队列中间件的实现方式,比如里边diskqueue组件,一个基于磁盘的消息队列,在某些场景下可能也可以进行二次利用。
参考资料:
1.Kafka vs. Pulsar vs. RabbitMQ: Performance, Architecture, and Features Compared
2.消息中间件选型分析:从Kafka与RabbitMQ的对比看全局
3.RabbitMQ的TTL(消息有效期)和DLX(死信交换机/队列) 4.Apache Pulsar延迟消息投递解析 5.深入理解RocketMQ延迟消息 6.三分钟了解RocketMQ与Kafka的异同
7.个推基于Apache Pulsar的优先级队列方案 8.消息中间件选型分析:从Kafka与RabbitMQ的对比看全局 9.RocketMQ中消息的优先级
10.消息队列--NSQ&Kafka
11.三分钟了解RocketMQ与Kafka的异同 12.告别传统金融消息架构:Apache Pulsar在平安证券的实践
13.【知识积累】MQ消息堆积和TTL过期
14.Pulsar官方文档-租户 15.Apache Pulsar的多租户消息系统
作者简介
李明宽
腾讯教育后台开发工程师
腾讯教育后台开发工程师,毕业于中山大学。目前负责腾讯教育相关产品的后台研发工作。
推荐阅读