RocketMQ的路由中心:NameServer源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: RocketMQ的路由中心:NameServer源码解析

1 前言


对于大部分开发过分布式系统的同学来说,对于服务发现肯定不陌生,分布式系统中的服务注册中心主要是提供服务调用的解析服务,服务调用者可以通过注册中心找到对应的服务提供者,从而进行方法的调用。类似地,RocketMQ里也有一个注册中心,称之为NameServer。接下来,我们就通过这篇文章,来对NameServer一探究竟吧!


2 RocketMQ的整体架构


首先来看看RocketMQ的整体部署架构,如图:



5c2e7678217a4740950392cc942e6416.png


分为下列四个部分:


Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。


Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。


NameServer:NameServer是一个非常简单的Topic路由注册中心,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer和Consumer仍然可以动态感知Broker的路由的信息。


BrokerServer:Broker主要负责消息的存储、投递和查询以及服务高可用保证。


3 NameServer的架构设计


RocketMQ的工作机制是基于Topic的发布订阅机制,Producer将某一Topic的消息发送给Broker存储,Broker根据订阅信息将消息推送给Consumer或Consumer主动向Broker拉取某个Topic下的消息。对于分布式系统而言,为了避免单点故障而导致系统瘫痪,一般都会部署多台Broker从而形成集群来共同承担消息的存储,如果Broker集群中某一台Broker挂了,那么Producer要怎么知道给哪个Broker发送消息呢?又是如何感知Broker的宕机的?NameServer就是为了解决这些问题而设计。


NameServer架构设计图如下:

ffc03436feea407eb173cec68430d6e9.png



每个NameServer都维护了5个HashMap,分别是:

public class RouteInfoManager { 
  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所对应的队列路由信息,包括broker名称、读写队列的数量等

brokerAddrTable:broker基本信息,包括broker名称、所属集群、broker ID、broker的地址等

clusterAddrTable:集群信息,每个broker集群下,包含多个broker,这里是用Set来存储broker的名称

brokerLiveTable:broker心跳信息,每个broker地址对应了broker的存活信息,存活信息状态包括最后更新的时间戳、注册到NameServer时的Channel通道、以及broker的HA地址

filterServerTable:顾名思义,是用来过滤Server的

根据上面的架构图,先简单来说明一下NameServer的工作原理:


Broker会向NameServer集群的每一台机器发送心跳包,包括Topic路由信息

生产者、消费者会每个30s从NameServer获取topic信息

NameServer收到Broker的心跳包时会记录时间戳

NameServer每10s会扫描一次brokerLiveTable,如果brokerLiveTable的最后更新的时间戳与当前扫描的时间戳进行对比,若超过120s,则认为该Broker已经无心跳,然后更新topic的路由信息,将失效的Broker信息移除


对NameServer有了一个大致的认识后,我们就来看看NameServer的启动流程吧~


4 NameServer的启动流程


NameServer的启动类是org.apache.rocketmq.namesrv.NamesrvStartup,启动Debug模式,一步步来对源码进行分析。


第一步:解析配置文件或命令参数,给NamesrvConfig、NettyServerConfig这两个核心对象赋值。 赋值的方式可以通过读取配置文件或者在启动命令时传递参数来进行:


    public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
      、、、、、、、
        final NamesrvConfig namesrvConfig = new NamesrvConfig();
        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        nettyServerConfig.setListenPort(9876);
        if (commandLine.hasOption('c')) {
            String file = commandLine.getOptionValue('c');
            if (file != null) {
                InputStream in = new BufferedInputStream(new FileInputStream(file));
                properties = new Properties();
                properties.load(in);
                MixAll.properties2Object(properties, namesrvConfig);
                MixAll.properties2Object(properties, nettyServerConfig);
                namesrvConfig.setConfigStorePath(file);
                System.out.printf("load config properties file OK, %s%n", file);
                in.close();
            }
        }
        if (commandLine.hasOption('p')) {
            InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
            MixAll.printObjectProperties(console, namesrvConfig);
            MixAll.printObjectProperties(console, nettyServerConfig);
            System.exit(0);
        }
        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
    、、、、、、、
}


