RocketMq-02-路由中心NameServer

584 阅读8分钟

上一篇:RocketMq-01-基于IDEA单机部署服务

内容简介

上文简单介绍了如何基于idea单机部署rocketmq服务,本章将介绍一下NameServer,包含以下几点:

  • 路由元信息存储结构
  • 路由信息注册和发现
  • 路由信息注销及故障移除

路由信息

    public RouteInfoManager() {
        /** 主题与队列关系 记录一个主题的队列分布在哪些Broker上 每个Broker上存在该主题的队列个数 */
        this.topicQueueTable = new HashMap<String/* topic */, List<QueueData>>(1024);
        /** 所有 Broker 信息 */
        this.brokerAddrTable = new HashMap<String/* brokerName */, BrokerData>(128);
        /** broker 集群信息 每个集群包含的broker名称列表 */
        this.clusterAddrTable = new HashMap<String/* clusterName */, Set<String/* brokerName */>>(32);
        /** 当前存活的broker信息 每隔10秒定时扫描维护 所以存在延迟问题 */
        this.brokerLiveTable = new HashMap<String/* brokerAddr */, BrokerLiveInfo>(256);
        /** broker上存储的过滤列表,用于类模式消息过滤 */
        this.filterServerTable = new HashMap<String/* brokerAddr */, List<String>/* Filter Server */>(256);
    }

NameServer主要为生产者和消费者提供关于topic的路由信息,NameServer通过org.apache.rocketmq.namesrv.routeinfo.RoutelnfoManager实现路由功能,其构造函数初始化了五个路由表,路由信息就是存储在这些属性中。接下来让我们依次看看这些路由表中到底存储了哪些数据。

TopicQueueTable

TopicQueueTable维护了topic与broker之间的关系,每一个消息队列都有一个brokerName表示队列对应的broker名称,每个broker为topic默认创建16个读写队列,用readQueueNums和writeQueueNums表示,其运行时数据结构大致如下:

topicQueueTable: {
	"topic-1": [{
		"brokerName": "broker-a",
		"writeQueueNums": 16,
		"readQueueNums": 16,
		"topicSynFlag": 0, //同步方式
		"perm": 6  //读写权限
	}, {
		"brokerName": "broker-b",
		"writeQueueNums": 16,
		"readQueueNums": 16,
		"topicSynFlag": 0,
		"perm": 6
	}],
	"topic-2": []
}

BrokerAddrTable

BrokerAddrTable维护了broker的基础信息,brokerName作为key,brokerData中还维护了相同brokerName的broker的地址映射:

HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs

brokerName相同的多台broker以master-slave模式部署,通过brokerId区分,brokerId=0代表master,其余代表slave,而brokerAddrs就是维护了brokerId和其ip地址的映射,BrokerAddrTable的运行时数据结构大致如下:

brokerAddrTable: {
	"broker-b": {
		"brokerAddrs": {
			0: "127.0.0.1:9999",
			1: "127.0.0.1:8888",
			2: "127.0.0.1:7777"
		},
		"brokerName": "broker-b",
		"cluster": "cluster-1"
	},
	"broker-a": {
		"brokerAddrs": {
			0: "127.0.0.1:6666",
			1: "127.0.0.1:5555",
			2: "127.0.0.1:4444"
		},
		"brokerName": "broker-a",
		"cluster": "cluster-1"
	}
}

ClusterAddrTable

ClusterAddrTable维护了broker的集群信息,存储了集群中所有broker的名称。 其运行时数据结构大致如下:

clusterAddrTable: {
	"c1": ["broker-a", "broker-b"],
	"c2": []
}

BrokerLiveTable

BrokerLiveTable维护了broker的状态信息,NameServer收到broker发过来的心跳包时,会更新broker当前的状态,其运行时数据结构大致如下:

brokerLiveTable: {
	"192.168.1.2:9999": {
		"lastUpdateTimestamp": 1575374971337,
		"dataVersion": "dataVersionObject",
		"channel": "channelObject",
		"haServerAddr": "192.168.1.1:9999"
	},
	"192.168.1.1:9999": {
		"lastUpdateTimestamp": 1575374971336,
		"dataVersion": "dataVersionObject",
		"channel": "channelObject",
		"haServerAddr": ""
	},
	"192.168.1.3:9999": {
		"lastUpdateTimestamp": 1575374971337,
		"dataVersion": "dataVersionObject",
		"channel": "channelObject",
		"haServerAddr": "192.168.1.1:9999"
	}
}

FilterServerTable

FilterServerTable维护了broker上的过滤列表,用于类模式消息过滤,后续再讲。

路由信息注册与发现

路由注册

路由信息的注册是通过broker和nameServer之间的心跳功能实现的,broker会在启动的时候向集群中所有nameServer发送心跳数据包,启动以后每隔30s会再次发送。NameServer接收到心跳包后,会更新BrokerLiveTable中BrokerLiveInfo的lastUpdateTimestamp字段,而nameServer中有一个后台线程,每隔10s扫描一次BrokerLiveTable,如果连续120s没有收到某个broker的心跳包,则移除该broker的路由信息,并关闭socket链接。

RocketMQ基于Netty封装了各大组件之间的通信,在这里介绍一下网络跟踪方法:每一个请求,RocketMQ都会定义一个RequestCode。
在服务端会对应相应的网络处理器(对应模块的processor包中),根据RequestCode即可找到相应的处理逻辑。

