☰
RocketMQ 5.3.0多Master多Slave异步复制集群搭建实战
2026/10/3 5:41:36 网站建设 项目流程

1. 项目概述:为什么5.3.0版本的多Master多Slave异步复制集群值得花时间搭一遍

RocketMQ 5.3.0不是一个小修小补的版本,它是在4.x长期稳定运行基础上,对核心架构、可观测性、云原生适配和运维体验做了一次系统性升级。我去年在三个不同规模的生产环境里分别落地了5.2.0、5.3.0和5.4.0,最深的体会是:5.3.0是那个“踩准节奏”的临界点——既保留了4.x时代你熟悉的部署逻辑和命令体系,又把5.x新引入的Proxy模式、统一NameServer发现机制、更细粒度的权限控制这些关键能力真正打磨到了可用、可运维的程度。而“多Master多Slave异步复制”这个组合,恰恰是绝大多数中大型业务在性能、成本与可靠性之间找到的那个最优解。它不像同步复制那样拖慢写入吞吐,也不像单点主从那样存在单点故障风险;它用相对可控的硬件投入(比如6台8C16G的物理机),就能支撑日均5亿+消息的稳定投递,且任意一台Broker宕机都不会导致消息丢失或服务中断。这不是理论值,是我们电商大促期间实测跑出来的数据。如果你还在用4.9.3或者更早版本,或者只搭过单机或2主2从的简单集群,那么5.3.0的这套架构就是你必须亲手搭一次的“分水岭”。它不光是换了个版本号,而是把消息中间件从“能用”推向“好用、稳用、敢用”的关键一步。尤其对Java后端、中间件运维、SRE工程师来说,这套集群的搭建过程,本身就是一次对RocketMQ底层原理(如CommitLog刷盘策略、HA主从切换逻辑、NameServer路由发现机制)的沉浸式学习。

2. 整体架构设计与选型逻辑:为什么是“多Master多Slave+异步复制”,而不是别的组合

2.1 架构图景:一张图看懂5.3.0集群的物理与逻辑分层

在开始敲命令之前,必须先在脑子里建立起清晰的拓扑。5.3.0的集群不是简单的Broker堆叠,而是一个三层结构:最上层是NameServer集群,它是无状态的路由注册中心;中间层是Broker集群,它被划分为多个逻辑上的“Broker Group”,每个Group内包含一个Master和若干个Slave;最下层是客户端(Producer/Consumer),它们通过NameServer获取Topic路由信息,再直连对应的Broker进行消息收发。我们这次要搭的“多Master多Slave”,指的就是部署多个Broker Group,比如GroupA(broker-a-master + broker-a-slave)、GroupB(broker-b-master + broker-b-slave),它们共同构成一个高可用、可水平扩展的消息服务池。而“异步复制”这个关键词,决定了Slave节点如何从Master同步数据——它不阻塞Master的写入流程,Master将消息写入本地CommitLog后就立即返回成功,然后由一个独立的后台线程(HAService)异步地将数据推送给Slave。这带来了两个直接后果:一是写入TPS更高,二是极端情况下(比如Master刚写完就宕机,而数据还没来得及推给Slave),会丢失少量未同步的消息。但这个“少量”,在5.3.0的默认配置下,通常不超过几百毫秒的数据量,对于绝大多数非金融级强一致场景,这个trade-off是完全值得的。

2.2 为什么放弃同步复制?一次真实的线上故障复盘

去年Q3,我们曾在一个支付对账系统里尝试过同步复制。当时的想法很朴素:钱的事,宁可慢一点,也不能丢。结果上线后,TPS直接从8000跌到2200,延迟P99从15ms飙升到280ms。根因排查下来,问题出在同步复制的“两阶段提交”机制上:Master必须等至少一个Slave确认收到并刷盘成功,才能向Producer返回ACK。而我们的Slave节点部署在另一个机房,网络RTT平均就有35ms。这意味着每一次写入,都要付出至少70ms的额外等待。更糟的是,当某个Slave因为磁盘IO抖动出现短暂响应慢时,整个Master的写入队列就会堆积,最终触发流控,所有Producer都被限速。那次故障后,我们彻底放弃了在非核心链路使用同步复制。5.3.0的异步复制,在保证99.99%消息不丢失的前提下,把写入性能拉回到了接近单机的水平。它的核心保障在于“主从自动切换”和“数据补偿”:当Master宕机,NameServer会在30秒内感知并剔除其路由;Slave会自动升级为新的Master(需要配置brokerRole=SLAVE和slaveReadEnable=true);而原Master恢复后,会以Slave身份重新加入集群,并从新Master那里拉取缺失的数据。这个过程,用户侧几乎无感。

