
作为一个常年折腾消息中间件的人我深知很多团队在 RocketMQ 集群搭建上踩过的坑。单机跑得好好的一旦要上双主双从异步复制集群各种问题就来了主从不同步、Broker 注册不上、客户端连不上、内存爆掉……网上的教程东拼西凑照着配完就是起不来。这篇文章把我实际搭建 RocketMQ 双主双从异步复制集群的完整过程、配置细节、启动顺序、验证方法以及排查套路全部整理出来保证配置能直接用、步骤能跟得上、坑能提前避开。在开始之前先说明这套架构的身份它适合大多数中小规模业务的削峰填谷、异步解耦、日志传输场景特点是写性能好、部署相对简单、成本可控。如果你正在选型或者已经决定用 RocketMQ 4.x 系列那这个双主双从异步复制的方案可以放心参考。1. 为什么选了双主双从异步复制1.1 几种集群形态的对比RocketMQ 的集群形态其实绕不开几个关键词Master、Slave、同步复制、异步复制、Dledger。单独一个 Master 的部署我们内部通常叫“能用但不敢出事”。只要那台机器宕机写入直接失败消息积累在客户端数据丢失风险非常高。主从同步模式下生产消息要等从节点也写完才返回数据最安全但性能损失不小。双主双从是目前生产环境里性价比很高的方案。两个 Master 都能处理写入请求每个 Master 背后挂一个 SlaveSlave 通过 HA 机制实时同步主节点的消息。当其中一个 Master 出现故障另一个 Master 还能继续服务写入不中断配合客户端的重试机制能实现一定程度的高可用。我把几个常见架构的差异整理成了表格方便大家直观对比架构形态写入能力故障容忍度数据安全复杂度单 Master普通无一般最低主从同步复制较差主节点故障时从节点兜底高中双主双从异步复制高单主故障仍可写入中中Dledger 模式较高自动切换主节点很高较高从对比里能看出双主双从异步复制更像是一个针对“既要写入吞吐又不想把架构搞得太复杂”的折中方案。1.2 为什么不用同步复制有人问我既然都做主从了为什么不同步复制求个踏实同步复制也就是 SYNC_MASTERBroker 会等待 Slave 确认写入完成后再返回客户端响应。这个确认过程如果 Slave 不在同一内网或者磁盘性能有波动RT 会明显拉高吞吐量也会掉下来。异步复制下Master 写完本地 commitlog 就立刻返回Slave 在后台拉取数据同步。这样单个请求的延迟更低整体吞吐更高。代价是极端情况下比如 Master 瞬间宕机而且断电损坏了磁盘那么尚未同步到 Slave 的数据会丢。这是架构层面做出的取舍不是配置失误。大多数业务场景比如订单状态流转、任务调度消息、异步通知能接受毫秒级甚至秒级的数据延迟真正无法容忍丢失的消息应该配合 RocketMQ 事务消息机制从业务层面做补偿而不是把同步复制的性能损失强行扛在链路上。1.3 这套架构适合谁团队规模不大、运维能力有限但业务量已经明显超过单机承载能力的场景最合适。双主双从要求至少 4 个节点但不需要额外引入协调组件部署思路和单机差不多启动参数改一改就能用。写入请求分散到两个 Master 上也能缓解单机磁盘 IO 和 CPU 的压力。如果你是刚接触 RocketMQ、第一套集群就想上高可用我反而建议先不要碰 Dledger 和自动选主相关功能。双主双从的运维模型是“我能看见谁活着”手动干预故障切换虽然原始但可控性强排查起来也直接。2. 架构设计与部署规划2.1 拓扑结构这一套集群总共 4 个 Broker 实例分成两组第一组broker-aMaster broker-a-sSlave第二组broker-bMaster broker-b-sSlave两个 Master 的 brokerName 不同各自的 Master 和 Slave 通过相同 brokerName 建立主从关系。集群对外暴露 2 个 NameServer 节点客户端配置 namesrvAddr 时写两个地址即可。NameServer 之间不需要互相通信各自保存全量路由信息客户端会同时连接两者从中获取 Topic 路由。注意RocketMQ 传统的主从架构中Slave 默认不会自动接管 Master 写入哪怕 Master 挂了Slave 依然是 Slave。要想自动切换需要引入 Dledger 或新版本 Controller。这一点很多人理解错配置完发现主节点宕机了写入还是失败然后跑来问我为什么。后面排查章节再详细讲。2.2 端口规划RocketMQ 默认使用几个关键端口提前规划好避免和已有服务冲突9876NameServer 监听端口10911Broker 对外服务端口10912Broker HA 主从同步端口10909Broker 用于 VIP 通道的端口默认是 10911 减去 2如果有防火墙或安全组需要把这些端口都放通。内部通信走 10912这个端口容易被遗漏主从同步失败时要优先排查它。我习惯在 /etc/hosts 里把四个节点的 IP 都配上主机名这样配置文件和日志里看起来更直观排查问题也更高效。2.3 磁盘与目录规划RocketMQ 对磁盘性能比较敏感尤其是 commitlog 的写入。不要把所有文件都塞到系统盘建议单独挂载一块数据盘专门放 store 目录。我的规划习惯是这样的/opt/rocketmq安装程序目录/data/rocketmq/storeBroker 数据存储目录/data/rocketmq/logs运行日志目录store 目录和 logs 目录分开放因为日志可能会无限增长如果跟数据混在一起磁盘满了会影响 commitlog 写入导致消息写入失败。实际部署时也尽量保证数据盘剩余空间充足整个 store 目录建议保留至少 20GB 的余量。3. 环境准备与依赖安装3.1 JDK 版本选择RocketMQ 4.x 依赖 JDK 8 或以上版本我推荐用官方 JDK 8。JDK 11 我也试过跑起来没有大问题但部分脚本和运维工具在 JDK 8 下表现最稳网上资料也多。安装完记得配置环境变量export JAVA_HOME/usr/local/jdk8 export PATH$JAVA_HOME/bin:$PATH验证一下java -version确保输出的是 1.8 版本的 JDK 信息。3.2 版本选择与下载我采用的是 4.9.x 系列。5.x 版本引入了 Controller 模式、Proxy 层架构变化比较大如果你有精力去理解和维护可以选择新版本但如果你希望照着一套文档快速把集群建起来生产环境求稳4.9.x 是更稳妥的选择。从 RocketMQ 官方 GitHub Releases 页面下载二进制包例如wget https://xxx/rocketmq-all-4.9.7-bin-release.zip unzip rocketmq-all-4.9.7-bin-release.zip mv rocketmq-all-4.9.7 /opt/rocketmq解压后的目录里bin 存放启动脚本conf 存放配置文件lib 存放依赖 jar 包。3.3 初始化目录在四个节点上都要创建数据目录和日志目录mkdir -p /data/rocketmq/store mkdir -p /data/rocketmq/logs如果是四台独立的机器每台机器按照角色创建不同的子目录比如 broker-a 节点创建 /data/rocketmq/store-abroker-b-s 节点创建 /data/rocketmq/store-b-s。用绝对路径区分开不要偷懒全部用同一个名字否则后期很容易搞混哪个目录对应哪个实例。4. 配置文件详解配置是这次搭建的核心。我把每个节点的配置文件完整列出来再逐一解释参数含义。4.1 NameServer 配置NameServer 本身不需要额外配置文件启动命令很简单nohup sh mqnamesrv /data/rocketmq/logs/namesrv.log 21 如果机器内存紧张可以先修改 runserver.sh 里的 JVM 参数把默认 4g 改小。我通常建议 NameServer 的堆内存设置在 512m 到 1g 之间。4.2 broker-a 主节点配置/opt/rocketmq/conf/broker-a.propertiesbrokerClusterNameDefaultCluster brokerNamebroker-a brokerId0 brokerRoleASYNC_MASTER flushDiskTypeASYNC_FLUSH namesrvAddr10.0.0.1:9876;10.0.0.2:9876 listenPort10911 storePathRootDir/data/rocketmq/store-a storePathCommitLog/data/rocketmq/store-a/commitlog storePathConsumeQueue/data/rocketmq/store-a/consumequeue storePathIndex/data/rocketmq/store-a/index storeCheckPeriod10000 deleteWhen04 fileReservedTime48 maxTransferBytesOnMessageInMemory262144 maxTransferCountOnMessageInMemory32 brokerIP110.0.0.11 autoCreateTopicEnabletruebrokerIP1 是很多新手的坑。如果机器有多个网卡RocketMQ 注册到 NameServer 上的地址可能是一个内网 IP客户端在其他网络访问不到。手动指定 brokerIP1 为对外提供服务的 IP能省掉很多麻烦。下面每个节点的配置文件我都显式写上了 brokerIP1建议你也这样做。4.3 broker-a-s 从节点配置/opt/rocketmq/conf/broker-a-s.propertiesbrokerClusterNameDefaultCluster brokerNamebroker-a brokerId1 brokerRoleSLAVE flushDiskTypeASYNC_FLUSH namesrvAddr10.0.0.1:9876;10.0.0.2:9876 listenPort10911 storePathRootDir/data/rocketmq/store-a-s storePathCommitLog/data/rocketmq/store-a-s/commitlog storePathConsumeQueue/data/rocketmq/store-a-s/consumequeue storePathIndex/data/rocketmq/store-a-s/index deleteWhen04 fileReservedTime48 brokerIP110.0.0.12 autoCreateTopicEnabletrue从节点的 brokerName 必须和主节点保持一致brokerId 必须大于 0brokerRole 必须是 SLAVE。只要这两个条件满足它就会被识别为主节点 broker-a 的从节点。4.4 broker-b 主节点配置/opt/rocketmq/conf/broker-b.propertiesbrokerClusterNameDefaultCluster brokerNamebroker-b brokerId0 brokerRoleASYNC_MASTER flushDiskTypeASYNC_FLUSH namesrvAddr10.0.0.1:9876;10.0.0.2:9876 listenPort10911 storePathRootDir/data/rocketmq/store-b storePathCommitLog/data/rocketmq/store-b/commitlog storePathConsumeQueue/data/rocketmq/store-b/consumequeue storePathIndex/data/rocketmq/store-b/index deleteWhen04 fileReservedTime48 brokerIP110.0.0.13 autoCreateTopicEnabletruebroker-b 和 broker-a 唯一的核心区别就是 brokerName 不同。两组主从共用同一个集群名各自独立提供写入服务。4.5 broker-b-s 从节点配置/opt/rocketmq/conf/broker-b-s.propertiesbrokerClusterNameDefaultCluster brokerNamebroker-b brokerId1 brokerRoleSLAVE flushDiskTypeASYNC_FLUSH namesrvAddr10.0.0.1:9876;10.0.0.2:9876 listenPort10911 storePathRootDir/data/rocketmq/store-b-s storePathCommitLog/data/rocketmq/store-b-s/commitlog storePathConsumeQueue/data/rocketmq/store-b-s/consumequeue storePathIndex/data/rocketmq/store-b-s/index deleteWhen04 fileReservedTime48 brokerIP110.0.0.14 autoCreateTopicEnabletrue4.6 核心参数逐项解析配置写完了但参数背后的逻辑如果不理解出了问题照样抓瞎。我挑几个关键参数详细解释一下。brokerClusterName 是集群的唯一标识四台 Broker 必须保持一致否则会被识别为不同集群。brokerName 是主从分组的标识同一个主从组内的主节点和从节点必须使用相同值。brokerId 是节点在组内的唯一编号0 表示 Master正整数表示 Slave这个值在同一组内不能重复。brokerRole 有枚举值ASYNC_MASTER、SYNC_MASTER、SLAVE。SYNC_MASTER 就是前面说的同步复制模式写入时等 Slave 确认ASYNC_MASTER 不等待。flushDiskType 控制刷盘方式。ASYNC_FLUSH 是异步刷盘消息写入 page cache 就返回性能高但机器断电可能丢数据SYNC_FLUSH 是同步刷盘每个消息都落盘才返回安全但慢。生产环境很多团队用 ASYNC_FLUSH ASYNC_MASTER性能拉满数据安全性靠业务层面补偿。storePathRootDir 是存储根路径不同的 Broker 实例必须使用不同的目录否则两个进程同时读写同一组文件会出现诡异的数据错乱问题。deleteWhen 是凌晨几点执行过期文件清理fileReservedTime 是文件保留小时数。默认 48 小时意味着磁盘里最多保留 48 小时的数据重放消费或者回溯更早的消息会失败需要调整的话根据业务需求和磁盘容量综合判断。autoCreateTopicEnable 默认 true 方便测试但生产环境建议改成 false由专门的运维创建 Topic避免手滑创建了一堆名字相似无意义的 Topic。5. 集群启动与验证5.1 修改启动脚本的 JVM 参数RocketMQ 默认启动脚本给的堆内存非常大runserver.sh 和 runbroker.sh 默认都是 4g。如果服务器内存一共只有 8g两个 Broker 加上两个 NameServer 直接吃满系统直接开始 swap性能惨不忍睹。我按照节点实际情况做了调整。NameServer 使用 512m 堆内存就足够JAVA_OPT${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256mBroker 根据消息量大小区分测试环境我给 1g 到 2g线上根据请求量逐步调整JAVA_OPT${JAVA_OPT} -server -Xms2g -Xmx2g -Xmn1g修改的是 bin 目录下的 runserver.sh 和 runbroker.sh。建议每台机器根据自己的真实内存情况来改不要四台机器复制一份脚本就什么都不动很可能内存小的机器直接 OOM。5.2 启动 NameServer先启动两台机器的 NameServernohup sh mqnamesrv /data/rocketmq/logs/namesrv.log 21 看日志确认启动成功tail -f /data/rocketmq/logs/namesrv.log出现 The Name Server boot success 类似字样就说明正常。5.3 启动 Broker启动顺序不强求但建议先启动 Master再启动 Slave这样 Slave 一开始就能找到主节点并建立 HA 连接。四个节点依次执行nohup sh mqbroker -c /opt/rocketmq/conf/broker-a.properties /data/rocketmq/logs/broker-a.log 21 同样检查日志tail -f /data/rocketmq/logs/broker-a.log出现 boot success 字样表示启动成功。5.4 验证集群状态关键一步来了。设置环境变量使用 mqadmin 命令查看集群export NAMESRV_ADDR10.0.0.1:9876;10.0.0.2:9876 mqadmin clusterList正常情况下输出里会有 broker-a、broker-a-s、broker-b、broker-b-s 四行记录同一组的 Master 和 Slave 的 brokerName 相同但是 brokerId 不同、地址不同。还可以看一下 Topic 路由信息验证集群注册是否完整mqadmin topicRoute -t TestTopic如果集群里还没有 TestTopic可以先自动创建后查看。这一步能确认客户端能从 NameServer 拿到哪些 Broker 的地址。5.5 消息收发功能验证光看进程和路由还不够要真实发一条消息测试。写一段简单的 Java 代码引入 rocketmq-client 的依赖Producer 发送、Consumer 订阅同一 Topic。Producer 示例核心逻辑DefaultMQProducer producer new DefaultMQProducer(test-group); producer.setNamesrvAddr(10.0.0.1:9876;10.0.0.2:9876); producer.start(); Message msg new Message(TestTopic, tagA, hello rocketmq cluster.getBytes(StandardCharsets.UTF_8)); producer.send(msg); producer.shutdown();Consumer 示例核心逻辑DefaultMQPushConsumer consumer new DefaultMQPushConsumer(test-group); consumer.setNamesrvAddr(10.0.0.1:9876;10.0.0.2:9876); consumer.subscribe(TestTopic, *); consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) - { System.out.println(new String(msgs.get(0).getBody())); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); consumer.start();如果 Producer 和 Consumer 分别部署在另外两台没有部署 RocketMQ 的机器上测试通过说明网络连通性、路由感知、消息收发整个链路都没问题。5.6 主宕机演练验证主从切换能力最直接的办法是手动模拟故障。找到 broker-a 进程将其 Kill 掉kill -9 $(pgrep -f broker-a.properties)观察集群状态mqadmin clusterList此时 broker-a 不再出现在列表里只有 broker-a-s 和另一组主从还在。Producer 继续往 TestTopic 发送消息如果客户端配置了多个 Broker 地址它会自动切换到 broker-b发送仍然成功。这里必须再次强调broker-a-s 仍然是 Slave不会自动写入新消息只是承载了对历史消息的读取。如果整个集群只有一个主节点可用且它宕机写入会失败。这就是为什么双主双从至少保留了两个写入点如果两个主节点都挂掉那就真出大事了。6. 常见问题与排查技巧6.1 客户端报 connect to 10.0.0.1:9876 failed这个问题出现频率最高。先确认 NameServer 进程是否存活ps -ef | grep namesrv然后确认端口是否监听ss -lntp | grep 9876如果进程有、端口也监听再用 telnet 从客户端机器测试网络连通性telnet 10.0.0.1 9876不通的话检查安全组和防火墙规则。阿里云之类的云环境入方向规则要放行所有相关端口。内网部署还要注意 brokerIP1 是否正确注册注册成了内网 IP 会导致客户端无法连接实际情况。6.2 主从同步延迟过大用 mqadmin 查看主从同步进度和 HA 连接状态mqadmin brokerStatus -b 10.0.0.11:10911输出里可以看到 slave 的同步进度、commitlog 的最大偏移量。如果 Master 和 Slave 的偏移量差值持续增长说明同步跟不上。原因多半是网络抖动、Slave 的磁盘写入性能太差、或者 Slave 机器负载过高。解决思路是优先保证 Slave 的磁盘性能和网络质量不要给 Slave 上太多其他业务负载。另外主从同步依赖 10912 端口这个端口没有放行的话Slave 进程虽然活着但一直连不上 Master。日志里会反复出现连接失败的报错。6.3 磁盘告警RocketMQ 只有在磁盘空间不足或者超出阈值时才会拒绝写入一旦触发生产端会收到系统繁忙的异常。日常运维注意监控 store 目录所在磁盘的使用率及时清理不再需要的消息或扩容。fileReservedTime 可以适当调小比如从 48 调整到 24腾出更多空间。但要注意如果业务存在延迟消费保留时间太短会导致 Consumer 拉取不到已被清理的消息要根据真实消费延迟来设置。6.4 Broker 启动失败日志报 Lock file 相关错误一个 store 目录如果被两个 Broker 进程同时使用后启动的那个会报文件锁错误直接退出。排查方法是 lsof 查看占用目录的进程或者确认配置文件里 storePathRootDir 是否指向了同一个路径。四个节点必须使用四个独立目录这是硬性要求。6.5 发送消息超时发送超时的原因很多Broker 的写入队列满了、系统负载高、刷盘慢、JVM GC 停顿。遇到超时先看 Broker 日志里有没有大量的 slow 日志或系统页告警再查看 CPU 和内存使用。其中一个容易忽略的参数是 sendMsgTimeoutProducer 发送消息默认超时 3 秒。如果跨地域网络延迟较高或者消息体偏大可以把超时时间适当调到 5 秒甚至 10 秒。但真正的问题还是要回到 Broker 负载本身单纯调大超时是治标不治本。7. 实操避坑与心得7.1 多网卡机器务必配置 brokerIP1我曾经遇到过一台服务器内网 IP 和外网 IP 都存在RocketMQ 启动时自动选择了内网 IP 注册到 NameServer。客户端和 Broker 不在同一内网的时候路由信息拿到的是一个不通的地址。最后手工指定 brokerIP1 为外网 IP 才解决。多网卡环境下这个配置不要在四个节点上漏掉任何一个。7.2 先看日志再问为什么很多新手启动失败后第一反应是看进程在不在、端口通不通然后对着配置文件一顿乱改。我的排查习惯是先看 nosp.log 或者 broker-xxx.log 的启动日志。日志里通常直接写着哪行配置不合法、哪个目录没权限、哪个端口被占用。改配置之前先搞清楚日志在说什么基本能省掉一半排查时间。7.3 纯机械磁盘谨慎上生产双主双从异步复制模式下Master 写入 commitlog 是顺序写机械硬盘勉强能用但 Slave 同步时不仅要写 commitlog还要同步消费队列索引文件机械盘很容易成为瓶颈导致主从延迟越来越大。有条件的话至少给 RocketMQ 节点配一块 SSD。没有 SSD 的测试环境你也会在跑压力测试时看到从集群报出的各种超时异常。7.4 备份策略RocketMQ 的 store 目录里有 commitlog、consumequeue、index 和 config 子目录。最直接的备份方式就是定期把这些文件复制到异地存储。另外因为消息消费后会被删除备份时最好配合消费者逻辑将关键消息落盘到其他地方别指望消息中间件本身长期保存历史数据。7.5 关于自动切换再重复一次这个关键点4.x 传统双主双从架构不具备自动主从切换能力。真到了 Master 宕机的时候Slave 还是 Slave写入请求不会自动转移到旧的 Slave 上。如果想要自动切换需要引入 Dledger 日志复制模式或者升级到带 Controller 的版本。我当时选择传统模式是因为团队运维水平有限手动切换加监控告警足以支撑业务而且故障切换流程可以演练、可以精确控制不容易出现自动切换引发的脑裂问题。想清楚自己的运维能力和业务容忍度再决定要不要把自动切换加进来。8. 最后聊点实在的这套双主双从异步复制集群搭建下来只要配置不出错、端口放通、目录分开、JVM 调好运行起来非常稳定。我实际维护过好几个这样的集群日常只需要关注磁盘使用率、主从同步延迟和 JVM 监控三个指标。消息发送偶尔抖动多半也是业务高峰期磁盘 IO 打满导致的而不是架构本身的问题。最后分享一个小经验上线前一定要做故障演练。不要觉得配置好了就万事大吉在主节点上用 kill 命令模拟宕机观察客户端行为确认另一组主节点能平滑接管心里才有底。演练过一次之后你对这套集群的信任度会完全不一样。