三种消息队列之间的比较

图片文字来源:小白debug

常用的消息队列有Kafka,RocketMQ,RabbitMQ这三种,我之前一直都是使用的都是RabbitMQ,今天来详细了解一下这三种消息队列。

基本模型

消息队列(MQ)一般都遵循 生产者-消息中间件-消费者 这一模型:

7aa51558c42bb5558829463f8959c296
  • 生产者(Producer):发送消息。
  • Broker(消息中间件/消息服务器):接收、存储和转发消息。
  • 消费者(Consumer):订阅并消费消息。

这个模式在 RabbitMQ、RocketMQ、Kafka 三者中都是一致的,只是实现机制不同。接下来分别来对比一下每个的实现步骤。

Kafka

Kafka 基本模型

Kafka 是一个分布式消息队列系统,遵循 生产者-中间件-消费者 模型。它的核心概念包括 Producer、Topic、Partition、Offset、Consumer、Consumer Group

1. 核心概念
  • Producer(生产者)负责向 Kafka 发送消息。每条消息可以包含一个 Key,用于确定分区路由。
  • Topic(主题)消息的分类单位。一个 Topic 可以存储同类消息,例如“订单消息”或“日志消息”。
  • Partition(分区)Topic 内部可以划分为多个 Partition,每个 Partition 是一个有序的消息序列,消息在 Partition 中按写入顺序分配 Offset(序号)。
  • Offset(偏移量)用于标识 Partition 内每条消息的位置,消费者通过 Offset 追踪已消费消息。
  • Consumer(消费者)订阅 Topic 并消费消息。消费者可以独立消费,也可以加入 Consumer Group 统一管理消费进度。
  • Consumer Group(消费者组)一组消费者组成一个消费单位,组内 Partition 分配策略保证每个 Partition 在同一时间只被组内一个消费者消费。不同 Consumer Group 之间互不影响,可以独立追踪 Offset。
2. 数据流

消息在 Kafka 中的流转可以简化为以下流程: Producer → Topic → Partition → Consumer / Consumer Group

  • 生产者发送消息到 Topic,Kafka 根据 Key(或轮询策略)选择 Partition。
  • 消息在 Partition 内有序存储,记录 Offset。
  • 消费者订阅 Topic 并读取 Partition 消息,更新自己的 Offset。
  • 不同消费者组可独立消费同一 Topic 的消息,实现多业务方同时处理同一类消息。
3. 顺序保证
  • Partition 内顺序保证:消息在 Partition 内严格按 Offset 顺序存储和消费。
  • 跨 Partition 顺序不保证:为了提升吞吐量,不同 Partition 是并行写入和消费的,Offset 仅在 Partition 内有意义。
  • 顺序需求解决方案
    1. 单 Partition 保证全局顺序(吞吐量受限)
    2. 按 Key 分区保证同 Key 消息顺序
    3. 应用层排序(不推荐,开销大)

Kafka 性能优化

在大规模消息系统中,单个队列容易出现 生产者和消费者争抢同一个队列 的情况,从而影响系统吞吐量。Kafka 提供了多种机制来优化性能:

1. Topic 划分
  • 为什么划分 Topic 不同类型的消息(例如订单、用户行为、日志)如果都堆在同一个队列里,会导致消费者竞争严重,影响处理效率。
  • 优化方式 将不同类型消息划分到不同 Topic,每个 Topic 内部独立存储,消费者可按需订阅。
  • 效果
    • 降低队列争抢
    • 提高消息处理并行度
image-20250819172526673
2. Partition 分区
  • 为什么需要 Partition 即使 Topic 划分好了,单个 Topic 的消息量仍可能过大。单个队列的消费仍可能成为瓶颈。
  • 优化方式 将 Topic 拆分为多个 Partition,每个 Partition 是一个独立有序队列。
    • 生产者根据 Key 或轮询策略将消息写入不同 Partition
    • 消费者可以并行读取多个 Partition
  • 效果
    • 提升吞吐量
    • 降低消费者竞争
    • 每个 Partition 内保持顺序,但跨 Partition 顺序不保证
image-20250819172539292
3. Consumer Group(消费者组)
  • 为什么需要 Consumer Group 当消费者实例增多时,如果每个实例独立消费,会重复处理消息,资源浪费;如果所有实例抢同一个 Partition,性能受限。
  • 优化方式 将多个消费者组成一个 Consumer Group:
    • 每个 Partition 在同一时间只被组内一个消费者消费
    • 同一个 Topic 可以被不同消费者组独立消费
  • 效果
    • 增加消费者实例即可提升 Topic 消费速度
    • 多个消费者组可以同时处理同一 Topic,实现多业务需求
    • 提高并行度与吞吐量
