通俗易懂系列 | RocketMQ源码分析之NameServer

语雀传送门

⚠️注意:本系列文章,计划按照NameServer、Broker、Producer、Consumer的顺序发布,因此本文中Broker和Producer链接的文章还未完成。

1、前言

相信大家都知道NameServer在RocketMq中的作用,它是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。那么NameServer中存储的是什么数据呢?作为一款高性能的消息中间件,如何避免NameServer的单点故障,提供高可用性呢?让我们带着上述疑问,一起开启NameServer的学习。

2、为什么需要NameServer?

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

3、NameServer架构设计

了解到为什么需要NameServer之后,接下来学习一下NameServer的架构设计。RocketMQ的逻辑部署图如下图所示。

image.png

消息服务器在启动时向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer与每台Broker服务器保持长连接,并间隔30s检测Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除。但是路由变化不会立马通知消息生产者,为什么要这样设计呢?这是为了降低NameServer实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性(这种设计思想很👍,将更新操作留到真正需要更新的时候去做。怎么理解,可能当路由信息发生变化时,并没有消息生产者需要发送消息,即没有人关心路由信息的变化,等到生产者真正去发送消息时,再去更新),这部分在后期的【通俗易懂系列 | RocketMQ:Producer】 中会有详细的描述。​

NameServer本身的高可用可通过部署多台NameServer服务器来实现,但彼此之间互不通信,也就是NameServer服务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,大家可以思考一下为什么不会造成影响?其实很简单,想想消息生产者的目的是啥?生产者的目的是将消息发送到Broker,至于发送到哪一个,生产者是不关心的。这也是RocketMQ NameServer设计的一个亮点,RocketMQ NameServer设计追求简单高效。

4、NameServer启动流程

NameServer的启动过程是围绕NamesrvController进行展开的,大致可以分为三步:NamesrvController的创建、初始化和启动。NameServer的启动类是:org.apache.rocketmq.namesrv.NamesrvStartup。接下来从源码的角度窥探一下NameServer启动流程,本文并不会对整个启动流程进行详细分析,重点关注以下点。

4.1、NamesrvController创建

NameServer在启动时,首先会创建NamesrvController,而创建NamesrvController就干了一件事,解析配置文件,填充NameServerConfig、NettyServerConfig属性值。

// TODO 2020-02-16 11:58 解析NamesrvConfig和NettyServerConfig配置文件
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
// TODO 2020-03-05 14:55 NameServer监听端口,9876
nettyServerConfig.setListenPort(9876);

// 省略部分代码。。。。。。

// TODO 2020-02-16 13:27 NameServerController实例为NameServer核心控制器
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

// 保存所有的属性到Configuration#allConfigs中
controller.getConfiguration().registerConfig(properties);

NamesrvConfig可以理解为,NameServer的业务属性配置;NettyServerConfig可以理解为NameServer的网络属性配置。

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

4.2、NamesrvController初始化

NamesrvController创建完成之后,便进行初始化,主要包括:KV配置加载,注册网络请求处理器和心跳检测。

// 加载KV配置
this.kvConfigManager.load();
// 创建NettyServer网络处理服务
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
// 创建网络处理线程池
this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// TODO 2020-02-19 20:28 注册处理网络请求处理器
this.registerProcessor();

// ZHOUJ 2020-03-06 18:10 定时任务1:[心跳检测]NameServer每隔10s扫描一次Broker,移除处于不激活状态的Broker
this.scheduledExecutorService.scheduleAtFixedRate(() -> NamesrvController.this.routeInfoManager.scanNotActiveBroker(), 5, 10, TimeUnit.SECONDS);

// TODO 2020-02-16 13:29 定时任务2:nameServer每隔10分钟打印一次KV配置。
this.scheduledExecutorService.scheduleAtFixedRate(() -> NamesrvController.this.kvConfigManager.printAllPeriodically(), 1, 10, TimeUnit.MINUTES);

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

Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
    controller.shutdown();
    return null;
}));

4.3、NamesrvController启动

NamesrvController的启动很简单,启动了NettyServer网络处理服务,以便监听网络请求。

public void start() throws Exception {
    this.remotingServer.start();
}

至此NameServer的启动流程结束!

5、NameServer功能介绍

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

5.1、NameServer路由注册

RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。Broker启动时向集群中所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,这部分在后期的【通俗易懂系列 | RocketMQ:Broker】,NameServer收到Broker心跳包时会更新相关路由信息,如果连续120s没有收到心跳包,NameServer将移除该Broker的路由信息同时关闭Socket连接。

NameServer中的所有的网络请求都是由org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor进行处理,DefaultRequestProcessor会根据请求类型进行分发,请求类型为RequestCode.REGISTER_BROKER,则请求最终转发到RouteInfoManager#registerBroker,经过一番铺垫,主角RouteInfoManager“闪亮登场“了。

RouteInfoManager是NameServer路由实现类,在了解路由注册之前,我们首先看一下NameServer到底存储哪些路由信息。

