Topic
Topic用于标识一些消息的分类,例如订单消息,通知消息。RocketMQ Producer发送消息,Consumer接收消息,Topic都是绕不过去的话题,消息就是围绕Topic组织的。Topic存储在NameSrv,Producer从NameSrv获取Topic的路由信息,找到broker,然后发送消息至broker。Consumer同样从NameSrv获取Topic路由信息,找到broker,然后从broker拉取消息,进行消费。
既然所有的topic路由信息是存在NameSrv端的,那么创建Topic,是不是就直接向NameSrv发送请求呢?
因为客户端只随机与NameSrv的其中一个建立长连接(很好理解吧?),如果直接在NameSrv注册路由信息的话,那么NameSrv势必需要与其它的NameSrv保持通讯,这样才能使得所有的结点的路由信息保持完整和一致。但官方文档上面明确表示,NameSrv是无状态的,结点之间无任何信息同步,如下图所示:
事情似乎没有想的那么简单。。。
从创建Topic命令说起
rocketmq bin目录下有mqadmin工具,创建topic命令如下所示:
./mqadmin updateTopic -n 192.168.77.129:9876 -c DefaultCluster -t TestTopic
这个命令很有迷惑性,通过-n参数指定了NameSrv地址,很容易以为真的就是直接向NameSrv注册Topic路由信息。
mqadmin工具的实现是在rocketmq-tools工程实现的,具体到updateTopic这个命令是UpdateTopicSubCommand这个类实现的。
在解析-c参数的时候,拿到broker集群名称,然后通过CommandUtil.fetchMasterAddrByClusterName() 获取broker集群下所有master的brokerName。然后调用defaultMQAdminExt.createAndUpdateTopicConfig() 向集群中的所有master结点注册topic。
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);
}
// 省略无关代码
System.out.printf("%s", topicConfig);
return;
}
TopicConfig
客户端向Broker提交的Topic信息封装在了TopicConfig里面,TopicConfig类如下所示:
public class TopicConfig {
private static final String SEPARATOR = " ";
public static int defaultReadQueueNums = 16; // 默认读队列数
public static int defaultWriteQueueNums = 16; // 默认写队列数
private String topicName; // topic名称
private int readQueueNums = defaultReadQueueNums;
private int writeQueueNums = defaultWriteQueueNums;
private int perm = PermName.PERM_READ | PermName.PERM_WRITE; // 操作权限,这里初始化是可读可写
private TopicFilterType topicFilterType = TopicFilterType.SINGLE_TAG; // topic过滤类型,默认单tag
private int topicSysFlag = 0; //
private boolean order = false; //
public TopicConfig() {
}
)
TopicConfig保存了Topic名称,读写队列数,读写权限等信息。
Broker拿到Topic路由信息后干啥了
RocketMQ的通讯是基于Netty的,对其进行了简单的封装,所有的通讯操作都是根据RquestCode来区分的,后面会单独讲mq的通讯。这里创建Topic的RequestCode是:
public static final int UPDATE_AND_CREATE_TOPIC = 17;
客户端的创建Topic的调用在这里:
根据UPDATE_AND_CREATE_TOPIC搜索代码,我们可以在broker工程里找到处理Topic请求的地方:
updateAndCreateTopic方法主要做了两件事,1-更新本地的topic配置,2-向所有的NameSrv注册broker信息。如下所示:
this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig);
this.brokerController.registerBrokerAll(false, true);
Topic信息在Broker端的保存
TopicConfig在broker端是保存在TopicConfigManager的,如下所示:
public class TopicConfigManager extends ConfigManager {
private static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME);
private static final long LOCK_TIMEOUT_MILLIS = 3000;
private transient final Lock lockTopicConfigTable = new ReentrantLock();
private final ConcurrentMap<String, TopicConfig> topicConfigTable =
new ConcurrentHashMap<String, TopicConfig>(1024); // topic信息保存在这里
private final DataVersion dataVersion = new DataVersion();
private final Set<String> systemTopicList = new HashSet<String>();
private transient BrokerController brokerController;
public TopicConfigManager() {
}
更新完内存中的topic信息后,broker还会以json格式将其持久化到磁盘上。
public void updateTopicConfig(final TopicConfig topicConfig) {
TopicConfig old = this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
if (old != null) {
LOG.info("update topic config, old:[{}] new:[{}]", old, topicConfig);
} else {
LOG.info("create new topic [{}]", topicConfig);
}
this.dataVersion.nextVersion();
this.persist();
}
我们进入到store/config目录下,可以看到topics.json文件:
打开topics.json文件后,内容如下:
注册Broker信息
之前讲到broker拿到topic信息后,做了两件事情:将Toppic保存到本地,然后向NameSrv注册broker信息。broker信息就是在topic信息之上附加了broker的相关信息,例如:集群名称、broker名称、ip地址、broker id等。注册代码如下:
RegisterBrokerResult registerBrokerResult = this.brokerOuterAPI.registerBrokerAll(
this.brokerConfig.getBrokerClusterName(),
this.getBrokerAddr(),
this.brokerConfig.getBrokerName(),
this.brokerConfig.getBrokerId(),
this.getHAServerAddr(),
topicConfigWrapper,
this.filterServerManager.buildNewFilterServerList(),
oneway,
this.brokerConfig.getRegisterBrokerTimeoutMills());
broker master会向所有的NameSrv结点发起注册请求,请求命令如下:
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
然后我们继续根据RequestCode.REGISTER_BROKER去NameSrv搜索相关代码。
broker信息是存放在RouteInfoManager的,RouterInfoManager类如下所示:
public class RouteInfoManager {
private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
......
}
topicQueueTable:存放topic路由信息,对应于之前的brokerTopic。
brokerAddrTable:broker信息,包括集群名称、broker名称、id与ip地址映射。
clusterAddrTable:集群信息。
brokerLiveTable:broker心跳信息,broker每隔一段时间就会和NameSrv发起心跳,获取最新的路由信息,并确认broker是否还存活。
filterServerTable:过滤相关,非重点。
注册broker信息,其实就是填充这几个表。因为有集群环境,因此注册上述表的时候,还需要添加写锁,具体代码这里就不贴了。
现在我们来复盘一下目前的过程,创建一个topic,首先向集群中的所有broker master注册topic信息。master broker拿到topic信息,保存到本地,然后再向所有的NameSrv结点发起注册broker信息请求。NamerSrv拿到topic和broker信息后,更新表(并没有持久化)。
Slave结点怎么办?
在集群模式下,Slave结点的topic路由信息是通过master结点同步过来的。在BrokerController的初始化时,如果是slave结点,会启动一个定时任务,每分钟从master结点同步路由信息。
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= 6) {
this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress());
this.updateMasterHAServerAddrPeriodically = false;
} else {
this.updateMasterHAServerAddrPeriodically = true;
}
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.slaveSynchronize.syncAll();
} catch (Throwable e) {
log.error("ScheduledTask syncAll slave exception", e);
}
}
}, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
Slave结点的同步器做了如下工作,包括同步topic路由信息,消息消费偏移、group信息等。
public void syncAll() {
this.syncTopicConfig();
this.syncConsumerOffset();
this.syncDelayOffset();
this.syncSubscriptionGroupConfig();
}
Slave结点拿到路由信息后,同样保存在本地,然后定时向所有NameSrv注册broker信息。这样,很短的一段时间过后,NameSrv就包含了所有的topic路由信息。最后再放上之前的图片,加深理解: