2.路由中心NameServer

本章主要介绍RocketMQ 路由管理、服务注册及服务发现的机制, NameServer 是整个RocketMQ 的“大脑” 。相信大家对“服务发现”这个词语并不陌生,分布式服务SOA 架构体系中会有服务注册中心,分布式服务SOA 的注册中心主要提供服务调用的解析服务,指引服务调用方(消费者)找到“远方”的服务提供者,完成网络通信,那么RocketMQ 的路由中心存储的是什么数据呢?作为一款高性能的消息中间件,如何避免NameServer 的单点故障,提供高可用性呢?让我们带着上述疑问, 一起进入RocketMQ NameServer 的精彩 世界中来。

2.1.NameServer 架构设计

消息中间件的设计思路一般基于主题的订阅发布机制消息生产者( Producer )发送某一主题的消息到消息服务器,消息服务器负责该消息的持久化存储,消息消费者(Consumer)订阅感兴趣的主题,消息服务器根据订阅信息(路由信息)将消息推送到消费者( PUSH 模式)或者消息消费者主动向消息服务器拉取消息( PULL 模式),从而实现消息生产者与消息消费者解调。为了避免消息服务器的单点故障导致的整个系统瘫痪,通常会部署多台消息服务器共同承担消息的存储。那消息生产者如何知道消息要发往哪台消息服务器呢?如果某一台消息服务器若机了,那么生产者如何在不重启服务的情况下感知呢?

NameServer 就是为了解决上述问题而设计的。

RocketMQ 的逻辑部署图如图2-1 所示。

rocketmq对于的spring版本_System

Broker 消息服务器在启动时向所有Name Server 注册,消息生产者(Producer)在发送消息之前先从Name Server 获取Broker 服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer 与每台Broker 服务器保持长连接,并间隔30s 检测Broker 是否存活,如果检测到Broker 右机, 则从路由注册表中将其移除。但是路由变化不会马上通知消息生产者,为什么要这样设计呢?这是为了降低NameServer 实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性,这部分在3.4 节中会有详细的描述

NameServer 本身的高可用可通过部署多台Nameserver 服务器来实现,但彼此之间互不通信,也就是NameServer 服务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,这也是RocketMQ NameServer 设计的一个亮点, RocketMQNameServer 设计追求简单高效。

2.2.NameServer 启动流程

从源码的角度窥探一下Names 巳rver 启动流程,重点关注Na meServer 相关启动参数. NameServer 启动类: org .apache.rocketrr吨name srv.NamesrvStartup 。

Step1 : 首先来解析配置文件,需要填充NameServerConfig 、NettyServerConfig 属性值。


public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
        //PackageConflictDetect.detectFastjson();

        Options options = ServerUtil.buildCommandlineOptions(new Options());
        commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
        if (null == commandLine) {
            System.exit(-1);
            return null;
        }

        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);

        if (null == namesrvConfig.getRocketmqHome()) {
            System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
            System.exit(-2);
        }

        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(lc);
        lc.reset();
        configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");

        log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);

        MixAll.printObjectProperties(log, namesrvConfig);
        MixAll.printObjectProperties(log, nettyServerConfig);

        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

        // remember all configs to prevent discard
        controller.getConfiguration().registerConfig(properties);

        return controller;
    }


从代码我们可以知道先创建NameServerConfig ( NameServer 业务参数)、N ettyServerConfig( NameServer 网络参数) , 然后在解析启动时把指定的配置文件或启动命令中的选项值,填充到nameServerConfig , nettyServerConfig 对象。参数来源有如下两种方式。

  1. -c configFile 通过,c 命令指定配置文件的路径。
  2. 使用“--属性名 属性值”,例如--listenPort 9876 。

NameSeverConfig 属性


