内容简介
上文简单介绍了如何基于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进行处理,处理步骤如下:
- 获取写锁(整个路由表并发控制是由HashMap+读写锁完成的,所以每次操作路由表时都需要先获取对应的锁);
- 更新或新增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);
- 紧接着是更新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);
- 如果当前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);
}
}
}
- 接下来就是将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);
}
- 如果当前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);
}
}
}
- 释放写锁。
以上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的路由信息;
以上两种方式都会删除路由表,其逻辑基本一致,大致如下:
- 获取写锁;
- 根据brokerAddr从brokerLiveTable和filterServerTable中移除;
- 通过brokerName从brokerAddrTable获取地址列表信息,遍历比对列表移除当前地址;
- 移除当前地址后,如果该brokerName下没有其他机器时,移除clusterAddrTable中的broker信息,并且移除topicQueueTable中对应的队列;
- 移除队列后,如果此topic无其他消息队列,则移除当前topic信息;
- 释放写锁;
本篇小结
本篇介绍了NameServer的路由信息表数据结构、服务发现注册逻辑和故障剔除逻辑,草图大致如下:
由于心跳包未发送超过120s才会被移除路由信息,所以有可能客户端获得的路由信息是不可用的,那么如果生产者获取了已经宕机的broker地址,是否会导致消息发送失败呢,如果不会,那么生产者是怎么处理的呢,后续的文章将会解答这个问题。