深入理解AP架构Nacos注册原理

Nacos是一款阿里巴巴开源用于管理分布式微服务的中间件,能够帮助开发人员快速实现动态服务发现、服务配置、服务元数据及流量管理等。这篇文章主要剖析一下Nacos作为注册中心时其服务注册与发现原理。,Nacos作为注册中心是为了更好更方便的管理应用中的每一个服务,是各个分布式节点之间的纽带。其作为注册中心主要提供以下核心功能:,CAP定理是分布式系统中最基础的原则,所以理解和掌握了CAP对系统架构的设计至关重要。分布式架构下所有系统不可能同时满足以下三点:Consisteny(一致性)、Availability(可用性)、Partition tolerance(分区容错性),CAP指明了任何分布式系统只能同时满足这三项中的两项。,图片,分布式系统肯定都要保证其容错性 ,那么可用性和一致性就只能选一个了。简单来说分布式系统的CAP理论就像你想买个新手机,这个手机不可能功能强大、便宜、又好看的,它最多只能满足两点的,要么功能强大便宜、要么功能强大好看、要么便宜好看,不可能同时满足三点。,注册中心在分布式应用中是经常用到的,也是必不可少的,那注册中心,又分为以下几种:Eureka、Zookeeper、Nacos等。这些注册中心最大的区别就是其基于AP架构还是CP架构,简单介绍一下:,本篇文章主要是深入研究一下Nacos基于AP架构微服务注册原理,由于篇幅有限基于CP架构的Nacos微服务注册下次再跟你们分析。,1.微服务在启动将自己的服务注册到Nacos注册中心,同时发布http接口供其他系统调用,一般都是基于SpringMVC。,2.服务消费者基于Feign调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对加了该注解的接口生成动态代理,服务消费者针对Feign生成的动态代理去调用方法时,会在底层生成Http协议格式的请求,类似 /stock/deduct? productId=100。,3.Feign最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均衡并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成调用的Http接口地址。,图片,服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。,服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。,服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它 的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复 发送心跳则会重新注册),服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清 单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存,服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。,看Nacos源码的不难发现,Nacos实际上就是一个基于Spring Boot的web应用,不管是服务注册还是发送心跳都是通过给Nacos服务端发送http请求实现的。下载并编译Nacos源码就不过多赘述了,首先需要搭建一个微服务作为Nacos的客户端。,Nacos客户端也是个Spring Boot项目,当客户端服务启动时Spring Boot项目启动时自动加载spring-cloud-starter-alibaba-nacos-discovery包的META-INF/spring.factories中包含自动装配的配置信息,并将文件中的类加载成bean放入Spring容器中,我们可以先看一下spring.factories文件:,找到Nacos注册中心的自动配置类:NacosServiceRegistryAutoConfiguration。,NacosServiceRegistryAutoConfiguration这个类是Nacos客户端启动时的一个入口类,代码如下:,看NacosServiceRegistryAutoConfiguration配置类有3个@Bean注解。,图片,利用IDEA查看类结构,如上图所示,NacosAutoServiceRegistration继承AbstractAutoServiceRegistration类,而AbstractAutoServiceRegistration类又实现了AutoServiceRegistration和ApplicationListener接口。,ApplicationListener接口是Spring提供的事件监听接口,Spring会在所有bean都初始化完成之后发布一个事件,ApplicationListener会监听所发布的事件,这里的事件是Spring Boot自定义的WebServerInitializedEvent事件,主要是项目启动时就会发布WebServerInitializedEvent事件,然后被AbstractAutoServiceRegistration监听到,从而就会执行onApplicationEvent方法,在这个方法里就会进行服务注册。,这里AbstractAutoServiceRegistration类实现了Spring监听器接口ApplicationListener,并重写了该接口的onApplicationEvent方法。,继续点下去看bind方法。,看到这里发现了bind方法里有个非常重要的start()方法,继续看该方法的register()就是真正的客户端注册方法。,跳过一些中间非关键性的代码,可以直接看该注册方法。,这里的serviceRegistry就是NacosServiceRegistryAutoConfiguration类中第一个@Bean定义的bean,第一个@Bean就是这里的serviceRegistry对象的实现;其中getRegistration()获取的就是第二个@Bean定义的NacosRegistration的实例,这两个bean实例都是通过第3个@Bean传进来的,所以这里就可以把NacosServiceRegistryAutoConfiguration类中那3个@Bean给串起来了。,不得不说,阿里巴巴开发的中间件,其底层源码的命名还是很规范的,register()方法从命名上来看就可以知道这是注册的方法,事实也确实是注册的方法,这个方法中会通过nacos-client包来调用nacos-server的服务注册接口来实现服务的注册功能。下面我看一下调用Nacos注册接口方法:,根据源码可以知道beatReactor.addBeatInfo()方法作用在于创建心跳信息实现健康检测,Nacos 服务端必须要确保注册的服务实例是健康的,而心跳检测就是服务健康检测的手段。而serverProxy.registerService()实现服务注册,综上可以分析出Nacos客户端注册流程:,图片,到此为止还没有真正的实现服务的注册,但是至少已经知道了Nacos客户端的自动注册原理是借助了Spring Boot的自动配置功能,在项目启动时通过自动配置类。NacosServiceRegistryAutoConfiguration将NacosServiceRegistry注入进来,通过Spring的事件监听机制,调用该类的注册方法register(registration)实现服务的自动注册。,当Nacos服务端启动后,会先从本地缓存的serviceInfoMap中获取服务实例信息,获取不到则通过NamingProxy调用Nacos服务端获取服务实例信息,最后开启定时任务每秒请求服务端获取实例信息列表进而更新本地缓存serviceInfoMap,服务发现拉取实例信息流程图如下:,图片,废话不多说,直接上服务发现源码:,这里值得注意的是,Nacos客户端拉取注册列表方法的最后又是一个定时任务任务,每隔10秒钟就会拉取一次服务端Nacos的注册表。为啥这里要定时任务拉取呢?因为上面到注册表map是缓存在客户端本地的,假如有新的服务注册到Nacos时,这时就要更新客户端注册表信息,所以这里会执行一个拉取的任务。,上面分析了当客户端在其本地缓存中没有找到注册表信息,就会调用Nacos服务端api拉取注册表信息,不难发现服务端查询注册表api为”/instance/list”。,这里通过doSrvIpxt()方法获取服务列表,根据namespaceId、serviceName获取service实例,service实例中srvIPs获取所有服务提供者的实例信息,遍历组装成json字符串并返回。,最后看一下获取服务端实例方法,最后就是将临时实例或者持久实例放在一个集合中返回给客户端。,总结一下Nacos客户端服务发现的核心流程:,如果没有开启订阅模式,则直接通过调用/instance/list接口获取服务实例列表信息;,如果开启订阅模式,则先会从本地缓存中获取实例信息,如果不存在,则进行订阅获并获取实例信息;在获得最新的实例信息之后,也会执行processServiceJson(result)方法来更新内存和本地实例缓存,并发布变更时间。,开启订阅时,会开启定时任务,定时执行UpdateTask获取服务器实例信息、更新本地缓存、发布事件等;,服务端的注册源码逻辑相对客户端的还是要复杂很多,所以这里我们先看一下Nacos服务端注册的完整流程图,避免一上来就看源码被绕晕。,图片,接下来我们就着重分析一下AP架构Nacos服务注册的源码。,Nacos服务端注册当然是本文的核心,那么首先我们来看一下Nacos服务端注册源码。从Nacos的客户端注册原理不难发现,客户端通过调用Nacos服务端提供的http接口实现注册,对外提供的服务接口请求地址为nacos/v1/ns/instance,实现代码咋nacos-naming模块下的InstanceController类中:,客户端就是通过调用该api实现Nacos的注册的,下面可以看一下Nacos的这个注册api是怎么实现的。,registerInstance()干了两件事儿,第一就是createEmptyService()方法从请求参数汇总获得serviceName(服务名)和namespaceId(命名空间Id),第二就是调用registerInstance注册实例。先看一下createEmptyService方法。,Nacos的注册表是多级存储结构,最外层是通过namespace来实现环境隔离,然后是group分组,分组下就是服务,一个服务有可以分为不同的集群,集群中包含多个实例。因此其注册表结构为一个Map,类型是:Map<String, Map<String, Service>>外层key是namespace_id,内层key是group  + serviceName,Service内部维护一个Map,结构是:Map<String, Cluster>的key是clusterName,其值是集群信息;Cluster内部维护一个Set集合Set<Instance> ephemeralInstances和Set<Instance> persistentInstances,元素是Instance类型,代表集群中的多个实例。,createEmptyService()方法就是服务端构建注册表的方法,基于AP架构的Nacos实际就是将注册实例信息保存在内存中。,createEmptyService()方法主要作用如下:,createServiceIfAbsent()方法主要作用在于第一次注册进来,从注册表里获取命名空间,肯定是为null,所以需要构建一个命名空间,设置nameSpace等信息并保存到缓存中。这个方法里值得注意的是putServiceAndInit()方法,可以点进来看一下这个方法:,这里我着重putService(service)方法,这里实际是将注册的实例缓存到内存的注册表中,接下来我们看一下 putServiceAndInit(Service service)方法中的,init()初始化方法是怎么保持心跳连接的。,可以看出init方法是开启了一个异步线程ClientBeatCheckTask去做了个周期性发送心跳的机制,方法中客户端心跳检查任务,开启延迟5s的任务,然后每隔5秒钟执行一次。,service.init()方法主要通过定时任务不断检测当前服务下所有实例最后发送心跳包的时间。在这个方法里面主要是循环当前service的每一个临时实例,用当前时间减去最后一次心跳时间是否大于15s来判断心跳是否超时,如果大于这个时间会执行instance.setHealthy(false)将实例的健康状态改为false,但是这个定时任务不会立即执行,会每5秒执行一次;当前时间 – 实例上一次心跳时间 > 实例的删除时间【默认30s】就会删除实例。,那么服务实例的最后心跳包更新时间是谁来触发的呢?实际上前面在说客户端注册时有说到, Nacos客户端注册服务的同时也建立了心跳机制。,上文中registerInstance注册实例方法中还有一个最最重要的方法就是addInstance()方法,其本质上就是把当前注册的服务实例保存到Service中。,这里着重看一下这个put方法,put方法主要做了两件事,第一对对客户端的请求过来的实例进行注册,第二是Nacos集群架构下的数据同步,Nacos默认用的是临时实例,也就是ephemeral = true,也就是本文的重点AP架构的Nacos注册原理。,先来看一下onPut()方法,不难发现当注册实例数据有改变时,就无脑将这个实例扔到这个task内存阻塞队列中去,具体可以看一下addTask()方法。,当有实例需要注册时,直接调用addTask()方法将这个实例信息无脑扔进内存阻塞队列中去,注册就结束了。这个应该算是Nacos注册的一个精髓吧,Nacos为了提高性能其源码使用了大量的异步任务、异步线程等操作,用这些方式对提升Nacos性能有很大帮助。不难猜到,这里把客户端实例对象放进内存队列,后续肯定是通过异步起线程的方式去注册。,不难发现addTask()方法是Notifier类的方法,Notifier实现了Runnable接口,很明显这就是一个异步线程,这里跟上面的猜想一致,Nacos就是通过开启了一个异步线程实现注册的,具体的注册方法直接可以看Notifier线程的run()方法即可。那么这个Notifier线程是啥时候开启的呢?,了解Spring的应该都知道这个是Spring加载的一种初始化方式,Spring启动时加载这个init方法初始化数据,就会开启一个线程加载Notifier任务。,看了Notifier线程的run()方法,不免会有几个疑问。第一、这里的for循环会占用cpu资源吗?第二、为什么要把实例信息都无脑先放在内存阻塞队列中,然后另起一个线程去异步注册呢?第三、阿里这里为什么要这么设计?这样设计好处是什么呢?,这里第一个问题不会占用cpu资源,因为tasks是个阻塞队列,如果tasks中没有实例信息,这里就会阻塞在这,不会无脑死循环,所以是不会占用cpu资源的;,第二个问题个人理解:Nacos是在阿里内部使用的中间件,肯定是需要满足高并发、高性能、高可扩展,阿里内部估计就有几十万台机器,如果不能实现高并发注册那么肯定会有很多问题。比如订单服务需要注册到nacos时,那么订单启动时就需要注册,服务注册到Nacos的逻辑还是比较复杂的【详见com.alibaba.nacos.naming.core.Service#updateIPs】,假如这里不用异步注册而是用同步注册的方式,那么服务注册到Nacos需要花费很多时间,这才是一个注册到Nacos的行为就花费了大量时间,那么如果多几个中间需要加载的话,那会浪费很多时间,所以这里采用异步注册。,那么我们再看一下异步写注册表的方法:,第三个问题:可以看上面这个写注册表的源码,当服务A需要注册到Nacos时,并不是直接写进Nacos的注册表里,实际上是先拷贝了一个副本,订单服务注册写注册表时直接写副本的注册表,副本写完后才会替换原来Nacos中的注册表,所以当库存服务需要从Nacos拉取服务时,拉取的是Nacos实际注册表中的信息,这种设计方式能够大大提高Nacos的注册性能。,类似于CopyOnWriteArrayList的copy on write机制,也就是写时复制、读写分离设计思想。这种读写分离对于客户端注册感知实时性可能会稍差点,但是这种情况并没什么关系,Eureka有时候实例注册都会感知几十秒,对当前的nacos架构而言,既然要实现高并发那么只能牺牲一点实例注册的及时响应时间。,当Nacos注册成功后,就需要发布事件,主动通知客户端,接下来可以看一下发布事件的源码:,这里Nacos会通过udp的方式将服务变动通知给订阅的客户端。Nacos的这种推送模式相对于zk那种利用tcp长连接而言还是会节约很多资源,即使有大量节点更新也不会使Nacos出现性能瓶颈。,当Nacos客户端接收到了udp消息后会给服务端返回一个ack,如果Nacos超时未收到ack,还会有重发机制,超过了这个超时时间就不再重发了。虽然udp是个不可靠协议不能保证消息一定能推送到客户端,但是Nacos客户端还是有定时轮训做兜底定时查询Nacos注册表。Nacas采用了这两种机制,既保证了实时性,又保证了数据更新不会被漏掉。,Nacos数据同步分为全量同步和增量同步,全量同步就是初始化数据一次性同步,而增量同步是指有数据增加的时候,只同步增加的数据。,7.3.5.1 Nacos集群全量数据同步,Nacos集群有新的节点启动时,DistroProtocol类就会在Spring加载时调用构造方法,同时开启一个数据同步任务,该方法会执行startVerifyTask()和startLoadTask(),我们重点关注startLoadTask(),具体代码如下:,上面方法会调用DistroLoadDataTask对象,而该对象其实是个线程,因此会执行它的run方法,run方法会调用load()方法实现数据全量加载,代码如下:,数据同步会通过Http请求从远程服务器获取数据,并同步到当前服务的缓存中。执行流程如下:,首先,loadAllDataSnapshotFromRemote()从远程加载所有数据,并处理同步到本机;,第二,transportAgent.getDatumSnapshot()远程加载数据,通过Http请求执行远程加载;,第三,dataProcessor.processSnapshot()处理数据同步到本地,到这为止实现数据全量同步,其实全量同步最终还是互相调用Nacos提供的api。总结一下全量数据同步的过程:,7.3.5.2 Nacos集群增量数据同步,当服务注册完成后,Nacos需要将客户端实例信息同步到Nacos集群其他节点,可以看一下Nacos底层是怎么实现的。我们再次回到put方法:,上文中已经解释过put方法中的 onPut(key, value)方法,接下来我们再了解一下AP结构Nacos下的节点数据是同步,也就是distroProtocol.sync方法。,这里直接把需要同步的信息放在了内存的ConcurrentHashMap中,我们看一下这里具体看一下怎么同步其他节点。,这个类中会创建一个任务执行引擎,代码如下:,将同步数据到其他Nacos实例到tasks任务放进一个queue中,然后在InnerWorker.run()方法中从queue队列中拿任务执行。看一下具体是怎么执行同步任务的:,这里从队列里面拿出来任务执行,不难发现这里的任务执行的具体方法就是DistroSyncChangeTask类的run方法:,每个Nacos服务端实例都会提供这样的一个api接口供其他Nacos实例调用,从而同步注册实例数据。DistroSyncChangeTask类的run方法,就是调用http接口同步任务接口,将本节点的注册实例数据同步到其他节点机器上。,总结一下上面增量数据同步方法:,DistroProtocol 使用 sync() 方法处理AP 架构下的节点数据同步,向其他节点发布广播任务调用 distroTaskEngineHolder 发布延迟任务,调用 DistroDelayTaskProcessor.process() 方法进行任务投递:将延迟任务转换为异步变更任务,执行变更任务 DistroSyncChangeTask.run() 方法:向指定节点发送消息,Nacos避免并发读写的冲突:Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将老得实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。,Nacos提高注册并发:为了应对阿里巴巴内部数十万服务的并发写请求Nacos内部会将服务注册的任务放入阻塞队列,采用线程池异步来完成实例更新,从而提高并发写能力。,Nacos的服务发现分为两种模式:主动拉取模式,消费者定期主动从Nacos服务端拉取服务列表并缓存起来,当服务调用时优先读取本地缓存中的服务列表。订阅模式,消费者订阅Nacos中的服务列表,并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时,会发送UDP广播给所有订阅者。与Eureka相比,Nacos的订阅模式服务状态更新更及时,消费者更容易及时发现服务列表的变化,剔除故障服务。

文章版权声明

 1 原创文章作者:cmcc,如若转载,请注明出处: https://www.52hwl.com/18597.html

 2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈

 3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)

 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023年3月5日 上午12:00
下一篇 2023年3月7日 下午10:34