private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
private String productEnvName = "center";
private boolean clusterTest = false;
private boolean orderMessageEnable = false;

 
• rocketmqhome: rocketmq 主目录,可以通过-Drocketmq.home.dir=path 或通过设置环境变量ROCKETMQ_HOME 来配置RocketMQ 的主目录。
• kvConfigPath: NameServer 存储KV 配置属性的持久化路径。
• configStorePath:nameServer 默认配置文件路径,不生效。name Server 启动时如果要通过配置文件配置NameServer 启动属性的话,请使用-c 选项。
• orderMessageEnable : 是否支持顺序消息,默认是不支持。
   private int listenPort = 8888;
    private int serverWorkerThreads = 8;
    private int serverCallbackExecutorThreads = 0;
    private int serverSelectorThreads = 3;
    private int serverOnewaySemaphoreValue = 256;
    private int serverAsyncSemaphoreValue = 64;
    private int serverChannelMaxIdleTimeSeconds = 120;

    private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
    private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
    private boolean serverPooledByteBufAllocatorEnable = true;


  • listenPort: N ameServer 监昕端口,该值默认会被初始化为9876 0
  • serverWorkerThreads: Net ty 业务线程池线程个数。
  • serverCallbackExecutorThreads : Netty public 任务线程池线程个数, Netty 网络设计,根据业务类型会创建不同的线程池,比如处理消息发送、消息消费、心跳检测等。如果该业务类型( R e que stCode )未注册线程池, 则由public 线程池执行。
  • serverSelectorThreads: IO 线程池线程个数,主要是NameServer 、Brok e r 端解析请求、返回相应的线程个数,这类线程主要是处理网络请求的,解析请求包, 然后转发到各个业务线程池完成具体的业务操作,然后将结果再返回调用方。
  • serverOnewaySemaphore Value: send oneway 消息请求井发度( Broker 端参数) 。
  • serverAsyncSemaphore Value : 异步消息发送最大并发度( Broker 端参数) 。
  • serverChannelMaxld l eTimeSeconds :网络连接最大空闲时间,默认120s 。如果连接空闲时间超过该参数设置的值,连接将被关闭。
  • serverSocketSndBufSize :网络socket 发送缓存区大小, 默认64k 。
  • serverSocketRcvBufSize :网络socket 接收缓存区大小,默认6 4k 。
  • serverPooledByteBufAllocatorEnable: ByteBuffer 是否开启缓存, 建议开启。
  • useEpollNativeSelector : 是否启用Epoll IO 模型, Linux 环境建议开启。\

在启动NameServer 时,可以先使用./mqnameserver -c configFile -p 打印当前配置属性。

Step2 :根据启动属性创建NamesrvController 实例,并初始化该实例, NameServerController实例为NameServer核心控制器。


public static NamesrvController main0(String[] args) {

        try {
            NamesrvController controller = createNamesrvController(args);
            start(controller);
            String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
            log.info(tip);
            System.out.printf("%s%n", tip);
            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
    }
        public static NamesrvController start(final NamesrvController controller) throws Exception {

        if (null == controller) {
            throw new IllegalArgumentException("NamesrvController is null");
        }

        boolean initResult = controller.initialize();
        if (!initResult) {
            controller.shutdown();
            System.exit(-3);
        }

        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));

        controller.start();

        return controller;
    }
    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 配置,创建NettyServer网络处理对象,然后开启两个定时任务,在RocketMQ中此类定时任务统称为心跳检测。

加载KV 配置,创建NettyServer 网络处理对象,然后开启两个定时任务,在RocketMQ中此类定时任务统称为心跳检测。

  • 定时任务I: NameServer 每隔I Os 扫描一次Broker , 移除处于不激活状态的Broker c
  • 定时任务2: names 巳rver 每隔10 分钟打印一次KV 配置。

Step3 :注册JVM 钩子函数并启动服务器, 以便监昕Broker 、消息生产者的网络请求。

Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
        @Override
        public Void call() throws Exception {
            controller.shutdown();
            return null;
        }
    }));
    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }

这里主要是向读者展示一种常用的编程技巧,如果代码中使用了线程池,一种优雅停机的方式就是注册一个JVM 钩子函数, 在JVM 进程关闭之前,先将线程池关闭,及时释放资源。

2.3.NameServer 路由注册、故障剔除

NameServer 主要作用是为消息生产者和消息消费者提供关于主题Topic 的路由信息,那么NameServer 需要存储路由的基础信息,还要能够管理Broker 节点,包括路由注册、路由删除等功能。

2.3.1.路由元信息

NameServer 路由实现类: org.apache.rocketmq.namesrv.routeinfo.RoutelnfoManager , 在了解路由注册之前,我们首先看一下NameServer 到底存储哪些信息。


private static final InternalLogger log = InternalLoggerFactory.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 消息队列路由信息,消息发送时根据路由表进行负载均衡。
  • brokerAddrTable : Broker 基础信息, 包含brokerName 、所属集群名称、主备Broker地址。
  • clusterAddrTable: Broker 集群信息,存储集群中所有Broker 名称。
  • brokerLiveTable: Broker 状态信息。NameServer 每次收到心跳包时会替换该信息。
  • filterServerTable : Broker 上的FilterServer 列表,用于类模式消息过滤,详细介绍请参考第6 章的内容。

