


在Spring-Cloud-Common包中有一个类org.springframework.cloud. client.serviceregistry .ServiceRegistry ,它是Spring Cloud提供的服务注册的标准。集成到Spring Cloud中实现服务注册的组件,都会实现该接口。

该接口有一个实现类是NacoServiceRegistry。
SpringCloud集成Nacos的实现过程:
在spring-clou-commons包的META-INF/spring.factories中包含自动装配的配置信息如下:

其中AutoServiceRegistrationAutoConfiguration就是服务注册相关的配置类:

在AutoServiceRegistrationAutoConfiguration配置类中,可以看到注入了一个AutoServiceRegistration实例,该类的关系图如下所示。

可以看出, AbstractAutoServiceRegistration抽象类实现了该接口,并且最重要的是NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration。
看到EventListener我们就应该知道,Nacos是通过Spring的事件机制继承到SpringCloud中去的。
AbstractAutoServiceRegistration实现了onApplicationEvent抽象方法,并且监听WebServerInitializedEvent事件(当Webserver初始化完成之后) , 调用this.bind ( event )方法。

最终会调用NacosServiceREgistry.register()方法进行服务注册。


在NacosServiceRegistry.registry方法中,调用了Nacos Client SDK中的namingService.registerInstance完成服务的注册。

跟踪NacosNamingService的registerInstance()方法:

通过beatReactor.addBeatInfo()创建心跳信息实现健康检测, Nacos Server必须要确保注册的服务实例是健康的,而心跳检测就是服务健康检测的手段。
serverProxy.registerService()实现服务注册

从上述代码看,所谓心跳机制就是客户端通过schedule定时向服务端发送一个数据包 ,然后启动-个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则认为服务器出现了故障。Nacos服务端会根据客户端的心跳包不断更新服务的状态。
Nacos提供了SDK和Open API两种形式来实现服务注册。
Open API:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
SDK:

这两种形式本质都一样,底层都是基于HTTP协议完成请求的。所以注册服务就是发送一个HTTP请求:

对于nacos服务端,对外提供的服务接口请求地址为nacos/v1/ns/instance,实现代码咋nacos-naming模块下的InstanceController类中:





service.init ( ) 方法的如下图所示,它主要通过定时任务不断检测当前服务下所有实例最后发送心跳包的时间。如果超时,则设置healthy为false表示服务不健康,并且发送服务变更事件。在这里请大家思考一一个问题,服务实例的最后心跳包更新时间是谁来触发的?实际上前面有讲到, Nacos客户端注册服务的同时也建立了心跳机制。

putService方法,它的功能是将Service保存到serviceMap中:

继续调用addInstance方法把当前注册的服务实例保存到Service中:

总结:
Open API:

SDK:

InstanceController中的list方法:



可以通过subscribe方法来实现监听,其中serviceName表示服务名、EventListener表示监听到的事件:

具体调用方式如下:

或者调用selectInstance方法,如果将subscribe属性设置为true,会自动注册监听:


Nacos客户端中有一个HostReactor类,它的功能是实现服务的动态更新,基本原理是:
客户端发起时间订阅后,在HostReactor中有一个UpdateTask线程,每10s发送一次Pull请求,获得服务端最新的地址列表
对于服务端,它和服务提供者的实例之间维持了心跳检测,一旦服务提供者出现异常,则会发送一个Push消息给Nacos客户端,也就是服务端消费者
服务消费者收到请求之后,使用HostReactor中提供的processServiceJSON解析消息,并更新本地服务地址列表
如果是 Nacos 集群环境,客户端会随机选择一个 Nacos 节点发起注册。
为了讲解客户端是如何注册到 Nacos 集群环境的底层原理,我在本地搭建了一个 Nacos 集群环境,有 3 个 Nacos 服务,它们的 IP 相同,端口号不同。
192.168.10.197:8848
192.168.10.197:8858
192.168.10.197:8868
然后服务 A 和服务 B 都是配置了 Nacos 集群的 IP 和 端口号的,配置如下所示
spring.cloud.nacos.discovery.server-addr
=192.168.10.197:8848,192.168.10.197:8858,192.168.10.197:8868
整体的结构如
下图所示,服务 A 和 服务 B 都往 Nacos 集群进行注册。
但是里面有一个问题:服务 A 注册时,是向所有 Nacos 节点发起注册呢?还是只向其中一个节点发起注册?如果只向一个节点注册,要向哪个节点注册呢?
答案:在 Client 发起注册之前,会有一个后台线程随机拿到 Nacos 集群服务列表中的一个地址。
Nacos 为什么会这样设计?
接下来我们看下服务 A 是怎么随机拿到一个 Nacos 节点的。
我们来看下客户端是如何随机选择一个节点的,流程图如下:

那么如何找到这些代码逻辑呢?思路是怎么样的?
我们之前讲过,RpcClient 会发起 request 请求,用的是和 Nacos 建立 currentConnection 连接来发起调用,代码如下:
// 发起调用
response = this.currentConnection.request(request, timeoutMills);
这个 currentConnection 是客户端和 Nacos 集群中的某个节点建立的连接,我们找下它在哪里赋值的。代码如下:
// 拿到 Nacos 节点信息
serverInfo = recommendServer.get() == null ? nextRpcServer() : recommendServer.get();
// 连接 Nacos 节点
connectToServer = connectToServer(serverInfo);
// 赋值 currentConnection
this.currentConnection = connectToServer;
而连接的信息是通过参数 serverInfo 传进去的,所以我们再看下 serverInfo 在哪里赋值的。
这个 nextRpcServer() 方法里面会拿到一个随机的 Nacos 地址:
// 一个 int 随机数,范围 [0 ~ Nacos 个数)
currentIndex.set(new Random().nextInt(serverList.size()));
// index 自增 1
int index = currentIndex.incrementAndGet() % getServerList().size();
// 返回 Nacos 地址
return getServerList().get(index);
小结:客户端生成一个随机数,然后通过这个随机数从 Nacos 服务列表中拿到一个 Nacos 服务地址返回给客户端,然后客户端通过这个地址和 Nacos 服务建立连接。Nacos 服务列表中的节点都是平等的,随机拿到的任何一个节点都是可以用来发起调用的。
为了演示发起注册的流程,我在这里模拟了一个注册请求。
用的是 curl 命令,对 Nacos 节点(127.0.0.1:8848)发起注册请求:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
请求 URL:/nacos/v1/ns/instance
请求参数:
之前我们讲到,Nacos 的有多个节点可以分别处理请求,当节点发现这个请求不是属于自己的,就会进行转发。
如下图所示:
服务 A 随机选择一个 Nacos 节点(图中为 Nacos1)发起注册请求,请求参数中包含了实例信息,Nacos 1 根据实例信息 hash + 取模拿到正确的节点,如果不属于自己,则将请求转发给其他节点(图中为 Nacos2)

步骤如下:
取余,拿到余数,比如 0、1、2、3。入口文件是 DistroFilter.java:
naming/src/main/java/com/alibaba/nacos/naming/web/DistroFilter.java
请求会先到 DistroFilter 类的 doFilter() 方法,拿到正确的节点地址后,将请求转发出去。
获取需要转发节点地址的代码如下:
// 找到 Nacos 集群中的目标节点
final String targetServer = distroMapper.mapSrv(distroTag);
// mapSrv 方法会先 hash,然后再取模,responsibleTag的值类似这样:"20.18.7.11:8080"
int index = distroHash(responsibleTag) % servers.size();
// distroHash 方法里面会对 客户端的 ip+port 字符串或者服务名字符串 进行 hash
Math.abs(responsibleTag.hashCode() % Integer.MAX_VALUE);
不论是自己处理注册请求还是转发给其他节点来处理,都会把实例信息存储起来,那么是如何进行存储的?
Nacos 目前有两个版本,v1 和 v2,如果是 v1,则是 instanceController 来处理注册请求,否则用 instanceControllerV2。本篇我们只讲解 v1 版本是怎么处理请求的。
先上流程图:

添加实例信息的流程
测试用的发起注册的命令:
curl -X POST 'http://127.0.0.1:8858/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
核心代码就是这个:
服务端注册实例的方法
首先有一个 synchronized 锁,然后执行 put 操作将临时的实例信息存放起来,所以重点看下 这个 consistencyService.put() 方法做了什么事情。
先看下源码:
onPut(key, value);
// 开启 1s 的延迟任务,将数据同步给其他 Nacos 节点
distroProtocol.sync(new DistroKey(key,KeyBuilder.INSTANCE_LIST_KEY_PREFIX),DataOperation.CHANGE,
DistroConfig.getInstance().getSyncDelayMillis());
这里面做了三件事情:
注意:针对第二点和第三点,属于 Distro 一致性协议的一部分,里面的内容还比较多,我们放到下一讲专门来讲。
一条注册请求的核心流程:
blog.csdn.net/cold___play/article/details/108032204