记一次RokcetMQ Topic自动创建问题

记一次RokcetMQ Topic自动创建问题

1. 问题背景

线上环境中,RocketMQ使用了两个Master,其中broker为broker-a、broker-b。观察某些topic分布,发现几种情况,且topic的队列数都是为4:

  • Topic存在broker-a、broker-b
  • Topic存在broker-a中。
  • Topic只存在broker-b中。

线上发送消息根据Producer设置的defaultTopicQueueNums=4,然后根据某个特征对defaultTopicQueueNums的值进行hash。

其原因和Topic的创建方式、发送消息选择的队列有关。因此分析一下Topic的创建。

2. Topic 创建方式

Topic创建分为:手动创建和自动创建

2.1 自动创建

默认情况,topic不用自动创建,当producer发送消息时,会从nameserver拉取topic的路由信息,如果topic路由信息不存在,那么会默认拉取broker启动时默认创建的名为TBW102的topic.

在broker启动的时候,会调用TopicConfigManager的构造方法,会将TBW102保存到topicConfigTable中。broker会通过心跳包将topicConfigTable的topic信息发送到nameserver中,nameserver将topic信息注册到RouteInfoManager中。

java 复制代码
//TopicConfigManager
public TopicConfigManager(BrokerController brokerController) {
...
{
    // MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC
    //默认:true,可配置
    if (this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) {
    //固定:TBW102
        String topic = MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC;
        TopicConfig topicConfig = new TopicConfig(topic);
        this.systemTopicList.add(topic);
  //默认:8,可配置     topicConfig.setReadQueueNums(this.brokerController.getBrokerConfig().getDefaultTopicQueueNums());
     //默认:8,可配置     topicConfig.setWriteQueueNums(this.brokerController.getBrokerConfig().getDefaultTopicQueueNums());
        //固定:1 + 2 + 4 = 7
        int perm = PermName.PERM_INHERIT | PermName.PERM_READ | PermName.PERM_WRITE;
        topicConfig.setPerm(perm);
        this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
    }
}
...

}

查看消息发送是如何从nameserver获取topic路由信息。以异步发送指定队列的消息为例,会调用以下方法。

java 复制代码
//DefaultMQProducerImpl#sendSelectImpl
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());

以下方法中,topic第一次发送消息,此时并不能从nameserver中获取到topic的路由信息,那么就会进行第二次请求nameserver,isDefault=true,开启默认TBW102,从namerserver中获取TBW102的路由信息,此时TBW102的topic信息已经被broker默认注册到nameserver中了。

java 复制代码
//DefaultMQProducerImpl#tryToFindTopicPublishInfo
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        //producer第一次发消息,尝试从nameserver中获取,但是并不存在
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }

    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        //所以会在这里进行第二次尝试,isDefault=true
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

以下方法,如果isDefalut=true,并且defaultMQProducer不空,那么从nameserver中获取TBW102的Topic的路由信息,并设置队列数。TBW102的Topic默认的读写队列数是8,但是项目中的Producer设置的默认的队列数是4,取两者的最小数,那么就是4,设置到QueueData中。

java 复制代码
//MQClientInstance#updateTopicRouteInfoFromNameServer
public boolean updateTopicRouteInfoFromNameServer() {
    if (isDefault && defaultMQProducer != null) {
        //getCreateTopicKey(),返回的是TBW102
        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(), 1000 * 3);
        if (topicRouteData != null) {
            for (QueueData data: topicRouteData.getQueueDatas()) {
                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                data.setReadQueueNums(queueNums);
                data.setWriteQueueNums(queueNums);
            }
        }
    }
}

以下代码中,会判断producer对这个topic的信息是否有变化,由于topic是第一次发送消息,这时本地并没有该topic的路由信息(只是默认空对象信息),所以对比该topic路由信息对比TBW102时changed为true,即有变化。

java 复制代码
//MQClientInstance#updateTopicRouteInfoFromNameServer
TopicRouteData old = this.topicRouteTable.get(topic);
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
    changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
    log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
}

当changed=true,会将topic为TBW102路由信息,构造TopicPublishInfo对象,并且更新Producer对该topic(实际的topic)的路由信息(本地缓存)。

