记一次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 问题
-
producer发送端存在问题,根据defaultTopicQueueNums=4的值hash的话,一般只会发送到一个broker。
-
如果某个topic存在于两个broker,消费端负载有问题,两个broker,8个队列,如果是两个consumer的话,那么有一台consumer是会订阅到没有消息的broker中。因为发送消息只往一个broker的4个队列发送。
4.2 解决方案
1)修改producer消息发送方式,根据路由列表的数量hash。
3)线上禁用自动创建。在集群下的topic自动创建,可能只创建在某个broker上的情况,而且队列数量不一定是符合预期的。
参考资料
-
深度解析RocketMQ Topic的创建机制:objcoding.com/2019/03/31/...
-
RocketMQ 源码 4.5.1