NameServ详解

Nameserv详解

Nameserv概述

Nameserv之于RocketMQ,即服务注册中心之于微服务。Nameserv在RocketMQ体系中是一个Topic路由注册和管理(Producer、Consumer从Nameserv获取topic路由信息),Broker注册和发现的管理者。

Nameserv在RocketMQ体系中主要用于保存元数据、提高Broker可用性。

Nameserv是专门针对RocketMQ开发的轻量级协调者,多个Nameserv节点可以组成一个Nameserv集群,帮助RocketMQ集群达到高可用。主要功能是临时保存、管理Topic路由信息,各个NameServ节点是无状态的,即每两个Nameserv节点之间不通信,互相不知道彼此的存在。在Broker、Producer、Consumer启动时,轮询Nameserv节点,拉取路由信息。

Nameserv核心数据结构和API

Nameserv中保存的数据结构被称为Topic路由信息,Topic路由决定了Topic消息发送到哪些Broker,消费者从哪些Broker消费消息。

具体的数据结构实现代码在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager类:

public class RouteInfoManager {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
    // Broker存活的时间周期,默认120s
    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    // 保存Topic和队列的信息,也叫真正的路由信息。一个Topic全部的Queue可能分布在不同的Broker中,也可能分布在同一个Broker中
    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
    // 保存Broker名称和Broker信息的对应信息
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
    // 集群和Broker的对应关系
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
    // 当前在线的Broker地址和Broker信息的对应关系
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
    // 过滤服务器信息
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
	  ……
}

Nameserv支持的全部API都在org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor类中。DefaultRequestProcessor#processRequest方法是Nameserv处理请求实现方法,可以看到其通过判断RemotingCommand的code与RequestCode中定义的值比对进行相应的处理,RequestCode中一个值代表一种功能或者一个接口。

RequestCode中常见的部分值说明:

  • REGISTER_BROKER:Broker注册自身信息到Nameserv
  • UNREGISTER_BROKER:Broker取消注册自身信息到Nameserv
  • GET_ROUTEINFO_BY_TOPIC:获取Topic路由信息
  • WIPE_WRITE_PERM_OF_BROKER:删除Broker的写权限
  • GET_ALL_TOPIC_LIST_FROM_NAMESERVER:获取全部Topic名字
  • DELETE_TOPIC_IN_NAMESRV:删除Topic名字
  • UPDATE_NAMESRV_CONFIG:更新Nameserv配置,当前配置是实时生效的
  • GET_NAMESRV_CONFIG:获取Nameserv配置

Nameserv架构

Nameserv包含4个功能模块:Topic路由管理模块、Remoting通信模块、定时模块、KV管理模块:

  • Topic路由管理模块:Topic路由决定Topic的分区数据会保存在哪些Broker上。这是Nameserv最核心的模块,Broker启动时将自身信息注册到Nameserv中,方便生产者和消费者获取。生产者、消费者启动和间隔的心跳时间会获取Topic最新的路由信息,以此发送或者接受消息。
  • Remoting通信模块:是基于Netty的一个网络通信封装,整个RocketMQ的公共模块在RocketMQ各个组件之间担任通信任务。
  • 定时任务模块:其实在Nameserv中定时任务并没有独立成一个模块,而是由*org.apache.rocketmq.namesrv.NamesrvController#initialize()*中调用的几个定时任务组成的。
  • KV管理模块:Nameserv维护一个全局的KV配置模块,方便全局配置。

Nameserv启动流程

在这里插入图片描述

Nameserv的启动流程分为以下几个步骤:

1、脚本和启动参数配置:

启动命令:nohup ./bin/mqnameserv -c ./conf/nameserv.conf> /devnull 2>&1 &。通过脚本配置启动基本参数,比如配置文件路径、JVM参数。调用NameservStartup.main()方法,解析命令行的参数,将处理好的参数转化为Java实例,传递给NameservController

