RocketMQ 的注册中心 NameServer

特征

  • NameServer 每30秒,检测一次 Broker 是否存活,如果检测到 Broker 宕机,则从路由表中将其移除;这是为了降低 NameServer 实现的复杂性,在消息发送端提供容错机制保证消息发送的高可用性。

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


    image.png

NameServer 启动流程

启动类

org.apache.rocketmq.namesrv.NamesrvStartup

步骤
  1. 解析命令行参数
  2. 解析配置文件
  3. 创建 NamesrvConfig、NettyServerConfig 配置对象
  4. 创建 NameServerController 的 NameServer 控制器
  5. 启动控制器
  6. 注册系统 ShutDown 钩子,释放系统资源
public static NamesrvController main0(String[] args) {

        try {
            // 创建 NameServer 控制器
            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;
    }

    /**
     * 根据配置文件和配置创建 NameServerController 实例
     * @param args
     * @return
     * @throws IOException
     * @throws JoranException
     */
    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;
        }
        // NameServerConfig 配置类
        final NamesrvConfig namesrvConfig = new NamesrvConfig();
        // NettyServerConfig 配置类,NettyServerConfig 为  NameServer 的网络配置参数
        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        // 设置监听端口
        nettyServerConfig.setListenPort(9876);
        // -c configFile 通过 -c 命令指定配置文件的路径 :-c /Users/wewe/rocketmq/conf/broker.conf(这是 broker 启动的参数)
        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);
                // 配置文件的 properties 键值对属性,对象化,变为属性配置类
                MixAll.properties2Object(properties, namesrvConfig);
                MixAll.properties2Object(properties, nettyServerConfig);

                // 设置配置文件存储路劲
                namesrvConfig.setConfigStorePath(file);

                System.out.printf("load config properties file OK, %s%n", file);
                in.close();
            }
        }
        // 在启动 NameServer 时,可以先使用 ./mqnameserver -c configFile -p 打印当前加载的配置属性,并退出系统。
        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);

        // 根据启动属性创建 NamesrvController 实例,NameServer 核心控制器
        final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

        // remember all configs to prevent discard
        // 注册配置文件和命令行配置和默认配置,注册到所有属性的配置文件中
        controller.getConfiguration().registerConfig(properties);

        return controller;
    }

    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);
        }
        // 注册 JVM 钩子函数并启动服务器,以便监听 Broker、消息生产者的网络请求
        // 如果代码中使用了线程池,一种优雅停机的方式就是注册一个 JVM 钩子函数,在 JVM 进程关闭之前,先将线程线程池关闭,及时释放资源
        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {
            @Override
            public Void call() throws Exception {
                controller.shutdown();
                return null;
            }
        }));

        controller.start();

        return controller;
    }

NamServer 路由注册、故障剔除

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

路由元信息类

org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager

    // Topic 消息队列路由信息,消息发送时根据路由表进行负载均衡
    private final HashMap> topicQueueTable;
    // Broker 基础信息,包含 brokerName、所属集群名称、主备 Broker 地址
    private final HashMap brokerAddrTable;
    // Broker 集群信息,存储集群中所有 Broker 名称
    private final HashMap> clusterAddrTable;
    //
    private final HashMap brokerLiveTable;
    // Broker 上的 FilterServer 列表,用于类模式消息过滤
    private final HashMap/* Filter Server */> filterServerTable;

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

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


image.png

image.png
image.png

路由注册

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

Broker 发送心跳

Broker 定时向 NameServer 注册信息,范围:10秒~60秒,默认 30秒注册一次。

/**
 * Broker 定时向 NameServer 注册信息,范围:10秒~60秒,默认 30秒注册一次
 */
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);


// Broker 向所有的 NameServer 服务器发送心跳包
public List registerBrokerAll(
    final String clusterName,
    final String brokerAddr,
    final String brokerName,
    final long brokerId,
    final String haServerAddr,
    final TopicConfigSerializeWrapper topicConfigWrapper,
    final List filterServerList,
    final boolean oneway,
    final int timeoutMills,
    final boolean compressed) {
    // 当前 Broker 向 NameServer 服务器们的注册结果集合
    final List registerBrokerResultList = Lists.newArrayList();
    List nameServerAddressList = this.remotingClient.getNameServerAddressList();
    if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
        // 构建注册请求
        //
        /**

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

         发送心跳包具体逻辑,首先封装请求包头 Header
         brokerAddr:broker 地址
         brokerId:brokerId:0:Master;大于 0:Slave
         brokerName: broker 名称
         clusterName: 集群名称
         haServerAddr:master地址,初次请求时该值为空,slave 向 NameServer 注册后返回。
         requestBody:
            filterServerList:消息过滤服务器列表。
            topicConfigWrapper: 主题配置, opicConfigWrapper 内部封装的是 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 中。
         */

        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());
        // 遍历所有 NameServer 列表
        for (final String namesrvAddr : nameServerAddressList) {
            // 一个 NameServer 服务器,一个注册线程
            brokerOuterExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 注册 Broker和注册结果
                        RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body); // 分别向NameServer 发送心跳包
                        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;
}

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;
    }
    // Broker 向 NameServer 发送注册请求
    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());
}
NamServer 处理心跳包

