Namesrv解析

概述

1.1 NameServer是什么

NameSrv是RocketMQ的重要组成组件,可以把NameSrv视为配置中心。主要完成整个MQ集群的主题和队列信息管理,维护broker集群,broker信息,broker存活信息等。

在实现上,Namesrv是一个多主无状态服务,多个Namesrv之间互相不通信,极大降低了Broker元数据管理的成本。

每个broker会定时向Namesrv上报自己的元数据信息。

1.2 为什么要有有NameSrv

部署完成之后我们思考如果没有NameSrv时以下几个场景:

  • producer和Consumer是需要知道每个master节点上的Queue分布情况的,也就是说在producer和Consumer启动时我们要指定所有的broker(包括从节点的地址)地址,不然producer无法进行有效的数据发送,Consumer也无法进行消息消费。
  • 在producer和Consume知道所有的broker地址信息之后,就需要对broker的状态进行管理,保证broker在线并可用,如果发现有不可用的broker要及时剔除并完成主从切换保证高可用,避免单点故障。那么这个客户端就变成了一个重客户端,要完成broker侧路由发现和状态维护。并且每增加一种新语言的支持都要实现一遍,就不利于开源推广。
  • 如果我要新增一个broker节点,是不是所有的producer和Consumer都要进行更新感知到新增的broker节点,那么在业务高峰期进行增加broker节点来扩容的流程就变得冗长,这样处理不及时就不能保证系统稳定性和可用性,下线一个broker节点也是同样的道理,

综上我们可以看到,producer和Consumer对broker的管理和处理流程是相同的,并且MQ这种一个轻客户端要求的中间件,可以将对broker路由信息的管理这部分功能抽离出来,这样客户端侧不需要处理路由发现和管理功能,降低了整体复杂度,后续即使新增broker节点producer和Consumer也无需启动就能实时感知。这就是NameSrv的意义。

1.3 NameSrv和其他注册中心

这里我们要考虑为什么RocketMQ自己实现了一个路由注册中心NameSrv,而不是使用已经有的zookeeper作为注册中心。核心原因在于"业务一致性需求 + 性能/可运维性权衡+推广"。

  • **业务一致性诉求低 ** 路由并非强一致场景,客户端本就"每隔一段时间拉取一次"即可,允许短暂陈旧(最终一致)。为此引入 ZooKeeper 的强一致(CP)和推送机制是过度设计。
  • 高吞吐与低时延 Broker 心跳/注册频繁且量大(默认每 30s 全量注册到所有 NameSrv),用 ZooKeeper 会走多数派写路径、受限于写吞吐与延迟;NameServer 纯内存哈希表,直连 RPC,时延更低、吞吐更高。
  • 高可用与简单可靠 NameServer 节点彼此独立、无主从、无共识协议,任一可用即可查询(AP 取向);宕机不相互牵连,部署和扩容非常简单。 路由是"心跳即注册、超时即剔除",无需外部依赖与共识存储,容错链更短,跨机房容灾也更直接。
  • 运维成本低 可以将NameSrv当一个普通的应用服务来进行维护。

所以最终单独实现broker的路由发现和管理已经状态维护就十分必要,NameSrv在这个场景下就产生了。

1.3.1 路由管理模块

RouterInfoManger中有五个Map

  • topicQueueTable *HashMap<String/ topic /, Map<String / brokerName */ , QueueData>>** 保存topic的队列信息,也是真正的路由信息。队列信息中包含了其所在broker名称和读写队列数量
  • **brokerAddrTable *HashMap<String/ brokerName */, BrokerData>:保存broker信息,包含每个broker名称,集群名称,主备broker的IP地址。
  • **clusterAddrTable *HashMap<String/ clusterName /, Set<String/ brokerName */>>:保存cluster信息,包含每个集群中所有的broker名称列表
  • **brokerLiveTable *HashMap<String/ brokerAddr */, BrokerLiveInfo>:broker状态信息,包含当前所有的存活的broker,以及最后一次上报心跳的事件和连接信息
  • filterServerTable:broker的FilterServer列表,用于类模式消息过滤,该机制在4.4版本之后废弃

1.3.2 各个映射表数据结构示例

1.3.2.1 topicQueueTable数据结构
  • QueueData
    • brokerName:所属 Broker 名
    • readQueueNums:读队列数量
    • writeQueueNums:写队列数量
    • perm:读写权限
    • topicSysFlag:Topic 同步标记
json 复制代码
HashMap<String/* topic */, Map<String /* brokerName */ , QueueData>> topicQueueTable;
{
  "DefaultCluster_REPLY_TOPIC": {
    "broker-b": {
      "readQueueNums": 1,
      "perm": 6,
      "writeQueueNums": 1,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    "broker-a": {
      "readQueueNums": 1,
      "perm": 6,
      "writeQueueNums": 1,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  },
  "broker-b": {
    "broker-b": {
      "readQueueNums": 1,
      "perm": 7,
      "writeQueueNums": 1,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    }
  },
  "OFFSET_MOVED_EVENT": {
    "broker-b": {
      "readQueueNums": 1,
      "perm": 6,
      "writeQueueNums": 1,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    "broker-a": {
      "readQueueNums": 1,
      "perm": 6,
      "writeQueueNums": 1,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  },
  "broker-a": {
    "broker-b": {
      "readQueueNums": 1,
      "perm": 7,
      "writeQueueNums": 1,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    "broker-a": {
      "readQueueNums": 1,
      "perm": 7,
      "writeQueueNums": 1,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  },
  "TBW102": {
    "broker-b": {
      "readQueueNums": 8,
      "perm": 7,
      "writeQueueNums": 8,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    "broker-a": {
      "readQueueNums": 8,
      "perm": 7,
      "writeQueueNums": 8,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  },
  "SELF_TEST_TOPIC": {
    "broker-b": {
      "readQueueNums": 1,
      "perm": 6,
      "writeQueueNums": 1,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    "broker-a": {
      "readQueueNums": 1,
      "perm": 6,
      "writeQueueNums": 1,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  },
  "DefaultCluster": {
    "broker-b": {
      "readQueueNums": 16,
      "perm": 7,
      "writeQueueNums": 16,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    "broker-a": {
      "readQueueNums": 16,
      "perm": 7,
      "writeQueueNums": 16,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  }
}
从这里我们可以看到,每个逻辑topic下的物理读写队列会均匀分散到当前brokerCluster下的所有的所有broker上
这样能保证一旦某个broker宕机之后,其他topic依然可用。
1.3.2.2 brokerAddrTable数据结构
  • BrokerData
    • cluster broker所在集群名称
    • brokerName 当前broker的brokerName
    • brokerAddrs HashMap<Long/* brokerId /, String/ broker address */> key是 broker角色(0-master节点 大于0都是从节点)
json 复制代码
HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
{
  "broker-b": {
    "cluster": "DefaultCluster",
    "brokerAddrs": {
      "0": "172.20.16.59:10911"
    },
    "brokerName": "broker-b"
  },
  "broker-a": {
    "cluster": "DefaultCluster",
    "brokerAddrs": {
      "-1": "172.20.16.59:30911"
    },
    "brokerName": "broker-a"
  }
}
1.3.2.3 clusterAddrTable数据结构
json 复制代码
HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;

{
  "clusterAddrTable": {
    "c1": ["broker-a", "broker-b"]
  }
}
1.3.2.4 brokerLiveTable数据结构
  • BrokerLiveInfo:Broker 状态信息,由 Broker 心跳上报
    • lastUpdateTimestamp:上次更新时间戳
    • dataVersion:元数据被更新的次数,在 Broker 中统计,每次更新 +1
    • channel:Netty Channel
    • haServerAddr:HA 服务器地址
json 复制代码
HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
{
    "127.0.0.1:10911":{
        "channel":{
            "active":true,
            "inputShutdown":false,
            "open":true,
            "outputShutdown":false,
            "registered":true,
            "shutdown":false,
            "writable":true
        },
        "dataVersion":{
            "counter":1,
            "timestamp":1651564857610
        },
        "haServerAddr":"10.0.0.2:10912",
        "lastUpdateTimestamp":1651564899813
    }
}

1.3.3 通信模块

DefaultRequestProcessor主要是NameSrv作为服务端处理来自broker和client的请求。RocketMQ的基础通信层是基于Netty实现的。DefaultRequestProcessor主要做请求路由分发,通过解析请求对应的RequestCode找到对应的Processor完成处理。

java 复制代码
// 根据不同的RequestCode找到不同的处理逻辑
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.PUT_KV_CONFIG:
            return this.putKVConfig(ctx, request);
        case RequestCode.GET_KV_CONFIG:
            return this.getKVConfig(ctx, request);
        case RequestCode.DELETE_KV_CONFIG:
            return this.deleteKVConfig(ctx, request);
        case RequestCode.QUERY_DATA_VERSION:
            return queryBrokerTopicConfig(ctx, request);
            // 处理broker注册请求
        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);
            }
            // broker下线处理
        case RequestCode.UNREGISTER_BROKER:
            return this.unregisterBroker(ctx, request);
            //处理客户端请求topic路由信息
            // RocketMQ 路由发现是非实时的,当Topic路由信息发生变化后,NameServer不会主动推送给客户端,而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为:GET_ROUTEINFO_BY_TOPIC
        case RequestCode.GET_ROUTEINFO_BY_TOPIC:
            return this.getRouteInfoByTopic(ctx, request);
        case RequestCode.GET_BROKER_CLUSTER_INFO:
            return this.getBrokerClusterInfo(ctx, request);
        case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
            return this.wipeWritePermOfBroker(ctx, request);
        case RequestCode.ADD_WRITE_PERM_OF_BROKER:
            return this.addWritePermOfBroker(ctx, request);
        case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
            return getAllTopicListFromNameserver(ctx, request);
        case RequestCode.DELETE_TOPIC_IN_NAMESRV:
            return deleteTopicInNamesrv(ctx, request);
        case RequestCode.GET_KVLIST_BY_NAMESPACE:
            return this.getKVListByNamespace(ctx, request);
        case RequestCode.GET_TOPICS_BY_CLUSTER:
            return this.getTopicsByCluster(ctx, request);
        case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
            return this.getSystemTopicListFromNs(ctx, request);
        case RequestCode.GET_UNIT_TOPIC_LIST:
            return this.getUnitTopicList(ctx, request);
        case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
            return this.getHasUnitSubTopicList(ctx, request);
        case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
            return this.getHasUnitSubUnUnitTopicList(ctx, request);
        case RequestCode.UPDATE_NAMESRV_CONFIG:
            return this.updateConfig(ctx, request);
        case RequestCode.GET_NAMESRV_CONFIG:
            return this.getConfig(ctx, request);
        default:
            break;
    }
    return null;
}

1.3.4 KVConfigManager

主要用于加载和管理NameSrvController启动过程中指定的KV配置,通常为KV.json

1.4 元数据交互

  • broker
    • 每隔30s向NameSrv集群的每台机器发送心跳包,同时发送当前broker所有的topic的队列信息
    • 当topic有变化(新增或修改),broker会将增量(发生变化的topic队列信息)发送给NameSrv,同时更新NameSrv的DataVersion变量标识当前topic元数据发生更新。DataVersion是在broker端生产的。
  • Client
    • 生产者第一次发送消息时,首先向NameSrv获取对应topic的路由信息
    • 消费者启动过程中会向NameSrv获取topic的路由信息
    • 每隔30s向NameSrv发送请求,获取关注的topic的路由信息(定时任务)
  • NameSrv
    • 将路由信息保存在内存中。它只被其他模块调用(被 Broker 上传,被客户端拉取),不会主动调用其他模块。
    • 启动一个定时任务线程,每隔 10s 扫描 brokerAddrTable 中所有的 Broker 上次发送心跳时间,如果超过 120s 没有收到心跳,则从存活 Broker 表中移除该 Broker。

源码分析

NameSrv启动

NamesrvStartup是NameSrv的启动类,

首先创建NamesrvController,由NamesrvController完成后续整体的NameSrv的启动

java 复制代码
public static void main(String[] args) {
    // args 承接启动参数
    main0(args);
}
public static NamesrvController main0(String[] args) {
    try {
        // 创建namesrvController
        // 初始化nameserver,启动namesrv,关闭namesrv
        // 读取配置信息,
        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;
}

createNamesrvController

  • 完成对通过shell命令行启动NameSrv时的启动参数解析
  • 完成NameSrv作为服务端时Netty相关参数的设置,如监听的端口号,boss组和worker组工作线程配置
java 复制代码
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,用于解析启动的命令参数
    commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
    if (null == commandLine) {
        System.exit(-1);
        return null;
    }

    // 创建namesrvConfig配置类
    final NamesrvConfig namesrvConfig = new NamesrvConfig();
    // 创建nettyServer配置类,用于处理连接
    final NettyServerConfig nettyServerConfig = new NettyServerConfig();
    //设置监听端口号为9876
    nettyServerConfig.setListenPort(9876);
    // 如果命令带有-c命令,加载指定的配置文件
    if (commandLine.hasOption('c')) {
        String file = commandLine.getOptionValue('c');
        if (file != null) {
            //将配置文件转换为properties类
            InputStream in = new BufferedInputStream(new FileInputStream(file));
            properties = new Properties();
            properties.load(in);
            //解析配置文件中是否有namesrvConfig属性的字段值,如果存在则调用对应的setXXX方法赋值
            MixAll.properties2Object(properties, namesrvConfig);
            //解析配置文件中是否有nettyServerConfig属性的字段值,如果存在则调用对应的setXXX方法赋值
            MixAll.properties2Object(properties, nettyServerConfig);

            // 记录配置文件
            namesrvConfig.setConfigStorePath(file);

            System.out.printf("load config properties file OK, %s%n", file);
            in.close();
        }
    }

    //如果启动命令带有-p,则执行退出
    if (commandLine.hasOption('p')) {
        InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
        MixAll.printObjectProperties(console, namesrvConfig);
        MixAll.printObjectProperties(console, nettyServerConfig);
        System.exit(0);
    }

    //将启动命令参数值设置到namesrvConfig中
    MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);

    // 如果namesrvConfig中没有rocketmqHome值则退出
    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
    final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

    // remember all configs to prevent discard
    // 记录所有的配置
    controller.getConfiguration().registerConfig(properties);

    return controller;
}

NamesrvStartup的start方法

  • 调用NamesrvController的initialize方法完成初始化,
  • NamesrvController的start方法启动NameSrv
java 复制代码
public static NamesrvController start(final NamesrvController controller) throws Exception {

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

    // 返回true,说明初始化成功
    boolean initResult = controller.initialize();
    if (!initResult) {
        controller.shutdown();
        System.exit(-3);
    }

    // 注册一个JVM级别的ShutdownHook,当JVM关闭之后会调用controller.shutdown()方法,实现优雅关机。
    Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
        controller.shutdown();
        return null;
    }));

    //启动NamesrvController
    controller.start();

    return controller;
}

NamesrvController

NamesrvController完成了一系列的NameSrv初始化工作,包括注册请求处理(DefaultRequestProcessor和ClusterTestDefaultRequestProcessor),以及注册broker状态监控定时任务

NamesrvController#initialize 初始化

完成NameSrv作为Netty服务端的启动处理工作,注册请求处理器和broker定时任务,同时注册相关业务处理线程池

java 复制代码
public boolean initialize() {

    //读取KvConfig对应的配置
    this.kvConfigManager.load();

    //创建网络服务对象
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

    //创建业务线程池
    this.remotingExecutor =
    Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

    //注册处理器
    this.registerProcessor();

    //注册定时任务,任务用于检查broker存活状态,每10s检查broker状态,没有存活的broker将会移出brokerLiveTable
    this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker, 5, 10, TimeUnit.SECONDS);

    //注册定时任务,每10分钟输出KvConfig配置信息
    this.scheduledExecutorService.scheduleAtFixedRate(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;
}

路由注册

路由注册信息主要由broker侧发起,因为broker维护了每个topic实际消息存储对象Queue实体。所以路由注册主要是两部分

  • broker心跳续期,这个在BrokerController的start方法中会开启一个30s为周期的定时任务,将自身的 Topic 队列路由信息发送给 NameServer。主节点和从节点都会发送心跳和路由信息。Broker 会遍历 NameServer 列表,向每个 NameServer 发送心跳包。
  • 另外一个触发 Broker 上报 Topic 配置的操作是修改 Broker 的 Topic 配置(创建/更新),由 TopicConfigManager 触发上报。

心跳包的请求头中包含

  • Broker 地址
  • BrokerId,0 表示主节点,大于 0 表示从节点
  • Broker 名称
  • 集群名称
  • 主节点地址

请求体中包含

  • topicConfigTable:包含了每个 Topic 的所有队列信息。
  • dataVersion:Broker 中 Topic 配置的版本号,每当配置更新一次,版本号 +1

上报的心跳包请求类型是:RequestCode.REGISTER_BROKER

DefaultRequestProcessor#registerBrokerWithFilterServer

NameSrv通过DefaultRequestProcessor解析出broker发起请求的路由注册请求(RequestCode.REGISTER_BROKER),然后转发给对应的请求处理器完成处理。这里首先DefaultRequestProcessor#registerBrokerWithFilterServer方法进行处理

java 复制代码
public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
throws RemotingCommandException {

    // 创建请求响应对象response
    final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);

    // 获取响应头
    final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();

    // 解析请求头
    final RegisterBrokerRequestHeader requestHeader =
    (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);

    // 看收到的信息是否是client发送的信息,安全验证
    if (!checksum(ctx, request, requestHeader)) {
        response.setCode(ResponseCode.SYSTEM_ERROR);
        response.setRemark("crc32 not match");
        return response;
    }

    // 创建响应对象
    RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();

    // 解析Request的body信息并创建registerBrokerBody对象,里面有broker的数据版本号DataVersion信息
    // DataVersion是在broker创建的
    if (request.getBody() != null) {
        try {
            registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
        } catch (Exception e) {
            throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
        }
    } else {
        registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
        registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
    }

    // 将当前broker信息注册到RouterInfoManager进行管理,主要将broker信息放入RouterInfoManager的多个映射表中
    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
        requestHeader.getClusterName(),
        requestHeader.getBrokerAddr(),
        requestHeader.getBrokerName(),
        requestHeader.getBrokerId(),
        requestHeader.getHaServerAddr(),
        registerBrokerBody.getTopicConfigSerializeWrapper(),
        registerBrokerBody.getFilterServerList(),
        ctx.channel());

    responseHeader.setHaServerAddr(result.getHaServerAddr());
    responseHeader.setMasterAddr(result.getMasterAddr());

    byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
    response.setBody(jsonValue);

    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
    return response;
}

注册broker RouteInfoManager#registerBroker

java 复制代码
    /**
     * 注册broker到NameSrv
     * @param clusterName broker集群名名称
     * @param brokerAddr broker地址
     * @param brokerName broker名字
     * @param brokerId 0-broker的master节点 大于0-broker的slave节点
     * @param haServerAddr broker的HA节点地址
     * @param topicConfigWrapper broker端的topic配置信息
     * @param filterServerList broker的filterServer列表
     * @param channel broker和NamSrv创建的通信通道Channel对象
     * @return RegisterBrokerResult
     */
public RegisterBrokerResult registerBroker(
    final String clusterName,
    final String brokerAddr,
    final String brokerName,
    final long brokerId,
    final String haServerAddr,
    final TopicConfigSerializeWrapper topicConfigWrapper,
    final List<String> filterServerList,
    final Channel channel) {
    // 返回结果封装对象
    RegisterBrokerResult result = new RegisterBrokerResult();
    try {
        try {
            // 获取锁,防止并发修改
            this.lock.writeLock().lockInterruptibly();

            //将当前集群放入到clusterAddrTable中,并获取集群对应的brokerName列表
            Set<String> brokerNames = this.clusterAddrTable.computeIfAbsent(clusterName, k -> new HashSet<>());
            brokerNames.add(brokerName);

            // 首次注册标志
            boolean registerFirst = false;

            //从brokerAddrTable中获取brokerName对应的brokerDate
            BrokerData brokerData = this.brokerAddrTable.get(brokerName);
            // 条件满足,首次注册
            if (null == brokerData) {
                registerFirst = true;
                // 创建brokerData
                brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
                // 将当前brokerData添加到brokerAddrTable中
                this.brokerAddrTable.put(brokerName, brokerData);
            }

            // 获取当前broker的物理节点数据,每个broker对应的ip地址
            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
            // 遍历这个broker物理节点列表
            Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
            while (it.hasNext()) {
                Entry<Long, String> item = it.next();
                // 条件成立,说明新上线的broker名字相同,brokerId确不同,说明broker信息发生变化
                if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
                    log.debug("remove entry {} from brokerData", item);
                    // 将当前brokerAddrsMap中对应的旧的broker物理地址信息移除
                    it.remove();
                }
            }

            // 重新将当前broker放入
            String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
            //条件成立,说明当前是master节点,
            if (MixAll.MASTER_ID == brokerId) {
                log.info("cluster [{}] brokerName [{}] master address change from {} to {}",
                         brokerData.getCluster(), brokerData.getBrokerName(), oldAddr, brokerAddr);
            }

            // 判断为首次注册
            registerFirst = registerFirst || (null == oldAddr);

            // 条件成立:当前broker上的topic不为空,当前broker为master节点
            if (null != topicConfigWrapper
                && MixAll.MASTER_ID == brokerId) {
                /**
                 * 条件成立,说明当前broker的topic信息发生变化或者当前broker为首次注册,此时需要创建或者更新topic的路由元数据
                 * 同时更新topicQueueTable数据。其实是为默认主题自动注册路由信息,其中包含MixAll.DEFAULT_TOPIC的路由信息。当消息生产者发送主题时,
                 * 如果该主题未创建,并且BrokerConfig的autoCreateTopicEnable为true,则返回MixAll.DEFAULT_TOPIC的路由信息,
                 */
                if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
                        || registerFirst) {
                    // 获取当前broker上的topic配置信息
                    ConcurrentMap<String, TopicConfig> tcTable =
                            topicConfigWrapper.getTopicConfigTable();
                    if (tcTable != null) {
                        // 遍历当前topic,更新topicQueueTable数据
                        for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                            this.createAndUpdateQueueData(brokerName, entry.getValue());
                        }
                    }
                }
            }

            // 创建BrokerLiveInfo保存当前活跃的Broker信息,并返回上一次心跳时当前broker节点的BrokerLiveInfo信息
            BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                    new BrokerLiveInfo(
                            System.currentTimeMillis(),
                            topicConfigWrapper.getDataVersion(),
                            channel,
                            haServerAddr));
            // 如果上一次是null,说明是新注册的broker
            if (null == prevBrokerLiveInfo) {
                log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
            }

            // 注册Broker的过滤器Server地址列表,一个Broker上会关联多个FilterServer消息过滤服务器
            if (filterServerList != null) {
                if (filterServerList.isEmpty()) {
                    this.filterServerTable.remove(brokerAddr);
                } else {
                    this.filterServerTable.put(brokerAddr, filterServerList);
                }
            }

            // 当前broker不是master节点
            if (MixAll.MASTER_ID != brokerId) {
                // 获取当前broker的master节点信息
                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;
}

路由注册这里有一个点要注意。就是注册broker信息的时候是以brokerName为主。如果两个不同的broker集群中有相同的brokerName节点进行注册,后面启动的broker无法注册成功。

比如一个节点A启动时参数设置为

plain 复制代码
brokerClusterName = RaftCluster
brokerName = RaftCluster-broker-a
brokerId = 0

节点B启动时参数设置为

plain 复制代码
brokerClusterName = RaftCluster
brokerName = RaftCluster-broker-a
brokerId = 0

节点A和节点B的brokerName相同。但是brokerClusterName不同。如果节点A启动后注册成功,此时节点B是无法注册成功的。

json 复制代码
                //从brokerAddrTable中获取brokerName对应的brokerDate
                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                log.info("registerBroker, {} {} {} {} {}", clusterName, brokerAddr, brokerName, brokerId);
                // 条件满足,首次注册
                if (null == brokerData) {
                    registerFirst = true;
                    // 创建brokerData
                    brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
                    // 将当前brokerData添加到brokerAddrTable中
                    this.brokerAddrTable.put(brokerName, brokerData);
                }

                // 获取当前broker的物理节点数据,每个broker对应的ip地址
                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
                // 遍历这个broker物理节点列表
                Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
                while (it.hasNext()) {
                    Entry<Long, String> item = it.next();
                    // 条件成立,说明新上线的broker名字相同,brokerId确不同,说明broker信息发生变化
                    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
                        log.debug("remove entry {} from brokerData", item);
                        // 将当前brokerAddrsMap中对应的旧的broker物理地址信息移除
                        it.remove();
                    }
                }
