深度解析RocketMQ Topic的創建機制

我還記得第一次使用rocketmq的時候,需要去控制檯預先創建topic,我當時就想爲什麼要這麼設計,於是我決定擼一波源碼,帶大家從根源上吃透rocketmq topic的創建機制。

topic在rocketmq的設計思想裏,是作爲同一個業務邏輯消息的組織形式,它僅僅是一個邏輯上的概念,而在一個topic下又包含若干個邏輯隊列,即消息隊列,消息內容實際是存放在隊列中,而隊列又存儲在broker中,下面我用一張圖來說明topic的存儲模型:

深度解析RocketMQ Topic的創建機制

其實rocketmq中存在兩種不同的topic創建方式,一種是我剛剛說的預先創建,另一種是自動創建,下面我開車帶大家從源碼的角度來詳細地解讀這兩種創建機制。

自動創建

默認情況下,topic不用手動創建,當producer進行消息發送時,會從nameserver拉取topic的路由信息,如果topic的路由信息不存在,那麼會默認拉取broker啓動時默認創建好名爲“TBW102”的Topic:

org.apache.rocketmq.common.MixAll:

// Will be created at broker when isAutoCreateTopicEnable
public static final String AUTO_CREATE_TOPIC_KEY_TOPIC = "TBW102";

自動創建的開關配置在BrokerConfig中,通過autoCreateTopicEnable字段進行控制,

org.apache.rocketmq.common.BrokerConfig:

@ImportantField
private boolean autoCreateTopicEnable = true;

在broker啓動時,會調用TopicConfigManager的構造方法,autoCreateTopicEnable打開後,會將“TBW102”保存到topicConfigTable中:

org.apache.rocketmq.broker.topic.TopicConfigManager#TopicConfigManager:

// MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC
if (this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) {
    String topic = MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC;
    TopicConfig topicConfig = new TopicConfig(topic);
    this.systemTopicList.add(topic);
    topicConfig.setReadQueueNums(this.brokerController.getBrokerConfig()
                                 .getDefaultTopicQueueNums());
    topicConfig.setWriteQueueNums(this.brokerController.getBrokerConfig()
                                  .getDefaultTopicQueueNums());
    int perm = PermName.PERM_INHERIT | PermName.PERM_READ | PermName.PERM_WRITE;
    topicConfig.setPerm(perm);
    this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
}

broker會通過發送心跳包將topicConfigTable的topic信息發送給nameserver,nameserver將topic信息註冊到RouteInfoManager中。

繼續看消息發送時是如何從nameserver獲取topic的路由信息:

org.apache.rocketmq.client.impl.producer.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());
    // 生產者第一次發送消息,topic在nameserver中並不存在
    this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
    topicPublishInfo = this.topicPublishInfoTable.get(topic);
  }

  if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
    return topicPublishInfo;
  } else {
    // 第二次請求會將isDefault=true,開啓默認“TBW102”從namerserver獲取路由信息
    this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
    topicPublishInfo = this.topicPublishInfoTable.get(topic);
    return topicPublishInfo;
  }
}

如上方法,topic首次發送消息,此時並不能從namserver獲取topic的路由信息,那麼接下來會進行第二次請求namserver,這時會將isDefault=true,開啓默認“TBW102”從namerserver獲取路由信息,此時的“TBW102”topic已經被broker默認註冊到nameserver了:

org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer:

if (isDefault && defaultMQProducer != null) {
  // 使用默認的“TBW102”topic獲取路由信息
  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);
    }
  }
}

如果isDefault=true並且defaultMQProducer不爲空,從nameserver中獲取默認路由信息,此時會獲取所有已開啓自動創建開關的broker的默認“TBW102”topic路由信息,並保存默認的topic消息隊列數量。

org.apache.rocketmq.client.impl.factory.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);
}

從本地緩存中取出topic的路由信息,由於topic是第一次發送消息,這時本地並沒有該topic的路由信息,所以對比該topic路由信息對比“TBW102”時changed爲true,即有變化,進入以下邏輯:

org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer:

// Update sub info
{
  Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
  Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
  while (it.hasNext()) {
    Entry<String, MQConsumerInner> entry = it.next();
    MQConsumerInner impl = entry.getValue();
    if (impl != null) {
      impl.updateTopicSubscribeInfo(topic, subscribeInfo);
    }
  }
}

將“TBW102”topic路由信息構建TopicPublishInfo,並將用topic爲key,TopicPublishInfo爲value更新本地緩存,到這裏就明白了,原來broker們千辛萬苦創建“TBW102”topic並將其路由信息註冊到nameserver,被新來的topic獲取後立即用“TBW102”topic的路由信息構建出一個TopicPublishInfo並且據爲己有,由於TopicPublishInfo的路由信息時默認“TBW102”topic,因此真正要發送消息的topic也會被負載發送到“TBW102”topic所在的broker中,這裏我們可以將其稱之爲偷樑換柱的做法。

