RocketMQ 同一 JVM 启动多个 Producer 导致 NameServer 只连一个的问题

RocketMQ 同一 JVM 启动多个 Producer 导致 NameServer 只连一个的问题

Hi,我是阿昌,今天记录一个容器化迁移 RocketMQ 时踩到的坑:

同一个 JVM 里同时存在两个 NameServer 配置,结果只改了某个 Producer 的推送地址,消息还是发到了旧集群。


一、问题现象

迁移 MQ 时,先把消费方迁移到了容器里,接着开始测试生产者推消息。

当时只改了某个生产者的推送地址,项目里随之出现了两个 NAME_SERVER

奇怪的地方在于:

  • 第一次启动时,推送消息是成功的;
  • 第二次启动后,消息推送成功了,但消费方一直消费不到;
  • 登录 MQ 后台才发现,新建的 Topic 跑到老 MQ 地址上去了。

也就是说:

消息确实发出去了,但发错了地方。


二、问题定位

初步判断是生产方出问题了:明明改了 name_server,为什么还会推到老地址?

顺着代码往下看,最终定位到获取 MQ 客户端工厂的地方。

核心结论是:

两个 Producer 的 IP 是一样的,而且都没有设置 instanceName

因为 IP 相同,instanceName 又没显式设置,所以这两个 Producer 最终会复用同一个 MQ 客户端实例。

流程可以简单画成这样:

text 复制代码
Producer A -> NameServer 新地址
Producer B -> NameServer 老地址
        ↓
两个 Producer 计算出的 ClientId 相同
        ↓
MQClientManager 返回同一个 MQClientInstance
        ↓
同一个客户端实例只维护了其中一个 NameServer 的连接
        ↓
消息自然就发到了那个唯一连上的集群

所以不是配置丢了,而是两个 Producer 在底层共用了一个客户端实例。


三、为什么多个 Producer 会共用一个客户端实例

RocketMQ 获取客户端实例的逻辑大致如下:

java 复制代码
public MQClientInstance getOrCreateMQClientFactory(ClientConfig clientConfig) {
    String clientId = clientConfig.buildMQClientId();
    MQClientInstance instance = factoryTable.get(clientId);
    if (instance == null) {
        instance = new MQClientInstance(clientConfig);
        factoryTable.put(clientId, instance);
    }
    return instance;
}

关键就在 clientId 上。

clientId 的生成逻辑简化后大概是这样:

text 复制代码
clientId = clientIP + "@" + instanceName + "@" + unitName

默认情况下如果不设置 instanceName,RocketMQ 会使用类似 ip@pid 作为唯一标识。

那么问题就来了:

  • 同一个 JVM,IP 相同;
  • 多个 Producer 都没有设置 instanceName
  • 计算出来的 clientId 一样;
  • MQClientManager 根据这个相同的 clientId,返回了同一个 MQClientInstance
  • 多个 Producer 最终只共享一个与 MQ 交互的客户端实例。

所以虽然两个 Producer 配了不同的 NameServer 地址,但底层只有一个客户端实例真正生效,最终只会往其中一个集群发消息。


四、解决方案

1、Producer

默认情况下不需要设置 instanceName,RocketMQ 会使用类似 ip@pid 作为唯一标识。

如果同一个 JVM 中,不同的 Producer 需要往不同的 RocketMQ 集群发送消息,就需要设置不同的 instanceName

否则多个 Producer 内部只会有一个 MQClientInstance,最终只往一个集群发消息。

示例写法:

java 复制代码
DefaultMQProducer producerA = new DefaultMQProducer("producerGroupA");
producerA.setNamesrvAddr("new-nameserver:9876");
producerA.setInstanceName("producer-a");

DefaultMQProducer producerB = new DefaultMQProducer("producerGroupB");
producerB.setNamesrvAddr("old-nameserver:9876");
producerB.setInstanceName("producer-b");

2、Consumer

Consumer 的情况也一样。

默认情况下不需要设置 instanceName,RocketMQ 会使用类似 ip@pid 作为标识。

如果显式设置了 instanceName,RocketMQ 会使用 ip@instanceName 作为 Consumer 的唯一标识。

需要注意:

  • 同一 IP 下的不同 JVM 实例,不能使用同一个 instanceName
  • 不同 IP 之间,可以使用相同的 instanceName

五、最终建议

这次迁移问题的最终处理方式是:

name_server 统一改成新地址。

也就是说,不建议在这种场景下通过增加 instanceName 来解决问题。

原因是:

  • 多个客户端实例意味着多套网络连接、多组线程资源;
  • 会增加线程数,占用更多内存;
  • 还容易引入其他难以排查的问题。

对于"迁移 MQ 集群"这种场景,统一切到新 NameServer 比维护多个客户端实例更干净。


六、参数说明

参数名 默认值 说明
instanceName DEFAULT 客户端实例名称。客户端创建的多个 Producer、Consumer 实际可能共用一个内部实例,这个实例包含网络连接、线程资源等。

七、总结

这个问题本质上不是 NameServer 配置没生效,而是:

同一 JVM 下,多个 Producer 因为 ClientId 相同,底层共用了同一个 MQClientInstance

短期解法

  • 如果确实要一个 JVM 连多个 MQ 集群,就为不同 Producer/Consumer 设置不同的 instanceName

长期解法

  • 如果只是集群迁移,直接统一所有 name_server 到新地址,避免维护多套客户端资源。

排查这类问题时,别只看业务代码里的 setNamesrvAddr,还要看到底层客户端实例是否被复用了。

以上就是这次 RocketMQ 同一 JVM 启动多个 Producer 导致 NameServer 只连一个的问题记录,感谢观看。

相关推荐
2602_959960922 小时前
大厂Java面试:Spring Boot、JVM、Redis、Kafka与Elasticsearch在内容社区UGC场景下的实战拷问
java·jvm·spring boot·redis·面试题
典典分享指南19 小时前
用指纹算法给文章去重:mid_signature 实战
jvm·maven·intellij-idea·jetty
xiaohua100921 小时前
Linux-JVM线程查询
linux·jvm
Super 含21 小时前
Android 启动优化(三):Perfetto 实战——启动时间到底花在哪里?
jvm·算法
会编程的吕洞宾1 天前
Full GC频繁接口卡顿?JVM排查到调优实战
jvm
不会就选b1 天前
Linux之线程进阶(一)
jvm
lazy H1 天前
不同的消息队列有什么区别?Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 选型对比
后端·中间件·kafka·rabbitmq·rocketmq
互联网中的一颗神经元1 天前
01. Go 内存管理全景架构
java·jvm·golang
互联网中的一颗神经元1 天前
02. 核心概念与术语表
java·jvm·spring