RocketMQ-NameServer详解

前言

​ RocketMQ架构上主要分为四部分, Broker、Producer、Consumer、NameServer,其他三个都会与NameServer进行通信。

Producer:

​ **消息发布的角色,可集群部署。**通过NameServer集群获得Topic的路由信息,包括Topic下面有哪些Queue,这些Queue分布在哪些Broker上等。(Producer只会将消息发送到Master节点,因此只需与Master节点建立连接)。

Consumer:

​ **消息消费的角色,可集群部署。**通过NameServer集群获得Topic的路由信息,连接到对应的Broker上拉取和消费消息。(Master和Slave都可以拉取消息,因此Consumer会与Master和Slave都建立连接)。

Broker:

主要负责消息的存储、投递和查询以及服务高可用保证。

介绍

​ NameServer 是专为 RocketMQ 设计的轻量级名称服务,也是一个简单的Topic路由注册中心。具有简单、可集群横向扩展、无状态,节点之间互不通信等特点。

NameServer 通常以集群的方式部署,各实例间相互不进行信息通讯,只是互为备份,达到高可用的效果。

NameServer具有两大功能:Broker管理和topic路由信息管理。

Broker管理:

​ **NameServer接受Broker的注册请求,处理请求数据作为路由信息的基础数据。**对broker进行心跳检测机制,检测是否还存活(120s);

Topic路由信息管理:

​ 每个NameServer都保存整个Broker集群的路由信息,用于Producer和Conumser查询的路由信息,从而进行消息的投递和消费。

​ NameServer 仅仅处理其他模块的请求,而不会主动向其他模块发起请求。它其实本质上就是一个 NettyServer,它主要有 3 个模块:Topic 路由管理模块(RouteInfoManager)、通信模块(DefaultRequestProcessorClusterTestRequestProcessor)、KV 数据存储模块(KVConfigManager)。

RouteInfoManager

​ **RouteInfoManager 中存储 5 个 HashMap,这就是 NameServer 中主要存储的数据。它们仅存在于内存中,并不会持久化。**其中数据内容如下:

  • **topicQueueTable:保存 Topic 的队列信息,也是真正的路由信息。**队列信息中包含了其所在的 Broker 名称和读写队列数量。
  • brokerAddrTable:保存 Broker 信息,包含其名称、集群名称、主备 Broker 地址。
  • clusterAddrTable:保存 Cluster信息,包含每个集群中所有的 Broker 名称列表。
  • brokerLiveTable:Broker 状态信息,包含当前所有存活的 Broker,和它们最后一次上报心跳的时间。
  • filterServerTable:Broker 上的 FilterServer 列表,用于类模式消息过滤,该机制在 4.4 版本后被废弃。

RequestProcessor 继承了 AsyncNettyRequestProcessor。作为 NameServer 的请求处理器,根据不同种类的请求做不同类型的处理。 其中 KV_CONFIG 类型的请求用于 KVConfig 模块,当前不会用到。其他请求类型由 Broker 和 Producer、Consumer 发起。

​ KVConfigManager 内部保存了一个二级 HashMapconfigTable,并且会将该对象进行持久化。

心跳机制

Broker
  • 每隔 30s 向 NameServer 集群的每台机器都发送心跳包,包含自身 Topic 队列的路由信息。
  • 当有 Topic 改动(创建/更新),Broker 会立即发送 Topic 增量信息到 NameServer,同时触发 NameServer 的数据版本号发生变更(+1)。
NameServer
  • 将路由信息保存在内存中。它只被其他模块调用(被 Broker 上传,被客户端拉取),不会主动调用其他模块。
  • 启动一个定时任务线程,每隔 10s 扫描 brokerAddrTable 中所有的 Broker 上次发送心跳时间,如果超过 120s 没有收到心跳,则从存活 Broker 表中移除该 Broker。
Client
  • 生产者第一次发送消息时,向 NameServer 拉取该 Topic 的路由信息。
  • 消费者启动过程中会向 NameServer 请求 Topic 路由信息。
  • 每隔 30s 向 NameServer 发送请求,获取它们要生产/消费的 Topic 的路由信息。