QueueData 、BrokerData 、BrokerLiveinfo 类图如图2-2 所示。

RocketMQ 基于订阅发布机制, 一个Topic 拥有多个消息队列,一个Broker 为每一主题默认创建4 个读队列4 个写队列。多个Broker 组成一个集群, BrokerName 由相同的多台Broker组成Master-Slave 架构, brokerId 为0 代表Master , 大于0 表示Slave 。BrokerLivelnfo 中的lastUpdateTimestamp 存储上次收到Broker 心跳包的时间。

rocketmq对于的spring版本_github_02

2.3.2.路由注册

RocketMQ 路由注册是通过Broker 与Name Server 的心跳功能实现的。Broker 启动时向集群中所有的NameServ巳r 发送心跳语句,每隔3 0s 向集群中所有NameServer 发送心跳包, NameServer 收到Broker 心跳包时会更新brokerLiveTable 缓存中BrokerLiveInfo 的lastUpdateTimestamp ,然后Nam巳Server 每隔10s 扫描brokerLiveTable ,如果连续120s 没有收到心跳包, NameServ er 将移除该Broker 的路由信息同时关闭Socket 连接。

Broker 发送心跳包

Broker 发送心跳包的核心代码如下所示。

//org.apache.rocketmq.broker.BrokerController#start
        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);

 
org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBrokerAll

    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 = Lists.newArrayList();
        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", namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            countDownLatch.countDown();
                        }
                    }
                });
            }

            try {
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }

        return registerBrokerResultList;
    }


该方法主要是遍历NameServer 列表, Broker 消息服务器依次向NameServer发送心跳包。

//org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBroker
    private RegisterBrokerResult registerBroker(
        final String namesrvAddr,
        final boolean oneway,
        final int timeoutMills,
        final RegisterBrokerRequestHeader requestHeader,
        final byte[] body
    ) throws RemotingCommandException, MQBrokerException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException,
        InterruptedException {
        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
        request.setBody(body);

        if (oneway) {
            try {
                this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
            } catch (RemotingTooMuchRequestException e) {
                // Ignore
            }
            return null;
        }

        RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
        assert response != null;
        switch (response.getCode()) {
            case ResponseCode.SUCCESS: {
                RegisterBrokerResponseHeader responseHeader =
                    (RegisterBrokerResponseHeader) response.decodeCommandCustomHeader(RegisterBrokerResponseHeader.class);
                RegisterBrokerResult result = new RegisterBrokerResult();
                result.setMasterAddr(responseHeader.getMasterAddr());
                result.setHaServerAddr(responseHeader.getHaServerAddr());
                if (response.getBody() != null) {
                    result.setKvTable(KVTable.decode(response.getBody(), KVTable.class));
                }
                return result;
            }
            default:
                break;
        }

        throw new MQBrokerException(response.getCode(), response.getRemark());
    }

发送心跳包具体逻辑,首先封装请求包头(Header)

  • brokerAddr: broker 地址。
  • brokerId: brokerld. (O:Master; 大于0: Slave)
  • brokerName: broker名称。
  • clusterName: 集群名称。
  • haServerAddr: master 地址,初次请求时该值为空, slave 向Nameserver 注册后返回。
  • requestBody:
  • filterServerList 。消息过滤服务器列表。
  • topicConfigWrapper。主题配置, topicConfigWrapper 内部封装的是TopicConfigManager中的topicConfigTable ,内部存储的是Broker 启动时默认的一些Topic, MixAll.SELF_TEST_TOPIC 、MixAll.DEFAULT_TOPIC ( AutoCreateTopicEnable=true ). MixAll.BENCHMARK_TOPIC 、MixAll.OFFSET_MOVED_EVENT 、BrokerConfig#brokerClusterName 、BrokerConfig#brokerName 。Broker中Topic 默认存储在${ Rocket_Home}/store/confg/topic.json 中。

RocketMQ 网络传输基于Netty , 具体网络实现细节本书不会过细去剖析,在这里介绍一下网络跟踪方法: 每一个请求, RocketMQ 都会定义一个RequestCode ,然后在服务端会对应相应的网络处理器(processor 包中) , 只需整库搜索R巳questCode 即可找到相应的处理逻辑。