2、new-个NameservController加载命令行传递的配置参数,调用controller.initialize()方法初始化NameservController

    public boolean initialize() {
        // 1、加载KV配置,主要是从本地文件中加载KV配置到内存中
        this.kvConfigManager.load();
        // 2、初始化Netty通信实例,通过参数nettyServiceConfig会启动9876端口监听
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

        this.remotingExecutor =
            Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
        // 3、注册DefaultRequestProcessor到remotingServer,用于netty channelhandler中调用
        this.registerProcessor();
        // 4、Nameserv主动监测Broker是否可用,如果不可用,则将broker以及其相关信息剔除,生产者、消费者也能通过心跳发现被提出的路由,从而感知Broker下线
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.routeInfoManager.scanNotActiveBroker();
            }
        }, 5, 10, TimeUnit.SECONDS);
        // 5、定时打印配置信息到日志
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                NamesrvController.this.kvConfigManager.printAllPeriodically();
            }
        }, 1, 10, TimeUnit.MINUTES);
        // 6、SSL配置
        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配置,通过加载NameservConig中的KV配置文件地址加载到KVConfigManagerconfigTableHashMap(即内存)中:
    private final HashMap<String/* Namespace */, HashMap<String/* Key */, String/* Value */>> configTable = new HashMap<String, HashMap<String, String>>();
			public void load() {
        String content = null;
        try {
            content = MixAll.file2String(this.namesrvController.getNamesrvConfig().getKvConfigPath());
        } catch (IOException e) {
            log.warn("Load KV config table exception", e);
        }
        if (content != null) {
            KVConfigSerializeWrapper kvConfigSerializeWrapper =
                KVConfigSerializeWrapper.fromJson(content, KVConfigSerializeWrapper.class);
            if (null != kvConfigSerializeWrapper) {
                this.configTable.putAll(kvConfigSerializeWrapper.getConfigTable());
                log.info("load KV config table OK");
            }
        }
    }
  • 初始化Netty通信实例,通过参数nettyServiceConfig会启动9876端口监听
  • 注册DefaultRequestProcessor到remotingServer,用于netty channelhandler中调用
  • Nameserv主动监测Broker是否可用,如果不可用,则将broker以及其相关信息剔除,生产者、消费者也能通过心跳发现被剔除的路由,从而感知Broker下线,每10s执行一次。
    public void scanNotActiveBroker() {
        // 遍历brokerLiveTable Map集合
        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, BrokerLiveInfo> next = it.next();
            long last = next.getValue().getLastUpdateTimestamp();
            // 判断最近更新时间距离现在是否超过Broker存活的时间周期
            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
                // 关闭channel
                RemotingUtil.closeChannel(next.getValue().getChannel());
                // 将这个Broker存活信息从map中移除
                it.remove();
                log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
                // 移除topicQueueTable、brokerAddrTable、clusterAddrTable、brokerLiveTable、filterServerTable中对应broker的关联信息
                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
            }
        }
    }

该方法会扫描全部已经注册的Broker,依次将每一个Broker心跳的最后更新时间和当前时间做对比,如果Broker心跳的最后更新时间超过 BROKER_CHANNEL_EXPIRED_TIME(1000 × 60 × 2 = 120s),则将Broker剔除

onChannelDestroy方法很好理解,就是遍历topicQueueTable、brokerAddrTable、clusterAddrTable、brokerLiveTable、filterServerTable,找到下线的broker在这四个集合中的关联信息并移除。

BrokerLiveInfo类是RouteInfoManager类的内部类,用于记录Broker存活时间相关信息:

class BrokerLiveInfo {
  	// 最近更新时间
    private long lastUpdateTimestamp;
  	// 数据版本
    private DataVersion dataVersion;
  	// brokerchannel
    private Channel channel;
  	// 是否有服务地址
    private String haServerAddr;
……
}
  • 定时打印配置信息到日志,每隔十分钟定时遍历KVConfigManagerconfigTable中的配置打印出来
  • 配置SSL配置变动监听器

3、NameservController在初始化后添加JVM HOOK。在Nameserv关闭前会调用HOOK,HOOK中会调用*NameservController.shutdown()*方法来关闭整个Nameserv服务。通常

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

4、调用*NameservController.start()*方法,启动整个Nameserv。其实就是启动nettyRemotingServer服务和FileWatchService线程

至此,整个Nameserv启动完成。

RocketMQ的路由原理

路由注册

Nameserv获取的Topic路由信息来自Broker定时心跳,心跳时Broker将Topic信息和其他信息发送到Nameserv。Nameserv通过RequestCode.REGISTER_BROKER接口将心跳中的Broker信息和Topic存储到Nameserv中。

Nameserv接受请求后,以3.0.11版本作为分水岭,按照版本分别做不同的处理,相关代码如下:

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