image-20250819172606893
4. 总结性能优化策略
优化手段作用核心原理
Topic 划分降低不同类型消息竞争不同类型消息独立 Topic
Partition 分区提升并行度和吞吐量Topic 内消息拆分到多个 Partition 并行读写
Consumer Group高效消费、支持多业务Partition 分配给组内不同消费者,不同组独立维护 Offset

Kafka 高可用设计

在实际生产环境中,单台 Broker 容易出现宕机或网络异常,直接影响消息的可用性。Kafka 通过 副本机制和分布式 Broker 设计实现高可用。

1. Broker 分布
  • Broker(消息服务器) Kafka 集群由多台 Broker 组成,每台 Broker 可以存储多个 Partition。
  • 优化效果
    • 将 Partition 分布在不同 Broker 上,避免单点故障
    • 提升整体吞吐量和系统扩展性
image-20250819173245475
2.Partition 副本(Replica)
  • 为什么需要副本 单个 Partition 所在 Broker 宕机,消息将不可用。
  • 副本设计
    • 每个 Partition 有多个副本(Replicas)
    • 副本分为 LeaderFollower
      • Leader:负责处理生产者写入和消费者读取请求
      • Follower:从 Leader 同步消息,不对外提供读写
  • 效果
    • Leader 宕机时,Follower 自动选举新的 Leader
    • 消息不会丢失,只要有同步完成的副本存在
    • 提高系统可用性
image-20250819173314297
3. ISR(In-Sync Replicas)
  • ISR 定义ISR 是与 Leader 保持同步的副本集合。只有 ISR 内的副本才能被选举为新的 Leader。
  • 优化效果
    • 保证消息不会丢失
    • 提供高可用和一致性保证
4. 数据流和容错

消息在 Kafka 高可用集群中的流转如下:

Producer → Leader Partition → Followers 同步 → Consumer 读取 Leader

  • 写入:生产者将消息发送到 Leader
  • 同步:Leader 将消息同步给 Follower
  • 读取:消费者从 Leader 拉取消息
  • 宕机容错:Leader 挂掉,ISR 内 Follower 自动选举新的 Leader
image-20250819173337461
5. 总结高可用策略
高可用机制作用核心原理
Broker 集群避免单点故障Partition 分布在不同 Broker
Partition 副本消息不丢失Leader-Follower 同步
ISR 集合选举新 Leader 保证一致性只有同步完成的副本才能成为 Leader
自动选举机制保证系统可用Leader 宕机,Follower 自动顶替

Kafka 持久化与消息保留策略

在实际生产环境中,消息量巨大且不可控,如果仅依赖内存存储,Broker 宕机或重启将导致数据丢失。Kafka 通过 持久化和消息保留策略 实现消息可靠存储和系统可控性。

1. 消息持久化
  • 磁盘存储Kafka 将每个 Partition 内的消息按顺序追加写入 日志文件(log segment)到磁盘,而不是仅保存在内存中。
  • 优势
    • Broker 重启后可以恢复消息
    • 支持大容量消息堆积
    • 消费者可以按需回溯消费历史消息
  • 数据流示意
