大家好,我是君哥,RocketMQ选择写NameServer作为注册中心,而不是Zookeeper。为什么?首先看一下RocketMQ的架构,如下图所示:RocketMQ的Broker注册到NameServer集群,生产者和消费者需要从NameServer拉取消息。1NameServer1.1Broker注册Broker启动时,会向NameServer发送注册消息。相关的UML类图如下:我们来看看BrokerOuterAPI的registerBrokerAll方法。代码如下://BrokerOuterAPI.javapublicListregisterBrokerAll(//Omitparametersfinalbooleancompressed){finalListregisterBrokerResultList=newCopyOnWriteArrayList<>();ListnameServerAddressList=this.remotingClient.getNameServerAddressList();if(nameServerAddressList!=null&&nameServerAddressList.size()>0){/省略requestHeader封装finalCountDownLatchcountDownLatch=newCountDownLatch(nameServerAddressList.size());for(finalStringnamesrvAddr:nameServerAddressList){brokerOuterExecutor.execute(newRunnable(){@Overridepublicvoidrun(){try{RegisterBrokerResultrokerresult=(namesrvAddr,oneway,timeoutMills,requestHeader,body);if(result!=null){registerBrokerResultList.add(result);}}catch(Exceptione){}最后{countDownLatch.countDown();}}});}try{countDownLatch.await(timeoutMills,TimeUnit.MILLISECONDS);}catch(InterruptedExceptione){}}returnregisterBrokerResultList;}OK可以看出,当Broker启动时,会向所有NameServer发送注册消息。NameServer端的注册内容如下:从上图可以看出NameServer上需要保存的数据其实很少注:Broker向NameServer注册时,会注册到所有NameServer服务器。NameServer不是分布式存储,NameServer集群是去中心化的。NameServer会有一个定时任务(每10s一次)检查Broker是否下线。判断是根据120s内是否收到心跳。如果没有,通道将被关闭,Broker信息将从本地缓存中移除。代码参见RouteInfoManager类的scanNotActiveBroker方法。Broker启动时,也会启动一个定时任务,每隔30s向NameServer发送一次注册消息,NameServer收到注册消息后更新心跳时间(BrokerLiveInfo.lastUpdateTimestamp)。下面是Broker向NameServer的两个请求码:Registration:RequestCode.REGISTER_BROKERHeartbeat:RequestCode.QUERY_DATA_VERSION1.2创建新topic创建topic时,Broker会向NameServer发送注册消息。代码如下://BrokerControllerclasspublicsynchronizedvoidregisterIncrementBrokerData(TopicConfigtopicConfig,DataVersiondataVersion){TopicConfigregisterTopicConfig=topicConfig;//省略ConcurrentMaptopicConfigTable=newConcurrentHashMaptopicConfig(topicConfig).getTopicName(),registerTopicConfig);TopicConfigSerializeWrappertopicConfigSerializeWrapper=newTopicConfigSerializeWrapper();topicConfigSerializeWrapper.setDataVersion(dataVersion);topicConfigSerializeWrapper.setTopicConfigTable(topicConfigTable);doRegisterBrokerAll(true,false,topicConfigSerializeWrapper);}最后调用了上一节的registerBrokerAll的方法。NameServer收到注册报文后,更新本地保存的数据,保存的数据不再增加新的数据。1.3Client对于生产者和消费者来说,在发送和拉取消息时,会先从本地缓存中获取Topic路由信息。如果获取失败,则需要从NameServer获取。下面是获取Topic路由信息的UML类图:看一下更新Topic路由信息的核心代码:publicbooleanupdateTopicRouteInfoFromNameServer(finalStringtopic,booleanisDefault,DefaultMQProducerdefaultMQProducer){try{if(this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS,TimeUnit.MILLISECONDS)){try{TopicRouteDatatopicRouteData;if(isDefault&&defaultMQProducer!=null){//根据默认的Topic获取topicRouteData=this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),clientConfigKey)Api(Omit)//省略部分逻辑}else{topicRouteData=this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(主题,clientConfig.getMqClientApiTimeout());}if(topicRouteData!=null){//判断是否发送路由信息TopicRouteDataold=this.topicRouteTable.get(topic);booleanchanged=topicRouteDataIsChange(old,topicRouteData);if(!changed){changed=this.isNeedUpdateTopicRouteInfo(topic);}别的{}如果(已更改){TopicRouteDatacloneTopicRouteData=topicRouteData.cloneTopicRouteData();对于(BrokerDatabd:topicRouteData.getBrokerDatas()){this.brokerAddrTable.put(bd.getBrokerName(),bd.getBrokerAddrs());}//更新Pub信息if(!producerTable.isEmpty()){//更新生产者缓存}//更新子信息if(!consumerTable.isEmpty()){//更新消费者缓存}log.info("topicRouteTable.put.Topic={},TopicRouteData[{}]",topic,cloneTopicRouteData);this.topicRouteTable.put(topic,cloneTopicRouteData);返回真;}}else{log.warn("updateTopicRouteInfoFromNameServer,getTopicRouteInfoFromNameServerreturnnull,Topic:{}[{}]",topic,this.clientId);}}catch(MQClientExceptione){}catch(RemotingExceptione){}最后{this.lockNamesrv.unlock();}}else{}}catch(InterruptedExceptione){}returnfalse;}注意:客户端会有定时任务,默认每30s从NameServer中拉取Topic路由信息刷新本地缓存2.放弃ZookeeperRocketMQ在设计之初也是使用Zookeeper作为注册中心,参考了Kafka的设计。Zookeeper是一个非常成熟的注册中心,也支持主节点选举、强一致性等特性。使用Zookeeper的架构如下:2.1轻量级从上面的分析可以看出,RocketMQ需要保存的数据非常少,不需要引入Zookeeper这样重量级的注册中心。2.2一致性NameServer集群的节点是平等的,相互之间不通信,所以确实会存在短暂的不一致。Broker启动时,会与所有NameServers建立长链接,并发送注册信息。注册成功后,每隔30s会向NameServer发送一次心跳,NameServer收到心跳后更新Broker的lastUpdateTimestamp。Zookeeper使用ZAB协议保证节点间数据的强一致性,这就要求每次写请求都需要在节点上写入事务日志,同时需要将内存数据持久化到磁盘上以保证一致性和耐用性。对于RocketMQ这种元数据非常少的简单场景来说,有点大材小用了。放弃强一致性,选择可用性,也是RocketMQ放弃Zookeeper的选择,也让NameServer的设计更加简单。2.3并发注册NameServer在处理Broker注册时,考虑到多个Broker并发注册的问题,ReadWriteLock中的写锁用于保存路由信息。代码如下:publicRegisterBrokerResultregisterBroker(//省略参数finalChannelchannel){RegisterBrokerResultresult=newRegisterBrokerResult();尝试{尝试{this.lock.writeLock().lockInterruptibly();SetbrokerNames=this.clusterAddrTable.get(clusterName);if(null==brokerNames){brokerNames=newHashSet();this.clusterAddrTable.put(clusterName,brokerNames);}BrokerDatabrokerData=this.brokerAddrTable.get(brokerName);if(null==brokerData){registerFirst=true;brokerData=newBrokerData(clusterName,brokerName,newHashMap());this.brokerAddrTable.put(brokerName,brokerData);}BrokerLiveInfoprevBrokerLiveInfo=this.brokerLiveTable.put(brokerAddr,newBrokerLiveInfo(System.currentTimeMillis(),topicConfigWrapper.getDataVersion(),channel,haS服务器地址));}最后{this.lock.writeLock().unlock();}}catch(Exceptione){}returnresult;}2.4当有新的Broker加入Broker时,NameServer不会主动向客户端推送新的Broker信息,而是需要客户端的定时任务(每30s)主动拉取,所以客户端保存的路由信息??会在短时间内与NameServer不一致。同样,Broker下线后,NameServer会使用定时任务(每10s一次)检查Broker的最后一次更新时间是否超过120s,如果超过则删除Broker路由信息。在client端也需要定时任务(每30s)主动拉取,client端保存的路由信息??也会有短时间与NameServer不一致。2.5可扩展性从上面的分析可以看出,NameServer集群的每个节点都是平等的,当集群承受压力时,水平扩展是非常容易的。而且Zookeeper在编写扩展方面非常不灵活。2.6Broker主从集群在Broker主从集群中,RocketMQ基于raft协议实现了DLedger算法,可以基于DLedger进行日志复制。如果Master节点出现故障,可以基于DLedger自动进行主从切换。这可以完全独立于Zookeeper实现来完成。2.7运维如果引入Zookeeper,运维人员必须具备运维Zookeeper的能力,增加了运维的复杂度。3、总结对于注册中心,RocketMQ集群需要保存的元数据非常少,不需要引入Zookeeper这样重量级的注册中心。RocketMQ基于raft协议实现了DLedger算法,可以在不依赖Zookeeper的情况下保证Broker集群的高可用。NameServer是RocketMQ的内部组件,实现简单,易于扩展,不受运维复杂度的影响。