因为节点A已使用broker-a经注册成功,
此时节点B注册请求到达时通过broker-a从brokerName从brokerAddrTable获取到信息,
就认为当前节点已经注册成功。然后再比对brokerId是否相同,如果不容就将节点A的地址移除掉。
这里实际发生了抢占。所以这里要注意

队列数据更新 RouteInfoManager#createAndUpdateQueueData

java 复制代码
// 创建或更新topicQueueTable数据
private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
    QueueData queueData = new QueueData();
    // 设置当前队列所在的brokerName
    queueData.setBrokerName(brokerName);
    // 设置当前队列的读写队列数量
    queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
    queueData.setReadQueueNums(topicConfig.getReadQueueNums());
    // 设置当前队列的权限
    queueData.setPerm(topicConfig.getPerm());
    queueData.setTopicSysFlag(topicConfig.getTopicSysFlag());

    // 创建或更新topicQueueTable数据
    Map<String, QueueData> queueDataMap = this.topicQueueTable.get(topicConfig.getTopicName());
    if (null == queueDataMap) {
        queueDataMap = new HashMap<>();
        queueDataMap.put(queueData.getBrokerName(), queueData);
        this.topicQueueTable.put(topicConfig.getTopicName(), queueDataMap);
        log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
    } else {
        QueueData old = queueDataMap.put(queueData.getBrokerName(), queueData);
        if (old != null && !old.equals(queueData)) {
            log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), old,
                     queueData);
        }
    }
}
  • 根据所属集群名称去 clusterAddrTable 表中获取Set,将brokerName加入到此set集合中。
  • 根据brokerName去 brokerAddrTable 表中获取 brokerData,如果获取不到,则创建 brokerData,写入brokerAddrTable 映射表中
  • 去判断是否存在brokerName下的主备节点切换,如果存在切换则更新其相关信息
  • 如果此次传入的topic相关信息不为空,并且broker节点是master节点的话,尝试去更新topic相关路由信息,主要是保存broker上管理了多少topic,以及管理了topic内的读队列和写队列数量。
  • 更新心跳信息,重点是更新 brokerLiveInfo的lastUpdateTimestamp字段,上次心跳时间。
  • 更新类模式消息过滤信息
  • 向broker返回结果。

路由管理

这部分主要是broker状态信息维护。在NamesrvController的initialize方法中我们注册了broker在线信息维护的定时任务(brokerLiveTable映射表信息维护),主要方法就是routeInfoManager#scanNotActiveBroker

json 复制代码
this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker, 5, 10, TimeUnit.SECONDS);

routeInfoManager#scanNotActiveBroker

java 复制代码
// 遍历心跳失败的broker并移除,每10s执行一次
public int scanNotActiveBroker() {
    // 统计本次移除的broker数量
    int removeCount = 0;

    // 获取当前活跃的broker列表
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();

    // 开始遍历
    while (it.hasNext()) {
        Entry<String, BrokerLiveInfo> next = it.next();

        // 当前broker上次上报心跳的时间
        long last = next.getValue().getLastUpdateTimestamp();
        // broker心跳超时
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
            // 关闭channel连接
            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());

            removeCount++;
        }
    }

    return removeCount;
}

routeInfoManager#onChannelDestroy

此时broker心跳超时,关闭和broker的Channel,同时更新RouterInfoManager的各种映射表

java 复制代码
public void onChannelDestroy(String remoteAddr, Channel channel) {
    // 记录要移除broker的地址
    String brokerAddrFound = null;
    if (channel != null) {
        try {
            try {
                // 获取锁
                this.lock.readLock().lockInterruptibly();
                Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
                this.brokerLiveTable.entrySet().iterator();

                // 遍历brokerLiveTable
                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 {
                // 申请写锁。根据brokerAddress从brokerLiveTable、filterServerTable中移除Broker相关的信息
                this.lock.writeLock().lockInterruptibly();
                this.brokerLiveTable.remove(brokerAddrFound);
                this.filterServerTable.remove(brokerAddrFound);
                String brokerNameFound = null;
                boolean removeBrokerName = false;

                // 更新brokerAddrTable中的数据,遍历brokerAddrTable,从brokerData的brokerAddrs中找到具体的broker从brokerData中移除
                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;
                        }
                    }
                    // 如果移除后的brokerAddrTable为空,则从brokerAddrTable中移除该brokerName
                    if (brokerData.getBrokerAddrs().isEmpty()) {
                        removeBrokerName = true;
                        itBrokerAddrTable.remove();
                        log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
                                brokerData.getBrokerName());
                    }
                }

                // 根据BrokerName,从clusterAddrTable中找到Broker并将其从集群中移除。如果移除后,集群中不包含任何Broker,则将该
                //集群从clusterAddrTable中移除,
                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;
                        }
                    }
                }
                // 根据BrokerName,遍历所有主题的队列,如果队列中包含当前Broker的队列,则移除,如果topic只包含待移除Broker的队列,从路由表中删除该topic
                if (removeBrokerName) {
                    String finalBrokerNameFound = brokerNameFound;
                    Set<String> needRemoveTopic = new HashSet<>();

                    topicQueueTable.forEach((topic, queueDataMap) -> {
                        QueueData old = queueDataMap.remove(finalBrokerNameFound);
                        log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
                                topic, old);

                        if (queueDataMap.size() == 0) {
                            log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
                                    topic);
                            needRemoveTopic.add(topic);
                        }
                    });

                    needRemoveTopic.forEach(topicQueueTable::remove);
                }
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("onChannelDestroy Exception", e);
        }
    }
}