启动流程

  1. 由启动脚本调用 NamesrvStartup#main 函数触发启动流程
  2. NamesrvStartup#createNamesrvController 函数中先解析命令行参数,然后初始化 NameServer 和 Netty remote server 配置,最后创建 NamesrvController 的实例。
  3. NamesrvStartup#start 初始化 NamesrvController;调用 NamesrvController#start() 方法,启动 Netty remoting server;最后注册关闭钩子函数,在 JVM 线程关闭之前,关闭 Netty remoting server 和处理线程池,关闭定时任务线程。

NamesrvController 实例是 NameServer 的核心控制器,它的初始化方法 initialize() 先加载 KVConfig manager,然后初始化 Netty remoting server。最后添加 2 个定时任务:一个每 10s 打印一次 KV 配置,一个每 10s 扫描 Broker 列表,移除掉线的 Broker。

为什么选择重新开发一个NameServer?

​ RocketMQ的架构设计决定了只需一个轻量级的元数据服务器,只需保持最终一致,所以AP模式直接pass。

NameServer互相独立,彼此没有通信关系,由于Broker向每个NameServer注册自己的路由信息,所以每个NameServer都保存一份完整的路由信息。

​ 单台NameServer挂掉,Broker仍然可以向其它NameServer同步路由信息,不影响其他NameServer,所以Producer,Consumer仍然可以动态感知Broker的路由的信息。

源码分析

NameServer的路由数据来源是broker注册提供,然后内部加工处理,而路由的数据的使用者是producer和consumer。

1、路由数据结构

​ RouteInfoManager是NameServer核心逻辑类,其代码作用就是维护路由信息管理,提供路由注册/查询等核心功能。

由于路由信息都是保存在NameServer应用内存里,其本质就是维护HashMap,而为了防止并发操作,添加了ReentrantReadWriteLock读写锁。

​ RouteInfoManager源码结构如下:

img

QueueData 属性解析:
/**
 * 队列信息
 */
public class QueueData implements Comparable<QueueData> {
    // 队列所属的Broker名称
    private String brokerName;
    // 读队列数量 默认:16
    private int readQueueNums;
    // 写队列数量 默认:16
    private int writeQueueNums;
    //todo Topic的读写权限(2是写 4是读 6是读写)
    private int perm;
    /** 同步复制还是异步复制--对应TopicConfig.topicSysFlag
     * {@link org.apache.rocketmq.common.sysflag.TopicSysFlag}
     */
    private int topicSynFlag;
        ...省略...
 }

map: topicQueueTable 数据格式demo(json):

{
    "TopicTest":[
        {
            "brokerName":"broker-a",
            "perm":6,
            "readQueueNums":4,
            "topicSynFlag":0,
            "writeQueueNums":4
        }
    ]
}
BrokerData 属性解析:
/**
 * broker的数据:Master与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0 表示Master,非0表示Slave。
 */
public class BrokerData implements Comparable<BrokerData> {
    // broker所属集群
    private String cluster;
    // brokerName
    private String brokerName;
    // 同一个brokerName下可以有一个Master和多个Slave,所以brokerAddrs是一个集合
    // brokerld=O表示 Master,大于 O表示从 Slave
    private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
    // 用于查找broker地址
    private final Random random = new Random();
    ...省略...
 }

map: brokerAddrTable 数据格式demo(json):

{
    "broker-a":{
        "brokerAddrs":{
            "0":"172.16.62.75:10911"
        },
        "brokerName":"broker-a",
        "cluster":"DefaultCluster"
    }
}
BrokerLiveInfo 属性解析:
/**
 *  存放存活的Broker信息,当前存活的 Broker,该信息不是实时的,NameServer 每10S扫描一次所有的 broker,根据心跳包的时间得知 broker的状态,
 *  该机制也是导致当一个 Broker 进程假死后,消息生产者无法立即感知,可能继续向其发送消息,导致失败(非高可用)
 */
class BrokerLiveInfo {
    //最后一次更新时间
    private long lastUpdateTimestamp;
    //版本号信息
    private DataVersion dataVersion;
    //Netty的Channel
    private Channel channel;
    //HA Broker的地址 是Slave从Master拉取数据时链接的地址,由brokerIp2+HA端口构成
    private String haServerAddr;
    ...省略...
 }