java 复制代码
//MQClientInstance#updateTopicRouteInfoFromNameServer
// Update Pub info
{
    TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
    publishInfo.setHaveTopicRouterInfo(true);
    Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, MQProducerInner> entry = it.next();
        MQProducerInner impl = entry.getValue();
        if (impl != null) {
            impl.updateTopicPublishInfo(topic, publishInfo);
        }
    }
}

到这里我们可以看出一些倪端:broker创建的TBW102的topic并将其路由注册到nameserver,被即将要新创建的topic获取后,用了TBW102的topic的路由信息,并且构造成TopicPublishInfo,当成新创建Topic的路由信息。因为用的是TBW102的topic的路由信息,所以消息发送也会负载到TBW102的topic所在的broker中

至此新创建topic的路由信息已经在该Producer中存在了,但是还没有实际注册到nameserver中,那么是在哪里处理的呢?在broker接收到消息后,会进行相关的逻辑处理。

在到broker处理之前,先看一下发送消息是会带的几个参数,后面会用到。

java 复制代码
//DefaultMQProducerImpl#sendKernelImpl
//实际topic名称
requestHeader.setTopic(msg.getTopic());
//TBW102
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
//默认为:4
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
requestHeader.setQueueId(mq.getQueueId());

消息的发会在SendMessageProcessor类中处理。在sendMessage中,会调用super.msgCheck方法。会判断是否有些权限,以及topic的名称不能为TBW102,因为个已经被默认使用了。如果没有权限或Topic非法直接报错。

java 复制代码
//AbstractSendMessageProcessor#msgCheck
if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())
    && this.brokerController.getTopicConfigManager().isOrderTopic(requestHeader.getTopic())) {
    response.setCode(ResponseCode.NO_PERMISSION);
    response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
        + "] sending message is forbidden");
    return response;
}
if (!this.brokerController.getTopicConfigManager().isTopicCanSendMessage(requestHeader.getTopic())) {
    String errorMsg = "the topic[" + requestHeader.getTopic() + "] is conflict with system reserved words.";
    log.warn(errorMsg);
    response.setCode(ResponseCode.SYSTEM_ERROR);
    response.setRemark(errorMsg);
    return response;
}

首次发送消息,broker中是没有这个topic的路由信息的,所以会进入createTopicInSendMessageMethod方法

java 复制代码
//AbstractSendMessageProcessor#msgCheck
TopicConfig topicConfig =
    this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
if (null == topicConfig) {
    log.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress());
            topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
                requestHeader.getTopic(),
                requestHeader.getDefaultTopic(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                requestHeader.getDefaultTopicQueueNums(), topicSysFlag);
}

在createTopicInSendMessageMethod中:会根据去找defaultTopic的路由信息,如果开启了自动创建Topic配置,那么默认找到的就是TBW102的topic。而TBW102的topic默认是有PERM_INHERIT权限的,PermName.isInherited方法为true。queueNums的数量取决于写队列和默认生产者队列数量中取较小值,会取较小的是clientDefaultTopicQueueNums=4(发送消息是带了这个字段),最后新队列的权限设置为6(读 + 写)。

java 复制代码
//TopicConfigManager#createTopicInSendMessageMethod
TopicConfig defaultTopicConfig = this.topicConfigTable.get(defaultTopic);
if (defaultTopicConfig != null) {
    //TBW102
    if (defaultTopic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
        //如果没有开启自动创建,权限设置为6
        if (!this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) {
            defaultTopicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE);
        }
    }

    if (PermName.isInherited(defaultTopicConfig.getPerm())) {
        topicConfig = new TopicConfig(topic);

        int queueNums =
            clientDefaultTopicQueueNums > defaultTopicConfig.getWriteQueueNums() ? defaultTopicConfig
                .getWriteQueueNums() : clientDefaultTopicQueueNums;

        if (queueNums < 0) {
            queueNums = 0;
        }

        topicConfig.setReadQueueNums(queueNums);
        topicConfig.setWriteQueueNums(queueNums);
        int perm = defaultTopicConfig.getPerm();
        perm &= ~PermName.PERM_INHERIT;
        topicConfig.setPerm(perm);
        topicConfig.setTopicSysFlag(topicSysFlag);
        topicConfig.setTopicFilterType(defaultTopicConfig.getTopicFilterType());
    } else {
        log.warn("Create new topic failed, because the default topic[{}] has no perm [{}] producer:[{}]",
            defaultTopic, defaultTopicConfig.getPerm(), remoteAddress);
    }
}