RocketMQ三个触发点来触发broker信息删除

  • 定时任务通过lastUpdateTimestamp信息判断broker已经失效,会触发 destory 操作,也就是路由删除操作。
  • 网络交互Netty层面的,NameServer和Broker会建立长连接Channel,在此期间,如果Channel中120s没有进行 读 | 写 操作的时候,同样会进行关闭socket,触发destory操作 (具体细节我们讲通信层的时候在讲)
  • broker正常关闭,会执行 unRegisterBroker 操作

路由发现

这部分主要是NameSrv收到客户端获取topic路由信息场景,此时会受到客户端发起的RequestCode.GET_ROUTEINFO_BY_TOPIC类型的请求,DefaultRequestProcessor接到这个RequestCode.GET_ROUTEINFO_BY_TOPIC会交给NamesrvController的getRouteInfoByTopic方法处理,处理完成之后响应给客户端NameSrv当前内存中最新的topic路由信息

NamesrvController#getRouteInfoByTopic

这里面的关键流程是

TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

通过这一步拿到TopicRouteData路由数据

java 复制代码
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
                                           RemotingCommand request) throws RemotingCommandException {
    //创建响应体
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader =
    (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
    // 通过RouterInfoManager中的topicQueueTable,TopicRouteData,brokerAddrTable,filterServerTable中获取对应topic的元数据信息,并创建TopicRouteData对象
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

    // 如果找到主题对应的路由信息并且该主题为顺序消息,则从NameServer的KVConfig中获取关于顺序消息相关的配置填充路由信息。
    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;
        Boolean standardJsonOnly = requestHeader.getAcceptStandardJsonOnly();
        if (request.getVersion() >= Version.V4_9_4.ordinal() || (null != standardJsonOnly && standardJsonOnly)) {
            content = topicRouteData.encode(SerializerFeature.BrowserCompatible,
                                            SerializerFeature.QuoteFieldNames, SerializerFeature.SkipTransientField,
                                            SerializerFeature.MapSortField);
        } else {
            content = RemotingSerializable.encode(topicRouteData);
        }

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

RouteInfoManager#pickupTopicRouteData

核心方法,通过解析topicQueueTable和brokerAddrTable映射表中的数据生成

java 复制代码
// 读取映射表中的数据
public TopicRouteData pickupTopicRouteData(final String topic) {

    // 创建TopicRouteData
    TopicRouteData topicRouteData = new TopicRouteData();
    boolean foundQueueData = false;
    boolean foundBrokerData = false;

    // 存放brokerName集合
    Set<String> brokerNameSet = new HashSet<>();

    // 存放broker数据集合
    List<BrokerData> brokerDataList = new LinkedList<>();
    topicRouteData.setBrokerDatas(brokerDataList);

    HashMap<String, List<String>> filterServerMap = new HashMap<>();
    topicRouteData.setFilterServerTable(filterServerMap);

    try {
        try {

            // 加读锁
            this.lock.readLock().lockInterruptibly();

            // 获取当前topic的队列元数据信息
            Map<String, QueueData> queueDataMap = this.topicQueueTable.get(topic);

            // 如果已经有对应topic的队列数据
            if (queueDataMap != null) {

                topicRouteData.setQueueDatas(new ArrayList<>(queueDataMap.values()));
                foundQueueData = true;

                brokerNameSet.addAll(queueDataMap.keySet());

                for (String brokerName : brokerNameSet) {
                    BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                    if (null != brokerData) {
                        BrokerData brokerDataClone = new BrokerData(brokerData.getCluster(), brokerData.getBrokerName(), (HashMap<Long, String>) brokerData
                                                                                                                          .getBrokerAddrs().clone());
                        brokerDataList.add(brokerDataClone);
                        foundBrokerData = true;

                        // skip if filter server table is empty
                        if (!filterServerTable.isEmpty()) {
                            for (final String brokerAddr : brokerDataClone.getBrokerAddrs().values()) {
                                List<String> filterServerList = this.filterServerTable.get(brokerAddr);

                                // only add filter server list when not null
                                if (filterServerList != null) {
                                    filterServerMap.put(brokerAddr, filterServerList);
                                }
                            }
                        }
                    }
                }
            }
        } finally {
            this.lock.readLock().unlock();
        }
    } catch (Exception e) {
        log.error("pickupTopicRouteData Exception", e);
    }

    log.debug("pickupTopicRouteData {} {}", topic, topicRouteData);

    if (foundBrokerData && foundQueueData) {
        return topicRouteData;
    }

    return null;
}
json 复制代码
{
  "brokerDatas": [
    {
      "cluster": "DefaultCluster",
      "brokerAddrs": {
        "0": "172.20.16.59:10911"
      },
      "brokerName": "broker-b"
    },
    {
      "cluster": "DefaultCluster",
      "brokerAddrs": {
        "0": "172.20.16.59:30821"
      },
      "brokerName": "broker-c"
    },
    {
      "cluster": "DefaultCluster",
      "brokerAddrs": {
        "0": "172.20.16.59:30911",
        "-1": "172.20.16.59:30921"
      },
      "brokerName": "broker-a"
    }
  ],
  "filterServerTable": {},
  "queueDatas": [
    {
      "readQueueNums": 8,
      "perm": 7,
      "writeQueueNums": 8,
      "brokerName": "broker-b",
      "topicSysFlag": 0
    },
    {
      "readQueueNums": 8,
      "perm": 7,
      "writeQueueNums": 8,
      "brokerName": "broker-c",
      "topicSysFlag": 0
    },
    {
      "readQueueNums": 8,
      "perm": 7,
      "writeQueueNums": 8,
      "brokerName": "broker-a",
      "topicSysFlag": 0
    }
  ]
}

总结

  • NameSrv通过四个映射表来实现路由管理。
  • NameSrv通过定时任务每10s扫描一次当前brokerLiveTable映射表。这个映射表中记录每个broker上次上报心跳的时间。如果超过2m则认为当前broker不可用。会直接下线。
相关推荐
调试人生的显微镜5 小时前
iOS 26 性能监控工具有哪些?多工具协同打造全方位性能分析体系
后端
苏三说技术5 小时前
千万级大表如何删除数据?
后端
John_ToDebug5 小时前
架构的尺度:从单机到分布式,服务端技术的深度演进
后端·程序人生
调试人生的显微镜5 小时前
Fastlane 结合 开心上架 命令行版本实现跨平台上传发布 iOS App
后端
weixin_545019325 小时前
Spring Boot 项目开启 HTTPS 完整指南:从原理到实践
spring boot·后端·https
掘金一周6 小时前
第一台 Andriod XR 设备发布,Jetpack Compose XR 有什么不同?对原生开发有何影响? | 掘金一周 10.30
前端·人工智能·后端
张乔246 小时前
spring boot项目快速整合xxl-job实现定时任务
spring boot·后端·xxl-job
程序定小飞6 小时前
基于springboot的论坛网站设计与实现
java·开发语言·spring boot·后端·spring
PFinal社区_南丞6 小时前
测试驱动开发(TDD):以测试为引擎的软件工程实践
后端