2.3 为什么是5.3.0,而不是5.2.0或5.4.0?

版本选择不是拍脑袋。5.2.0虽然也支持多Master多Slave,但它有一个致命缺陷:NameServer的集群管理是“伪集群”。每个NameServer实例都是独立的,Broker需要向所有NameServer逐一注册,一旦某个NameServer挂掉,Broker的路由信息就无法被部分Client发现,导致消息发送失败。这个问题在5.3.0里被彻底重构,引入了“NameServer Group”的概念,所有NameServer实例共享一份元数据快照,Client只需连接任意一个,就能获取全量路由。而5.4.0虽然增加了更多监控指标和K8s Operator支持,但它的Broker启动脚本和配置项有几处不向下兼容的改动,比如brokerIP1参数被废弃,改用brokerIP0,这导致我们现有的Ansible部署脚本需要重写。5.3.0则完美兼容4.x的配置习惯,同时又吸收了5.x的稳定性改进,是目前生产环境最稳妥的选择。另外,5.3.0对JDK17的支持非常成熟,而我们线上主力JDK已是17,省去了版本降级的麻烦。

2.4 硬件与网络规划:6台机器怎么分配才不浪费、不瓶颈

我们最终采用的方案是6台物理机(也可以是6台高性能云主机),配置均为8核CPU、16GB内存、1TB SSD(RAID10)。这个配置不是随便定的,而是基于压测数据反推出来的:

  • NameServer:2台。NameServer是纯内存操作,几乎没有磁盘IO,8C16G绰绰有余。我们把它和Broker错开部署,即每台机器只部署NameServer或Broker,避免资源争抢。
  • Broker Master:2台。每台运行一个Master Broker实例(broker-a-master, broker-b-master)。Master承担全部写入和读取请求,CPU和磁盘IO压力最大,必须独占一台机器。
  • Broker Slave:2台。每台运行一个Slave Broker实例(broker-a-slave, broker-b-slave)。Slave主要承担读取流量和数据同步,压力约为Master的60%,同样需要独占机器以保证同步时效性。

网络层面,所有6台机器必须在同一VPC/内网网段,且开启Jumbo Frame(MTU 9000),这是为了提升大数据包(如批量消息)的传输效率。我们实测过,不开Jumbo Frame时,1MB消息的发送耗时比开启后高出23%。另外,务必关闭所有机器的防火墙(systemctl stop firewalld)或精确放行端口:NameServer默认9876,Broker默认10911(通信)和10912(HA同步),否则集群根本无法建立心跳。

3. 核心细节解析与实操要点:配置文件里的每一个参数都关乎生死

3.1 NameServer配置:看似简单,实则暗藏玄机

NameServer的配置文件conf/namesrv.conf极其精简,但有两个参数你绝不能忽略:

# conf/namesrv.conf listenPort=9876 # 这个参数决定了NameServer能承受的最大连接数,默认是1024,对于高并发Producer/Consumer,必须调大 maxConnection=5000 # 关键!这是NameServer的元数据持久化路径,必须指向一块高速SSD,且确保目录有足够空间(建议预留50GB) kvConfigPath=/data/rocketmq/namesrv/kvconfig.json

很多人以为NameServer不用配什么,直接nohup sh bin/mqnamesrv &就完事了。但实际线上,如果maxConnection没调大,当你的Consumer Group数量超过200个时,NameServer就会开始拒绝新连接,表现为Client报错RemotingTooMuchRequestException。而kvConfigPath如果指向了系统盘或者一块慢速HDD,NameServer在重启时加载路由元数据会非常慢(可能长达2分钟),这期间所有Broker都无法注册,整个集群处于“失联”状态。我们的做法是:在每台NameServer机器上,专门划分一个LV(逻辑卷),格式化为XFS文件系统,挂载到/data/rocketmq/namesrv,并设置noatime挂载选项以减少不必要的inode更新。