registerBrokerWithFilterServer方法源码解析如下:

    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();
                // 获取注册的broker的集群下的所有broker,并添加注册的broker
                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
                if (null == brokerNames) {
                    brokerNames = new HashSet<String>();
                    this.clusterAddrTable.put(clusterName, brokerNames);
                }
                brokerNames.add(brokerName);

                boolean registerFirst = false;
                // 获取brokerAddrTable中注册的broker的BrokerData信息,不存在则保存。
                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);
                }
                // 遍历注册broker的broker地址,如果存在key为1(Slave)的记录则移除,再添加Master(key为0)的记录。
                // 同样的value在brokerAddrTable中必须只有一条记录。
                Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
                //The same IP:PORT must only have one record in brokerAddrTable
              	// 相同的地址,但brokerId不同只允许有一个
                Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
                while (it.hasNext()) {
                    Entry<Long, String> item = it.next();
                    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
                        it.remove();
                    }
                }

                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
                registerFirst = registerFirst || (null == oldAddr);

                if (null != topicConfigWrapper
                        && MixAll.MASTER_ID == brokerId) {
                    // 判断Broker的Topic配置是否变更,根据Broker的TopicConfigSerializeWrapper的DataVersion信息判断
                    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
                            || registerFirst) {
                        // 如果Broker Topic配置有改动或者第一次注册,则根据TopicConfig创建或更新topicQueueTable中注册的broker的QueueData信息
                        ConcurrentMap<String, TopicConfig> tcTable =
                                topicConfigWrapper.getTopicConfigTable();
                        if (tcTable != null) {
                            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                                // 很简单,遍历创建或更新
                                this.createAndUpdateQueueData(brokerName, entry.getValue());
                            }
                        }
                    }
                }
                // 保存或更新brokerLiveTable中注册的broker的BrokerLiveInfo
                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);
                }
                // 保存或更新filterServerList
                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;
    }

    public boolean isBrokerTopicConfigChanged(final String brokerAddr, final DataVersion dataVersion) {
        // 从brokerLiveTable Broker存活时间信息中获取DataVersion。
        DataVersion prev = queryBrokerTopicConfig(brokerAddr);
        // 如果Broker不存在brokerLiveTable中,获取之前的DataVersion和传入的DataVersion不一致表示有变动
        return null == prev || !prev.equals(dataVersion);
    }

    public DataVersion queryBrokerTopicConfig(final String brokerAddr) {
        BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr);
        if (prev != null) {
            return prev.getDataVersion();
        }
        return null;
    }

registerBroker方法实际调用的注册Broker的实现就是上面的方法,只是少了filterServerList参数,其他内容一致。

至此,Nameserv完成Broker注册,并将Broker的相关信息保存在内存中。

路由剔除

如果Broker长久没有心跳或者宕机,那么Nameserv就会将这些不提供服务的Boker剔除。同时生产者和消费者在与Nameserv定时更新topic路由信息后也会感知被踢掉的Broker,如此Broker扩容或者宕机对生产、消费无感知的情况下就处理完了。

Nameserv有两种剔除Broker的方式:

  • Broker主动关闭时,会调用Nameserv的取消注册Broker的接口RequestCode.UNREGISTER_BROKER将自身从集群中删除,详细可以看*org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor.processRequest()*方法,如下:
            case RequestCode.UNREGISTER_BROKER:
                return this.unregisterBroker(ctx, request);

unregisterBroker方法实际调用的实现是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unregisterBroker方法,很简单,其实就是跟路由注册反着来,将RouteInfoManager中保存Broker的各类信息:topicQueueTablebrokerAddrTableclusterAddrTablebrokerLiveTablefilterServerTable中把对应的Broker关联信息删除。

  • Namserv通过定时扫描已经下线的Broker,将其主动剔除,实现在*org.apache.rocketmq.namesrv.NamesrvController.initialize()*方法中,详细解析请看上面启动流程第四步

egisterBroker方法实际调用的实现是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unregisterBroker方法,很简单,其实就是跟路由注册反着来,将RouteInfoManager*中保存Broker的各类信息:topicQueueTablebrokerAddrTableclusterAddrTablebrokerLiveTablefilterServerTable中把对应的Broker关联信息删除。

  • Namserv通过定时扫描已经下线的Broker,将其主动剔除,实现在*org.apache.rocketmq.namesrv.NamesrvController.initialize()*方法中,详细解析请看上面启动流程第四步
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值