具体的配置属性、命令参数在这里不过多赘述。我们只需要知道,NameServer启动的第一步就是初始化NamesrvConfig业务配置类、NettyServerConfig网络配置类。


第二步:根据NamesrvConfig和NettyServerConfig来创建NamesrvController对象,然后使用initialize方法来初始化NamesrvController。


NamesrvController的构造方法如下:

  public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig) {
        this.namesrvConfig = namesrvConfig;
        this.nettyServerConfig = nettyServerConfig;
        this.kvConfigManager = new KVConfigManager(this);
        this.routeInfoManager = new RouteInfoManager();
        this.brokerHousekeepingService = new BrokerHousekeepingService(this);
        this.configuration = new Configuration(
            log,
            this.namesrvConfig, this.nettyServerConfig
        );
        this.configuration.setStorePathFromConfig(this.namesrvConfig, "configStorePath");
    }


其中,维护路由信息、Broker状态等的HashMap是在new RouteInfoManager() 中进行初始化的。

NamesrvController的initialize方法如下:

    public boolean initialize() {
        this.kvConfigManager.load();
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
        this.remotingExecutor =
            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
        this.registerProcessor();
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                NamesrvController.this.kvConfigManager.printAllPeriodically();
            }
        }, 1, 10, TimeUnit.MINUTES);
        if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
            // Register a listener to reload SslContext
            try {
                fileWatchService = new FileWatchService(
                    new String[] {
                        TlsSystemConfig.tlsServerCertPath,
                        TlsSystemConfig.tlsServerKeyPath,
                        TlsSystemConfig.tlsServerTrustCertPath
                    },
                    new FileWatchService.Listener() {
                        boolean certChanged, keyChanged = false;
                        @Override
                        public void onChanged(String path) {
                            if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
                                log.info("The trust certificate changed, reload the ssl context");
                                reloadServerSslContext();
                            }
                            if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
                                certChanged = true;
                            }
                            if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
                                keyChanged = true;
                            }
                            if (certChanged && keyChanged) {
                                log.info("The certificate and private key changed, reload the ssl context");
                                certChanged = keyChanged = false;
                                reloadServerSslContext();
                            }
                        }
                        private void reloadServerSslContext() {
                            ((NettyRemotingServer) remotingServer).loadSslContext();
                        }
                    });
            } catch (Exception e) {
                log.warn("FileWatchService created error, can't load the certificate dynamically");
            }
        }
        return true;
    }

代码中是先加载KV配置,然后初始化Netty网络处理服务器对象,再开启两个定时任务:


任务一:scanNotActiveBroker,每隔10秒扫描一次brokerLiveTable,如果brokerLiveTable的最后更新的时间戳与当前扫描的时间戳进行对比,若超过120s,则认为该Broker已经无心跳,然后更新topic的路由信息,将失效的Broker信息移除


任务二:printAllPeriodically,每隔10分钟打印一次KV配置


第三步:注册JVM钩子函数,如果一个类中使用了线程池,一种优雅的停机方式就是注册一个JVM钩子函数,在JVM进程关闭之前,先将线程池关闭,释放资源。


  Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));
    // NamesrvController 的shutdown方法    
  public void shutdown() {
        this.remotingServer.shutdown();
        this.remotingExecutor.shutdown();
        this.scheduledExecutorService.shutdown();
        if (this.fileWatchService != null) {
            this.fileWatchService.shutdown();
        }
    }

第四步:启动Netty网络服务器

  public void start() throws Exception {
        this.remotingServer.start();
        if (this.fileWatchService != null) {
            this.fileWatchService.start();
        }
    }

至此,NameServer的整个启动流程完成。


5 路由信息元数据存储


前面我们分析到,NameServer主要采用5个HashMap来存储路由信息:


public class RouteInfoManager { 
  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所对应的队列路由信息,包括broker名称、读写队列的数量等

brokerAddrTable:broker基本信息,包括broker名称、所属集群、broker ID、broker的地址等

clusterAddrTable:集群信息,每个broker集群下,包含多个broker,这里是用Set来存储broker的名称

brokerLiveTable:broker心跳信息,每个broker地址对应了broker的存活信息,存活信息状态包括最后更新的时间戳、注册到NameServer时的Channel通道、以及broker的HA地址

filterServerTable:broker上的FilterServer列表,用于类模式消息过滤,类模式过滤机制在4.4版本以及以后被废弃

对于HashMap里的具体数据结构,相信大家都能过理解,在此就不贴代码了,只需要注意:RocketMQ的一个topic拥有多个队列,一个Broker默认为每一个topic创建4个读队列和4个写队列。多个Broker组成一个集群,BrokerName由相同的Broker组成主从架构,brokerId = 0表示主节点,brokerId>0表示从节点。


为了方便大家把这几个HashMap串起来,以RocketMQ的2主2从架构为例,画出图来分析:


46d7165110784a36b98268f5d13c1ee3.png

topicQueueTable内存结构:

fb263199a66a4be895e9edcc2a3c22d1.png

brokerAddrTable内存结构:


eb17b2b5ee844c5da00732e6029fbeb5.png

clusterAddrTable内存结构:


81c7a4c5cc9b41258196b41cef4ab891.png


brokerLiveTable内存结构:

ffef42abcba34aaa8a6480141e6f3fc1.png


6 路由注册


6.1 Broker向NameServer发送心跳包


RocketMQ路由注册是通过Broker与NameServer的心跳功能实现。Broker启动时,会开启一个定时任务,每隔60s向集群中的所有NameServer发送心跳包:

    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);

遍历每一个NameServer,Broker依次向NameServer发送心跳包,发送心跳包的逻辑是采用了Netty框架。


 public List<RegisterBrokerResult> registerBrokerAll(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final boolean oneway,
        final int timeoutMills,
        final boolean compressed) {
        final List<RegisterBrokerResult> registerBrokerResultList = new CopyOnWriteArrayList<>();
        List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
        if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
            final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
            requestHeader.setBrokerAddr(brokerAddr);
            requestHeader.setBrokerId(brokerId);
            requestHeader.setBrokerName(brokerName);
            requestHeader.setClusterName(clusterName);
            requestHeader.setHaServerAddr(haServerAddr);
            requestHeader.setCompressed(compressed);
            RegisterBrokerBody requestBody = new RegisterBrokerBody();
            requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
            requestBody.setFilterServerList(filterServerList);
            final byte[] body = requestBody.encode(compressed);
            final int bodyCrc32 = UtilAll.crc32(body);
            requestHeader.setBodyCrc32(bodyCrc32);
            final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
            for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            RegisterBrokerResult result = registerBroker(namesrvAddr, oneway, timeoutMills, requestHeader, body);
                            if (result != null) {
                                registerBrokerResultList.add(result);
                            }
                            log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            countDownLatch.countDown();
                        }
                    }
                });
            }
            try {
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }
        return registerBrokerResultList;
    }

6.2 NameServer处理心跳包


org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor 是用来处理网络请求的,当请求类型为RequestCode.REGISTER_BROKER时,则将请求交给org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager 处理

public RemotingCommand processRequest(ChannelHandlerContext ctx,
     RemotingCommand request) throws RemotingCommandException {
     if (ctx != null) {
         log.debug("receive request, {} {} {}",
             request.getCode(),
             RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
             request);
     }
     switch (request.getCode()) {
        // 省略
         case RequestCode.REGISTER_BROKER:
             Version brokerVersion = MQVersion.value2Version(request.getVersion());
             if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
                 return this.registerBrokerWithFilterServer(ctx, request);
             } else {
                 return this.registerBroker(ctx, request);
             }
// 省略
     }
     return null;
 }

RouteInfoManager中的registerBroker方法执行处理逻辑。

第一步:由于RouteInfoManager维护路由信息的数据结构都为HashMap,存在并发修改的问题,所以要更改路由信息前,需要先加写锁。 如果集群信息不存在,则先创建。

 this.lock.writeLock().lockInterruptibly();
 Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
 if (null == brokerNames) {
    brokerNames = new HashSet<String>();
    this.clusterAddrTable.put(clusterName, brokerNames);
 }
 brokerNames.add(brokerName);

第二步:维护BrokerData信息。 先从brokerAddrTable中根据brokerName查信息,不存在则新创建,registerFirst=true;存在则替换原来的信息,registerFirst=false

boolean registerFirst = false;
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
    registerFirst = true;
    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
    this.brokerAddrTable.put(brokerName, brokerData);
}
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
//Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
//The same IP:PORT must only have one record in brokerAddrTable
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
    Entry<Long, String> item = it.next();
    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
        it.remove();
    }
}
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);

第三步:若Broker为主节点,且Broker的Topic配置信息发生变化或者初次注册,则需要创建或更新Topic路由信息,并填充topicQueueTable。

if (null != topicConfigWrapper
    && MixAll.MASTER_ID == brokerId) {
    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
        || registerFirst) {
        ConcurrentMap<String, TopicConfig> tcTable =
            topicConfigWrapper.getTopicConfigTable();
        if (tcTable != null) {
            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                this.createAndUpdateQueueData(brokerName, entry.getValue());
            }
        }
    }
}

第四步:更新BrokerLiveInfo,存储状态正常的Broker信息表。

BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
    new BrokerLiveInfo(
        System.currentTimeMillis(),
        topicConfigWrapper.getDataVersion(),
        channel,
        haServerAddr));
if (null == prevBrokerLiveInfo) {
    log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
}

第五步:更新过滤器Server地址列表。

if (filterServerList != null) {
    if (filterServerList.isEmpty()) {
        this.filterServerTable.remove(brokerAddr);
    } else {
        this.filterServerTable.put(brokerAddr, filterServerList);
    }
}


第六步:如果Broker是从节点,则从主节点中拿到最新的brokerLiveInfo,并更新到对应的masterAddr属性。

if (MixAll.MASTER_ID != brokerId) {
    String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
    if (masterAddr != null) {
        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
        if (brokerLiveInfo != null) {
            result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
            result.setMasterAddr(masterAddr);
        }
    }
}

7 路由删除


当Broker挂了,无法向NameServer发送心跳包,这时NameServer需要把挂掉的Broker从路由信息表中删除。根据上面对NameServer启动流程的源码分析,我们知道:NameServer会维护一个定时任务,每10s会扫描一次brokerLiveTable,如果brokerLiveTable的lastUpdateTimestamp与当前扫描的时间戳进行对比,若超过120s,则认为该Broker已经无心跳,将失效的Broker信息移除,关闭对应的channel,同时更新topicQueueTable、 brokerAddrTable、clusterAddrTable、brokerLiveTable、filterServerTable。 另外,还有一种方式是:Broker正常关闭,则会执行unregisterBroker指令。


scanNotActiveBroker:扫描失活的Broker,代码如下:

public void scanNotActiveBroker() {
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, BrokerLiveInfo> next = it.next();
        long last = next.getValue().getLastUpdateTimestamp();
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
            RemotingUtil.closeChannel(next.getValue().getChannel());
            it.remove();
            log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
        }
    }
}

执行删除失活Broker的代码中,需要先加写锁防止并发修改异常,然后一个个路由表进行删除,最后释放锁,代码如下:

public void onChannelDestroy(String remoteAddr, Channel channel) {
    String brokerAddrFound = null;
    if (channel != null) {
        try {
            try {
                this.lock.readLock().lockInterruptibly();
                Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
                    this.brokerLiveTable.entrySet().iterator();
                while (itBrokerLiveTable.hasNext()) {
                    Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
                    if (entry.getValue().getChannel() == channel) {
                        brokerAddrFound = entry.getKey();
                        break;
                    }
                }
            } finally {
                this.lock.readLock().unlock();
            }
        } catch (Exception e) {
            log.error("onChannelDestroy Exception", e);
        }
    }
    if (null == brokerAddrFound) {
        brokerAddrFound = remoteAddr;
    } else {
        log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
    }
    if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
        try {
            try {
                this.lock.writeLock().lockInterruptibly();
                this.brokerLiveTable.remove(brokerAddrFound);
                this.filterServerTable.remove(brokerAddrFound);
                String brokerNameFound = null;
                boolean removeBrokerName = false;
                Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
                    this.brokerAddrTable.entrySet().iterator();
                while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
                    BrokerData brokerData = itBrokerAddrTable.next().getValue();
                    Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
                    while (it.hasNext()) {
                        Entry<Long, String> entry = it.next();
                        Long brokerId = entry.getKey();
                        String brokerAddr = entry.getValue();
                        if (brokerAddr.equals(brokerAddrFound)) {
                            brokerNameFound = brokerData.getBrokerName();
                            it.remove();
                            log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
                                brokerId, brokerAddr);
                            break;
                        }
                    }
                    if (brokerData.getBrokerAddrs().isEmpty()) {
                        removeBrokerName = true;
                        itBrokerAddrTable.remove();
                        log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
                            brokerData.getBrokerName());
                    }
                }
                if (brokerNameFound != null && removeBrokerName) {
                    Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
                    while (it.hasNext()) {
                        Entry<String, Set<String>> entry = it.next();
                        String clusterName = entry.getKey();
                        Set<String> brokerNames = entry.getValue();
                        boolean removed = brokerNames.remove(brokerNameFound);
                        if (removed) {
                            log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
                                brokerNameFound, clusterName);
                            if (brokerNames.isEmpty()) {
                                log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
                                    clusterName);
                                it.remove();
                            }
                            break;
                        }
                    }
                }
                if (removeBrokerName) {
                    Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
                        this.topicQueueTable.entrySet().iterator();
                    while (itTopicQueueTable.hasNext()) {
                        Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
                        String topic = entry.getKey();
                        List<QueueData> queueDataList = entry.getValue();
                        Iterator<QueueData> itQueueData = queueDataList.iterator();
                        while (itQueueData.hasNext()) {
                            QueueData queueData = itQueueData.next();
                            if (queueData.getBrokerName().equals(brokerNameFound)) {
                                itQueueData.remove();
                                log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
                                    topic, queueData);
                            }
                        }
                        if (queueDataList.isEmpty()) {
                            itTopicQueueTable.remove();
                            log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
                                topic);
                        }
                    }
                }
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("onChannelDestroy Exception", e);
        }
    }
}

8 路由发现


RocketMQ的路由发现是非实时的,当topic路由出现变化后,NameServer并不会主动推送给客户端,而是客户端定时拉取topic的最新路由。客户端拉取路由信息的结果为TopicRouteData:

public class TopicRouteData extends RemotingSerializable {
    private String orderTopicConf;
    private List<QueueData> queueDatas;
    private List<BrokerData> brokerDatas;
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}    

orderTopicConf:顺序消息配置内容,来自kvConfig

queueDatas:topic的队列信息列表

brokerDatas:topic存储的broker信息列表

filterServerTable:Broker上过滤服务器的地址列表


路由发现的实现类为org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor,核心方法如下:

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader =
        (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
    if (topicRouteData != null) {
        if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
            String orderTopicConf =
                this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
                    requestHeader.getTopic());
            topicRouteData.setOrderTopicConf(orderTopicConf);
        }
        byte[] content = topicRouteData.encode();
        response.setBody(content);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }
    response.setCode(ResponseCode.TOPIC_NOT_EXIST);
    response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
        + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
    return response;
}

9 总结