// RouteInfoManager路由元数据
/**
 * Topic消息队列路由信息,消息发送时根据路由表进行负载均衡
 */
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
/**
 * Broker基础信息,包含brokerName、所属集群名称、主备Broker地址。
 */
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
/**
 * Broker集群信息,存储集群中所有Broker名称。
 */
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
/**
 * Broker状态信息。NameServer每次收到心跳包时会替换该信息。
 */
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
/**
 * Broker上的FilterServer列表,用于类模式消息过滤
 */
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

Tips 网络跟踪方法:每一个请求,RocketMQ都会定义一个RequestCode,然后在服务端会对应相应的网络处理器,只需整库搜索RequestCode即可找到相应的处理逻辑。

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

Broker心跳包请求最终转发到RouteInfoManager#registerBroker,接下来分析一下心跳包处理流程,代码如下:

try {
    // TODO 2020-02-16 14:06 路由注册需要加写锁,防止并发修改RouteInfoManager中的路由表。
    this.lock.writeLock().lockInterruptibly();

    // TODO 2020-03-05 21:19 首先判断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;
    // TODO 2020-02-16 14:07 维护BrokerData信息,首先从brokerAddrTable根据BrokerName尝试获取Broker信息,
    //  如果不存在,则新建BrokerData并放入到brokerAddrTable, registerFirst设置为true;
    BrokerData brokerData = this.brokerAddrTable.get(brokerName);
    if (null == brokerData) {
        registerFirst = true;
        brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
        this.brokerAddrTable.put(brokerName, brokerData);
    }
    // TODO 2020-03-06 16:08 [主从切换]如果当前添加的broker已经存在于brokerAddrs,但是brokerId不相等,则把旧的broker移除
    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
    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();
        }
    }
    // TODO 2020-02-16 14:11 替换原先的broker
    String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
    registerFirst = registerFirst || (null == oldAddr);
    // TODO 2020-03-06 16:23 如果Broker为Master,并且配置信息发生变化或者是初次注册
    if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId) { 
        if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion()) || registerFirst) {
            // TODO 2020-03-06 16:24 获取所有topic配置
            ConcurrentMap<String, TopicConfig> tcTable = topicConfigWrapper.getTopicConfigTable();
            if (tcTable != null) {
                for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                    // TODO 2021/1/16 16:52:42 创建或更新Topic路由元数据
                    this.createAndUpdateQueueData(brokerName, entry.getValue());
                }
            }
        }
    }

    // TODO 2020-02-16 14:18 更新存活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);
    }

    // TODO 2020-03-06 16:45 注册消息过滤器
    if (filterServerList != null) {
        if (filterServerList.isEmpty()) {
            this.filterServerTable.remove(brokerAddr);
        } else {
            this.filterServerTable.put(brokerAddr, filterServerList);
        }
    }

    // TODO 2020-03-06 16:46 如果此Broker为从节点,则需要查找该Broker的Master的节点信息,并更新对应的masterAddr属性。
    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();
}

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

5.2、NameServer路由发现

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

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader = (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);

    // TODO 2020-02-16 14:47 调用RouterInfoManager的方法,获取路由信息
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

    if (topicRouteData != null) {
        // TODO 2020-02-16 14:48 如果为顺序消息,从NameServer KVconfig中获取关于顺序消息相关的配置填充路由信息
        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;
}

5.3、NameServer路由删除

根据上面介绍,Broker每隔30s向NameServer发送一个心跳包。但是如果Broker宕机,NameServer无法收到心跳包,此时NameServer如何来剔除这些失效的Broker呢?NameServer会每隔10s扫描brokerLiveTable状态表,如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接,并同时更新相关路由信息。

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

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

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

不管是何种方式触发的路由删除,最终执行都会执行到 RouteInfoManager#scanNotActiveBroker方法

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();
        // TODO 2020-02-16 14:24 如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接
        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);
            // TODO 2020-02-19 20:35 删除与该Broker相关的路由信息,路由表维护过程,需要申请写锁。
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
        }
    }
}

删除的逻辑很简单,遍历brokerLiveInfo路由表,检测BrokerLiveInfo的lastUpdateTimestamp上次收到心跳包的时间如果超过当前时间120s, NameServer则认为该Broker已不可用,故需要将它移除,关闭Channel,然后删除与该Broker相关的路由信息,路由表维护过程,需要申请写锁。

6、总结

本文主要介绍了NameServer的启动流程,路由注册、发现和删除功能。路由发现机制可以用下图来形象解释。

image.png

NameServer启动及路由注册发现与删除机制就介绍完了。

 

思考: 通过前面的介绍,会发现NameServer路由发现的机制存在这样一种情况:NameServer需要等Broker失效至少120s才能将该Broker从路由表中移除掉,那如果在Broker故障期间,消息生产者Producer根据主题获取到的路由信息包含已经宕机的Broker,会导致消息发送失败,那这种情况怎么办,岂不是消息发送不是高可用的?

这里就不做阐述了,答案会在【通俗易懂系列 | RocketMQ:Producer】中揭晓!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值