NameServer 处理心跳包
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor 网络处理器解析请求类型, 如果请求类型为RequestCode.REGISTER_BROKER ,则请求最终转发到RoutelnfoManager#registerBroker 。

clusterAddrTable 维护

//org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
    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);

Step1:路由注册需要加写锁,防止并发修改RoutelnfoManager 中的路由表。首先判断Broker 所属集群是否存在, 如果不存在,则创建,然后将broker 名加入到集群Broker 集合中。

brokeAddrTable 维护


//org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
    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);
    }
    String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
    registerFirst = registerFirst || (null == oldAddr);


Step2 :维护BrokerData 信息,首先从brokerAddrTable 根据BrokerName 尝试获取Broker 信息,如果不存在, 则新建BrokerData 并放入到brokerAddrTable , registerFirst 设置为true ;如果存在, 直接替换原先的, registerFirst 设置为false,表示非第一次注册。

topicQueueTable 维护


//org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
    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());
                }
            }
        }
    }


Step3 :如果Broker为Master ,并且Broker Topic 配置信息发生变化或者是初次注册,则需要创建或更新Topic 路由元数据,填充topicQueueTable,其实就是为默认主题自动注册路由信息,其中包含MixAll.DEFAULT_TOPIC 的路由信息。当消息生产者发送主题时,如果该主题未创建并且BrokerConfig 的autoCreateTopicEnable 为true 时, 将返回MixAll.DEFAULT_TOPIC 的路由信息。

createAndUpdateQueueData


private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
        QueueData queueData = new QueueData();
        queueData.setBrokerName(brokerName);
        queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
        queueData.setReadQueueNums(topicConfig.getReadQueueNums());
        queueData.setPerm(topicConfig.getPerm());
        queueData.setTopicSynFlag(topicConfig.getTopicSysFlag());

        List<QueueData> queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());
        if (null == queueDataList) {
            queueDataList = new LinkedList<QueueData>();
            queueDataList.add(queueData);
            this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList);
            log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
        } else {
            boolean addNewOne = true;

            Iterator<QueueData> it = queueDataList.iterator();
            while (it.hasNext()) {
                QueueData qd = it.next();
                if (qd.getBrokerName().equals(brokerName)) {
                    if (qd.equals(queueData)) {
                        addNewOne = false;
                    } else {
                        log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd,
                            queueData);
                        it.remove();
                    }
                }
            }

            if (addNewOne) {
                queueDataList.add(queueData);
            }
        }
    }


根据TopicConfig 创建QueueData 数据结构,然后更新topicQueueTable 。


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);
  }


Step4 : 更新BrokerLiveInfo ,存活Broker 信息表, BrokeLiveInfo 是执行路由删除的重要依据。


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

  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);
          }
      }
  }


Step5:注册Broker 的过滤器Server 地址列表,一个Broker 上会关联多个FilterServer消息过滤服务器,此部分内容将在第6 章详细介绍;如果此Broker 为从节点,则需要查找该Broker 的Master 的节点信息,并更新对应的masterAddr 属性。

设计亮点: NameServe 与Broker 保持长连接, Broker 状态存储在brokerLiveTable 中,NameServer 每收到一个心跳包,将更新brokerLiveTable 中关于Broker 的状态信息以及路由表( topicQueueTable 、brokerAddrTab le 、brokerLiveTable 、fi lterServerTable ) 。更新上述路由表( HashTable )使用了锁粒度较少的读写锁,允许多个消息发送者(Producer )并发读,保证消息发送时的高并发。但同一时刻NameServer 只处理一个Broker 心跳包,多个心跳包请求串行执行。

2.3.3.路由删除

根据上面章节的介绍, Broker 每隔30s 向NameServer 发送一个心跳包,心跳包中包含BrokerId 、Broker 地址、Broker 名称、Broker 所属集群名称、Broker 关联的FilterServer 列表。但是如果Broker 若机, NameServer 无法收到心跳包,此时NameServer 如何来剔除这些失效的Broker 呢? Name Server 会每隔I Os 扫描brokerLiveTable 状态表,如果BrokerLive 的lastUpdateTimestamp 的时间戳距当前时间超过120s ,则认为Broker 失效,移除该Broker,关闭与Broker 连接,并同时更新topicQueueTable 、brokerAddrTable 、brokerLiveTable 、filterServerTable 。