最后,如果topic创建成功,那么会加入到broker的topic配置中,并且进行持久化(写到文件里)。createNew=true,把topic的路由信息上报到nameserver上

java 复制代码
//TopicConfigManager#createTopicInSendMessageMethod
if (topicConfig != null) {
    log.info("Create new topic by default topic:[{}] config:[{}] producer:[{}]", defaultTopic, topicConfig, remoteAddress);

    this.topicConfigTable.put(topic, topicConfig);

    this.dataVersion.nextVersion();

    createNew = true;

    this.persist();
}

if (createNew) {
    this.brokerController.registerBrokerAll(false, true,true);
}

2.2 手动创建

在broker中创建好topic的相关信息并注册到nameserver中,然后Producer发送消息时,直接从nameserver中获取topic的路由信息。手动创建的命令:./mqadmin updateTopic,几个必填参数解释一下:

shell 复制代码
-n   //nameserver地址。
-b   //-b或-c必填一个,broker地址,表示主题只在指定broker上创建
-c   //-b或-c必填一个,broker集群名称,依次从nameserver根据集群民称获取集群下所有Master
-t   //主题名称

如果使用集群名称去创建topic时,集群里面每个broker的queue的数量相同,当用单个broker模式去创建topic时,每个broker的queue数量可以不用一致。(4.5.1版本,默认创建的读写队列为8)

2.3 小结

在RocketMQ开发指南中,针对 autoCreateTopicEnable配置的说明中,指出建议线下开启,线上关闭

通过上述源码分析,可以得到,rocketmq在发送消息时,会先去获取topic的路由信息,如果topic是第一次发送消息,由于nameserver没有topic的路由信息,所以会以默认的名为TBW102的topic获取路由信息。假设broker都开启自动创建开关,那么此时会获取所有broker的路由信息,消息发送会根据负载算法选择其中一台broker发送。消息到broker后,发现本地没有该topic,会在创建该topic的信息缓存本地且持久化,同时会将topic信息注册到nameserver中。这样可能会导致的情况是:以后所有该topic的消息,都将发送到这台broker上。

3. 现象解释

线上环境已经无法追踪历史原因了。并且根据produer的defaultTopicQueueNums=4进行hash的话,按理只会到其中的一台broker。

3.1 topic只在一个broker

即之前分析的,因为自动创建,消息只会发送到一个broker上,导致只有一个broker创建了topic并且上报到nameserver,后续Producer再根据topic从nameserver找路由已经能找到了,不会在另外一个broker上创建了。

3.2 topic在两个broker

在并发的情况下,可能会对不同的broker同时发送消息,broker内部同时创建。

4. 其他问题及解决方案

4.1 问题

  1. producer发送端存在问题,根据defaultTopicQueueNums=4的值hash的话,一般只会发送到一个broker。

  2. 如果某个topic存在于两个broker,消费端负载有问题,两个broker,8个队列,如果是两个consumer的话,那么有一台consumer是会订阅到没有消息的broker中。因为发送消息只往一个broker的4个队列发送。

4.2 解决方案

1)修改producer消息发送方式,根据路由列表的数量hash。

3)线上禁用自动创建。在集群下的topic自动创建,可能只创建在某个broker上的情况,而且队列数量不一定是符合预期的。

参考资料

  1. 深度解析RocketMQ Topic的创建机制:objcoding.com/2019/03/31/...

  2. RocketMQ 源码 4.5.1

相关推荐
神仙别闹4 分钟前
基于C#实现的(WinForm)模拟操作系统文件管理系统
java·git·ffmpeg
小爬虫程序猿5 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
程序猿-瑞瑞7 分钟前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
组合缺一10 分钟前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·后端·spring·solon
程序猿零零漆12 分钟前
SpringCloud 系列教程:微服务的未来(二)Mybatis-Plus的条件构造器、自定义SQL、Service接口基本用法
java·spring cloud·mybatis-plus
dzend13 分钟前
Kafka、RocketMQ、RabbitMQ 对比
kafka·rabbitmq·rocketmq
猿来入此小猿14 分钟前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码27 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
带多刺的玫瑰28 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
栗豆包44 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven