0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Nacos服务基本概念和核心能力以及实现原理

jf_ro2CN3Fa 来源:芋道源码 2023-05-17 17:51 次阅读

现如今市面上注册中心的轮子很多,我实际使用过的就有三款:Eureka、Gsched、Nacos,由于当前参与 Nacos 集群的维护和开发工作,期间也参与了 Nacos 社区的一些开发和 Bug Fix 工作,过程中对 Nacos 原理有了一定的积累,今天给大家分享一下 Nacos 动态服务发现的原理。

ef701128-f496-11ed-90ce-dac502259ad0.png

01 什么是动态服务发现?

服务发现是指使用一个注册中心来记录分布式系统中的全部服务的信息,以便其他服务能够快速的找到这些已注册的服务。

在单体应用中,DNS+Nginx 可以满足服务发现的要求,此时服务的IP列表配置在 nginx 上。在微服务架构中,由于服务粒度变的更细,服务的上下线更加频繁,我们需要一款注册中心来动态感知服务的上下线,并且推送IP列表变化给服务消费者,架构如下图。

ef7aa14c-f496-11ed-90ce-dac502259ad0.png

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

02 Nacos 实现动态服务发现的原理

Nacos实现动态服务发现的核心原理如下图,我们接下来的内容将围绕这个图来进行。

ef8124d6-f496-11ed-90ce-dac502259ad0.png

2.1 通讯协议

整个服务注册与发现过程,都离不开通讯协议,在1.x的 Nacos 版本中服务端只支持 http 协议,后来为了提升性能在2.x版本引入了谷歌的 grpc,grpc 是一款长连接协议,极大的减少了 http 请求频繁的连接创建和销毁过程,能大幅度提升性能,节约资源。

据官方测试,Nacos服务端 grpc 版本,相比 http 版本的性能提升了9倍以上。

2.2 Nacos 服务注册

简单来讲,服务注册的目的就是客户端将自己的ip端口等信息上报给 Nacos 服务端,过程如下:

创建长连接:Nacos SDK 通过Nacos服务端域名解析出服务端ip列表,选择其中一个ip创建 grpc 连接,并定时检查连接状态,当连接断开,则自动选择服务端ip列表中的下一个ip进行重连。

健康检查请求:在正式发起注册之前,Nacos SDK 向服务端发送一个空请求,服务端回应一个空请求,若Nacos SDK 未收到服务端回应,则认为服务端不健康,并进行一定次数重试,如果都未收到回应,则注册失败。

发起注册:当你查看Nacos java SDK的注册方法时,你会发现没有返回值,这是因为Nacos SDK做了补偿机制,在真实给服务端上报数据之前,会先往缓存中插入一条记录表示开始注册,注册成功之后再从缓存中标记这条记录为注册成功,当注册失败时,缓存中这条记录是未注册成功的状态,Nacos SDK开启了一个定时任务,定时查询异常的缓存数据,重新发起注册。

Nacos SDK注册失败时的自动补偿机制时序图。

ef8a0074-f496-11ed-90ce-dac502259ad0.png

相关源码如下:

@Override
publicvoidregisterService(StringserviceName,StringgroupName,Instanceinstance)throwsNacosException{
NAMING_LOGGER.info("[REGISTER-SERVICE]{}registeringservice{}withinstance{}",namespaceId,serviceName,
instance);
//添加redo日志
redoService.cacheInstanceForRedo(serviceName,groupName,instance);

doRegisterService(serviceName,groupName,instance);
}
publicvoiddoRegisterService(StringserviceName,StringgroupName,Instanceinstance)throwsNacosException{
//向服务端发起注册
InstanceRequestrequest=newInstanceRequest(namespaceId,serviceName,groupName,
NamingRemoteConstants.REGISTER_INSTANCE,instance);
requestToServer(request,Response.class);
//标记注册成功
redoService.instanceRegistered(serviceName,groupName);
}

执行补偿定时任务RedoScheduledTask。

@Override
publicvoidrun(){
if(!redoService.isConnected()){
LogUtils.NAMING_LOGGER.warn("GrpcConnectionisdisconnect,skipcurrentredotask");
return;
}
try{
redoForInstances();
redoForSubscribes();
}catch(Exceptione){
LogUtils.NAMING_LOGGER.warn("Redotaskrunwithunexpectedexception:",e);
}
}
privatevoidredoForInstances(){
for(InstanceRedoDataeach:redoService.findInstanceRedoData()){
try{
redoForInstance(each);
}catch(NacosExceptione){
LogUtils.NAMING_LOGGER.error("Redoinstanceoperation{}for{}@@{}failed.",each.getRedoType(),
each.getGroupName(),each.getServiceName(),e);
}
}
}

服务端数据同步(Distro协议):Nacos SDK只会与服务端某个节点建立长连接,当服务端接受到客户端注册的实例数据后,还需要将实例数据同步给其他节点。Nacos自己实现了一个一致性协议名为Distro,服务注册的时候会触发Distro一次同步,每个Nacos节点之间会定时互相发送Distro数据,以此保证数据最终一致。

服务实例上线推送:Nacos服务端收到服务实例数据后会将服务的最新实例列表通过grpc推送给该服务的所有订阅者。

服务注册过程源码时序图:整理了一下服务注册过程整体时序图,对源码实现感兴趣的可以按照根据这个时序图view一下源码。

ef9610c6-f496-11ed-90ce-dac502259ad0.png

2.3 Nacos 心跳机制

目前主流的注册中心,比如Consul、Eureka、Zk包括我们公司自研的Gsched,都是通过心跳机制来感知服务的下线。Nacos也是通过心跳机制来实现的。

Nacos目前SDK维护了两个分支的版本(1.x、2.x),这两个版本心跳机制的实现不一样。其中1.x版本的SDK通过http协议来定时向服务端发送心跳维持自己的健康状态,2.x版本的SDK则通过grpc自身的心跳机制来保活,当Nacos服务端接受不到服务实例的心跳,会认为实例下线。如下图:

ef9d705a-f496-11ed-90ce-dac502259ad0.png

grpc监测到连接断开事件,发送ClientDisconnectEvent。

publicclassConnectionBasedClientManagerextendsClientConnectionEventListenerimplementsClientManager{
//连接断开,发送连接断开事件
publicbooleanclientDisconnected(StringclientId){
Loggers.SRV_LOG.info("Clientconnection{}disconnect,removeinstancesandsubscribers",clientId);
ConnectionBasedClientclient=clients.remove(clientId);
if(null==client){
returntrue;
}
client.release();
NotifyCenter.publishEvent(newClientEvent.ClientDisconnectEvent(client));
returntrue;
}
}

移除客户端注册的服务实例

publicclassClientServiceIndexesManagerextendsSmartSubscriber{

@Override
publicvoidonEvent(Eventevent){
//接收失去连接事件
if(eventinstanceofClientEvent.ClientDisconnectEvent){
handleClientDisconnect((ClientEvent.ClientDisconnectEvent)event);
}elseif(eventinstanceofClientOperationEvent){
handleClientOperation((ClientOperationEvent)event);
}
}
privatevoidhandleClientDisconnect(ClientEvent.ClientDisconnectEventevent){
Clientclient=event.getClient();
for(Serviceeach:client.getAllSubscribeService()){
removeSubscriberIndexes(each,client.getClientId());
}
//移除客户端注册的服务实例
for(Serviceeach:client.getAllPublishedService()){
removePublisherIndexes(each,client.getClientId());
}
}

//移除客户端注册的服务实例
privatevoidremovePublisherIndexes(Serviceservice,StringclientId){
if(!publisherIndexes.containsKey(service)){
return;
}
publisherIndexes.get(service).remove(clientId);
NotifyCenter.publishEvent(newServiceEvent.ServiceChangedEvent(service,true));
}
}

2.4 Nacos 服务订阅

当一个服务发生上下线,Nacos如何知道要推送给哪些客户端?

Nacos SDK 提供了订阅和取消订阅方法,当客户端向服务端发起订阅请求,服务端会记录发起调用的客户端为该服务的订阅者,同时将服务的最新实例列表返回。当客户端发起了取消订阅,服务端就会从该服务的订阅者列表中把当前客户端移除。

当客户端发起订阅时,服务端除了会同步返回最新的服务实例列表,还会异步的通过grpc推送给该订阅者最新的服务实例列表,这样做的目的是为了异步更新客户端本地缓存的服务数据。

当客户端订阅的服务上下线,该服务所有的订阅者会立刻收到最新的服务列表并且将服务最新的实例数据更新到内存。

efa92922-f496-11ed-90ce-dac502259ad0.png

我们也看一下相关源码,服务端接收到订阅数据,首先保存到内存中。

@Override
publicvoidsubscribeService(Serviceservice,Subscribersubscriber,StringclientId){
Servicesingleton=ServiceManager.getInstance().getSingletonIfExist(service).orElse(service);
Clientclient=clientManager.getClient(clientId);
//校验长连接是否正常
if(!clientIsLegal(client,clientId)){
return;
}
//保存订阅数据
client.addServiceSubscriber(singleton,subscriber);
client.setLastUpdatedTime();
//发送订阅事件
NotifyCenter.publishEvent(newClientOperationEvent.ClientSubscribeServiceEvent(singleton,clientId));
}

privatevoidhandleClientOperation(ClientOperationEventevent){
Serviceservice=event.getService();
StringclientId=event.getClientId();
if(eventinstanceofClientOperationEvent.ClientRegisterServiceEvent){
addPublisherIndexes(service,clientId);
}elseif(eventinstanceofClientOperationEvent.ClientDeregisterServiceEvent){
removePublisherIndexes(service,clientId);
}elseif(eventinstanceofClientOperationEvent.ClientSubscribeServiceEvent){
//处理订阅操作
addSubscriberIndexes(service,clientId);
}elseif(eventinstanceofClientOperationEvent.ClientUnsubscribeServiceEvent){
removeSubscriberIndexes(service,clientId);
}
}

然后发布订阅事件。

privatevoidaddSubscriberIndexes(Serviceservice,StringclientId){
//保存订阅数据
subscriberIndexes.computeIfAbsent(service,(key)->newConcurrentHashSet<>());
//Fix#5404,Onlyfirsttimeaddneednotifyevent.
if(subscriberIndexes.get(service).add(clientId)){
//发布订阅事件
NotifyCenter.publishEvent(newServiceEvent.ServiceSubscribedEvent(service,clientId));
}
}

服务端自己消费订阅事件,并且推送给订阅的客户端最新的服务实例数据。

@Override
publicvoidonEvent(Eventevent){
if(!upgradeJudgement.isUseGrpcFeatures()){
return;
}
if(eventinstanceofServiceEvent.ServiceChangedEvent){
//Ifservicechanged,pushtoallsubscribers.
ServiceEvent.ServiceChangedEventserviceChangedEvent=(ServiceEvent.ServiceChangedEvent)event;
Serviceservice=serviceChangedEvent.getService();
delayTaskEngine.addTask(service,newPushDelayTask(service,PushConfig.getInstance().getPushTaskDelay()));
}elseif(eventinstanceofServiceEvent.ServiceSubscribedEvent){
//Ifserviceissubscribedbyoneclient,onlypushthisclient.
ServiceEvent.ServiceSubscribedEventsubscribedEvent=(ServiceEvent.ServiceSubscribedEvent)event;
Serviceservice=subscribedEvent.getService();
delayTaskEngine.addTask(service,newPushDelayTask(service,PushConfig.getInstance().getPushTaskDelay(),
subscribedEvent.getClientId()));
}
}

2.5 Nacos 推送

推送方式

前面说了服务的注册和订阅都会发生推送(服务端->客户端),那推送到底是如何实现的呢?

在早期的Nacos版本,当服务实例变化,服务端会通过udp协议将最新的数据发送给客户端,后来发现udp推送有一定的丢包率,于是新版本的Nacos支持了grpc推送。Nacos服务端会自动判断客户端的版本来选择哪种方式来进行推送,如果你使用1.4.2以前的SDK进行注册,那Nacos服务端会使用udp协议来进行推送,反之则使用grpc。

推送失败重试

当发送推送时,客户端可能正在重启,或者连接不稳定导致推送失败,这个时候Nacos会进行重试。Nacos将每个推送都封装成一个任务对象,放入到队列中,再开启一个线程不停的从队列取出任务执行,执行之前会先删除该任务,如果执行失败则将任务重新添加到队列,该线程会记录任务执行的时间,如果超过1秒,则会记录到日志。

推送部分源码

添加推送任务到执行队列中。

privatestaticclassPushDelayTaskProcessorimplementsNacosTaskProcessor{

privatefinalPushDelayTaskExecuteEngineexecuteEngine;

publicPushDelayTaskProcessor(PushDelayTaskExecuteEngineexecuteEngine){
this.executeEngine=executeEngine;
}

@Override
publicbooleanprocess(NacosTasktask){
PushDelayTaskpushDelayTask=(PushDelayTask)task;
Serviceservice=pushDelayTask.getService();
NamingExecuteTaskDispatcher.getInstance()
.dispatchAndExecuteTask(service,newPushExecuteTask(service,executeEngine,pushDelayTask));
returntrue;
}
}

推送任务PushExecuteTask 的执行。

publicclassPushExecuteTaskextendsAbstractExecuteTask{

//..省略

@Override
publicvoidrun(){
try{
//封装要推送的服务实例数据
PushDataWrapperwrapper=generatePushData();
ClientManagerclientManager=delayTaskEngine.getClientManager();
//如果是服务上下线导致的推送,获取所有订阅者
//如果是订阅导致的推送,获取订阅者
for(Stringeach:getTargetClientIds()){
Clientclient=clientManager.getClient(each);
if(null==client){
//meansthisclienthasdisconnect
continue;
}
Subscribersubscriber=clientManager.getClient(each).getSubscriber(service);
//推送给订阅者
delayTaskEngine.getPushExecutor().doPushWithCallback(each,subscriber,wrapper,
newNamingPushCallback(each,subscriber,wrapper.getOriginalData(),delayTask.isPushToAll()));
}
}catch(Exceptione){
Loggers.PUSH.error("Pushtaskforservice"+service.getGroupedServiceName()+"executefailed",e);
//当推送发生异常,重新将推送任务放入执行队列
delayTaskEngine.addTask(service,newPushDelayTask(service,1000L));
}
}

//如果是服务上下线导致的推送,获取所有订阅者
//如果是订阅导致的推送,获取订阅者
privateCollectiongetTargetClientIds(){
returndelayTask.isPushToAll()?delayTaskEngine.getIndexesManager().getAllClientsSubscribeService(service)
:delayTask.getTargetClients();
}

执行推送任务线程InnerWorker 的执行。

/**
*Innerexecuteworker.
*/
privateclassInnerWorkerextendsThread{

InnerWorker(Stringname){
setDaemon(false);
setName(name);
}

@Override
publicvoidrun(){
while(!closed.get()){
try{
//从队列中取出任务PushExecuteTask
Runnabletask=queue.take();
longbegin=System.currentTimeMillis();
//执行PushExecuteTask
task.run();
longduration=System.currentTimeMillis()-begin;
if(duration>1000L){
log.warn("task{}takes{}ms",task,duration);
}
}catch(Throwablee){
log.error("[TASK-FAILED]"+e.toString(),e);
}
}
}
}

2.6 Nacos SDK 查询服务实例

服务消费者首先需要调用Nacos SDK的接口来获取最新的服务实例,然后才能从获取到的实例列表中以加权轮询的方式选择出一个实例(包含ip,port等信息),最后再发起调用。

前面已经提到Nacos服务发生上下线、订阅的时候都会推送最新的服务实例列表到当客户端,客户端再更新本地内存中的缓冲数据,所以调用Nacos SDK提供的查询实例列表的接口时,不会直接请求服务端获取数据,而是会优先使用内存中的服务数据,只有内存中查不到的情况下才会发起订阅请求服务端数据。

Nacos SDK内存中的数据除了接受来自服务端的推送更新之外,自己本地也会有一个定时任务定时去获取服务端数据来进行兜底。Nacos SDK在查询的时候也了容灾机制,即从磁盘获取服务数据,而这个磁盘的数据其实也是来自于内存,有一个定时任务定时从内存缓存中获取然后加载到磁盘。Nacos SDK的容灾机制默认关闭,可通过设置环境变量failover-mode=true来开启。

架构图

efae80a2-f496-11ed-90ce-dac502259ad0.png

用户查询流程

efb811c6-f496-11ed-90ce-dac502259ad0.png

查询服务实例部分源码

privatefinalConcurrentMapserviceInfoMap;
@Override
publicListgetAllInstances(StringserviceName,StringgroupName,Listclusters,
booleansubscribe)throwsNacosException{
ServiceInfoserviceInfo;
StringclusterString=StringUtils.join(clusters,",");
//这里默认传过来是true
if(subscribe){
//从本地内存获取服务数据,如果获取不到则从磁盘获取
serviceInfo=serviceInfoHolder.getServiceInfo(serviceName,groupName,clusterString);
if(null==serviceInfo||!clientProxy.isSubscribed(serviceName,groupName,clusterString)){
//如果从本地获取不到数据,则调用订阅方法
serviceInfo=clientProxy.subscribe(serviceName,groupName,clusterString);
}
}else{
//适用于不走订阅,直接从服务端获取数据的情况
serviceInfo=clientProxy.queryInstancesOfService(serviceName,groupName,clusterString,0,false);
}
Listlist;
if(serviceInfo==null||CollectionUtils.isEmpty(list=serviceInfo.getHosts())){
returnnewArrayList();
}
returnlist;
}
}
//从本地内存获取服务数据,如果开启了故障转移则直接从磁盘获取,因为当服务端挂了,本地启动时内存中也没有数据
publicServiceInfogetServiceInfo(finalStringserviceName,finalStringgroupName,finalStringclusters){
NAMING_LOGGER.debug("failover-mode:{}",failoverReactor.isFailoverSwitch());
StringgroupedServiceName=NamingUtils.getGroupedName(serviceName,groupName);
Stringkey=ServiceInfo.getKey(groupedServiceName,clusters);
//故障转移则直接从磁盘获取
if(failoverReactor.isFailoverSwitch()){
returnfailoverReactor.getService(key);
}
//返回内存中数据
returnserviceInfoMap.get(key);
}

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/yudao-cloud

视频教程:https://doc.iocoder.cn/video/

03 结语

本篇文章向大家介绍 Nacos 服务发现的基本概念和核心能力以及实现的原理,旨在让大家对 Nacos 的服务注册与发现功能有更多的了解,做到心中有数。

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 管理系统
    +关注

    关注

    1

    文章

    2503

    浏览量

    35929
  • nacos
    +关注

    关注

    0

    文章

    10

    浏览量

    203

原文标题:4 个维度搞懂 Nacos 注册中心

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Nacos概念和功能

    1、Nacos简介 Nacos概念和功能 Nacos是一个面向微服务架构的动态服务发现、配置管
    的头像 发表于 09-25 11:02 2339次阅读

    支持Dubbo生态发展,阿里巴巴启动新的开源项目 Nacos

    共享和服务的可持续发展”是“共享服务体系”的核心价值主张支持创新从小苗长成参天大树,服务平台不断演进,这一切需要一个强大的服务平台和
    发表于 07-05 17:35

    单片机的基本概念

    单片机的基本概念1.1单片机的组成*由CPU、RAM(随机存储器)、ROM(只读存储器)、I/O接口、以及内部功能部件组成。1.2单片机内部数据传输*单片机内部数据传输通过总线完成,输入数据时会
    发表于 07-21 08:13

    操作系统原理基本概念

    操作系统原理基本概念计算机硬件系统组成中央处理器中央处理器是计算机的运算核心(Core)和控制单元( Control Unit) ,主要包括:运算逻辑部件: 一个或多个运算器寄存器部件: 包括通用
    发表于 07-26 07:46

    单片机中断的基本概念

    文章目录一.中断的基本概念二.中断相关的寄存器三.中断的实际使用四.中断的优点:一.中断的基本概念1.中断的概念:在单片机中,中断是指:对于CPU来说,当它在正常处理事件A时,突然发生了另一件事件B
    发表于 11-25 08:14

    服务嵌入式SDK的基本概念都有哪些呢

    服务嵌入式SDK的基本概念都有哪些呢?什么是差分账号?有何应用?
    发表于 12-27 07:59

    STM32的中断系统基本概念

    STM32 中断系统概述笔记(一)中断概述中断相关的基本概念STM32的中断系统基本概念:NVIC 嵌套向量中断控制器中断通道中断优先级优先级分组EXTI 外部中断控制器三种外部中断触发方式引脚分组
    发表于 01-07 07:32

    无线定位基本概念与原理

    无线定位基本概念简介,以及其原理分析
    发表于 11-11 18:01 147次下载

    Nacos v0.7.0:对接CMDB,实现基于标签的服务发现能力

    一些bug。 对接CMDB实现就近访问 在服务进行多机房或者多地域部署时,跨地域的服务访问往往延迟较高,一个城市内的机房间的典型网络延迟在1ms左右,而跨城市的网络延迟,例如上海到北京大概为30ms。此时自然而然的一个想法就是能
    发表于 12-28 17:50 492次阅读
    <b class='flag-5'>Nacos</b> v0.7.0:对接CMDB,<b class='flag-5'>实现</b>基于标签的<b class='flag-5'>服务</b>发现<b class='flag-5'>能力</b>

    Nacos服务地址动态感知原理

    Nacos Server:Nacos服务提供者,里面包含的Open API是功能访问入口,Conig Service、Naming Service 是Nacos提供的配置
    的头像 发表于 09-26 10:40 1771次阅读

    Nacos为什么这么强?Nacos注册中心的底层原理,从服务注册到服务发现

    来源:码猿技术专栏 1. Nacos介绍 2. Nacos注册中心实现原理分析 2.1 Nacos架构图 2.2 注册中心的原理 3. Nacos
    的头像 发表于 10-08 16:46 1.2w次阅读

    华为云微服务引擎0停机迁移Nacos?它是这样做的

    dubbo-servicecomb接入CSE需要投入的成本高,且社区dubbo-servicecomb未投入人力维护,可能遇到很多适配问题。 • 仅想使用CSE的治理能力,配置中心仍然使用Nacos,或者后期微服务整改后
    的头像 发表于 12-29 20:01 773次阅读

    基于Nacos的简单动态化线程池实现

    本文以Nacos作为服务配置中心,以修改线程池核心线程数、最大线程数为例,实现一个简单的动态化线程池。
    发表于 01-06 14:14 864次阅读

    Linux内核实现内存管理的基本概念

    本文概述Linux内核实现内存管理的基本概念,在了解基本概念后,逐步展开介绍实现内存管理的相关技术,后面会分多篇进行介绍。
    发表于 06-23 11:56 838次阅读
    Linux内核<b class='flag-5'>实现</b>内存管理的<b class='flag-5'>基本概念</b>

    Nacos实现原理:SpringCloud集成Nacos实现过程

    Nacos服务提供者,里面包含的Open API是功能访问入口,Conig Service、Naming Service 是Nacos提供的配置服务、命名
    发表于 10-09 16:08 1085次阅读
    <b class='flag-5'>Nacos</b><b class='flag-5'>实现</b>原理:SpringCloud集成<b class='flag-5'>Nacos</b>的<b class='flag-5'>实现</b>过程