1. 为什么是 RocketMQ:消息中间件选型背后的取舍
我第一次认真研究 RocketMQ,是因为一个订单系统的线上事故。那会儿团队用的还是自研的基于 Redis 的队列方案,业务一上来,延迟和丢消息的问题接踵而至。订单创建后要同步通知积分、库存、风控好几个下游,Redis List 虽然简单,但一旦服务重启或主从切换,消息就无声无息地没了。那时候我才意识到,消息中间件不是"能用就行",它要扛住的其实是整个系统对可靠性、吞吐量、顺序性和事务一致性的综合要求。
后来我把主流的消息中间件都大致摸了一遍。Kafka 以高吞吐和日志场景见长,但是运维要依赖 Zookeeper,而且它在事务消息、延迟消息这类业务场景上能力偏弱;RabbitMQ 路由灵活、文档多,但基于 Erlang 的二次开发和深度调优对很多 Java 团队来说是个黑盒;RocketMQ 是阿里巴巴开源后捐给 Apache 的项目,用 Java 写的,源码可读性好,事务消息、延迟消息、顺序消息这些分布式系统里的高频需求都是开箱即用的能力。对于大多数互联网业务团队来说,RocketMQ 往往是最容易上手、也最容易"消化"的一套方案。
如果你正在做技术选型,我的建议是先别急着看各种大厂分享,而是回到你自己的场景去回答三个问题:你可以容忍多大的消息丢失概率?你的消息是否依赖顺序?你是否需要事务级的最终一致性?一旦这些问题有了明确答案,RocketMQ 的取舍逻辑其实是很清楚的——它是一个面向业务消息场景的完整解决方案,而不是单纯追求极致的吞吐。
多说一句,衡量一个中间件是否值得长期投入,除了功能外还要看社区活力和演进速度。RocketMQ 从 2016 年进入 Apache 孵化,到成为顶级项目,版本迭代一直没有停,生态里也有 rocketmq-dashboard、rocketmq-exporter 这类配套工具,这一点在长期维护上帮我们省了不少心。
1.1 Kafka、RabbitMQ、RocketMQ 三选一,我先说结论
很多刚接触消息队列的同学最喜欢问的,就是这三者到底怎么选。我把它们放在一张表格里对比,能省掉很多纠结:
| 维度 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 核心定位 | 分布式日志流、大数据管道 | 企业级消息路由 | 业务消息、事务、顺序场景 |
| 开发语言 | Java/Scala | Erlang | Java |
| 事务消息 | 较弱 | 支持但复杂 | 原生支持 |
| 延迟消息 | 不支持 | 插件支持 | 原生支持(18 个级别) |
| 顺序消息 | 分区内有序 | 不保证 | 分区内有序,实现简单 |
| 消费模型 | 拉模式(消费者协调) | 推拉结合 | 长轮询拉模式 |
| 运维依赖 | Zookeeper(或 KRaft) | 无 | NameServer,轻量 |
| 吞吐量 | 极高 | 中等 | 高 |
| 源码可读性 | 中 | 低 | 高 |
这不是说 RocketMQ 处处都比另外两个强。Kafka 在吞吐和日志沉淀能力上确实领先,如果你的场景就是海量日志采集、指标数据管道,Kafka 是最顺手的。RabbitMQ 在复杂路由、灵活交换机上有独到优势,适合系统间集成和传统企业架构。而 RocketMQ 的价值在于它把业务开发最常踩的那些坑,比如"发消息和更新数据库怎么保持一致""多副本消息怎么不乱序""消息积压后怎么快速恢复",都给到了相对成熟的解法。
我自己在实际项目里的一般建议是:偏业务系统、交易链路、需要事务和延迟能力,优先 RocketMQ;偏数据管道、日志接入、流式计算,优先 Kafka;小团队、消息量不大、需要灵活路由,RabbitMQ 也够用。选型从来不是找最好的,是找最匹配你团队维护能力和业务模型的。
1.2 RocketMQ 解决的几个真实痛点
聊完选型,我用几个真实场景来说明它到底解决了什么问题。
第一个痛点是"发消息和写数据库不一致"。比如下单后既要入库又要发一条 MQ 通知库存系统。如果先写库再发消息,消息发送失败怎么办?先发消息再写库,消息给了但事务回滚怎么办?RocketMQ 的事务消息就是冲着这个场景去的,它用半消息加回查机制,把"本地事务"和"消息发送"绑成一个整体,虽然没有数据库本地事务那样强一致,但最终一致性是有保证的。
第二个痛点是"顺序"。有些场景对顺序要求极高,比如同一用户的操作日志必须按时间顺序处理,否则状态会错乱。RocketMQ 允许你把相同业务 Key 的消息投递到同一个 MessageQueue 里,再配合顺序消费模式,就能在一个队列内的维度保证先进先出。Kafka 其实也支持分区内有序,但 RocketMQ 的 MessageQueue 和消费者线程模型更直观,代码写起来不绕。
第三个痛点是"延迟与积压运维"。业务高峰期一秒打进来几十万条消息,消费端一时半会消化不掉,消息堆积了怎么办?RocketMQ 的 CommitLog 顺序写机制让 Broker 写入基本不存在 IO 瓶颈,堆积时只要你补上消费者实例或调大消费线程,积压就能快速追平。这个特性在电商大促场景里太重要了——削峰填谷的本质就是从"生产者瞬间高压"转变成"消费者匀速消化",中间靠的就是 MQ 的堆积能力。
2. 拆开看核心架构:NameServer、Broker、Producer、Consumer 怎么协作
RocketMQ 的整体架构不复杂,但很多初次接触的人会被它和 Kafka 在架构上的差异搞得一头雾水。一句话概括:Kafka 依赖 Zookeeper 选主和保存元数据,RocketMQ 则用一套更轻的 NameServer 完成路由发现,Broker 负责真正存储和转发,客户端负责生产和消费。
我把 RocketMQ 的组件关系理顺给你看:
- Producer(生产者):负责创建消息,按 Topic 发送到 Broker。
- Consumer(消费者):从 Broker 拉取消息并处理,处理完再上报消费位点。
- NameServer:无状态的路由中心,Broker 启动后向它注册 Topic 和队列信息,生产者和消费者从它这里获取路由表。
- Broker:真正干活的存储节点,负责接收、存储、投递消息,同时维护消息的消费进度。
这里有一个容易忽略的点:NameServer 之间不互相通信,不搞数据同步,也不做主从选举。每个 NameServer 都保存全量路由信息,生产者和消费者的客户端会同时维持与多个 NameServer 的连接,某一个挂了,请求自动切换到另一个。这个设计比 Zookeeper 轻太多,既不用处理复杂的选举和会话,也不会因为 ZK 集群抖动导致整个 MQ 集群不可用。
Broker 内部还可以拆成多个模块,比如处理客户端 RPC 请求的 Netty 服务、管理 Topic 配置的 TopicConfigManager、记录消费进度的 ConsumerOffsetManager、负责消息文件存储的 CommitLog 和 ConsumeQueue 等。消息进来以后不是散落在各种队列文件里,而是统一追加到 CommitLog 这个大文件里,再异步构建逻辑队列索引——这个设计后面我专门讲。
2.1 NameServer:为什么它敢不用 Zookeeper
很多人问的第一个问题是:Kafka 用 Zookeeper 做协调,为什么 RocketMQ 就敢用一个看起来这么简单的 NameServer?
关键差异在于,RocketMQ 的 Broker 主从切换逻辑并不依赖一个外部选举组件。你可以这么理解:Zookeeper 是一个通用协调服务,它能做分布式锁、选举、配置管理;但换来的是部署复杂度、心跳会话机制、节点间的强一致写。RocketMQ 需要的只有一个能力——"让客户端知道 Topic 在哪个 Broker 上",这个需求根本不需要强一致。
NameServer 的做法是:Broker 每隔 30 秒向所有 NameServer 发送心跳,带上自己的地址、Topic 路由、存活信息;NameServer 收到后更新内存中的路由表,超过 120 秒没收到心跳就把这个 Broker 标记为不可用。客户端(生产者和消费者)每隔一段时间从 NameServer 拉一次路由,并缓存到本地。整个过程没有选举、没有数据强同步、没有分布式锁,所以部署上只需要 2 到 3 台轻量机器就够了。
这也带来一个值得注意的副作用:NameServer 的不可用不会立刻让正在运行的客户端挂掉,因为客户端已经缓存了路由表,只是新主题的发现会受影响。所以生产环境永远至少部署 2 台 NameServer,并且客户端配置里要把两个地址都加上,用;分隔。
2.2 Broker 的主从与多副本:一台挂了消息不丢的底气
Broker 是真正存放消息的地方,它的高可用设计值得仔细看。
每个 Broker 组里有一个主节点(brokerId=0)和一个或多个从节点(brokerId 非 0)。主节点负责读写请求,从节点实时从主节点同步数据。如果主节点挂掉,从节点能不能顶上来,取决于你是否启用了主从自动切换能力。RocketMQ 的 DLedger(基于 Raft)模式可以支持自动选主,但传统模式下主从切换是半自动的,需要运维介入把从节点提升为主节点,或者让客户端切换连接到新的主节点上。
我个人的生产建议是:如果团队有专门的中间件运维能力,DLedger 模式可以上,它能把 30 分钟的故障恢复时间缩短到几十秒;如果团队小、只想稳定,传统的主从同步加监控告警也够用,毕竟真正出现主节点物理宕机并且短时间内无法恢复的情况并不算多。
还有个容易被忽视的细节:主从同步方式有同步复制和异步复制两种。同步复制下,消息写入主节点后要等从节点也写成功才返回给生产者,消息最安全但延迟会高一些;异步复制则主节点写完就返回,性能好但主节点突然宕机时可能丢少量数据。这就要你自己在可靠性和性能之间做选择,后面在存储部分我会再展开说。
2.3 Producer 端发送消息的三条路径与可靠性选择
很多人以为发消息就是把消息丢到 Broker 就完事了,其实 Producer 有足足三种发送方式,对应的可靠性强度完全不同。
第一种是同步发送。Producer 发消息后会阻塞等待 Broker 返回写入结果,返回成功你才能确定消息真的被接受。这是最可靠的方式,适合交易类、订单类消息。代价是单条发送 RT 会高一点,吞吐比异步低。第二种是异步发送。你在回调里拿到结果,不阻塞主流程,适合对发送耗时不敏感但不想丢消息的场景。第三种是单向发送。发出去就不管结果,吞吐最高但可能丢,适合日志这种丢了也不心疼的数据。
我见过很多团队一股脑全用同步发送,结果把 Broker 压力打满,发送 RT 飙升,整体吞吐反而下来了。更合理的做法是:核心链路用同步或异步发送 + 重试,日志和统计类的数据用单向发送。别忘了发送端还要设置合理的超时时间和重试次数,RocketMQ 默认会重试 2 次,但你要根据下游响应时间适当调整,避免由于重试过度导致消息重复。
3. 存储设计:CommitLog 顺序写一骑绝尘,ConsumeQueue 承上启下
存储是消息中间件最核心的部分,因为它直接决定吞吐、延迟和可靠性。RocketMQ 的存储方案没有用常规的"每个队列一个文件"的做法,而是把设计拆成了两层:所有 Topic 的消息统一写进一个大文件——CommitLog,然后由系统异步构建每个队列的索引——ConsumeQueue。
这个设计的精髓在于"顺序写"。
你可以把 CommitLog 理解成一个流水账本。生产者不断把新消息追加到账本末尾,一行接一行,写完一行就记下当前写到哪。磁盘的顺序写性能是非常高的,机械硬盘顺序写都能跑到百兆每秒,SSD 上更是夸张。而如果用"每个队列一个文件"的模型,多个队列同时写入会变成随机 IO,磁头到处跳,性能立刻崩掉。
所以 RocketMQ 在写入路径上几乎做到了极致:不管你有多少 Topic、多少队列,消息进入 Broker 后统统追加到同一个 CommitLog 文件,避免任何随机 IO。剩下的问题就是"怎么根据 Topic 和队列把消息找出来"——这就要靠第二层 ConsumeQueue。
ConsumeQueue 可以理解成书末的索引目录。里面每个条目只有固定 20 字节:8 字节物理偏移量(指向 CommitLog 中的位置)、4 字节消息长度、8 字节 Tag 哈希。消费端拉消息时,先根据 Topic 和队列找到对应的 ConsumeQueue,顺着索引读出一批物理偏移量,再去 CommitLog 里把真正的消息内容读出来。因为索引条目是定长的,定位非常快,而且 ConsumeQueue 本身也是顺序追加写,对 IO 同样友好。
3.1 顺序写为什么快,这个设计省了什么
要理解顺序写为什么快,得先说机械硬盘和固态硬盘的基本脾气。机械硬盘最怕磁头移动,随机写一次可能要往返几十毫秒;SSD 没有机械结构,但随机写依然会触发垃圾回收、写放大等问题。顺序写的厉害之处在于无论哪种存储介质,它都能最大程度利用连续 IO 的优势。
RocketMQ 在这个基础上还用了内存映射文件(mmap)。CommitLog 文件被映射到进程的虚拟地址空间后,写入操作直接作用于内存页,再由操作系统内核在合适时机刷回磁盘。这样既减少了用户态和内核态之间的数据拷贝,也让写入延迟变得很低。如果你在 Linux 上跑过 RocketMQ 的压测就会发现,单 Broker 写入 TPS 上万是很轻松的事。
省掉的其实还不只是 IO 时间。因为所有 Topic 共用一份 CommitLog,Broker 不需要维护大量队列文件,文件数量少了,页缓存的命中率更高,磁盘碎片也更少。代价是什么呢?代价是消息删除变得不那么直接——你不能单独删某一个 Topic 的所有文件,只能等 CommitLog 中全部消息都过期后,整段文件才会被清理。好在 RocketMQ 支持按时间维度做到期删除,默认 72 小时,这个适应大多数业务场景。
3.2 ConsumeQueue 的"索引"角色与消息消费的路径
顺着一笔交易的完整生命周期走一遍,你就能彻底理解这套存储怎么工作。
消息从 Producer 到达 Broker 后,Broker 把消息体追加到当前写入的 CommitLog 文件里,拿到一个物理偏移量。然后把之前讲到的 20 字节条目追加到对应 Topic 下的 ConsumeQueue 里。注意这里有个细节:如果消息带 Tag,ConsumeQueue 条目里记录的是 Tag 的哈希值,消费者订阅时用 Tag 过滤,但如果消费者订阅的是表达式(比如TagA || TagB),Broker 必须在服务端做真正的过滤判断,这就要读完整的消息内容了。
消费端来拉消息时,请求里带的是"我要从哪个队列的哪个位点开始读"。Broker 先查 ConsumeQueue 对应位置的条目,拿到 CommitLog 偏移量,再读取真正的消息内容,批量返回给消费者。消费者处理完这一批消息后,会把消费位点上报给 Broker,Broker 记录在 ConsumerOffsetManager 里。下次重启或扩容时,消费者从保存的位点继续消费,既不会从头读,也不会整体跳过。
这里有一个新手特别容易踩的坑:ConsumeQueue 里的 Tag 哈希冲突会导致消息被误过滤或漏过滤。所以如果业务对 Tag 过滤的准确性有强要求,建议不要用太短、太常见的 Tag 字符串,或者干脆不在服务端过滤,全拉到客户端再用完整 Tag 做匹配。
3.3 刷盘策略:同步刷盘 vs 异步刷盘的代价账
消息写进页缓存不代表已经安全落盘。操作系统可能还没来得及把内存中的数据写回磁盘,机器就断电了,这部分数据就丢了。所以 RocketMQ 提供了两种刷盘策略:同步刷盘和异步刷盘。
同步刷盘的意思是,Broker 每写入一批消息,必须等数据真正落到磁盘后才向生产者返回成功。异步刷盘则是消息写进页缓存就立即返回,由后台线程定期把脏页刷回磁盘。同步刷盘数据最安全,但每条消息都要等一次磁盘 IO,吞吐会明显下降;异步刷盘吞吐高,但极端情况下(突然断电、宕机)可能丢最近几秒的数据。
我测试过一组对比:在相同配置的机器上,同步刷盘下单机 TPS 大概比异步刷盘低 30% 到 50%。所以选型时你得自己想清楚:这笔消息丢了到底有没有不可逆的业务损失?像订单支付成功通知这种,丢了可能引发资损隐患的,必须同步刷盘 + 同步复制双保险;像用户操作日志这种,异步刷盘就够了,完全没必要用性能换那一点点安全性。
4. 事务消息与顺序消息:RocketMQ 最有含金量的两个场景
如果说存储设计决定了 RocketMQ 的基础性能,那事务消息和顺序消息就是它在功能层面的护城河。这两个能力是业务开发里最常被用到、也最容易被用错的,我单独拿出来聊。
4.1 事务消息的半消息机制与回查为什么能保证最终一致性
先说一个常见问题:为什么不用"先执行本地事务,再发消息"这种简单方案?因为本地事务可能成功但消息发送失败,也可能本地事务回滚了但消息已经发出去了。两条独立的操作之间没有原子性,就永远存在不一致的可能。
RocketMQ 事务消息的做法分三步。第一步是发送半消息(Half Message)。所谓半消息,就是这条消息先到 Broker,但对消费者不可见,它不会投递出去。第二步是执行本地事务。你这边的业务逻辑,比如扣库存、更新订单状态,正常执行。第三步是根据本地事务结果向 Broker 提交 commit 或 rollback 指令。commit 之后半消息才会变成普通消息,消费者才能看到;rollback 则直接丢弃。
这里最巧妙的部分是回查机制。如果第二步和第三步之间进程崩了,或者网络超时导致 Broker 一直没收到最终的 commit/rollback 指令怎么办?Broker 会定期反向询问生产者"你这笔本地事务到底成功了没有",这就是回查。你的程序需要实现一个回查接口,根据本地事务的执行状态给 Broker 一个明确答复。有了这个机制,哪怕某个环节失败了,最终也能收敛到"要么消息被消费、要么消息被丢弃"的一致状态。
不过要说清楚,事务消息不是强一致。它的本质是"最终一致性",在极端情况下,消息的可见性会比本地事务提交稍微晚一点。而且事务消息对数据库事务本身没有任何魔法,你该用本地事务还是得用。它解决的是"协调"问题,不是"原子性"问题。
4.2 顺序消息:全局顺序 vs 分区顺序,怎么选才对
顺序是业务开发里特别容易踩坑的领域。我先说结论:全局顺序消息十有八九是不需要的,大部分场景要的其实是分区顺序。
分区顺序的意思是把"有相同特征"的消息放到同一个队列里。比如我们做一个订单系统,要求同一订单号的消息必须被顺序消费。实现方式很简单:发送消息时用 MessageQueueSelector 按订单号取模,保证同一个订单号的落点队列相同。同一个队列在消费端只会被同一批消费者串行消费,因此顺序自然得到保证。
这里有两个特别容易犯的错误。第一,全局顺序消息是把所有消息都发到同一个队列,虽然完全有序,但单队列等于完全放弃并发度,吞吐惨不忍睹,非必要绝对不要用。第二,顺序消息被消费失败后不能像普通消息那样并发重试,因为如果消息 A 失败跳过,A 后面的消息又处理了,依赖就全乱了。RocketMQ 对顺序消息的处理是抛出异常后暂停这个队列的消费,等待重试并保证后续消息继续排着,直到它成功为止。这就要求你消费顺序消息的代码要特别稳健,不是"能处理完就行",而是要尽量保证"一次就能处理成功"。
4.3 延迟消息的坑:18 个级别和定时精度问题
延迟消息是很多人刚上手 RocketMQ 时最喜欢问的功能之一。订单 30 分钟未支付自动关单、活动开始前 10 分钟提醒用户,都是典型的延迟场景。RocketMQ 原生支持 18 个固定延迟级别,而不是任意时间秒数。
分别对应的是:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。也就是说,如果你想延迟 2 分 30 秒,那是做不到的,只能选 3 分钟这个档位。底层实现也很巧妙,延迟消息不会直接进业务队列,而是先进入一组系统级的 SCHEDULE_TOPIC 队列,由定时线程等到时间到了以后,再重新投递到真正的目标 Topic。因为线程调度和时钟精度限制,它的实际延迟误差通常在秒级。
更精确的延迟时间怎么办?两个思路。一是把消息改成"定时触发"模式,用延迟消息投递一个调度任务,任务到达后由你自己的系统判断是否已经到执行时间,未到就继续延迟;二是升级到更灵活的调度架构,比如自研时间轮配合 RocketMQ 的普通消息,但这种要投入不少研发力量。绝大多数业务场景下,RocketMQ 的 18 个级别已经覆盖了 90% 以上的需求,先确认自己的场景是否真的需要"任意精度延迟"再动手。
5. 生产环境避坑:我踩过的消费堆积、Rebalance 抖动与消息丢失
这一节是我最想写给正在接手 RocketMQ 的同学们看的,因为这些问题网上文档很少提到,但线上十次事故里有八次是它们引起的。
5.1 消费堆积:先看拉取速度还是先看处理速度
消费堆积(消息积压)几乎是每个用过消息中间件的人都遇到过的现象。一旦业务高峰期来了,消费者处理不过来,队列里的消息数量就像雪球一样越滚越大。此时第一反应不要是加机器,先判断到底卡在哪个环节。
我有一套固定的排查顺序。第一步,确认消费 TPS 和处理耗时:如果消费 TPS 持续低于生产 TPS,大概率是处理逻辑里有慢操作——数据库查询慢、下游接口超时、存在锁竞争等。第二步,看消费线程数:RocketMQ 默认消费线程数是 20,如果处理逻辑本身不慢但单机消费能力不够,可以调大 consumeThreadMin 和 consumeThreadMax。第三步,确认是否有消费失败重试:大量消息处理抛异常后进入重试队列,也会表现为业务队列积压。如果积压特别严重,还可以临时调整消费位点,跳过部分非关键消息,但要谨慎操作,跳过之前必须先保存原 offset 以便事后补处理。
还有一个很反直觉的点:RocketMQ 的 Broker 写入能力理论上远大于你单台消费实例的处理能力,所以积压的瓶颈几乎都在消费者这端。如果你的消费端逻辑是同步调用第三方接口,且对方响应慢,建议你做两件事:给消费逻辑加一层本地队列异步化,或者干脆把某些非核心数据的消费线程降级,先保住核心链路。
5.2 Rebalance 抖动:客户端卡顿引发的分配风暴
Rebalance 是 RocketMQ 消费端最容易被忽视的机制。它的职责是当消费者组内有消费者上下线时,重新分配各消费者负责的 MessageQueue。理想情况下,一个消费者挂了,它负责的队列会被平滑分给剩余消费者;但实际上,如果消费者只是短暂卡顿、心跳超时,就可能导致它被误判为下线,触发一次不必要的 Rebalance。
Rebalance 抖动最典型的场景是:消费者在长时间执行高耗时任务,心跳线程被阻塞,Broker 认为它下线了,于是把它的队列重新分配给其他消费者。旧消费者恢复后又重新加入,再来一次 Rebalance,队列再分配一次。这个反复横跳的过程不仅造成消息重复消费,还可能让某台消费者长时间拿不到队列,整体消费能力剧烈波动。
要避免这类问题,第一个准则是消费线程和心跳线程要分离,不要在极耗时的处理里完全阻塞线程的执行;第二个准则是尽量保证每个业务消费组的消费者数量稳定,避免一次大范围的上下线;第三个准则是给消费端设置合理的心跳超时和会话超时参数,不要用默认值硬扛极端场景。我用过最有效的手段其实很简单:监控 Rebalance 发生时消费组的队列分配变化,把它打到日志里,配合告警,一旦发现频繁 Rebalance 立刻干预。
5.3 消息丢失的三道防线:发送端、Broker、消费端分别怎么守
很多人以为"消息发送成功"就等于"绝对不丢",这个想法需要纠正。消息丢失可能发生在三个环节:发送端发出后丢失、Broker 存储后丢失、消费端处理过程中丢失。
发送端这条线比较好守:用同步发送并处理发送失败的异常,加上重试机制。如果你用异步发送,一定要在回调里检查发送结果,不能只发不查。这也是我之前提到的,日志类数据用单向发送没问题,核心业务消息打死也不要走单向。
Broker 这条线靠配置兜底:确认刷盘策略是同步还是异步,复制方式同步还是异步,主从节点的磁盘空间是否充足。真实事故里有一种很隐蔽的丢法:消息已经写入主节点,但从节点因为网络延迟没有及时同步,主节点在同步完成前宕机,那么消费端可能读到主节点未同步完整的数据,切换节点后消息就少了。同步复制能解决这个问题,代价就是每一笔写入都要多等一次网络往返。
消费端这条线最容易被忽视。RocketMQ 的消费逻辑是"先处理业务、再提交消费进度",但如果你在代码里把"提交进度"和"业务成功"的顺序搞反了(例如先更新本地 offset,再执行业务逻辑),一旦业务处理失败,这条消息不会再被拉取,等于静默丢失。所以消费端务必要保证提交 offset 前,这条消息已经处理成功,且能接受"重复消费"这个现实——也就是说,你的消费逻辑必须做幂等,用数据库唯一键、Redis 状态位或本地去重表挡掉重复消息。
6. 集群部署与运维参数:从单机到生产集群的务实建议
最后聊部署和运维。这一部分我尽量给可以直接抄作业的配置思路,而不是堆一堆空洞的原则。
6.1 一台 8C16G 的机器该怎么配
先给单机配置思路。RocketMQ 的重头戏在 Broker,它要吃内存做页缓存,所以 JVM 堆不要贪大,留出大量内存给操作系统做页缓存才是关键。很多第一次部署的人会犯一个错:把 16G 内存全部分给 JVM 堆,结果消息全写到堆里,垃圾回收频繁,吞吐直线下降。
我的习惯配置是,8C16G 的机器上 Broker JVM 堆设 4G 到 5G,剩下的内存留给页缓存。修改runbroker.sh里的JAVA_OPT,把-Xms4g -Xmx4g调到你想要的值。NameServer 就轻量多了,1G 到 2G 堆足够,它只是维护路由表和心跳。
存储路径要独立规划,CommitLog 所在目录不要和系统盘放在一起,最好单独挂一块磁盘,并预留至少 30% 的空闲空间。磁盘写满时 RocketMQ 会拒绝写入新消息,这是保护机制,但也意味着业务会立即受到影响,所以磁盘水位必须进监控。
还有一个不要忽略的参数是maxMessageSize,默认是 4MB。如果你们的业务需要传大消息(比如超过 4MB 的报文),记得提前调大,并同步调大客户端的maxMessageSize配置,否则发送端会报CODE: 13的错误。但我不建议把消息体搞得太大,大消息对 Broker 的内存和磁盘压力都高,能压缩就压缩,能走对象存储就放对象存储,MQ 里只放引用或摘要。
6.2 监控哪些指标,出了问题第一时间看什么
日常运维最怕无从下手,我先按优先级列一个监控清单。
第一梯队:Broker 可用性、生产消费 TPS、消费积压数量、磁盘使用率。这几个指标任何一个出问题,都意味着线上马上有感知。第二梯队:主从同步延迟、刷盘耗时、网络 IO、堆内存使用率。这些指标是事故的前兆,比如主从同步延迟持续拉高,说明主节点写入压力大或者网络有问题。第三梯队:消费失败次数、重试队列深度、Rebalance 触发次数。平时容易忽略,但事故一旦发生,这些是定位的起点。
我用过一版比较顺手的告警阈值,供参考:消费积压超过 5 万条持续 5 分钟触发 Warning;超过 20 万条持续 5 分钟触发 Critical;磁盘使用率超过 80% 触发 Warning,超过 85% 触发 Critical;主从同步延迟超过 3 秒触发 Warning。具体数字要根据业务量调,但积压的"持续时间"比瞬时值更容易反映真实风险。
真的出了事故,我的排查路径是:先看监控确认积压还是丢消息,积压就顺着消费端排查,丢消息就按发送端、Broker、消费端三层逐步过滤。第一步打开消费组在 mqadmin 和 dashboard 上的详情,确认消费者是否在线、消费位点变化、各队列分配的客户端的对应关系;第二步看 Broker 的写入日志和消费端异常日志;第三步做消息轨迹追踪,用mqadmin queryMsgById和消息轨迹功能定位一条具体消息从生产到消费的完整路径。整个链路一旦梳理顺畅,大部分问题都能在半小时内定位到根因。
最后分享一个我自己非常受用的小技巧:给每个消费组配置独立的消费线程池,不要在消费代码里共享一个全局线程池。曾有一次我把多个消费组的处理逻辑都丢进了同一个线程池,某个消费组处理阻塞后,其他组的消费全部被拖垮,整个系统的消息延迟瞬间飙高。后来我把线程池拆开,每个组独立调度,这类问题再没出现过。消息中间件本身的能力再强,终究是给业务用的,消费端代码的健壮性和隔离性,才是线上稳定运行的最后一道防线。