當broker接收到消息後,會在msgCheck方法中調用createTopicInSendMessageMethod方法,將topic的信息塞進topicConfigTable緩存中,並且broker會定時發送心跳將topicConfigTable發送給nameserver進行註冊。

自動創建與消息發送時獲取topic信息的時序圖:

深度解析RocketMQ Topic的創建機制

 

預先創建

其實這個叫預先創建似乎更加適合,即預先在broker中創建好topic的相關信息並註冊到nameserver中,然後client端發送消息時直接從nameserver中獲取topic的路由信息,但是手動創建從動作上來將更加形象通俗易懂,直接告訴你,你的topic信息需要在控制檯上自己手動創建。

預先創建需要通過mqadmin提供的topic相關命令進行創建,執行:

./mqadmin updateTopic

官方給出的各項參數如下:

usage: mqadmin updateTopic [-b <arg>] [-c <arg>] [-h] [-n <arg>] [-o <arg>] [-p <arg>] [-r <arg>] [-s <arg>]
-t <arg> [-u <arg>] [-w <arg>]
-b,--brokerAddr <arg>       create topic to which broker
-c,--clusterName <arg>      create topic to which cluster
-h,--help                   Print help
-n,--namesrvAddr <arg>      Name server address list, eg: 192.168.0.1:9876;192.168.0.2:9876
-o,--order <arg>            set topic's order(true|false
-p,--perm <arg>             set topic's permission(2|4|6), intro[2:W 4:R; 6:RW]
-r,--readQueueNums <arg>    set read queue nums
-s,--hasUnitSub <arg>       has unit sub (true|false
-t,--topic <arg>            topic name
-u,--unit <arg>             is unit topic (true|false
-w,--writeQueueNums <arg>   set write queue nums

我們直接定位到其實現類執行命令的方法:

通過broker模式創建:

org.apache.rocketmq.tools.command.topic.UpdateTopicSubCommand#execute:

// -b,--brokerAddr <arg>   create topic to which broker
if (commandLine.hasOption('b')) {
  String addr = commandLine.getOptionValue('b').trim();
  defaultMQAdminExt.start();
  defaultMQAdminExt.createAndUpdateTopicConfig(addr, topicConfig);
  return;
}

從commandLine命令行工具獲取運行時-b參數重的broker的地址,defaultMQAdminExt是默認的rocketmq控制檯執行的API,此時調用start方法,該方法創建了一個mqClientInstance,它封裝了netty通信的細節,接着就是最重要的一步,調用createAndUpdateTopicConfig將topic配置信息發送到指定的broker上,完成topic的創建。

通過集羣模式創建:

org.apache.rocketmq.tools.command.topic.UpdateTopicSubCommand#execute:

// -c,--clusterName <arg>   create topic to which cluster
else if (commandLine.hasOption('c')) {
  String clusterName = commandLine.getOptionValue('c').trim();
  defaultMQAdminExt.start();
  Set<String> masterSet =
    CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName);
  for (String addr : masterSet) {
    defaultMQAdminExt.createAndUpdateTopicConfig(addr, topicConfig);
    System.out.printf("create topic to %s success.%n", addr);
  }
  return;
}

通過集羣模式創建與通過broker模式創建的邏輯大致相同,多了根據集羣從nameserver獲取集羣下所有broker的master地址這個步驟,然後在循環發送topic信息到集羣中的每個broker中,這個邏輯跟指定單個broker是一致的。

這也說明了當用集羣模式去創建topic時,集羣裏面每個broker的queue的數量相同,當用單個broker模式去創建topic時,每個broker的queue數量可以不一致。

高質量編程視頻shangyepingtai.xin

預先創建時序圖:

深度解析RocketMQ Topic的創建機制

 

何時需要預先創建Topic?

建議線下開啓,線上關閉,不是我說的,是官方給出的建議:

深度解析RocketMQ Topic的創建機制

rocketmq爲什麼要這麼設計呢?經過一波源碼深度解析後,我得到了我想要的答案:

根據上面的源碼分析,我們得出,rocketmq在發送消息時,會先去獲取topic的路由信息,如果topic是第一次發送消息,由於nameserver沒有topic的路由信息,所以會再次以“TBW102”這個默認topic獲取路由信息,假設broker都開啓了自動創建開關,那麼此時會獲取所有broker的路由信息,消息的發送會根據負載算法選擇其中一臺Broker發送消息,消息到達broker後,發現本地沒有該topic,會在創建該topic的信息塞進本地緩存中,同時會將topic路由信息註冊到nameserver中,那麼這樣就會造成一個後果:以後所有該topic的消息,都將發送到這臺broker上,如果該topic消息量非常大,會造成某個broker上負載過大,這樣的消息存儲就達不到負載均衡的效果了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章