Producer → Leader Partition → 磁盘日志 → Follower 同步
2. 消息保留策略(Retention Policy)
  • 为什么需要保留策略消息不断生产,如果无限制存储,会导致磁盘耗尽。Kafka 提供灵活的消息保留策略,定期清理过期消息。
  • 主要策略
  1. 按时间保留(time-based retention
    • 配置如 log.retention.hours,超过保留时间的消息被删除
  1. 按大小保留(size-based retention)
    • 配置如 log.retention.bytes,超过磁盘限制的旧消息被删除
  2. 按消费者偏移控制(compact log)
    • 对某些 Key 的消息只保留最新一条,实现增量更新场景
  • 效果
    • 控制磁盘使用
    • 支持历史消息回溯
    • 消费者独立维护 Offset,不受其他消费者消费影响
3. 与 RabbitMQ 的区别
  • RabbitMQ
    • 消费者消费后消息即删除(ACK 机制)
    • Broker 内存优先,磁盘持久化可选
  • Kafka
    • 消费后消息不删除,由 Topic 保留策略 决定
    • 消费者可独立控制 Offset,支持历史消息回溯
    • 更适合 大规模、长时存储和多消费者场景
4. 总结
特性KafkaRabbitMQ
消息删除Retention Policy 控制消费 ACK 后删除
消费者独立性每个 Consumer Group 独立维护 Offset消费者共享队列状态
持久化方式磁盘追加日志内存 + 可选磁盘
历史消息回溯支持不支持(已删除)

Kafka 的持久化与保留策略,使其在 高吞吐、高可靠、多消费者、历史回溯 等场景中具有明显优势。

好的,我们整理 Kafka 的集群协调组件:ZooKeeper / KRaft 角色 部分。这个部分主要讲 为什么需要集群协调 → ZooKeeper 的角色 → KRaft 的变化 → 集群管理逻辑,逻辑上紧接高可用章节。

Kafka 集群协调:ZooKeeper / KRaft 角色

在 Kafka 集群中,除了 Broker、Partition、Replica 外,还需要一个组件 统一管理集群状态、选举 Leader 和维护消费者组信息。传统 Kafka 使用 ZooKeeper,新版本可以使用 KRaft 模式 替代。

1.为什么需要集群协调

Kafka 集群中的 Broker、Partition、Replica、Consumer Group 都是分布式的:

  • 哪个 Broker 挂了
  • 哪个 Partition 的 Leader 需要选举
  • 消费者组的 Offset 和订阅信息 这些信息需要统一管理,否则无法保证高可用和一致性。
2.ZooKeeper 角色

在传统 Kafka 架构中,ZooKeeper 承担集群协调角色:

角色功能
Broker 注册Broker 启动时向 ZooKeeper 注册,报告存活状态
Leader 选举Partition 的 Leader 由 ZooKeeper 进行选举,保证集群一致性
消费者组管理ZooKeeper 保存 Consumer Group 的 Offset 和订阅信息
元数据同步Broker 和消费者通过 ZooKeeper 获取集群状态和 Partition 分布

数据流示意图

Broker → ZooKeeper 注册 / 获取元数据Producer → Leader Partition → ConsumerZooKeeper → Leader 选举 / Offset 管理
  • 优点:稳定、成熟
  • 缺点:额外依赖 ZooKeeper,增加运维成本,扩展复杂
3. KRaft 模式(Kafka Raft Metadata)

从 Kafka 2.8+ 开始,引入 KRaft 模式,逐步替代 ZooKeeper:

  • 核心思想:用 Kafka 自身的 Raft 协议实现元数据管理
  • 集群角色
    • Controller:管理集群元数据、Leader 选举、Partition 副本分布
    • Broker:只负责存储和转发消息
  • 优势
    • 消除了外部依赖 ZooKeeper
    • 集群操作更简单,延迟更低
    • 单一组件管理集群状态,便于扩展 KRaft 数据流示意图
Controller (Raft) → Broker 管理 Partition / ReplicaProducer → Leader Partition → ConsumerController → 选举 Leader / 同步元数据
4. 总结
方面ZooKeeperKRaft
集群协调外部依赖内置 Kafka
Leader 选举ZooKeeper 负责Controller (Raft) 负责
消费者组管理ZooKeeper 保存 OffsetController 管理元数据
优缺点成熟,但依赖外部服务,复杂内置简化架构,更易运维,延迟低

Kafka 通过 ZooKeeper 或 KRaft 统一管理集群状态,保证 高可用、高一致性,并为分布式 Partition、Replica 和 Consumer Group 提供可靠支撑。

RocketMQ

RocketMQ 是阿里自研的国产消息队列,目前已经是 Apache 的顶级项目。和其他消息队列一样,它接受来自生产者的消息,将消息分类,每一类是一个 topic消费者根据需要订阅 topic,获取里面的消息。

b3968c08ccf07f0cf2a8866d5e3ec326

RocketMQ 和 Kafka 的区别

RocketMQ 的架构其实参考了 Kafka 的设计思想,同时又在 Kafka 的基础上做了一些调整。

这些调整,用一句话总结就是,”和 Kafka 相比,RocketMQ 在架构上做了减法,在功能上做了加法“。我们来看下这句话的含义。

在架构上做减法

简化协调节点

RocketMQ和Kafka都是通过多个topic对消息进行分类。

为了提升单个 topic 的并发性能,将单个 topic 拆为多个 partition

为了提升系统扩展性,将多个 partition 分别部署在不同 broker 上。

为了提升系统的可用性,为 partition 加了多个副本。

Kafka为了协调和管理集群的数据信息,引入Zookeeper作为协调节点。

Zookeeper 在 Kafka 架构中会和 broker 通信,维护 Kafka 集群信息。一个新的 broker 连上 Zookeeper 后,其他 broker 就能立马感知到它的加入,像这种能在分布式环境下,让多个实例同时获取到同一份信息的服务,就是所谓的分布式协调服务

627f0aff3920f82cd4774ffcf77b020e

但 Zookeeper 作为一个通用的分布式协调服务,它不仅可以用于服务注册与发现,还可以用于分布式锁、配置管理等场景。Kafka 其实只用到了它的部分功能,多少有点杀鸡用牛刀的味道。太重了

所以 RocketMQ 直接将 Zookeeper 去掉,换成了 nameserver,用一种更轻量的方式,管理消息队列的集群信息。生产者通过 nameserver 获取到 topic 和 broker 的路由信息,然后再与 broker 通信,实现服务发现负载均衡的效果。

18515c4b1499212bf240f7760e949524

当然,开发 Kafka 的大佬们后来也意识到了 Zookeeper 过重的问题,所以从 2.8.0 版本就支持将 Zookeeper 移除,通过 在 broker 之间加入一致性算法 raft 实现同样的效果,这就是所谓的 KRaftQuorum 模式。

1dc3da8424592d92f97a81952c559de7
简化分区

我们知道,Kafka 会将 topic 拆分为多个 partition,用来提升并发性能

图片

在 RocketMQ 里也一样,将 topic 拆分成了多个分区,但换了个名字,叫 Queue,也就是”队列“。

图片

Kafka 中的 partition 会存储完整的消息体,而 RocketMQ 的 Queue 上却只存一些简要信息,比如消息偏移 offset,而消息的完整数据则放到”一个”叫 commitlog 的文件上,通过 offset 我们可以定位到 commitlog 上的某条消息。 Kafka 消费消息,broker 只需要直接从 partition 读取消息返回就好,也就是读第一次就够了。

图片

而在 RocketMQ 中,broker 则需要先从 Queue 上读取到 offset 的值,再跑到 commitlog 上将完整数据读出来,也就是需要读两次

图片

那么问题就来了,看起来 Kafka 的设计更高效?为什么 RocketMQ 不采用 Kafka 的设计? 这就不得说一下 他们俩的底层存储了。

底层存储

Kafka 的底层存储

Kafka 的 partition 分区,其实在底层由很多segment)组成,每个 segment 可以认为就是个小文件。将消息数据写入到 partition 分区,本质上就是将数据写入到某个 segment 文件下。

