前言
前面我们介绍了Broker每隔30s会向Namesrv发送心跳包,心跳包中包含Broker路由信息,Namesrv会更新RouteInfoManager中的路由信息。如果Broker宕机,Namesrv是如何判断Broker不可用,并将broker从路由信息中剔除的呢?
路由失效源码分析
NamesrvController初始化会启动定时线程池,其中就包括扫描不健康Broker的schedule线程池。NamesrvController启动时会调用NamesrvController#startScheduleService,启动namesrv中的定时任务,其中就包括检查不健康Broker的schedule线程池。
private void startScheduleService() {
// 每5秒检测一次不健康的broker
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
5, this.namesrvConfig.getScanNotActiveBrokerInterval()/*默认5s*/, TimeUnit.MILLISECONDS);
}
在定时任务中遍历brokerLiveTable,如果当前时间大于broker最后更新时间+超时时间(默认是120s),也就是说namesrv距离最后一次收到broker的心跳已经超过了120s。namesrv会主动关闭与broker的channel,并且发送broker注销请求。
public void scanNotActiveBroker() {
try {
log.info("start scanNotActiveBroker");
// 遍历存活brokerTable
for (Entry<BrokerAddrInfo, BrokerLiveInfo> next : this.brokerLiveTable.entrySet()) {
long last = next.getValue().getLastUpdateTimestamp();
long timeoutMillis = next.getValue().getHeartbeatTimeoutMillis();
if ((last + timeoutMillis) < System.currentTimeMillis()) {
// 关闭channel
RemotingHelper.closeChannel(next.getValue()/*BrokerLiveInfo*/.getChannel());
log.warn("The broker channel expired, {} {}ms", next.getKey(), timeoutMillis);
// 发送broker注销请求
this.onChannelDestroy(next.getKey()/*BrokerAddrInfo*/);
}
}
} catch (Exception e) {
log.error("scanNotActiveBroker exception", e);
}
}
注销请求会提交给BatchUnregistrationService#unregistrationQueue,BatchUnregistrationService继承了Thead,在RouteInfoManager构建时创建,RouteInfoManager启动时也会启动BatchUnregistrationService,它是一个守护线程,在while死循环中获取broker注销请求
public class BatchUnregistrationService extends ServiceThread {
// 注销Broker请求队列
private BlockingQueue<UnRegisterBrokerRequestHeader> unregistrationQueue;
@Override
public void run() {
while (!this.isStopped()) {
try {
// 调用take方法会阻塞
final UnRegisterBrokerRequestHeader request = unregistrationQueue.take();
Set<UnRegisterBrokerRequestHeader> unregistrationRequests = new HashSet<>();
// 拿到请求后,把所有的Request加到未注册的requestSet中
unregistrationQueue.drainTo(unregistrationRequests);
unregistrationRequests.add(request);
// 调用注销broker
this.routeInfoManager.unRegisterBroker(unregistrationRequests);
} catch (Throwable e) {
log.error("Handle unregister broker request failed", e);
}
}
}
}
这里有一个编程小技巧,JUC的阻塞队列中并没有提供从队列中批量获取对象的阻塞方法。BatchUnregistrationService中批量获取注销请求对象先使用take方法从队列中获取一个对象,如果队列为空,take方法阻塞。如果take方法返回一个注销请求,说明阻塞队列中的对象不空,再调用drainTo方法尝试将队列中的对象放入到SET中。这样写代码同时满足了阻塞和批量获取注销请求对象的需求。
BatchUnregistrationService获取到注销请求后,会将注销请求发送给RouteInfoManager#unRegisterBroker,路由注销逻辑都是在这个方法中,路由注销逻辑大致可以分为下面6个步骤。
第一步,获取RouteInfoManager中路由操作写锁,循环遍历要注销的请求,并将要注销broker从broker存活table中删除。
this.lock.writeLock().lockInterruptibly();
for (final UnRegisterBrokerRequestHeader unRegisterRequest : unRegisterRequests) {
// ...
BrokerAddrInfo brokerAddrInfo = new BrokerAddrInfo(clusterName, brokerAddr);
// 1.存活的brokerLiveTable要删除brokerAddrInfo
BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddrInfo);
// ... 第2~6步的逻辑
}
第二步,从filterServerTable中注销broker
// 2.过滤service删除BrokerAddrInfo
this.filterServerTable.remove(brokerAddrInfo);
第三步,从brokerAddrTable中获取BrokerData,删除BrokerData要注销的Broker地址,注销完如果BrokerData中存活的broker地址为空,说明brokerName下所有broker都已经注销,则会从brokerAddrTable中删除BrokerData。如果要注销的Broker是主节点,还会将当前broker的信息放入到通知map中。
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null != brokerData) {
if (!brokerData.getBrokerAddrs().isEmpty() &&
unRegisterRequest.getBrokerId().equals(Collections.min(brokerData.getBrokerAddrs().keySet()))) {
isMinBrokerIdChanged = true;
}
// 删除brokerData中要下线的broker地址
boolean removed = brokerData.getBrokerAddrs().entrySet().removeIf(item -> item.getValue().equals(brokerAddr));
// 如果broker地址为空,说明当前broker所有节点都宕机了
if (brokerData.getBrokerAddrs().isEmpty()) {
this.brokerAddrTable.remove(brokerName);
removeBrokerName = true;
} else if (isMinBrokerIdChanged) { // 主节点宕机,从节点还存在
needNotifyBrokerMap.put(brokerName, new BrokerStatusChangeInfo(
brokerData.getBrokerAddrs(), brokerAddr, null));
}
}
第四步,如果集群中的所有broker都已经注销,则会将clusterAddrTable中的集群信息删除。
if (removeBrokerName/*如果所有节点都宕机*/) {
Set<String> nameSet = this.clusterAddrTable.get(clusterName);
if (nameSet != null) {
// 删除brokerName
boolean removed = nameSet.remove(brokerName);
// 如果集群中所有broker都宕机,则删除集群信息
if (nameSet.isEmpty()) {
this.clusterAddrTable.remove(clusterName);
}
}
removedBroker.add(brokerName);
} else {
reducedBroker.add(brokerName);
}
第五步,清理要注销Broker关联的Topic信息,如果brokerName都已经宕机,则会清除topicQueueTable中BrokerName对应的QueueData。如果要注销的Broker是主节点,则会将brokerAddrTable中当前brokerName的BrokerData置为不可写状态。
private void cleanTopicByUnRegisterRequests(Set<String> removedBroker, Set<String> reducedBroker) {
Iterator<Entry</*topic*/, Map<String/*brokerName*/, QueueData>>> itMap = this.topicQueueTable.entrySet().iterator();
while (itMap.hasNext()) {
Entry<String, Map<String, QueueData>> entry = itMap.next();
String topic = entry.getKey();
Map<String, QueueData> queueDataMap = entry.getValue();
// 相同brokerName都宕机,则会清除queueData
for (final String brokerName : removedBroker) {
final QueueData removedQD = queueDataMap.remove(brokerName);
}
// ...
for (final String brokerName : reducedBroker) {
final QueueData queueData = queueDataMap.get(brokerName);
// 如果是主节点,则会把brokerAddrTable置为不可写状态
if (queueData != null) {
if (this.brokerAddrTable.get(brokerName).isEnableActingMaster()) {
// 如果主节点宕机,则broker状态改成不可写
if (isNoMasterExists(brokerName)) {
queueData.setPerm(queueData.getPerm() & (~PermName.PERM_WRITE));
}
}
}
}
}
}
第六步,如果broker的主节点宕机,并且在namesrv中开启了主节点宕机通知其他节点的配置,就会通知当前BrokerName的其他节点。
主节点宕机通知其他节点的配置是notifyMinBrokerIdChanged,默认是不开启。
if (!needNotifyBrokerMap.isEmpty() && namesrvConfig.isNotifyMinBrokerIdChanged()) {
notifyMinBrokerIdChanged(needNotifyBrokerMap);
}
将上面步骤整理成时序图如下所示
总结
namesrv在启动后每5s检查一次brokerLiveTable中broker更新时间,namesrv会注销brokerLiveTable中信息更新超过120秒的broker。首先会关闭与Broker的连接并注销topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable、clusterAddrTable中的路由信息。