NameServer 接受 Broker 的注册心跳包

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

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

// NameServer 接受 Broker 的注册心跳包

// 路由注册需要加写锁,防止并发修改 RouteInfoManager 中的路由表。
// 首先判断Broker 所属集群是否存在,如果不存在,则创建,然后将broker名加入到集群broker集合中
public RegisterBrokerResult registerBroker(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List filterServerList,
        final Channel channel) {
    RegisterBrokerResult result = new RegisterBrokerResult();
    try {
        try {
            // Step1:路由注册需要加写锁 ,防止并发修改 RouteInfoManager 中的路由表。 首先判断
            // Broker 所属集群是否存在, 如果不存在,则创建,然后将 broker 名加入到集群 Broker 集合中。
            this.lock.writeLock().lockInterruptibly();
            // 集群中的broker
            Set brokerNames = this.clusterAddrTable.get(clusterName);
            if (null == brokerNames) {
                brokerNames = new HashSet();
                this.clusterAddrTable.put(clusterName, brokerNames);
            }
            brokerNames.add(brokerName);
            // Step2 :维护 BrokerData 信息,首先从 brokerAddrTable 根据 BrokerName 尝试获取 Broker 信息,
            // 如果不存在,则新建 BrokerData 并放入 brokerAddrTable 中, registerFirst 设置为 true,如果存在,直接替换原先的, registerFirst 设置 false ,表示非第一次注册
            boolean registerFirst = false;
            // 集群信息
            BrokerData brokerData = this.brokerAddrTable.get(brokerName);
            // 集群不存在,创建,broker 放入集群中
            if (null == brokerData) {
                registerFirst = true;
                brokerData = new BrokerData(clusterName, brokerName, new HashMap());
                this.brokerAddrTable.put(brokerName, brokerData);
            }
            Map 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> it = brokerAddrsMap.entrySet().iterator();
            while (it.hasNext()) {
                Entry 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);
            // Step3: 如果 Broker 为 Master,并且 Broker Topic 配置信息发生变化或者是初次注册,则需要创建或更新 Topic 路由元数据,填充 topicQueueTable,其实就是为默认主题自动注册路由信息,
            // 其中包含 MixAll.DEFAULT_TOPIC 的路由信息。当消息生产者发送主题时,如果该主题未创建并且 BrokerConfig 的 autoCreateTopicEnable 为 true 时,将返回 MixAll.DEFAULT_TOPIC 的路由信息。
            if (null != topicConfigWrapper
                    && MixAll.MASTER_ID == brokerId) {
                if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
                        || registerFirst) {
                    ConcurrentMap tcTable =
                            topicConfigWrapper.getTopicConfigTable();
                    if (tcTable != null) {
                        for (Map.Entry entry : tcTable.entrySet()) {
                            // 根据 TopicConfig 创建 QueueData 数据结构 ,然后更新 topicQueueTable。
                            this.createAndUpdateQueueData(brokerName, entry.getValue());
                        }
                    }
                }
            }
            // Step4: 更新 BrokerLiveInfo,存活 Broker 信息表, BrokeLiveInfo 是执行路由删除的重要依据
            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);
            }
            // Step5:注册 Broker 的过滤器 Server 地址列表,一个 Broker 会关联多个 FilterServer消息过滤服务器。
            // 如果此 Broker 为从节点,则需要查找该 Broker 的 Master 的节点信息,并更新对应 masterAddr 属性。
            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);
                    }
                }
            }
        } finally {
            this.lock.writeLock().unlock();
        }
    } catch (Exception e) {
        log.error("registerBroker Exception", e);
    }

    return result;
}

路由删除

RocketMQ 两个触发点来触发路由删除

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

NamesrvController 中的定时任务移除未活跃的Broker

// 每隔 10 秒,扫描一次 brokerLiveTable,如果连续 120s 没有有收到心跳包, NameServer 将移除该 Broker 的路由信息,同时关闭 Socket 连接。
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    }
}, 5, 10, TimeUnit.SECONDS);


// 每隔 10 秒,扫描一次 brokerLiveTable,如果连续 120s 没有有收到心跳包, NameServer 将移除该 Broker 的路由信息,同时关闭 Socket 连接。
public void scanNotActiveBroker() {
    Iterator> it = this.brokerLiveTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry next = it.next();
        long last = next.getValue().getLastUpdateTimestamp();
        // 超过 120秒,移除 Broker,关闭 Socket 连接
        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);
            // 关闭 Socket 连接,更新topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
        }
    }
}