图片

我们知道,操作系统的机械磁盘,顺序写的性能会比随机写快很多,差距高达几十倍。为了提升性能,Kafka 对每个小文件都是顺序写。 如果只有一个 segment 文件,那写文件的性能会很好。 但当 topic 变多之后,topic 底下的 partition 分区也会变多,对应的 partition 底下的 segment 文件也会变多。同时写多个 topic 底下的 partition,就是同时写多个文件,虽然每个文件内部都是顺序写,但多个文件存放在磁盘的不同地方,原本顺序写磁盘就可能劣化变成了随机写。于是写性能就降低了。

图片

那问题又又来了,我给一个经验值仅供参考,8 个分区的情况下,超过 64 topic, Kafka 性能就会开始下降。

** RocketMQ 的底层存储**

为了缓解同时写多个文件带来的随机写问题,RocketMQ 索性将单个 broker 底下的多个 topic 数据,全都写到”一个“逻辑文件 CommitLog 上,这就消除了随机写多文件的问题,将所有写操作都变成了顺序写。大大提升了 RocketMQ 在多 topic 场景下的写性能。

图片

注意上面提到的”一个“是带引号的,虽然逻辑上它是一个大文件,但实际上这个 CommitLog 由多个小文件组成。每个文件的大小是固定的,当一个文件被写满后,会创建一个新的文件来继续存储新的消息。这种方式可以方便地管理和清理旧的消息。

简化备份模型

我们知道,Kafka 会将 partiton 分散到多个 broker 中,并为 partiton 配置副本,将 partiton 分为 leaderfollower,也就是主和从。broker 中既可能有 A topic 的主 partiton,也可能有 B topic 的从 partiton。 主从 partiton 之间会建立数据同步,本质上就是同步 partiton 底下的 segment 文件数据

64579ec747732cffee9425b44ba4577c

RocketMQ 将 broker 上的所有 topic 数据到写到 CommitLog 上。如果还像 Kafka 那样给每个分区单独建立同步通信,就还得将 CommitLog 里的内容拆开,这就还是退化为随机读了。 于是 RocketMQ 索性以 broker 为单位区分主从,主从之间同步 CommitLog 文件,保持高可用的同时,也大大简化了备份模型。

图片

好了,到这里,我们熟悉的 Kafka 架构,就成了 RocketMQ 的架构。

图片

是不是跟 Kafka 的很像但又简化了不少?

图片

在功能上做加法

消息过滤

我们知道,Kafka 支持通过 topic 将数据进行分类,比如订单数据和用户数据是两个不同的 topic,但如果我还想再进一步分类呢?比如同样是用户数据,还能根据 vip 等级进一步分类。假设我们只需要获取 vip6 的用户数据,在 Kafka 里,消费者需要消费 topic 为用户数据的所有消息,再将 vip6 的用户过滤出来。

而 RocketMQ 支持对消息打上标记,也就是打 tag,消费者能根据 tag 过滤所需要的数据。比如我们可以在部分消息上标记 tag=vip6,这样消费者就能只获取这部分数据,省下了消费者过滤数据时的资源消耗。

相当于 RocketMQ 除了支持通过 topic 进行一级分类,还支持通过 tag 进行二级分类。

图片
支持事务

我们知道 Kafka 支持事务,比如生产者发三条消息 ABC,这三条消息要么同时发送成功,要么同时发送失败。

图片

是,这确实也叫事务,但跟我们要的不太一样

写业务代码的时候,我们更想要的事务是,”执行一些自定义逻辑“和”生产者发消息“这两件事,要么同时成功,要么同时失败。

而这正是 RocketMQ 支持的事务能力。

图片
延时队列

如果我们希望消息投递出去之后,消费者不能立马消费到,而是过个一定时间后才消费,也就是所谓的延时消息。如果我们使用 Kafka, 要实现类似的功能的话,就会很费劲。 但 RocketMQ 天然支持延时队列,我们可以很方便实现这一功能。

死信队列

消费消息是有可能失败的,失败后一般可以设置重试。如果多次重试失败,RocketMQ 会将消息放到一个专门的队列,方便我们后面单独处理。这种专门存放失败消息的队列,就是死信队列。 Kafka 原生不支持这个功能,需要我们自己实现。

消息回溯

Kafka 支持通过调整 offset 来让消费者从某个地方开始消费,而 RocketMQ,除了可以调整 offset, 还支持调整时间(kafka在0.10.1后支持调时间)

RabbitMQ

RabbitMQ基于AMQP(Advanced Message Queuing Protocol,高级消息队列协议)协议,是最经典的队列实现。

在 AMQP 协议中,消息从生产者到消费者不是直接传递的,而是经过一套中间抽象:

  1. Producer(生产者):发送消息的一方。
  2. Exchange(交换机):接收生产者发来的消息,并按照规则(绑定键、路由规则)分发到不同的队列。
    • Direct(直连交换机)
    • Fanout(广播交换机)
    • Topic(主题交换机)
    • Headers(头交换机)
  3. Queue(队列):消息的存储容器,消费者从中取消息。
  4. Binding(绑定):把交换机和队列连接起来的规则。
  5. Consumer(消费者):从队列中取消息处理。

所以,消息流转的路径一般是:

Producer → Exchange → Queue → Consumer

接下来详细了解一下各个结构。

Queue

消息队列本质上就是一个类似链表的独立进程。链表里的每个节点是一个消息,它介于生产者消费者之间,在流量高峰时先暂存数据,再慢慢消费数据,可以很好的保护消费者,也就是所谓的削峰填谷。这个类似链表的进程,就是Queue,也叫队列

image-20250819151610518

但消息也分很多种类,比如订单消息和用户消息属于两类,为了更好地管理不同种类的数据, 可以提供多个 队列,生产者可以自定义 Queue 的名字,并且根据需要将消息投递到不同的 Queue 中。每个 Queue 都设计为独立的进程,某个进程挂了,不影响其他进程正常工作。

image-20250819151636559

Exchange

有些生产者想将消息发到一个 Queue 中,有些则是发给多个queue,甚至广播给所有 Queue ,于是 我们还需要一个可以定制消息路由分发策略的组件,交换器Exchange,将它与 Queue 绑定在一起,通过一个类似正则表达式的字符串 bindingKey 声明绑定的关系,让用户根据需要选择要投递的队列。

image-20250819151948834

Exchange(交换机)一般分为四种:

  • Direct(直连交换机):一对一,精准投递。消息根据 Routing Key(路由键) 精确匹配到绑定的队列。常用于点对点消息,例如 订单支付完成 → 通知订单服务
  • Fanout(广播交换机):一对多,群发消息。不看 Routing Key,消息会 广播到所有绑定的队列。常用于广播消息,批量推送等场景。
  • Topic(主题交换机):模糊匹配,支持多维度订阅。根据 通配符规则 进行路由。常用于日志系统或者电商业务。
  • Headers(头交换机):根据消息 Header 中的键值对 匹配路由规则,更加灵活,不依赖 routing key。性能较低,一般使用Topic替代。

这种维护在 Exchange 里的路由方式和绑定关系,我们称为元数据

image-20250819153149663

Broker

每一台服务器上的 RabbitMQ 实例,就代表一个 Broker