好了,本节主要从源码的角度来对NameServer的整体架构、启动流程、路由注册、路由删除、路由发现等进行了简要分析,相信大家对于NameServer有了进一步的认识!后续还会有更多关于RocketMQ的解读

相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
2月前
|
消息中间件 Java Apache
RocketMQ消息回溯实践与解析
在分布式系统和高并发应用的开发中,消息队列扮演着至关重要的角色,而RocketMQ作为阿里巴巴开源的一款高性能消息中间件,以其高吞吐量、高可用性和灵活的配置能力,在业界得到了广泛应用。本文将围绕RocketMQ的消息回溯功能进行实践与解析,分享工作学习中的技术干货。
72 3
|
16天前
|
存储 JavaScript 前端开发
Vue3权限控制全攻略:路由与组件层面的用户角色与权限管理方法深度解析
Vue3权限控制全攻略:路由与组件层面的用户角色与权限管理方法深度解析
68 2
|
23天前
|
消息中间件 存储 监控
RocketMQ消息重试机制解析!
RocketMQ消息重试机制解析!
RocketMQ消息重试机制解析!
|
28天前
|
消息中间件 存储 SQL
代码很少,却很优秀!RocketMQ的NameServer做到了!
本文深入剖析了RocketMQ的注册中心NameServer,基于RocketMQ release-5.2.0版本。NameServer作为Broker、Producer与Consumer之间的纽带,仅由少数几个类构成,却实现了高性能与轻量化。文章详细介绍了NameServer的AP设计思想、简洁的数据结构及心跳机制。AP设计避免了复杂的分布式协议,简化了网络开销;数据结构主要包括路由表、Broker信息等;心跳机制则通过定时扫描确保Broker的活跃状态。通过这些核心设计,NameServer实现了高效稳定的注册与发现功能。
36 5
|
1月前
|
移动开发 JavaScript 前端开发
Javaweb之Vue路由的详细解析
Vue.js是一款备受欢迎的前端框架,以其简洁的API和组件化开发模式著称。Vue Router作为其官方路由管理器,在构建单页面应用(SPA)时发挥关键作用,通过URL变化管理组件切换,实现无刷新过渡。本文将详细介绍Vue Router的基础概念、主要功能及使用步骤,帮助JavaWeb开发者快速掌握其工作原理及实践应用。
13 1
|
24天前
|
传感器 数据可视化 网络协议
DIY可视化整合MQTT生成UniApp源码
DIY可视化整合MQTT生成UniApp源码
38 0
|
3月前
|
消息中间件 开发者
【RabbitMQ深度解析】Topic交换器与模式匹配:掌握消息路由的艺术!
【8月更文挑战第24天】在消息队列(MQ)体系中,交换器作为核心组件之一负责消息路由。特别是`topic`类型的交换器,它通过模式匹配实现消息的精准分发,适用于发布-订阅模式。不同于直接交换器和扇形交换器,`topic`交换器支持更复杂的路由策略,通过带有通配符(如 * 和 #)的模式字符串来定义队列与交换器间的绑定关系。
55 2
|
3月前
|
消息中间件 Java RocketMQ
微服务架构师的福音:深度解析Spring Cloud RocketMQ,打造高可靠消息驱动系统的不二之选!
【8月更文挑战第29天】Spring Cloud RocketMQ结合了Spring Cloud生态与RocketMQ消息中间件的优势,简化了RocketMQ在微服务中的集成,使开发者能更专注业务逻辑。通过配置依赖和连接信息,可轻松搭建消息生产和消费流程,支持消息过滤、转换及分布式事务等功能,确保微服务间解耦的同时,提升了系统的稳定性和效率。掌握其应用,有助于构建复杂分布式系统。
61 0
|
3月前
|
JavaScript 前端开发 API
如何在 Angular 路由中使用路由解析器
如何在 Angular 路由中使用路由解析器
34 0
|
4月前
|
消息中间件 安全 fastjson
消息队列 MQ使用问题之NameServer集群是什么结构
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。

推荐镜像

更多