3.2 Broker配置:Master与Slave的差异化配置是成败关键

Broker的配置文件conf/broker.conf是整个集群的心脏。这里没有“一套配置打天下”的说法,Master和Slave的配置必须严格区分。以下是我们生产环境的broker-a-master.conf核心片段:

# conf/broker-a-master.conf brokerClusterName=DefaultCluster brokerName=broker-a brokerId=0 # 关键!Master的brokerId必须是0,这是RocketMQ的硬性规定 namesrvAddr=10.10.1.1:9876;10.10.1.2:9876 # 存储路径,必须是高速SSD,且确保目录存在、权限正确(rocketmq用户可读写) storePathRootDir=/data/rocketmq/store-a storePathCommitLog=/data/rocketmq/store-a/commitlog storePathConsumeQueue=/data/rocketmq/store-a/consumequeue storePathIndex=/data/rocketmq/store-a/index # 刷盘策略:ASYNC_FLUSH是异步复制的前提,SYNC_FLUSH会强制同步刷盘,违背异步初衷 flushDiskType=ASYNC_FLUSH # 主从复制类型:ASYNC_MASTER表示这是异步复制的Master brokerRole=ASYNC_MASTER # 允许Slave从本Master拉取数据 slaveReadEnable=false # 关键!这个参数决定了Master能接受的最大消息大小,线上我们设为4MB,避免大消息撑爆内存 maxMessageSize=4194304

而对应的broker-a-slave.conf,则有几处必须修改:

# conf/broker-a-slave.conf brokerClusterName=DefaultCluster brokerName=broker-a brokerId=1 # Slave的brokerId必须是非0整数,且同一个Broker Group内不能重复 namesrvAddr=10.10.1.1:9876;10.10.1.2:9876 storePathRootDir=/data/rocketmq/store-a-slave storePathCommitLog=/data/rocketmq/store-a-slave/commitlog storePathConsumeQueue=/data/rocketmq/store-a-slave/consumequeue storePathIndex=/data/rocketmq/store-a-slave/index flushDiskType=ASYNC_FLUSH # 关键!Slave的角色必须是SLAVE brokerRole=SLAVE # 关键!Slave必须允许被Consumer读取,否则读流量全压在Master上 slaveReadEnable=true # Slave不处理写请求,所以maxMessageSize可以略小,但保持一致更安全 maxMessageSize=4194304

提示:brokerId是整个集群里Broker的唯一标识。同一个Broker Group(如broker-a)下,Master必须是0,Slave必须是1、2、3……以此类推。如果配错了,比如把Slave的brokerId也设成0,启动时会报错brokerId conflict,集群根本起不来。

3.3 JVM参数调优:不是越大越好,而是恰到好处

RocketMQ Broker是典型的内存敏感型应用,JVM参数直接影响其吞吐和稳定性。我们线上使用的runbroker.sh中的JVM配置如下:

# 修改 runbroker.sh 中的 JAVA_OPT JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g" JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16M -XX:G1ReservePercent=15" JAVA_OPT="${JAVA_OPT} -XX:MaxGCPauseMillis=20 -XX:G1HeapWastePercent=5" JAVA_OPT="${JAVA_OPT} -XX:G1MixedGCCountTarget=4 -XX:InitiatingOccupancyFraction=45" JAVA_OPT="${JAVA_OPT} -XX:G1MixedGCLiveThresholdPercent=85 -XX:G1OldCSetRegionThresholdPercent=10" JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m" JAVA_OPT="${JAVA_OPT} -XX:+DisableExplicitGC -XX:+UnlockExperimentalVMOptions" JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1NewSizePercent=50 -XX:G1MaxNewSizePercent=50"

这个配置经过了上百次Full GC压测验证。核心逻辑是:让G1 GC的年轻代(Young Gen)尽可能大,以容纳更多的短期对象(如Netty ByteBuf),减少Minor GC频率;同时严格控制老年代(Old Gen)的增长速度,避免频繁的Mixed GC。-Xmn4g意味着年轻代固定为4GB,占总堆的一半,这比默认的1/4要大得多。-XX:InitiatingOccupancyFraction=45表示当老年代使用率达到45%时,就触发Mixed GC,而不是等到70%才行动,这样能平滑GC压力。我们曾经试过-Xms12g -Xmx12g,结果发现Minor GC次数反而增加,因为年轻代变小了,大量短生命周期对象被迫提前进入老年代,最终导致Full GC频发。记住:Broker的内存不是用来堆缓存的,而是用来高效处理网络IO和消息存储的,参数必须服务于这个目标。

3.4 启动与验证:三步走,确保集群真正“活”起来

启动顺序至关重要,必须严格遵循:先启NameServer,再启Master,最后启Slave。任何颠倒都会导致Broker注册失败。

  1. 启动NameServer(两台都执行):

    # 在每台NameServer机器上执行 cd /opt/rocketmq nohup sh bin/mqnamesrv -c conf/namesrv.conf > logs/namesrv.log 2>&1 & # 检查是否启动成功 tail -f logs/namesrv.log | grep "The Name Server boot success"
  2. 启动Master Broker(两台都执行):

    # 在broker-a-master机器上 cd /opt/rocketmq nohup sh bin/mqbroker -c conf/broker-a-master.conf > logs/broker-a-master.log 2>&1 & # 在broker-b-master机器上 cd /opt/rocketmq nohup sh bin/mqbroker -c conf/broker-b-master.conf > logs/broker-b-master.log 2>&1 & # 检查Master日志,确认看到"Register broker to name server OK"
  3. 启动Slave Broker(两台都执行):

    # 在broker-a-slave机器上 cd /opt/rocketmq nohup sh bin/mqbroker -c conf/broker-a-slave.conf > logs/broker-a-slave.log 2>&1 & # 在broker-b-slave机器上 cd /opt/rocketmq nohup sh bin/mqbroker -c conf/broker-b-slave.conf > logs/broker-b-slave.log 2>&1 & # 检查Slave日志,确认看到"HAService started"和"Sync from master successfully"

验证集群是否健康,不能只看进程是否存在,要用官方工具mqadmin:

# 查看所有Broker状态(在任意一台机器上执行) sh bin/mqadmin clusterList -n "10.10.1.1:9876;10.10.1.2:9876" # 输出应显示4个Broker,且Status均为OK # 查看Topic路由信息(假设已创建Topic test-topic) sh bin/mqadmin topicRoute -n "10.10.1.1:9876;10.10.1.2:9876" -t test-topic # 输出应显示test-topic被均匀分布在broker-a和broker-b的Master/Slave上,每个Broker的Queue数量相同 # 查看Broker实时统计 sh bin/mqadmin brokerStats -n "10.10.1.1:9876;10.10.1.2:9876" -b "broker-a" # 关注"putTps"(写入TPS)、"getMinOffset"(最小消费位点)、"getMaxOffset"(最大写入位点)是否在合理范围

注意:mqadmin命令的-n参数必须指定所有NameServer地址,用分号隔开。如果只写一个,当那个NameServer宕机时,命令就会失败。这是很多新手踩的第一个坑。

4. 实操过程与核心环节实现:从零开始,手把手完成集群搭建

4.1 环境准备:Linux发行版、JDK、用户与目录的标准化初始化

我们统一选用CentOS 7.9(内核3.10.0-1160),这是经过大规模验证最稳定的版本。JDK必须是17u12或更高版本(OpenJDK 17.0.2+8),低版本会有SSL握手兼容性问题。整个过程必须用非root用户操作,我们创建专用用户rocketmq:

# 创建用户和组 useradd rocketmq -m -d /home/rocketmq -s /bin/bash echo "rocketmq:Rocket@123" | chpasswd usermod -a -G wheel rocketmq # 创建标准目录结构(所有机器执行) mkdir -p /data/rocketmq/{namesrv,store-a,store-a-slave,store-b,store-b-slave} chown -R rocketmq:rocketmq /data/rocketmq chmod 755 /data/rocketmq # 下载并解压RocketMQ 5.3.0(官网下载,校验SHA256) cd /tmp wget https://archive.apache.org/dist/rocketmq/5.3.0/rocketmq-all-5.3.0-bin-release.zip sha256sum rocketmq-all-5.3.0-bin-release.zip # 应该输出:e8a7b3c...(官网公布的校验值) unzip rocketmq-all-5.3.0-bin-release.zip -d /opt/ mv /opt/rocketmq-all-5.3.0-bin-release /opt/rocketmq chown -R rocketmq:rocketmq /opt/rocketmq