cdb2fed2bd8aae089759f81725add3e5

以上就是RabbitMQ的基本架构。

大佬们在这个架构的基础上,为 RabbitMQ 实现了各种丰富的特性,你能想到的 MQ 功能它基本都实现了,比如延时队列死信队列优先级队列等等。

d65340d54548d4b73d9ad6cb74ac0537

优先级队列

RabbitMQ 支持在生产者发送消息的时候,为消息标记上优先级,消费者总是消费优先级高的消息。

513f78fb977e84b55fb9f1e969ffe815

RabbitMQ 集群

通过上面的介绍我们可以了解到,RabbitMQ 功能很丰富,但它的架构就是个单实例节点,有些过于简单了,像什么高可用高扩展,那是一个都不沾。

既然单节点存在诸多问题,那就让多个节点构成集群

我们可以在多个服务器上各部署一个 RabbitMQ 实例,并通过执行 RabbitMQ 提供的命令,将这些实例组成一个集群。

5bb55da10f8fb5c821abc710833a91d5

RabbitMQ 支持多种集群模式。我们依次来看下。

图片
普通集群模式

在普通集群模式中,每个 Broker 都是一个完整功能的 RabbitMQ 实例,都能进行读写

他们之间会互相同步 Exchange 里的元数据,但不会同步 Queue 数据。

假设 Queue1、Queue2、Queue3 分别部署在 Broker1、Broker2、Broker3中。

  • 对于操作:生产者将消息写入到 Broker1 的 Queue1 后,Queue1 里的数据并不会同步给其他 broker。但如果此时 Broker1 的 Exchange 元数据有变化,则会将元数据同步到其他两个 Broker 中。图片
  • 对于操作:消费者想要读取 Queue1 数据时,如果访问的是 Broker1,则直接返回 Queue1 中的数据。图片但如果访问的是 Broker2,Broker2 则会根据 Exchange 里的元数据,从 Broker1 那读取数据,再返回给消费者。图片

这样就可以通过增加 Broker,提升 RabbitMQ 集群整体的吞吐量,保证了扩展性。

但问题也很明显。

  • 虽然支持读写 Queue 的数量是增加了,但对于单个Queue 本身的读写能力,并没有提升。
  • 而且更重要的是,每个 Broker 依然有单点问题,Broker 之间并不同步 Queue 里的数据。某个 Queue 所在的 Broker 要是挂了,就没法读写这个 Queue 了。
图片

这跟高可用毫不沾边。

镜像队列集群

我们可以在普通集群模式的基础上, 给 queue 在其他 broker 中加几个副本, 它们有主从关系,主 queue 负责读写数据,从 queue 负责同步复制主 queue 数据, 所以从 Queue 也叫镜像队列

一旦主 Queue 所在的 broker 挂了,从 Queue 就可以顶上成为新的主 Queue,实现高可用。这就是所谓的镜像队列集群

图片镜像队列集群

  • 对于写操作:数据写入主 Queue 后,会将 Exchange 和 Queue 数据同步给其他 Broker 上。图片
  • 对于读操作:消费者读取数据时,如果访问的是主 Queue所在的broker,则直接返回数据。图片
  • 否则,当前 broker 会从主 queue 所在的 broker 上读取数据,之后返回给消费者。图片

但这个方案的缺点也很明显,broker 间同步的数据量会变大,集群节点越多带宽压力越大,本质上镜像队列模式是通过牺牲吞吐量换取的高可用。

反观前面的普通集群模式,虽然吞吐高但却牺牲了高可用

还是那句话,做架构做到最后,都是在做折中

Quorum 队列集群

看到这里不知道大家有没有发现一个问题,RabbitMQ 集群中每个节点都能知道某个 Queue 具体在哪个 Broker 上,说明 Broker 间有个机制可以互相同步元数据,但架构中却没有一个类似 kafka 的 zookeeper 那样的中心节点。那它是怎么在多个节点间同步数据的呢?

这是因为 RabbitMQ 基于 erlang 进行开发,这是个很特别的语言,它自带虚拟机和分布式通信框架,RabbitMQ 通过这个分布式通信框架,在 Broker 间同步元数据。

图片

但它有个问题,如果broker间通信断开,镜像队列可能出现多个节点都认为自己是主节点的情况,导致数据不一致,也就是所谓的脑裂问题

图片

有解法吗?有!

我们可以使用更靠谱的一致性算法 raft ,来同步多个 broker 的元数据和队列数据,通过引入选举机制来解决网络分区问题,这就是所谓的 Quorum 队列集群。