public void onChannelDestroy(String remoteAddr, Channel channel) {
    String brokerAddrFound = null;
    if (channel != null) {
        try {
            try {
                this.lock.readLock().lockInterruptibly();
                Iterator> itBrokerLiveTable =
                        this.brokerLiveTable.entrySet().iterator();
                while (itBrokerLiveTable.hasNext()) {
                    Entry 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 {
                // Step1:申请写锁,根据 brokerAddress 从 brokerLiveTable、filterServerTable移除
                this.lock.writeLock().lockInterruptibly();
                this.brokerLiveTable.remove(brokerAddrFound);
                this.filterServerTable.remove(brokerAddrFound);

                // Step2 :维护 brokerAddrTable。 遍历从 HashMap brokerAddrTable,
                // 从 BrokerData 的 HashMap brokerAddrs 中,找到具体的 Broker,
                // 从 BrokerData 移除 ,如果移除后在 BrokerData 中不再包含其他 Broker ,则 brokerAddrTable 中移除该 brokerName 对应的条目
                String brokerNameFound = null;
                boolean removeBrokerName = false;
                Iterator> itBrokerAddrTable =
                        this.brokerAddrTable.entrySet().iterator();
                while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
                    BrokerData brokerData = itBrokerAddrTable.next().getValue();

                    Iterator> it = brokerData.getBrokerAddrs().entrySet().iterator();
                    while (it.hasNext()) {
                        Entry 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());
                    }
                }
                // Step3:根据 BrokerName,从 clusterAddrTable 中找到 Broker 并从集群中移除。如果移除后,集群中不包含任何 Broker,则将该集群从 clusterAddrTable 中移除。
                if (brokerNameFound != null && removeBrokerName) {
                    Iterator>> it = this.clusterAddrTable.entrySet().iterator();
                    while (it.hasNext()) {
                        Entry> entry = it.next();
                        String clusterName = entry.getKey();
                        Set 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;
                        }
                    }
                }
                // Step4: 根据 brokerName,遍历所有主题的队列,如果队列中包含了当前 Broker 的队列,则移除,如果 topic 只包含待移除Broker 的队列的话,从路由表中删除该 topic
                if (removeBrokerName) {
                    Iterator>> itTopicQueueTable =
                            this.topicQueueTable.entrySet().iterator();
                    while (itTopicQueueTable.hasNext()) {
                        Entry> entry = itTopicQueueTable.next();
                        String topic = entry.getKey();
                        List queueDataList = entry.getValue();

                        Iterator 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 {
                // step5:释放锁,完成路由删除
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("onChannelDestroy Exception", e);
        }
    }
}


路由发现

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

DefaultRequestProcessor 处理 GET_ROUTEINTOBY_TOPIC 客户端请求。

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader =
        (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
    // 调用 pickupTopicRouteData(),从路由表 topicQueueTable、brokerAddrTable、filterServerTable 中分别填充 TopicRouteData 中的 List 、List 和 filterServer 地址表
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

    if (topicRouteData != null) {
        // 如果找到主题对应的路由信息并且该主题为顺序消息,则从 NameServerKVconfig 中获取关于顺序消息的相关的配置填充路由信息。
        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;
    }
    // 如果找不到路由信息 CODE 则使用 TOPIC_NOT_EXISTS,表示没有找到对应的路由。
    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;
}

总结

本章主要介绍了 NameServer 路由功能,包含路由元数据、路由注册与发现机制。

NameServer 集群各个机器之间互不通信,Broker 每 30s 向 NameServer 注册一次,不能保证各个 NameServer 中注册的 Broker 路由信息实时的一致,NameServer 集群也没有 Master、Slave 节点区分,不能保证 NameServer 单个机器的高可用,通过多个 NameServer 节点,保证了注册中心的集群高可用,然后NameServer 中的 Broker 的路由信息存在延后性,也不能保证 Producer 发送消息的 Broker 机器实时可用,从而发送消息不一定能发送到 Broker 机器是否成功。然后 Producer 发送消息的高可用,采用的是 Producer 向 Broker 发送消息,Broker 进行消息 Ack 应答,来确定消息发送给了 Broker,Broker 接受到了消息。Producer 高可用还采用了另外两种方法。

  1. Broker 故障的延迟机制,Broker 故障了进行一定时间的隔离,然后保证不再向这个 Broker 发送消息,一定时间后进行隔离结束。
  2. Producer 发送消息的重试机制,一般总共3次。
  3. Producer 发送消息不能保证Exactly once(恰好一次),它只能保证At least once(最少一次),发送消息可能重复问题,需要开发者自己进行处理。
/**
 * Send message to broker asynchronously. 

* * This method returns immediately. On sending completion, sendCallback will be executed.

* * Similar to {@link #send(Message)}, internal implementation would potentially retry up to {@link * #retryTimesWhenSendAsyncFailed} times before claiming sending failure, which may yield message duplication and * application developers are the one to resolve this potential issue. * */ @Override public void send(Message msg, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException { msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQProducerImpl.send(msg, sendCallback); }
image.png

你可能感兴趣的:(RocketMQ 的注册中心 NameServer)