NameServer 源码分析

NameServer 源码分析

流程图

在这里插入图片描述

1.NameServer 架构设计

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

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

2.NameServer 启动流程

NameServer 启动类: org .apache.rocket.namesrv.NamesrvStartup

 public static NamesrvController main0(String[] args) {

        try {
          //创建NamesrvController,首先来解析配置文件 需要填充nameServerConfig,nettyServerConfig 属性值
            NamesrvController controller = createNamesrvController(args);
          //根据启动属性创建namesrvController实例,并初始化该实例.NameServerController实例为NameServet核心控制器
            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;
    }

//创建NameServerConfig 、nettyServerConfig 然后在解析启动时把指配置文件或启动命令中的选项值,填充到 nameServerConfig nettyServerConfig 
 public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
        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);
        }
        return controller;
    }
  1. 在解析启动时把指配置文件或启动命令中的选项值,填充到 nameServerConfig nettyServerConfig
  2. 根据启动属性创建namesrvController实例,并初始化该实例.NameServerController实例为NameServet核心控制器
public boolean initialize() {

        this.kvConfigManager.load();

        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
       
        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);
        return true;
    }

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

定时任务 I: NameServer 每隔 1Os 扫描一次 Broker 移除处于不激活状态的 Broker

定时任务 2: names rver 每隔 10 分钟打印一次 KV

  1. :注册 JVM 钩子函数并启动服务器, 以便监昕 Broker 、消息生产者的网络请求
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
    @Override
    public Void call() throws Exception {
        controller.shutdown();
        return null;
    }
}));

如果代码中使用了线程池,一种优雅停机的方式就是注册 JVM 钩子函数,在 JVM 进程闭之前,先将线程关闭 ,及时释放资源

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

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

3.1路由元信息

NameServer路由实现类org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager

NameServer存储了哪些信息?

    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;
  1. topicQueueTable: Topic 消息队列路由信息,消息发送时根据路由表进行负载均衡
  2. brokerAddrTable : Broker 基础信息, brokerName 属集群名称 主备 Broker地址
  3. clusterAddrTable: Broker 集群信息,存储集群中所有 Broker 名称
  4. brokerLiveTable: Broker 状态信息 NameServer 每次收到心跳包时会替换该信息
  5. filterServerTable : Broker 上的 FilterServer 列表,用于类模式消息过滤

image-20200926004003249

数据结构如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LTX8WnHb-1601057485862)(/Users/laiyanxin/Library/Application Support/typora-user-images/image-20200926004042948.png)]

image-20200926004053807

3.2路由注册

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

1.Broke发送心跳包
org.apache.rocketmq.broker.BrokerController#start
public void start() throws Exception {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
              //注册所有的broke
                BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
            } catch (Throwable e) {
                log.error("registerBrokerAll Exception", e);
            }
        }
    }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
}

//该方法主要是遍历nameServer表,Broker消息服务器依次向 NameSerer发送心跳包
   for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                            if (result != null) {
                                registerBrokerResultList.add(result);
                            }
                            log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            countDownLatch.countDown();
                        }
                    }
                });
            }
2 NameServer 处理心跳包

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor网络处理器解析请求类型,如果请求类型REGISTER_BROKER.最终转发到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);
  1. 路由注册需要加写锁,防止并发修改 RoutelnfoManager 中的路由表,首先判断Broker 所属集群是否存在, 如果不存在,则创建,然后将 broker 名加入到集群 Broker集合中
 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);
  1. 维护 BrokerData ,首 先从 brokerAddrTable 根据 BrokerName 尝试获取Broker 信息 ,如不存在,则新建 BrokerData 并放入 brokerAddrTable,registerFirst设置为true ;如果存在,直接替换原先的, registerFirst 设置 false ,表示非第一次注册
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());
            }
        }
    }
}
  1. 如果Broke为Master ,并且 Broker Topic 配置信息发生变化或者是 首次注册,则需要创建或更新 Topic 路由元数据,填充 topicQueueTable 其实就是为默认主题自动注册路由信息
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);
        }
    }
}
  1. 根据 Top ic Config 创建 Queue Data 数据结构 ,然后更新 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);
}

更新 BrokerLivelnfo ,存活 Broker 信息表, BrokeLivelnfo 是执行路由删除的重要依据

3.设计亮点

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

3.3路由删除

Broker 每隔30s 向NameServer发送一个心跳包,心跳包中包含BrokeId,Broker地址,Broker名称、Broker 所属集群名称,Broker 关联 的FilterServer列表. 但是如果 Broker 宕机, NameServer 无法收到心跳包.此时 NameServer 如何来剔除这些失效的 Broker?

NameServer 会每隔10s 扫描 brokerLiveTable 状态表,如果 BrokerLive的LastUpdateTimestamp 的时间戳距当前时间超过 120s ,则认为 Broker 失效,移除该 Broker,关闭Broker连接,并同 更新 topicQueueTable,brokerAddrTabl, brokerLiveTable,filterServerTable

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

1 ) NameServer 定时扫描 brokerLiveTable 检测上次心跳包与 当前系统时间的时间差,如果时间戳大于 120s ,则需要移除 Broker 信息

2 ) Broker 在正常被关闭的情况下,会执行 unr gisterBroker 令。

由于不管是何种方式触发的路由删除,路由删除的方法都是一样的,就是从 topic-QueueTable,brokerAddrTable,brokerLiveTable filterServerTable 删除与 Broker 关的信息

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

遍历 brokerLivelnfo 路由表(HashMap ),检 BrokerLiveInfo的lastUpdateTimestamp上次收到 心跳包的时间如果超过当前时间120s, NameServer 则认为 Broker 已不可用,需要将它移除,关闭 Channel ,然后删除与 Broker 相关的路由信息,路由表维护过程,需要 申请写锁. 后面就是从 topic-QueueTable,brokerAddrTable,brokerLiveTable filterServerTable 删除与 Broker 关的信息,释放锁

3.4路由发现

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

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic
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;
}
  1. 调用 RouterlnfoManager 的方法,从路由 topicQueueTable ,brokerAddrTable,fi terServerTable 中分别填充 TopicRouteData 中的 List<Queu Data>、 List BrokerData>和filterServer 地址表
  2. 如果找到主题对应的路由信息并且该主题为顺序消息,则从 NameServerKVconfig 中获取关于顺序消息相 的配置填充路由信息

如果找不到路由信息 CODE 则使用 TOPIC NOT_EXISTS ,表示没有找到对应的路由

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值