map: brokerLiveTable 数据格式demo(json):

 {
    "172.16.62.75:10911":{
        "channel":{
            "active":true,
            "inputShutdown":false,
            "open":true,
            "outputShutdown":false,
            "registered":true,
            "writable":true
        },
        "dataVersion":{
            "counter":2,
            "timestamp":1630907813571
        },
        "haServerAddr":"172.16.62.75:10912",
        "lastUpdateTimestamp":1630907814074
    }
}

brokerAddrTable -Map 数据格式demo(json)

{"DefaultCluster":["broker-a"]}

​ 从RouteInfoManager维护的HashMap数据结构和QueueData、BrokerData、BrokerLiveInfo类属性得知,NameServer维护的信息既简单但极其重要。

2、路由注册

路由注册流程:

roker主动注册的几种情况:

  1. 启动时向集群中所有的NameServer注册
  2. 定时30s向集群中所有NameServer发送心跳包注册
  3. 当broker中topic信息发送变更(新增/修改/删除)发送心跳包注册。

​ NameServer中注册的核心处理逻辑是RouteInfoManager#registerBroker

public RegisterBrokerResult registerBroker(
    final String clusterName, final String brokerAddr,
    final String brokerName, final long brokerId,
    final String haServerAddr,
    //TopicConfigSerializeWrapper比较复杂的数据结构,主要包含了broker上所有的topic信息
    final TopicConfigSerializeWrapper topicConfigWrapper,
    final List<String> filterServerList, final Channel channel) {
    RegisterBrokerResult result = new RegisterBrokerResult();
    try {
        try {
            this.lock.writeLock().lockInterruptibly(); // 锁
            //1: 此处维护 clusterAddrTable 集群元数据
            Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
            if (null == brokerNames) {
                brokerNames = new HashSet<String>();
                this.clusterAddrTable.put(clusterName, brokerNames);
            }
            brokerNames.add(brokerName);
            //2:此处维护 brokerAddrTable broker元数据
            boolean registerFirst = false;//是否第一次注册(如果Topic配置信息发生变更或者该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);
            }
            //3: 此处维护 topicQueueTable  主题队列数据,数据更新操作方法在:createAndUpdateQueueData
            String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
            registerFirst = registerFirst || (null == oldAddr);
            if (null != topicConfigWrapper
                && MixAll.MASTER_ID == brokerId) { //小知识点:只处理主节点请求,因为备节点的topic信息是同步主节点的
                // 如果Topic配置信息发生变更或者该broker为第一次注册
                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());
                        }
                    }
                }
            }
             //4: 此处维护:brokerLiveTable数据,关键点:BrokerLiveInfo构造器第一个参数:System.currentTimeMillis(),用于存活判断
            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);
            }
            //5-维护:filterServerTable 数据
            if (filterServerList != null) {
                if (filterServerList.isEmpty()) {
                    this.filterServerTable.remove(brokerAddr);
                } else {
                    this.filterServerTable.put(brokerAddr, filterServerList);
                }
            }
            //返回值(如果当前broker为slave节点)则将haServerAddr、masterAddr等信息设置到result返回值中
            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;
}

​ 从源码中分析,Broker注册的路由信息对于NameServer来说,其实就是维护clusterAddrTable、brokerAddrTable、topicQueueTable、brokerLiveTable、filterServerTable。

3、路由删除

​ **路由删除有两种方式,一种是broker主动上报删除,一种是NameServer主动删除。**从根本上来说,是删除clusterAddrTable、brokerAddrTable、topicQueueTable、brokerLiveTable、filterServerTable相关信息.

1)Broker主动上报删除

Broker在正常被关闭的情况下,会执行unregisterBroker指令。向NameServer发送取消注册请求,之后执行删除信息操作(加写锁)。

​ 1-1)直接删除 brokerLiveTable 信息,无需判断时间

​ 1-2)删除 filterServerTable 信息

​ 1-3)维护删除 brokerAddrTable 信息

​ 1-4)维护删除 clusterAddrTable 信息

​ 1-5)根据 BrokerName,从 topicQueueTable 中移除该 Broker 的队列。也就是维护删除 topicQueueTable 信息

2)NameServer主动删除:

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