图片
小结
集群模式数据同步高可用吞吐量缺点
普通集群只同步元数据✅ 高单点问题
镜像队列主从复制❌ 低带宽消耗大
Quorum 队列Raft 共识中等性能比镜像好,但仍低于

性能对比

对于互联网三大消息队列,kafka、RocketMQ、RabbitMQ,阿里云团队对它们做过压测,同等条件下,kafka 吞吐量是每秒 17w ,rocketMQ 每秒 10w,而 RabbitMQ 则是 5w。

那么问题来了。为什么RoketMQ性能不如Kafka?

Kafka与RocketMQ

RocketMQ 的架构其实参考了 kafka 的设计思想,同时又在 kafka 的基础上做了一些调整(简化协调节点,简化分区,简化备份模型)。看起来,RocketMQ 好像各方面都比 kafka 更能打。但为什么RocketMQ的性能实际不如Kafka。

零拷贝技术

我们知道,消息队列的消息为了防止进程崩溃后丢失,一般不会放内存里,而是放磁盘上。那么问题就来了,消息从消息队列的磁盘,发送到消费者,过程是怎么样的呢?

操作系统分为用户空间内核空间。程序处于用户空间,而磁盘属于硬件,操作系统本质上是程序和硬件设备的一个中间层。程序需要通过操作系统去调用硬件能力。

图片

如果用户想要将数据从磁盘发送到网络。那么就会发生下面这几件事:程序会发起系统调用read(),尝试读取磁盘数据,

  • • 磁盘数据从设备拷贝到内核空间的缓冲区。
  • • 再从内核空间的缓冲区拷贝到用户空间。

程序再发起系统调用write(),将读到的数据发到网络:

  • • 数据从用户空间拷贝到 socket 发送缓冲区
  • • 再从 socket 发送缓冲区拷贝到网卡。

最终数据就会经过网络到达消费者。

图片

整个过程,本机内发生了 2系统调用,对应 4 次用户空间和内核空间的切换,以及 4 次数据拷贝

一顿操作猛如虎,结果就是同样一份数据来回拷贝。有没有办法优化呢?有,它就是零拷贝技术,常见的方案有两种,分别是 mmapsendfile。我们来看下它们是什么。

mmap 是什么

mmap 是操作系统内核提供的一个方法,可以将内核空间的缓冲区映射到用户空间。

图片

用了它,整个发送流程就有了一些变化。程序发起系统调用mmap(),尝试读取磁盘数据,具体情况如下:

  • • 磁盘数据从设备拷贝到内核空间的缓冲区。
  • • 内核空间的缓冲区映射到用户空间,这里不需要拷贝。

程序再发起系统调用write(),将读到的数据发到网络:

  • • 数据从内核空间缓冲区拷贝到 socket 发送缓冲区。
  • • 再从 socket 发送缓冲区拷贝到网卡。
图片

整个过程,发生了 2 次系统调用,对应 4 次用户空间和内核空间的切换,以及 3 次数据拷贝,对比之前,省下一次内核空间到用户空间的拷贝。

图片

看到这里大家估计也蒙了,不是说零拷贝吗?怎么还有 3 次拷贝。mmap 作为一种零拷贝技术,指的是用户空间到内核空间这个过程不需要拷贝,而不是指数据从磁盘到发送到网卡这个过程零拷贝。

图片

确实省了一点,但不多。有没有更彻底的零拷贝?有,用 sendfile.

sendfile 是什么

sendfile,也是内核提供的一个方法,从名字可以看出,就是用来发送文件数据的。程序发起系统调用sendfile(),内核会尝试读取磁盘数据然后发送,具体情况如下:

  • • 磁盘数据从设备拷贝到内核空间的缓冲区。
  • • 内核空间缓冲区里的数据可以直接拷贝到网卡。
图片

整个过程,发生了 1 次系统调用,对应 2 次用户空间和内核空间的切换,以及 2 次数据拷贝。这时候问题很多的小明就有意见了,说好的拷贝怎么还有 2 次拷贝?

图片

其实,这里的零拷贝指的是零 CPU拷贝。也就是说 sendfile 场景下,需要的两次拷贝,都不是 CPU 直接参与的拷贝,而是其他硬件设备技术做的拷贝,不耽误我们 CPU 跑程序。

kafka 为什么性能比 RocketMQ 好

聊完两种零拷贝技术,我们回过头来看下 kafka 为什么性能比 RocketMQ 好。这是因为 RocketMQ 使用的是 mmap 零拷贝技术,而 kafka 使用的是 sendfile。kafka 以更少的拷贝次数以及系统内核切换次数,获得了更高的性能。但问题又来了,为什么 RocketMQ 不使用 sendfile?参考 kafka 抄个作业也不难啊?我们来看下 sendfile 函数长啥样。

ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);// num = sendfile(xxx);