下面介绍一下nameServer端处理心跳包的逻辑,其请求类型为RequestCode.REGISTER_BROKER。经过网络处理器解析后,broker发送的心跳包会被路由到RoutelnfoManager#registerBroker进行处理,处理步骤如下:

  1. 获取写锁(整个路由表并发控制是由HashMap+读写锁完成的,所以每次操作路由表时都需要先获取对应的锁);
  2. 更新或新增ClusterAddrTable集群表中数据;
        // 根据当前集群名获取brokerName列表
        Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
        // 列表不存在 则空列表
        if (null == brokerNames) {
            brokerNames = new HashSet<String>();
            this.clusterAddrTable.put(clusterName, brokerNames);
        }
        // 将当前brokerName加入列表中
        // 列表本身是set结构,会自动去重
        brokerNames.add(brokerName);
  1. 紧接着是更新brokerAddrTable;
// 标识当前broker是否是第一次与nameServer通信
boolean registerFirst = false;
// 从broker地址信息表中获取当前brokerName的所有地址信息
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);
}
// 获取地址信息表中对应的地址列表
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();

// 每一个IP:PROT只会在记录表中存在一条记录
// 此处遍历地址列表,如果地址已经存在,但是所属信息已经发生了改变 则先移除当前地址信息
// 比如从切换成主时,入参的brokerId=0,而地址表中的brokerId>0,此时就需要先删除原有路由信息,后续加入新的路由信息
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);
  1. 如果当前broker是master则需要判断是否需要更新topicQueueTable主题消费队列信息表;
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());
            }
        }
    }
}

下面是具体更新主题消费队列信息表的createAndUpdateQueueData(brokerName, topicConfig);

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());
    
    // 根据topic名称获取对应的消费队列组
    List<QueueData> queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());
    if (null == queueDataList) {
        // 若队列不存在 则创建新队列组
        // 此处无队列 也可以认为当前topic是新增topic
        queueDataList = new LinkedList<QueueData>();
        // 将之前创建的消费队列加入到新创建的消费队列组中
        queueDataList.add(queueData);
        // 将当前topic的队列信息维护到路由表中
        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()) {
            // 遍历已经存在的消费队列,判断是否是新增或变更的topic
            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. 接下来就是将broker信息新增或更新到broker存活表中,主要是维护了最后通信时间;
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
    new BrokerLiveInfo(
        // 此处会刷新lastUpdateTimestamp
        System.currentTimeMillis(),
        topicConfigWrapper.getDataVersion(),
        channel,
        haServerAddr));
if (null == prevBrokerLiveInfo) {
    log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
}
  1. 如果当前broker是slave则会尝试获取master的地址和高可用服务地址,获取并返回给上游;
if (MixAll.MASTER_ID != brokerId) {
    // 尝试获取master服务器地址
    String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
    if (masterAddr != null) {
        // 获取master服务器存活信息
        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
        if (brokerLiveInfo != null) {
           result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
           result.setMasterAddr(masterAddr);
        }
    }
}
  1. 释放写锁。

以上7个步骤便是路由注册和心跳包处理的大致逻辑,主要通过读写锁控制了并发,将当前broker的信息更新或新增到4张路由信息表中。

路由发现

路由信息发现是由客户端主动向nameServer发起请求获取的,请求类型为RequestCode.GET_ROUTEINTO_BY_TOPIC,经过网络处理器解析后,会被路由到RoutelnfoManager#pickupTopicRouteData进行处理。处理逻辑比较简单,主要是获取读锁以后,从路由信息表中拉取所需信息,我们看一下返回的数据结构即可;

public class TopicRouteData extends RemotingSerializable {
    // 顺序消息相关配置-暂不说明
    private String orderTopicConf;
    // 当前主题消费队列列表 根据topicName从TopicQueueTable中获取
    private List<QueueData> queueDatas;
    // broker地址信息  根据QueueData中的brokerName获取其对应的broker地址信息
    private List<BrokerData> brokerDatas;
    // 暂不涉及过滤服务地址
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}

路由信息注销及故障移除

RocketMQ移除路由表信息有两种出发方式

  • broker正常关闭时,发送类型为RequestCode.UNREGISTER_BROKER的请求,nameServer删除对应路由信息;
  • nameServer后台线程会每隔10s扫描一次BrokerLiveTable,如果连续120s没有收到某个broker的心跳包,则关闭socket链接,并移除该broker的路由信息;

以上两种方式都会删除路由表,其逻辑基本一致,大致如下:

  1. 获取写锁;
  2. 根据brokerAddr从brokerLiveTable和filterServerTable中移除;
  3. 通过brokerName从brokerAddrTable获取地址列表信息,遍历比对列表移除当前地址;
  4. 移除当前地址后,如果该brokerName下没有其他机器时,移除clusterAddrTable中的broker信息,并且移除topicQueueTable中对应的队列;
  5. 移除队列后,如果此topic无其他消息队列,则移除当前topic信息;
  6. 释放写锁;

本篇小结

本篇介绍了NameServer的路由信息表数据结构、服务发现注册逻辑和故障剔除逻辑,草图大致如下:

由于心跳包未发送超过120s才会被移除路由信息,所以有可能客户端获得的路由信息是不可用的,那么如果生产者获取了已经宕机的broker地址,是否会导致消息发送失败呢,如果不会,那么生产者是怎么处理的呢,后续的文章将会解答这个问题。