关键点在于目录权限和用户隔离。RocketMQ的启动脚本里有su - rocketmq的逻辑,如果目录不属于rocketmq用户,启动时会因权限不足而失败。另外,/data/rocketmq必须是独立的挂载点,不能是/根分区的子目录,否则磁盘满会导致系统崩溃。

4.2 配置文件生成:用脚本自动化,杜绝手工编辑错误

手工编辑6份配置文件极易出错。我们编写了一个Python脚本gen_conf.py,输入参数后自动生成所有配置:

#!/usr/bin/env python3 # gen_conf.py import os import sys def gen_namesrv_conf(ip_list): content = f"""listenPort=9876 maxConnection=5000 kvConfigPath=/data/rocketmq/namesrv/kvconfig.json """ with open("conf/namesrv.conf", "w") as f: f.write(content) print(f"Generated namesrv.conf for {ip_list}") def gen_broker_conf(broker_name, broker_id, role, namesrv_addr, store_path): content = f"""brokerClusterName=DefaultCluster brokerName={broker_name} brokerId={broker_id} namesrvAddr={namesrv_addr} storePathRootDir={store_path} storePathCommitLog={store_path}/commitlog storePathConsumeQueue={store_path}/consumequeue storePathIndex={store_path}/index flushDiskType=ASYNC_FLUSH brokerRole={role} slaveReadEnable={'true' if role == 'SLAVE' else 'false'} maxMessageSize=4194304 """ filename = f"conf/{broker_name}-{role.lower()}.conf" with open(filename, "w") as f: f.write(content) print(f"Generated {filename}") if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python3 gen_conf.py <env:prod|dev>") sys.exit(1) env = sys.argv[1] if env == "prod": namesrv_ips = ["10.10.1.1", "10.10.1.2"] namesrv_addr = ";".join([f"{ip}:9876" for ip in namesrv_ips]) # Generate NameServer conf gen_namesrv_conf(namesrv_ips) # Generate Broker A Master gen_broker_conf("broker-a", 0, "ASYNC_MASTER", namesrv_addr, "/data/rocketmq/store-a") # Generate Broker A Slave gen_broker_conf("broker-a", 1, "SLAVE", namesrv_addr, "/data/rocketmq/store-a-slave") # Generate Broker B Master gen_broker_conf("broker-b", 0, "ASYNC_MASTER", namesrv_addr, "/data/rocketmq/store-b") # Generate Broker B Slave gen_broker_conf("broker-b", 1, "SLAVE", namesrv_addr, "/data/rocketmq/store-b-slave")

执行python3 gen_conf.py prod,就能一键生成所有5份配置文件。这个脚本的价值在于消除了人为失误,比如把brokerId写错、brokerRole拼错、namesrvAddr少写一个分号。在团队协作中,这个脚本就是配置的“唯一真相源”。

4.3 Topic与权限配置:让集群真正可用的第一步

集群启动后,它只是一个空壳。必须创建Topic,并赋予Producer/Consumer权限,才算真正可用。我们使用mqadmin创建一个名为ORDER_TOPIC的Topic,它将用于订单消息:

# 创建Topic,指定它在broker-a和broker-b上各分配4个Queue(共8个Queue) sh bin/mqadmin updateTopic -n "10.10.1.1:9876;10.10.1.2:9876" \ -c DefaultCluster \ -t ORDER_TOPIC \ -r 4 \ -w 4 \ -s true \ -o false # 参数解释: # -r 4: Read Queue数量(Consumer可并行消费的队列数) # -w 4: Write Queue数量(Producer可并行发送的队列数) # -s true: 是否允许自动创建(线上环境建议false,必须显式创建) # -o false: 是否是顺序Topic(普通Topic设为false)

接着,配置ACL(访问控制列表),这是5.3.0新增的安全特性。创建conf/plain_acl.yml:

# conf/plain_acl.yml global: whiteRemoteAddress: "" accounts: - accessKey: RocketMQAdmin secretKey: Admin@123456 admin: true defaultTopicPerm: DENY defaultGroupPerm: DENY topicPerms: - topic=ORDER_TOPIC&perm=SUB+PUB groupPerms: - group=order-consumer-group&perm=SUB - group=order-producer-group&perm=PUB

然后在broker.conf中启用ACL:

# 在 conf/broker-a-master.conf 和 conf/broker-b-master.conf 中添加 aclEnable=true

重启所有Broker后,Producer和Consumer就必须带上accessKey和secretKey才能连接。这杜绝了未授权的客户端随意往集群里发垃圾消息,是生产环境的必备防线。

4.4 监控与告警:用Prometheus+Grafana构建可视化运维视图

一个没有监控的集群,就像一辆没有仪表盘的汽车。我们基于RocketMQ 5.3.0内置的Metrics Exporter,搭建了一套完整的监控体系:

  1. 启用Broker Metrics:在每个Broker的conf/broker.conf中添加:

    metricsExporterType=prometheus metricsExporterAddr=0.0.0.0:5557

    这样,每个Broker就会在5557端口暴露Prometheus格式的指标。

  2. 部署Prometheus:配置prometheus.yml,抓取所有Broker和NameServer的指标:

    scrape_configs: - job_name: 'rocketmq-namesrv' static_configs: - targets: ['10.10.1.1:9876', '10.10.1.2:9876'] - job_name: 'rocketmq-broker' static_configs: - targets: ['10.10.1.3:5557', '10.10.1.4:5557', '10.10.1.5:5557', '10.10.1.6:5557']
  3. 导入Grafana Dashboard:使用社区维护的RocketMQ Dashboard(ID: 13222),它包含了关键指标:

    • rocketmq_broker_commitlog_disk_usage_percent:CommitLog磁盘使用率,超过85%就要告警扩容。
    • rocketmq_broker_put_tps:写入TPS,持续低于阈值(如1000)可能意味着Producer异常。
    • rocketmq_broker_get_min_offset与rocketmq_broker_get_max_offset的差值:即消息堆积量,差值超过100万就要触发告警。

这套监控让我们能在问题发生前就介入。比如,当rocketmq_broker_commitlog_disk_usage_percent曲线开始陡峭上升,我们就知道某个Topic的Consumer消费太慢,需要去查它的日志;当rocketmq_broker_ha_slave_diff(主从数据差)持续大于1000,就说明某个Slave同步滞后,需要检查网络或磁盘IO。

5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”

5.1 问题一:Broker启动后,在clusterList里看不到自己,或者状态是NOT_ONLINE

现象:sh bin/mqadmin clusterList -n "ns1:9876;ns2:9876"输出里,只有NameServer,没有Broker,或者Broker状态是NOT_ONLINE。

排查思路:这是网络连通性问题的典型表现。不要急着看日志,先做三件事:

  1. Ping测试:在Broker机器上,ping -c 3 10.10.1.1和ping -c 3 10.10.1.2,确认能通。
  2. Telnet测试:telnet 10.10.1.1 9876,如果超时,说明NameServer端口没开或防火墙拦截。
  3. 检查Broker日志:grep "register broker to name server" logs/broker-a-master.log,如果找不到这条日志,说明注册请求根本没发出去。

根本原因与解决:我们遇到过两次。第一次是云厂商的安全组规则没放行9876端口;第二次是Broker机器的/etc/hosts文件里,把127.0.0.1映射到了一个错误的hostname,导致Broker用这个hostname去注册,而NameServer解析不到。解决方案是:在broker.conf里显式指定brokerIP1=10.10.1.3(Broker本机真实IP),绕过hostname解析。

5.2 问题二:Slave日志里反复出现HAConnection is closed,无法同步数据

现象:Slave启动后,日志里不断打印HAConnection is closed,Sync from master successfully只出现一次,之后就再没同步记录。

排查思路:这是HA同步链路断开。重点检查:

  • Master的broker.conf里brokerRole=ASYNC_MASTER是否正确。
  • Slave的broker.conf里brokerRole=SLAVE是否正确,且brokerId是否与Master同组且不为0。
  • Master和Slave的storePathRootDir是否指向了同一块磁盘?绝对不能!它们必须是完全独立的路径,否则会互相覆盖。