RocktMQ 有两个触发点来触发路由删除。

  1. NameServer 定时扫描brokerLiveTable 检测上次心跳包与当前系统时间的时间差,如果时间戳大于120s ,则需要移除该Broker 信息。
  2. Broker 在正常被关闭的情况下,会执行unr巳gisterBroker 指令。

由于不管是何种方式触发的路由删除,路由删除的方法都是一样的,就是从topicQueueTable 、brokerAddrTable 、brokerLiveTable 、filterServerTable 删除与该Broker 相关的信息,但RocketMQ 这两种方式维护路由信息时会抽取公共代码,本文将以第一种方式展开分析。


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());
        }
    }
}


我们应该不会忘记scanNotActi veBrok巳r 在NameServer 中每10s 执行一次。逻辑也很简单,遍历brokerLivelnfo 路由表( HashMap ),检测BrokerLiv巳Info 的lastUpdateTimestamp上次收到心跳包的时间如果超过当前时间l 20s, NameServer 则认为该Broker 已不可用,故需要将它移除,关闭Channel ,然后删除与该Broker 相关的路由信息,路由表维护过程,需要申请写锁。


//RoutelnfoManager#onChannelDestroy
this.lock.writeLock().lockInterruptibly ( ) ;
this.brokerLiveTable.remove(brokerAddrFound);
this.filterServerTable.remove(brokerAddrFound);


Step I :申请写锁,根据brokerAddress 从brokerLiveTable 、filterServerTable 移除,如 代码清单所示。


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());
    }
}


Step2 :维护brokerAddrTable 。遍历从HashMap<String/ * brokerName 灯, BrokerData>brokerAddrTable,从BrokerData 的HashMap<Long/* brokerl d 勺, String/ * broker address */>brokerAddrs 中,找到具体的Broker ,从BrokerData 中移除,如果移除后在BrokerData 中不再包含其他Broker ,则在brokerAddrTable 中移除该brokerName 对应的条目。


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;
        }
    }
}


Step3 : 根据BrokerName ,从clusterAddrTable 中找到Broker 并从集群中移除。如果移除后,集群中不包含任何Broker ,则将该集群从clusterAddrTable 中移除。


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);
        }
    }
}


Step4 : 根据brokerName ,遍历所有主题的队列,如果队列中包含了当前Broker 的队列,则移除,如果topic 只包含待移除Broker 的队列的话,从路由表中删除该topic ,如代码清单2-21 所示。

finally {
  this.lock.writeLock().unlock();
}


Step5 :释放锁,完成路由删除。

2.3.4.路由发现

RocketMQ 路由发现是非实时的,当Topic 路由出现变化后, NameServer 不主动推送给客户端, 而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为: GET_ROUTEINTO_BY_TOPIC 。RocketMQ 路由结果如图2-6 所示。

rocketmq对于的spring版本_rocketmq_03

  • orderTopicConf :顺序消息配置内容,来自于kvConfig 。
  • List<QueueData> queueData: topic 队列元数据。
  • List<BrokerData> brokerDatas: topic 分布的broker 元数据。
  • HashMap< String/* brokerAdress/,List<String> / filt巳rServer* /> : broker 上过滤服务器地址列表。
  • NameServer 路由发现实现类:DefaultRequestProcessor#getRoutelnfoByTopic ,如代码清单2-22 所示。
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;
    }


  • Step1:调用RouterlnfoManager 的方法,从路由表topicQueueTable 、brokerAddrTable 、filterServerTable 中分别填充TopicRouteData 中的List<Queu巳Data>、List<BrokerData>和filterServer 地址表。
  • Step2: 如果找到主题对应的路由信息并且该主题为顺序消息,则从NameServerKVconfig 中获取关于顺序消息相关的配置填充路由信息。如果找不到路由信息CODE 则使用TOPIC NOT_EXISTS ,表示没有找到对应的路由。

2.4.本章小结

本章主要介绍了NameServer 路由功能,包含路由元数据、路由注册与发现机制。为了加强对本章的理解,路由发现机制可以用图2-6 来形象解释。

rocketmq对于的spring版本_github_04

Name Server 路由发现与删除机制就介绍到这里了,我们会发现这种设计会存在这样一种情况: NameServer 需要等Broker 失效至少120s 才能将该Broker 从路由表中移除掉,那如果在Broker 故障期间,消息生产者Producer 根据主题获取到的路由信息包含已经看机的Broker ,会导致消息发送失败,那这种情况怎么办,岂不是消息发送不是高可用的?让我们带着这个疑问进入RocketMQ 消息发送的学习。