再来看下 mmap 函数长啥样。

void *mmap(void *addr, size_t length, int prot, int flags,           int fd, off_t offset);// buf = mmap(xxx)

注释里写的是两个函数的用法,mmap 返回的是数据的具体内容,应用层能获取到消息内容并进行一些逻辑处理。

图片

sendfile 返回的则是发送成功了几个字节数具体发了什么内容,应用层根本不知道

图片

而 RocketMQ 的一些功能,却需要了解具体这个消息内容,方便二次投递等,比如将消费失败的消息重新投递到死信队列中,如果 RocketMQ 使用 sendfile,那根本没机会获取到消息内容长什么样子,也就没办法实现一些好用的功能了。

图片

而 kafka 却没有这些功能特性,追求极致性能,正好可以使用 sendfile。

除了零拷贝以外,kafka 高性能的原因还有很多,比如什么批处理,数据压缩啥的,但那些优化手段 rocketMQ 也都能借鉴一波,唯独这个零拷贝,那是毫无办法。

图片

RabbitMQ与RocketMQ

RabbitMQ 虽然比 RocketMQ 功能要丰富些,但差异却并不大,为什么性能比 RocketMQ 差这么多?

一句话先给答案: RabbitMQ 比 RocketMQ 慢,并不是因为它“功能多了一点点”,而是它的存储模型、复制机制和语言运行时都与高吞吐无缘——这三点把上限直接锁死在了 万级 TPS,而 RocketMQ 做了专门的面向高吞吐优化,因此差距被放大到 5~10 倍。

下面把关键差异拆开说:

  1. 存储/复制模型 • RabbitMQ 采用经典 AMQP 队列模型——每条消息独立落盘,持久化时要写索引+消息体两次磁盘 I/O;为保证可靠性,镜像队列采用同步复制,写主写从都刷盘才算成功。 • RocketMQ 采用顺序追加 CommitLog,一次磁盘 I/O 写多条消息;主从复制默认异步,且只复制字节流,不解析消息,磁盘压力和网络开销都低。
  2. 零拷贝/批量能力 • RabbitMQ 没有实现 sendfile 或 mmap 零拷贝,消息从磁盘→内核→用户空间→网络栈,四次拷贝;默认一次只传一条消息,批量能力弱。 • RocketMQ 利用mmap + 顺序读接近零拷贝,同时支持客户端批量发送/拉取,网络包利用率远高于 RabbitMQ。
  3. 语言运行时差异 • RabbitMQ 用 Erlang:进程邮箱模型带来微秒级低延迟,但进程调度/ GC 在 万级 TPS 以上会成为瓶颈,且难以利用多核并行。 • RocketMQ 用 Java,虽然也有 GC,但线程池 + 顺序写盘避免了大量小对象,更容易做垂直扩展。
  4. 功能≠性能损失 RabbitMQ 的灵活路由、插件体系并不会直接拖慢核心链路,真正的性能差异还是来自上面三条底层实现。换句话说,即使关掉所有插件,RabbitMQ 的 TPS 上限仍然在几万级;而 RocketMQ 通过存储和复制层面的“减法”,把上限抬到了几十万级

MQ的选择

一张图就能看懂差异,但我还是给你文字版,方便收藏。

维度KafkaRocketMQRabbitMQ
设计初衷大数据级日志 / 流式处理在线交易高可靠消息企业级 AMQP 通用队列
单机吞吐17w 级10w 级5w 级
实时性ms 级ms 级亚 ms 级
消息顺序分区级顺序全局严格顺序队列级顺序
事务/可靠性幂等 + 事务(弱)分布式事务、事务消息事务、确认、重试最全
功能丰富度少(日志为主)中(顺序、延迟、事务)最丰富(路由、插件、优先级…)
协议&生态自建协议 + 大数据生态自有协议,Java 友好AMQP,多语言客户端成熟
运维复杂度中等(依赖 ZooKeeper/KRaft)偏高(Namesrv + Broker + Dledger)低(单节点即可,镜像队列简单)
二次开发Scala/Java,社区大Java,阿里系文档多Erlang,国内定制人少

一句话选型口诀

  1. 日志/指标/流计算 ➜ Kafka 高吞吐、海量分区、与 Flink/Spark 深度集成,就是为它而生。
  2. 交易/订单/金融场景 ➜ RocketMQ 需要事务消息、严格顺序、亿级堆积而不掉速。
  3. 中小系统、快速上线、协议互通 ➜ RabbitMQ 功能最全、文档最多,插件一把梭就能搞定延迟队列、优先级、RPC 等花式需求。

文末附加内容
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