资讯中心

深入 RocketMQ 内核:从整体架构到消息模型

📅 2026/7/12 0:06:21
深入 RocketMQ 内核:从整体架构到消息模型
今天这篇文章我们要做的就是——打开引擎盖看看这辆车到底是怎么跑起来的。我们会先拆解 RocketMQ 的四大核心组件弄清楚它们各自干什么、怎么配合然后再深入 Topic 和消息模型搞懂消息在 RocketMQ 里到底是怎么存储、怎么路由、怎么被找到的。为了让你看得更直观这次我准备了大量的流程图和结构图咱们边看图边拆解。准备好了吗我们发车。三、RocketMQ 整体架构RocketMQ 的四大核心组件NameServer、Broker、Producer、ConsumerRocketMQ 的架构可以用四个词来概括它们各司其职配合默契NameServer轻量级的“服务注册中心”和“路由表”负责管理所有 Broker 的信息Broker真正干活的“消息存储服务器”负责消息的写入、读取和持久化Producer消息的生产者负责发送消息Consumer消息的消费者负责拉取并处理消息用人话打个比方NameServer 是“通讯录”Broker 是“仓库”Producer 是“发货方”Consumer 是“收货方”。各组件职责与协作关系它们之间是怎么协作的呢单纯看文字很难形成立体感下面这张全链路架构协作图能帮你一眼看清从消息发送到消费的完整数据流向存储层 - Broker 主从集群注册中心层 - 无状态集群客户端层Broker 副本组 A① 获取路由信息② 发送消息③ 获取路由信息④ 拉取/消费消息⑤ 定时心跳注册⑤ 定时心跳注册⑤ 定时心跳注册⑥ 数据同步复制/异步复制Broker 副本组 B⑥ 数据同步Master BSlave BProducer 生产者Consumer 消费者NameServer 1NameServer 2NameServer 3Master A写入/读取Slave A数据备份/读取整个协作流程大致是这样的第一步Broker 启动后主动向所有 NameServer 节点注册自己的信息IP、端口、存储的 Topic 信息等并定时发送心跳。第二步Producer 发送消息前先从 NameServer 拉取路由信息知道消息应该发到哪个 Broker 去。第三步Producer 根据路由信息把消息发到对应的 Broker 上。第四步Consumer 消费消息前同样先从 NameServer 获取路由信息知道去哪个 Broker 拉取消息。第五步Consumer 主动从 Broker 拉取消息进行消费底层是 Pull 模式Push 模式是在此基础上封装的。第六步Broker Master 接收消息写入Broker Slave 从 Master 同步数据实现高可用。整个流程环环相扣但核心思想并不复杂NameServer 负责“指路”Broker 负责“收发货”客户端跟着指引走。NameServer 与 Broker 的关联方式NameServer 是整个 RocketMQ 集群的“大脑”。虽然它不存储消息但它知道所有 Broker 的信息。Broker 启动的时候会在配置文件中指定 NameServer 的地址列表。启动后Broker 会向所有 NameServer 发送注册请求把自己“登记”上去。之后每隔 30 秒Broker 会发送一次心跳告诉 NameServer“我还活着”。Producer 和 Consumer 在启动时同样需要配置 NameServer 地址。它们会从 NameServer 拉取 Topic 对应的 Broker 路由信息并缓存到本地。NameServer 的搭建与 Broker 的搭建在搭建 RocketMQ 集群时通常的步骤是NameServer 搭建下载 RocketMQ 二进制包后NameServer 的启动脚本在 bin/mqnamesrv启动命令nohup sh bin/mqnamesrv NameServer 本身非常轻量不保存任何持久化数据所有路由信息都在内存中Broker 搭建Broker 的启动脚本是 bin/mqbroker启动时需要指定配置文件sh bin/mqbroker -c conf/broker.conf配置文件中要指定 namesrvAddrNameServer 地址列表和 brokerClusterName集群名称 小贴士RocketMQ 5.x 推荐使用 Proxy Broker 存算分离架构部署其中 Proxy 是无状态的可以独立水平扩展。不过对于理解核心原理我们还是先从经典的 NameServerBroker 架构入手。NameServer 的多节点部署与无状态设计NameServer 最巧妙的设计在于它的无状态性。所谓“无状态”指的是 NameServer 自身不持久化任何数据所有的路由信息都是通过 Broker 的心跳上报动态构建的。这就意味着任意一个 NameServer 挂掉其他节点照样能提供服务NameServer 之间不需要互相通信不需要选主不需要数据同步水平扩展极其简单——多启动几个实例就行正因为 NameServer 节点之间完全对等、互不依赖我们才能轻松部署 3 个甚至更多节点来保证高可用。客户端会连接所有 NameServer 节点如果某个节点连不上自动切换到下一个。Broker 的 Master-Slave 主从架构和 NameServer 不同Broker 是有“身份”的。在 RocketMQ 4.x 及之前的经典部署模式中Broker 分为两种角色Master主节点负责接收 Producer 的消息写入也负责消息的读取。Master 可以配置多个每个 Master 都有自己的 Slave。Slave从节点不能接收写入只能从 Master 同步数据并承担一部分读请求可以配置是否开启 Slave 读权限。Master 和 Slave 之间的数据同步方式有两种同步方式 特点 适用场景同步复制 Master 收到消息后等 Slave 也写入成功才返回 ACK 对数据可靠性要求极高允许牺牲一点吞吐量异步复制 Master 写入成功就返回 ACKSlave 异步同步 追求高吞吐能容忍极少量数据丢失这里说的“同步复制”指的是消息数据本身在主从间的同步策略。它是 RocketMQ 保证消息零丢失的重要手段——如果你的业务连磁盘损坏这种极端情况都不能丢消息就必须配置同步复制。Broker 集群的多种部署模式RocketMQ 非常灵活Broker 集群支持多种部署方式你可以根据业务需求来选择。下面这张图清晰地展示了从简单到复杂的四种部署模式演进模式四DLedger 多主多从LeaderFollowerFollower优点自动故障切换Raft 协议保障模式三多主多从Master ASlave AMaster BSlave B优点高可用缺点需人工切主模式二多主无从Master 1无备份Master 2无备份缺点单主宕机数据不可达模式一单主单从MasterSlave缺点存在单点故障单主模式单 Master只有一个 Broker没有 Slave。部署最简单但存在单点故障。适用开发测试环境。2. 多主模式多 Master无 Slave多个 Master 节点每个 Master 存储不同的 Topic 分区数据。吞吐量高但某个 Master 宕机后该节点上的消息在恢复前无法消费。适用对可用性要求不高、允许少量数据暂时不可达的业务。3. 多主多从模式多 Master每个 Master 带 Slave生产环境的标准选择。每个 Master 至少有一个 Slave 作为备份高可用。缺点Master 宕机后虽然 Slave 可读但写服务会停且需要人工介入切换。4. 多主多从 Dledger 模式自动故障切换适用核心交易链路、金融级业务。Dledger 高可用架构与自动故障切换在传统的多主多从模式下如果 Master 挂了虽然 Slave 可以继续提供读服务但写服务就停了而且需要人工介入把 Slave 切换为 Master。Dledger 的出现解决了这个问题。Dledger 是 RocketMQ 基于 Raft 共识算法实现的高可用解决方案。当 Leader即 Master挂掉时集群如何秒级完成自动切换下图展示了它的内部选举流程客户端/监控Follower BLeader (Master)Follower A客户端/监控Follower BLeader (Master)Follower ALeader 正常运行处理读写请求 Leader 宕机进入选举超时计时 (Election Timeout)成为新 Leader同步数据同步数据连接中断发起投票请求 (RequestVote)投票响应 (Vote Granted)获得多数派投票 (1/2)恢复服务秒级切换无需人工干预在 Dledger 模式下一组 Broker 节点通常为 3 个组成一个副本组。当 Leader 挂了之后剩余的 Follower 会自动重新选举出新的 Leader整个过程无需人工干预秒级完成切换。NameServer 与 Broker 的启动顺序与关联流程部署时启动顺序很重要正确的启动顺序先启动 NameServer所有节点再启动 Broker所有节点如果 Broker 启动时 NameServer 还没启动Broker 会不断重试连接直到连上为止。NameServer 的心跳检测机制与 Broker 故障剔除NameServer 是怎么知道一个 Broker 是“活着”还是“死了”的答案就是心跳检测机制。这套机制非常精巧完全是 Broker 主动上报、NameServer 被动超时剔除的模式极大地降低了 NameServer 的压力。我们通过下面这张时序图来看清全流程Producer/ConsumerNameServer 集群Broker 节点Producer/ConsumerNameServer 集群Broker 节点记录心跳时间loop[每 30 秒 (心跳上报)]alt[当前时间 - 最后存活时间120 秒]loop[每 10 秒 (故障扫描)]alt[路由表发生变更 (Broker 被剔除)]loop[每 30 秒 (客户端拉取)]发送心跳包 (包含存活状态、路由信息)更新 Broker 的最后存活时间戳扫描 Broker 存活列表判定 Broker 失联从路由表剔除更新内存中的路由表拉取最新的 Topic 路由信息返回最新的 Broker 列表 (已剔除故障节点)更新本地缓存不再往故障 Broker 发消息Broker 主动上报Broker 每隔 30 秒向所有 NameServer 发送一次心跳心跳里包含了 Broker 的当前状态、存储容量、Topic 信息等NameServer 主动超时剔除NameServer 会定期扫描自己维护的 Broker 列表。如果一个 Broker 连续 120 秒默认 没有发送心跳NameServer 就会认为它“失联”了将其从路由表中移除客户端感知变化Producer 和 Consumer 每隔 30 秒会从 NameServer 拉取最新的路由信息。一旦某个 Broker 被剔除客户端在下次拉取时就能感知到从而不再往“死”掉的 Broker 发送消息四、Topic 与消息模型搞清楚了架构层面的组件协作现在我们向下深挖一层来到 RocketMQ 中最核心的数据模型——Topic。Topic 与 MessageQueue 的关系在 RocketMQ 中Topic 是一个逻辑概念MessageQueue 是物理概念。打个比方Topic 就像是一个“主题文件夹”而 MessageQueue 是这个文件夹下面的“物理文件”。Topic生产者发送消息时指定 Topic消费者订阅时指定 Topic。它是消息的分类标签。MessageQueueTopic 下面实际存储消息的最小物理单元。一条消息最终会被写到某个具体的 MessageQueue 里。一个 Topic 下面有多个 MessageQueue默认是 4 个。生产者发消息时会根据负载均衡策略比如轮询、哈希取模选择其中一个 Queue 来发送。Topic 与 Broker 的关系多对多映射Topic 和 Broker 之间的关系是多对多的一个 Topic 可以分布在多个 Broker 上每个 Broker 上存放该 Topic 的部分 Queue一个 Broker 上也可以存放多个 Topic 的 Queue这种多对多的映射关系让 RocketMQ 具备了水平扩展的能力。举个例子如果你的 Topic 流量太大一个 Broker 扛不住你可以在更多 Broker 上创建这个 Topic 的 Queue把流量分散开。物理节点Broker Master 2物理节点Broker Master 1逻辑概念Topic: order_topic映射映射映射映射Queue 0Queue 1Queue 2Queue 3Queue 0Queue 1Queue 0Queue 1如图order_topic 的 4 个 Queue 分布在 2 个 Broker 上每个 Broker 各存 2 个 Queue。这种设计不仅实现了存储的负载均衡也为后续的高可用打下了基础。MessageQueue 的物理存储结构这是 RocketMQ 最精妙的设计之一也是它为什么能具备极高写入吞吐量的核心机密。RocketMQ 采用了 CommitLog ConsumeQueue 的两层存储结构。光说概念可能有点抽象我们来看下面这张物理存储结构图它能帮你直观地理解“顺序写入”和“索引读取”是如何共存的消费流程快速定位异步索引构建写入流程极高吞吐量顺序追加写入异步构建索引异步构建索引异步构建索引根据逻辑偏移量查询返回物理偏移量 offset1024根据物理偏移量精准读取ProducerCommitLog单一物理文件所有 Topic 共用所有消息无论属于哪个 Topic都按到达顺序依次追加到 CommitLogConsumeQueue: order_topic-0存储偏移量 大小 Tag 哈希ConsumeQueue: order_topic-1ConsumeQueue: user_topic-0ConsumerCommitLog仓库所有消息顺序写入同一个大文件每个 Broker 只有一个 CommitLog。无论消息属于哪个 Topic都顺序追加到 CommitLog 里——这种顺序写入的方式让 RocketMQ 拥有了极高的写入吞吐量因为磁盘顺序写远快于随机写。ConsumeQueue货架标签每个 MessageQueue 对应一个 ConsumeQueue 文件它相当于一个“索引”里面记录了每条消息在 CommitLog 中的物理偏移量、大小、Tag 哈希值等信息。Consumer 消费时先查 ConsumeQueue 找到消息在 CommitLog 里的位置再去 CommitLog 读取消息内容。为什么一个 Topic 需要多个队列这可能是很多初学者最困惑的问题为什么一个 Topic 下面要有多个 Queue核心原因有三个提高并发吞吐量如果 Topic 只有一个 Queue那么无论你有多少个消费者实例同时只能有一个消费者从该 Queue 拉取消息为了避免重复消费。这就成了性能瓶颈。多个 Queue 意味着多个消费者可以并行消费吞吐量成倍提升。实现顺序消息RocketMQ 的顺序消息保证的是同一个 Queue 内的消息严格有序。所以如果你要保证某一批消息的顺序比如同一个订单的创建→支付→发货只需要把它们的 Key比如订单 ID用哈希取模分配到同一个 Queue 即可。负载均衡多个 Queue 分布在不同的 Broker 上可以实现存储和流量的负载均衡。路由发现机制Producer/Consumer 如何从 NameServer 获取路由信息路由发现是客户端和 NameServer 之间的核心交互流程。客户端并不是每次都去查 NameServer而是有本地缓存和定时更新的机制如下图所示是有变化无变化客户端启动从配置读取 NameServer 地址列表尝试建立连接发送拉取路由请求携带 Topic 名称NameServer 返回 Broker 地址列表及 Queue 分布信息缓存路由信息到本地内存定时器触发默认 30 秒再次拉取最新路由与本地缓存对比是否有变化更新本地缓存保持不变发消息/消费时直接从本地缓存获取 Broker客户端启动时Producer 或 Consumer 从配置的 NameServer 地址列表中选择一个可用的 NameServer 节点建立连接拉取路由客户端发送请求携带 Topic 名称NameServer 返回该 Topic 对应的所有 Broker 信息包括 Master 和 Slave 的 IP、端口、Queue 分布等缓存更新客户端将路由信息缓存在本地同时启动一个定时任务默认 30 秒定期从 NameServer 拉取最新路由异常处理如果客户端连接某个 Broker 失败会主动向 NameServer 重新拉取路由看是不是路由信息发生了变化 小贴士路由信息在客户端是有缓存的不会每次发消息都去查 NameServer所以 NameServer 不会成为性能瓶颈。队列数量与消费并行度的关系这是一个非常重要的概念消费者的最大并行度 该 Topic 的 MessageQueue 数量。为什么因为 RocketMQ 的集群消费模式下一个 Queue 在同一时刻只能被同一个消费者组里的一个消费者实例消费。下面这张图完美地展示了 Queue 数量与消费者实例数量之间的负载均衡关系以及为什么多余的消费者会处于空闲状态消费者过少队列数 消费者数Queue 1Consumer 实例 1Queue 2Queue 3Consumer 实例 2Queue 4并行度 2未能充分利用队列并发消费者过多队列数 消费者数Queue 1Consumer 实例 1Queue 2Consumer 实例 2Queue 3Consumer 实例 3Queue 4Consumer 实例 4Consumer 实例 5处于空闲等待状态⚠️ 资源浪费最大并行度仍为 4理想情况队列数 消费者数Queue 1Consumer 实例 1Queue 2Consumer 实例 2Queue 3Consumer 实例 3Queue 4Consumer 实例 4并行度 4硬件资源充分利用场景 A理想4 个 Queue 配 4 个消费者完美并行性能拉满。场景 B过多4 个 Queue 配 5 个消费者第 5 个完全空闲白白浪费资源。场景 C过少4 个 Queue 配 2 个消费者每个消费者扛 2 个 Queue并发度只有 2。结论通常建议 消费者实例数量 ≤ Queue 数量且 Queue 数量最好是消费者数量的整数倍这样分配最均匀。如何合理规划 Topic 和队列数量这是一个实战中经常遇到的问题没有一个“万能数字”但可以参考以下经验队列数量规划的黄金法则场景 建议 Queue 数量 说明普通业务TPS 1000 4~8 个 够用且轻量中等流量TPS 1000~10000 8~16 个 根据实际压力测试调整高并发场景TPS 10000 16~32 个 配合更多 Broker 分散压力严格顺序消息场景 根据业务 Key 的分布来定 比如按订单 ID 哈希Queue 数最好远大于消费者数保证分布均匀另外几个原则Queue 数量一旦确定后期调整比较麻烦虽然 RocketMQ 5.x 支持动态扩缩但生产环境建议谨慎所以初期可以稍微设大一点Queue 数量要和 Broker 数量配合尽量均匀分布不要盲目设大——每个 Queue 都要消耗内存和文件句柄过多的 Queue 会增加 Broker 的负担Tag 的作用与使用场景在 RocketMQ 中Tag标签 是 Topic 之下的第二级消息分类。想象一下你有一个 order_topic里面既有下单消息order_create也有支付消息order_pay还有退款消息order_refund。如果用三个独立的 Topic 来区分管理起来太繁琐了——这时候 Tag 就派上了用场。Consumer 在订阅 Topic 时可以用 Tag 来过滤消息只消费自己感兴趣的那一类。Tag 的使用方式生产者发送时指定 TagSendResult result producer.send(msg, “order_create”);消费者订阅时指定 Tagconsumer.subscribe(“order_topic”, “order_create || order_pay”);适用场景同一业务类型下的不同子类型区分消费者只关注某个 Topic 下的部分消息节省网络传输和处理开销Tag 与 SQL92 过滤方式的对比RocketMQ 提供了两种消息过滤方式Tag 过滤和 SQL92 过滤。它们在 Broker 内部的处理逻辑完全不同下图对比了这两种过滤方式的执行路径方式二SQL92 过滤灵活条件为 TRUE条件为 FALSE消息携带 属性: age18, nameTomBroker 接收Broker 解析 SQL 表达式遍历消息属性进行条件判断发送给 Consumer丢弃性能消耗 CPU适用于复杂业务规则筛选方式一Tag 过滤高效匹配成功匹配失败消息携带 Tag_ABroker 接收根据 ConsumeQueue 中的 Tag 哈希码进行快速匹配发送给 Consumer丢弃不网络传输性能极高适用于简单分类对比维度 Tag 过滤 SQL92 过滤过滤能力 仅支持精确匹配和 || 组合 支持复杂的条件表达式 AND OR 等性能 极高Broker 侧基于哈希码快速过滤 较低需要解析 SQL 表达式遍历消息属性灵活性 低只能打一个 Tag且只能是精确匹配 高可以基于多个用户自定义属性进行复杂过滤适用场景 简单分类对性能要求极高 复杂筛选逻辑对实时性要求不高 小贴士SQL92 过滤依赖于 Broker 的 enablePropertyFilter 配置默认是开启的。但它会消耗更多 Broker 的 CPU 资源所以能走 Tag 过滤就尽量用 Tag。消息的 Key 与业务查询最后我们来聊聊消息的 Key。Key 是消息的一个自定义属性通常用来标识业务的唯一键。比如订单消息可以用 orderId 作为 Key支付消息可以用 paymentId 作为 Key。Key 有什么用消息查询RocketMQ 提供了根据 Key 查询消息的功能。当你在控制台输入订单号查询消息时背后利用的就是 Key 索引。这个索引的核心是Hash 索引——消息发送时Broker 会根据 Key 的哈希值构建索引条目存储在索引文件中查询时通过哈希快速定位到具体的消息。幂等处理Consumer 可以根据 Key 来做幂等判断防止消息重复消费导致业务数据出错。Key 的设计建议尽量用业务上唯一且不变的字段如订单 ID、用户 ID如果一条消息包含多个业务维度选最核心的那个作为 Key不建议把时间戳、随机数这类变化频繁的值作为 Key无法精准定位 小贴士RocketMQ 的消息体最大支持 4MB5.x 版本可通过配置调整但如果你的消息体很大比如超过 1MB建议考虑压缩或把大内容存到 OSS消息里只存引用路径。小结这篇文章我们完成了一次“深度拆解”并且通过 7 张核心流程图/结构图把抽象的概念变得肉眼可见架构层面我们通过全链路协作图、心跳时序图和 DLedger 选举图直观地认识了四大组件NameServer、Broker、Producer、Consumer的职责和协作流程搞懂了 NameServer 的无状态设计、Broker 的主从架构和自动故障切换原理。