面試官:用過(guò)Nacos,那就說(shuō)說(shuō)Nacos服務(wù)注冊(cè)的原理吧!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來(lái),我們一起精進(jìn)!你不來(lái),我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
cnblogs.com/wuzhenzhao/p/13625491.html
推薦:https://www.xttblog.com/?p=5277
Nacos 是目前國(guó)內(nèi)非?;鸬囊粋€(gè)服務(wù)注冊(cè)與發(fā)現(xiàn)的中間件,有不少公司都在采用 Nacos,因此面試中被問(wèn)到的概率也是非常高的!
Nacos 服務(wù)注冊(cè)需要具備的能力:
服務(wù)提供者把自己的協(xié)議地址注冊(cè)到Nacos server 服務(wù)消費(fèi)者需要從Nacos Server上去查詢服務(wù)提供者的地址(根據(jù)服務(wù)名稱) Nacos Server需要感知到服務(wù)提供者的上下線的變化 服務(wù)消費(fèi)者需要?jiǎng)討B(tài)感知到Nacos Server端服務(wù)地址的變化
作為注冊(cè)中心所需要的能力大多如此,我們需要做的是理解各種注冊(cè)中心的獨(dú)有特性,總結(jié)他們的共性。
Nacos的實(shí)現(xiàn)原理:
下面我們先來(lái)了解一下 Nacos 注冊(cè)中心的實(shí)現(xiàn)原理,通過(guò)下面這張圖來(lái)說(shuō)明。

圖中的流程是大家所熟悉的,不同的是在Nacos 中,服務(wù)注冊(cè)時(shí)在服務(wù)端本地會(huì)通過(guò)輪詢注冊(cè)中心集群節(jié)點(diǎn)地址進(jìn)行服務(wù)得注冊(cè),在注冊(cè)中心上,即Nacos Server上采用了Map保存實(shí)例信息,當(dāng)然配置了持久化的服務(wù)會(huì)被保存到數(shù)據(jù)庫(kù)中,在服務(wù)的調(diào)用方,為了保證本地服務(wù)實(shí)例列表的動(dòng)態(tài)感知,Nacos與其他注冊(cè)中心不同的是,采用了 Pull/Push同時(shí)運(yùn)作的方式。通過(guò)這些我們對(duì)Nacos注冊(cè)中心的原理有了一定的了解。我們從源碼層面去驗(yàn)證這些理論知識(shí)。
Nacos的源碼分析(結(jié)合spring-cloud-alibaba +dubbo +nacos 的整合):
「服務(wù)注冊(cè)的流程:」
在基于Dubbo服務(wù)發(fā)布的過(guò)程中, 自動(dòng)裝配是走的事件監(jiān)聽(tīng)機(jī)制,在 DubboServiceRegistrationNonWebApplicationAutoConfiguration 這個(gè)類中,這個(gè)類會(huì)監(jiān)聽(tīng) ApplicationStartedEvent 事件,這個(gè)事件是spring boot在2.0新增的,就是當(dāng)spring boot應(yīng)用啟動(dòng)完成之后會(huì)發(fā)布這個(gè)時(shí)間。而此時(shí)監(jiān)聽(tīng)到這個(gè)事件之后,會(huì)觸發(fā)注冊(cè)的動(dòng)作。
@EventListener(ApplicationStartedEvent.class)
public?void?onApplicationStarted()?{
????setServerPort();
????register();
}
private?void?register()?{
????if?(registered)?{
????????return;
????}
????serviceRegistry.register(registration);
????registered?=?true;
}
serviceRegistry是 spring-cloud 提供的接口實(shí)現(xiàn)(org.springframework.cloud.client.serviceregistry.ServiceRegistry),很顯然注入的實(shí)例是:NacosServiceRegistry。