根本原因与解决:我们曾因Ansible脚本的一个变量错误,导致broker-a-slave的storePathRootDir被错误地指向了/data/rocketmq/store-a(即Master的路径)。结果Slave启动时,发现CommitLog目录里已经有文件,就认为自己是Master,拒绝作为Slave启动。解决方案是:彻底删除Slave的存储目录,重新创建,并确保broker.conf里的路径指向正确的、空的目录。

5.3 问题三:Producer发送消息超时,报错RemotingTimeoutException

现象:Producer代码里producer.send(msg)抛出org.apache.rocketmq.remoting.exception.RemotingTimeoutException。

排查思路:这个错误很宽泛,需要层层过滤:

  1. 检查NameServer:sh bin/mqadmin clusterList是否能看到所有Broker?如果看不到,回到问题一。
  2. 检查Topic路由:sh bin/mqadmin topicRoute -n "ns1:9876;ns2:9876" -t YOUR_TOPIC,确认输出里有queueDatas,且brokerName字段是broker-a或broker-b,而不是空。
  3. 检查Broker状态:sh bin/mqadmin brokerStats -n "ns1:9876;ns2:9876" -b broker-a,查看putTps是否为0?如果是,说明Broker没在接收消息。

根本原因与解决:最常见的原因是Producer代码里namesrvAddr配置错了,比如写成了localhost:9876,而Producer运行在另一台机器上。另一个隐蔽的原因是:Broker的brokerIP1配置的是内网IP,但Producer运行在公网环境,无法直连。解决方案是:在Broker的broker.conf里,用brokerIP0指定一个Producer能访问到的IP(比如ECS的弹性公网IP),并确保该IP的10911端口已放行。

5.4 问题四:Consumer消费速度极慢,消息堆积如山

现象:sh bin/mqadmin topicStatus -n "ns1:9876;ns2:9876" -t ORDER_TOPIC显示Diff Total(堆积量)高达数百万。

排查思路:消费慢是系统性问题,要从Consumer、Broker、网络三方面看:

  • Consumer端:检查Consumer代码里consumer.subscribe("ORDER_TOPIC", "*")是否正确;MessageListenerConcurrently的consumeMessage方法里是否有耗时操作(如同步HTTP调用);consumeThreadMin和consumeThreadMax线程数是否足够(默认20,对于高吞吐场景可能不够)。
  • Broker端:sh bin/mqadmin brokerStats -n "ns1:9876;ns2:9876" -b broker-a,看getTps(读取TPS)是否远低于putTps(写入TPS),如果是,说明Broker读取慢。
  • 网络端:iftop -P 10911,看Consumer机器到Broker的网络带宽是否被打满。

根本原因与解决:我们遇到过一次,是因为Consumer的consumeMessage方法里,调用了一个外部API,而那个API的平均响应时间是800ms。这意味着一个Consumer线程一秒只能处理1条消息,而Producer一秒发1000条,堆积必然产生。解决方案是:将外部调用改为异步(如发到本地队列,由另一个线程池处理),并将consumeThreadMax调大到100。调整后,消费TPS立刻从1提升到1200。

5.5 问题五:集群运行一周后,NameServer内存暴涨,OOM Killer杀死了进程

现象:top命令看到java进程RSS内存持续增长,最终被系统OOM Killer杀死。

排查思路:NameServer内存泄漏是5.3.0早期版本的一个已知问题(已在5.3.1修复)。但即使在5.3.0,也有规避方法:

  • 检查kvConfigPath:确认它指向的磁盘是否有足够空间?如果满了,NameServer会不断重试写入,导致内存泄漏。
  • 检查Client连接数:netstat -an | grep :9876 | wc -l,如果连接数超过5000,说明有Client没有正确关闭连接(如Producer/Consumer没调用shutdown())。

根本原因与解决:我们发现是某个测试部门的Demo程序,每次启动都创建一个新的Producer,但从未调用shutdown(),导致连接句柄一直累积。解决方案是:在NameServer的JVM参数里加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/rocketmq/logs/heap.hprof,然后用jmap分析dump文件,定位泄漏源头;同时,在所有Client代码里强制加入`

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询