​ 2-1)判断是否有时间戳大于120s的,若是有,则执行移除操作

​ 2-2)移除前,加读锁,查询需要删除的broker信息。

​ 2-3)根据broker信息brokerAddrFound(加写锁),删除相关信息brokerLiveTable、filterServerTable、brokerAddrTable、clusterAddrTable、topicQueueTable 信息。

4、路由发现

​ 当Topic路由出现变化后,NameServer不会主动推送给客户端,而是由生产端和消费端定时拉取主题最新的路由,所以路由信息非实时的。NameServer 收到客户端获取路由信息请求后,调用 DefaultRequestProcessor#getRouteInfoByTopic() 方法,返回 Topic 路由信息。

​ 源码中,实际上是调用 RouteInfoManager#pickupTopicRouteData() 方法从topicQueueTable、brokerAddrTable、filterServerTable这些Map中查询数据,组装给TopicRouteData,然后返回给客户端使用。

如果该主题为顺序消息,从 KVConfig 中获取顺序消息相关的配置,填充进 TopicRouteData 对象。之后将 TopicRouteData 对象编码,并返回给客户端。

RouteInfoManager#pickupTopicRouteData

public TopicRouteData pickupTopicRouteData(final String topic) {
    TopicRouteData topicRouteData = new TopicRouteData();
    boolean foundQueueData = false;
    boolean foundBrokerData = false;
    Set<String> brokerNameSet = new HashSet<String>();
    List<BrokerData> brokerDataList = new LinkedList<BrokerData>();
    topicRouteData.setBrokerDatas(brokerDataList);
    HashMap<String, List<String>> filterServerMap = new HashMap<String, List<String>>();
    topicRouteData.setFilterServerTable(filterServerMap);
    try {
        try {
            this.lock.readLock().lockInterruptibly();
            List<QueueData> queueDataList = this.topicQueueTable.get(topic);
            if (queueDataList != null) {
                topicRouteData.setQueueDatas(queueDataList);
                foundQueueData = true;
                Iterator<QueueData> it = queueDataList.iterator();
                while (it.hasNext()) {
                    QueueData qd = it.next();
                    brokerNameSet.add(qd.getBrokerName());
                }
                // 处理构建:BrokerData数据
                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;
                        for (final String brokerAddr : brokerDataClone.getBrokerAddrs().values()) {
                            List<String> filterServerList = this.filterServerTable.get(brokerAddr);
                            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;
}

public class TopicRouteData extends RemotingSerializable {
    //topic排序的配置,和"ORDER_TOPIC_CONFIG"这个NameSpace有关,参照DefaultRequestProcessor#getRouteInfoByTopic,后续可讲解此小知识点
    private String orderTopicConf;
    // topic 队列元数据
    private List<QueueData> queueDatas;
    // topic分布的 broker元数据
    private List<BrokerData> brokerDatas;
    // broker上过滤服务器地址列表
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
    ...省略...
}
为什么不用ConcurrentHashMap存储呢?

​ 首先ConcurrentHashMap如果是1.8之前,初始化容量之后,是不能扩容的。其容量默认是16,且锁的粒度是segment,那么最大并发度也就是容量大小。

​ 如果是1.8之后的版本是支持扩容,且在加锁方面有优化,调整为synchronized+CAS,但是同时会要求jdk版本。

​ 最主要是RocketMQ 中的路由信息比较多。

疑问

1、假设Broker异常宕机,导致生产者发送消息失败怎么解决?

​ 假设Broker异常宕机,NameServer至少等120s才将该Broker从路由信息中剔除,在Broker故障期间,消息生产者Producer根据topic获取到的路由信息包含已经宕机的Broker,会导致消息在短时间内发送失败。

​ 那么生产者是否也有相关的尝试策略?

之后再做补充…

总结

​ NameServer作为RocketMQ的“大脑”,保存着集群MQ的路由信息,具体就是记录维护Topic、Broker的信息,及监控Broker的运行状态,为client提供路由能力。

​ 而从源代码的角度总结:NameServer就是维护了多个HashMap,Broker的注册,Client的查询都是围绕其Map操作,当然为了解决并发问题添加了ReentrantReadWriteLock(读写锁)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值