然后進(jìn)入到實(shí)現(xiàn)類的注冊(cè)方法:
@Override
????public?void?register(Registration?registration)?{
????????if?(StringUtils.isEmpty(registration.getServiceId()))?{
????????????log.warn("No?service?to?register?for?nacos?client...");
????????????return;
????????}
????????//對(duì)應(yīng)當(dāng)前應(yīng)用的application.name
????????String?serviceId?=?registration.getServiceId();
????????//表示nacos上的分組配置
????????String?group?=?nacosDiscoveryProperties.getGroup();
????????//表示服務(wù)實(shí)例信息
????????Instance?instance?=?getNacosInstanceFromRegistration(registration);
????????try?{
????????????//通過(guò)命名服務(wù)進(jìn)行注冊(cè)
????????????namingService.registerInstance(serviceId,?group,?instance);
????????????log.info("nacos?registry,?{}?{}?{}:{}?register?finished",?group,?serviceId,
????????????????????instance.getIp(),?instance.getPort());
????????}
????????catch?(Exception?e)?{
????????????log.error("nacos?registry,?{}?register?failed...{},",?serviceId,
????????????????????registration.toString(),?e);
????????????//?rethrow?a?RuntimeException?if?the?registration?is?failed.
????????????//?issue?:?https://github.com/alibaba/spring-cloud-alibaba/issues/1132
????????????rethrowRuntimeException(e);
????????}
????}????
接下去就是開(kāi)始注冊(cè)實(shí)例,主要做兩個(gè)動(dòng)作
如果當(dāng)前注冊(cè)的是臨時(shí)節(jié)點(diǎn),則構(gòu)建心跳信息,通過(guò)beat反應(yīng)堆來(lái)構(gòu)建心跳任務(wù) 調(diào)用registerService發(fā)起服務(wù)注冊(cè)
@Override
public?void?registerInstance(String?serviceName,?String?groupName,?Instance?instance)?throws?NacosException?{
????????////是否是臨時(shí)節(jié)點(diǎn),如果是臨時(shí)節(jié)點(diǎn),則構(gòu)建心跳信息
????????if?(instance.isEphemeral())?{
????????????BeatInfo?beatInfo?=?new?BeatInfo();
????????????beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName,?groupName));
????????????beatInfo.setIp(instance.getIp());
????????????beatInfo.setPort(instance.getPort());
????????????beatInfo.setCluster(instance.getClusterName());
????????????beatInfo.setWeight(instance.getWeight());
????????????beatInfo.setMetadata(instance.getMetadata());
????????????beatInfo.setScheduled(false);
????????????//beatReactor,?添加心跳信息進(jìn)行處理
????????beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName,?groupName),?beatInfo);
????????}
??????????//調(diào)用服務(wù)代理類進(jìn)行注冊(cè)???
??????????serverProxy.registerService(NamingUtils.getGroupedName(serviceName,?groupName),?groupName,?instance);
}
然后調(diào)用 NamingProxy ?的注冊(cè)方法進(jìn)行注冊(cè),代碼邏輯很簡(jiǎn)單,構(gòu)建請(qǐng)求參數(shù),發(fā)起請(qǐng)求。
public?void?registerService(String?serviceName,?String?groupName,?Instance?instance)?throws?NacosException?{
????????NAMING_LOGGER.info("[REGISTER-SERVICE]?{}?registering?service?{}?with?instance:?{}",
????????????namespaceId,?serviceName,?instance);
????????final?Map?params?=?new?HashMap(8);
????????params.put(CommonParams.NAMESPACE_ID,?namespaceId);
????????params.put(CommonParams.SERVICE_NAME,?serviceName);
????????params.put(CommonParams.GROUP_NAME,?groupName);
????????params.put(CommonParams.CLUSTER_NAME,?instance.getClusterName());
????????params.put("ip",?instance.getIp());
????????params.put("port",?String.valueOf(instance.getPort()));
????????params.put("weight",?String.valueOf(instance.getWeight()));
????????params.put("enable",?String.valueOf(instance.isEnabled()));
????????params.put("healthy",?String.valueOf(instance.isHealthy()));
????????params.put("ephemeral",?String.valueOf(instance.isEphemeral()));
????????params.put("metadata",?JSON.toJSONString(instance.getMetadata()));
????????reqAPI(UtilAndComs.NACOS_URL_INSTANCE,?params,?HttpMethod.POST);
????}
往下走我們就會(huì)發(fā)現(xiàn)上面提到的,服務(wù)在進(jìn)行注冊(cè)的時(shí)候會(huì)輪詢配置好的注冊(cè)中心的地址:
public?String?reqAPI(String?api,?Map?params,?List?servers,?String?method) ?{
????????params.put(CommonParams.NAMESPACE_ID,?getNamespaceId());
????????if?(CollectionUtils.isEmpty(servers)?&&?StringUtils.isEmpty(nacosDomain))?{
????????????throw?new?IllegalArgumentException("no?server?available");
????????}
????????Exception?exception?=?new?Exception();
????????//如果服務(wù)地址不為空
????????if?(servers?!=?null?&&?!servers.isEmpty())?{
????????????//隨機(jī)獲取一臺(tái)服務(wù)器節(jié)點(diǎn)
????????????Random?random?=?new?Random(System.currentTimeMillis());
????????????int?index?=?random.nextInt(servers.size());
????????????//?遍歷服務(wù)列表
????????????for?(int?i?=?0;?i?????????????????String?server?=?servers.get(index);//獲得索引位置的服務(wù)節(jié)點(diǎn)
????????????????try?{//調(diào)用指定服務(wù)
????????????????????return?callServer(api,?params,?server,?method);
????????????????}?catch?(NacosException?e)?{
????????????????????exception?=?e;
????????????????????NAMING_LOGGER.error("request?{}?failed.",?server,?e);
????????????????}?catch?(Exception?e)?{
????????????????????exception?=?e;
????????????????????NAMING_LOGGER.error("request?{}?failed.",?server,?e);
????????????????}
???????????????//輪詢
????????????????index?=?(index?+?1)?%?servers.size();
????????????}
???????//?..........
}
最后通過(guò) callServer(api, params, server, method) 發(fā)起調(diào)用,這里通過(guò) JSK自帶的 HttpURLConnection 進(jìn)行發(fā)起調(diào)用。我們可以通過(guò)斷點(diǎn)的方式來(lái)看到這里的請(qǐng)求參數(shù):

期間可能會(huì)有多個(gè) GET 的請(qǐng)求獲取服務(wù)列表,是正常的,會(huì)發(fā)現(xiàn)有如上的一個(gè)請(qǐng)求,會(huì)調(diào)用 http://192.168.200.1:8848/nacos/v1/ns/instance 這個(gè)地址。那么接下去就是Nacos Server 接受到服務(wù)端的注冊(cè)請(qǐng)求的處理流程。需要下載Nacos Server 源碼,源碼下載可以參考官方文檔,本文不做過(guò)多闡述。
「Nacos服務(wù)端的處理:」
服務(wù)端提供了一個(gè)InstanceController類,在這個(gè)類中提供了服務(wù)注冊(cè)相關(guān)的API,而服務(wù)端發(fā)起注冊(cè)時(shí),調(diào)用的接口是:[post]: /nacos/v1/ns/instance ,serviceName: 代表客戶端的項(xiàng)目名稱 ,namespace: nacos 的namespace。
@CanDistro
@PostMapping
@Secured(parser?=?NamingResourceParser.class,?action?=?ActionTypes.WRITE)
public?String?register(HttpServletRequest?request)?throws?Exception?{
????????
????????final?String?serviceName?=?WebUtils.required(request,?CommonParams.SERVICE_NAME);
????????final?String?namespaceId?=?WebUtils
????????????????.optional(request,?CommonParams.NAMESPACE_ID,?Constants.DEFAULT_NAMESPACE_ID);
????????//?從請(qǐng)求中解析出instance實(shí)例
????????final?Instance?instance?=?parseInstance(request);
????????
????????serviceManager.registerInstance(namespaceId,?serviceName,?instance);
????????return?"ok";
}
然后調(diào)用 ServiceManager 進(jìn)行服務(wù)的注冊(cè)
public?void?registerInstance(String?namespaceId,?String?serviceName,?Instance?instance)?throws?NacosException?{
????????//創(chuàng)建一個(gè)空服務(wù),在Nacos控制臺(tái)服務(wù)列表展示的服務(wù)信息,實(shí)際上是初始化一個(gè)serviceMap,它是一個(gè)ConcurrentHashMap集合
????????createEmptyService(namespaceId,?serviceName,?instance.isEphemeral());
????????//從serviceMap中,根據(jù)namespaceId和serviceName得到一個(gè)服務(wù)對(duì)象
????????Service?service?=?getService(namespaceId,?serviceName);
????????
????????if?(service?==?null)?{
????????????throw?new?NacosException(NacosException.INVALID_PARAM,
????????????????????"service?not?found,?namespace:?"?+?namespaceId?+?",?service:?"?+?serviceName);
????????}
????????//調(diào)用addInstance創(chuàng)建一個(gè)服務(wù)實(shí)例
????????addInstance(namespaceId,?serviceName,?instance.isEphemeral(),?instance);
}
在創(chuàng)建空的服務(wù)實(shí)例的時(shí)候我們發(fā)現(xiàn)了存儲(chǔ)實(shí)例的map:
public?void?createServiceIfAbsent(String?namespaceId,?String?serviceName,?boolean?local,?Cluster?cluster)
????????????throws?NacosException?{
????????//從serviceMap中獲取服務(wù)對(duì)象
????????Service?service?=?getService(namespaceId,?serviceName);
????????if?(service?==?null)?{//如果為空。則初始化
??????Loggers.SRV\_LOG.info("creating?empty?service?{}:{}",?namespaceId,?serviceName);
??????service?=?new?Service();
??????service.setName(serviceName);
??????service.setNamespaceId(namespaceId);
??????service.setGroupName(NamingUtils.getGroupName(serviceName));
??????//?now?validate?the?service.?if?failed,?exception?will?be?thrown
??????service.setLastModifiedMillis(System.currentTimeMillis());
??????service.recalculateChecksum();
??????if?(cluster?!=?null)?{
??????????cluster.setService(service);
??????????service.getClusterMap().put(cluster.getName(),?cluster);
??????}
??????service.validate();
??????putServiceAndInit(service);
??????if?(!local)?{
??????????addOrReplaceService(service);
??????}
}
在 getService 方法中我們發(fā)現(xiàn)了Map:
/*
*?Map(namespace,?Map(group::serviceName,?Service)).
*/
private?final?Map>?serviceMap?=?new?ConcurrentHashMap<>();
通過(guò)注釋我們可以知道,Nacos是通過(guò)不同的 namespace 來(lái)維護(hù)服務(wù)的,而每個(gè)namespace下有不同的group,不同的group下才有對(duì)應(yīng)的Service ,再通過(guò)這個(gè) serviceName 來(lái)確定服務(wù)實(shí)例。
第一次進(jìn)來(lái)則會(huì)進(jìn)入初始化,初始化完會(huì)調(diào)用?putServiceAndInit
private?void?putServiceAndInit(Service?service)?throws?NacosException?{
????????putService(service);//把服務(wù)信息保存到serviceMap集合
????????service.init();//建立心跳檢測(cè)機(jī)制
????????//實(shí)現(xiàn)數(shù)據(jù)一致性監(jiān)聽(tīng),ephemeral(標(biāo)識(shí)服務(wù)是否為臨時(shí)服務(wù),默認(rèn)是持久化的,也就是true)=true表示采用raft協(xié)議,false表示采用Distro
????????consistencyService
????????????????.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(),?service.getName(),?true),?service);
????????consistencyService
????????????????.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(),?service.getName(),?false),?service);
????????Loggers.SRV_LOG.info("[NEW-SERVICE]?{}",?service.toJson());
????}
獲取到服務(wù)以后把服務(wù)實(shí)例添加到集合中,然后基于一致性協(xié)議進(jìn)行數(shù)據(jù)的同步。然后調(diào)用 addInstance
public?void?addInstance(String?namespaceId,?String?serviceName,?boolean?ephemeral,?Instance...?ips)
????????????throws?NacosException?{
????????//?組裝key
????????String?key?=?KeyBuilder.buildInstanceListKey(namespaceId,?serviceName,?ephemeral);
????????//?獲取剛剛組裝的服務(wù)
????????Service?service?=?getService(namespaceId,?serviceName);
????????
????????synchronized?(service)?{
????????????List?instanceList?=?addIpAddresses(service,?ephemeral,?ips);
????????????
????????????Instances?instances?=?new?Instances();
????????????instances.setInstanceList(instanceList);
????????????//?也就是上一步實(shí)現(xiàn)監(jiān)聽(tīng)的類里添加注冊(cè)服務(wù)
????????????consistencyService.put(key,?instances);
????????}
????}
然后給服務(wù)注冊(cè)方發(fā)送注冊(cè)成功的響應(yīng),結(jié)束服務(wù)注冊(cè)流程。以上內(nèi)容,希望大家有一個(gè)大概的認(rèn)識(shí),收藏起來(lái),后面慢慢多看幾次,牢記心中,面試中肯定是加分項(xiàng)。
