1、客户端服务注册
Nacos在1.x版本的通信方式采用的HTTP协议,2.0版本以后会添加gRPC协议,本文写作采用的版本为1.4.2,所以本文还是基于HTTP协议来分析,服务注册的接口地址为:/nacos/v1/ns/instance,此接口的源码全路径为:com.alibaba.nacos.naming.controllers.InstanceController#register。
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
final Instance instance = parseInstance(request);
// 进行注册
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
Nacos提供了多种客户端的集成方式,本文主要剖析 Spring Cloud 的集成方式的源码,如果读者感兴趣其它的集成方式,可以去Nacos官网查看其它的集成方式。
1.添加依赖
// 这些需要先添加Spring Cloud的依赖,我做了省略。。。。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${latest.version}</version>
</dependency>
2.配置Nacos server地址
spring:
application:
name: nacos-demo
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 // Nacos server地址
接下来分析一下Spring Cloud集成Nacos客户端的源码,在spring-cloud-starter-alibaba-nacos-discovery-2.2.2.RELEASE.jar中/META-INF/spring.factories有如下代码:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-- 省略其它代码。。。。。
com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\ -- 实现服务注册与发现的核心注册类
熟悉Spring Boot的同学都知道这个文件,这里是配置AutoConfiguration的地方,在这里只需要关注其中的:NacosServiceRegistryAutoConfiguration,这个类中分别注入了NacosServiceRegistry、NacosRegistration、NacosAutoServiceRegistration三个类,Nacos的客户端主要是靠这几个类来实现服务注册和发现的,首先我们来看一下NacosAutoServiceRegistration的类继承关系,如下:
细心的同学可能已经发现NacosAutoServiceRegistration的继承的AbstractAutoServiceRegistration类实现了ApplicationListener接口,那么必定在AbstractAutoServiceRegistration类中监听的某个Event,果然在AbstractAutoServiceRegistration类中发现了如下代码:
// 监听了WebServerInitializedEvent事件,WebServerInitializedEvent有一个子类ServletWebServerApplicationContext
public void onApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}
@Deprecated
public void bind(WebServerInitializedEvent event) {
// 省略其它代码。。。。。。
this.start();
}
public void start() {
// 省略其它代码。。。。。。。
if (!this.running.get()) {
// 省略其它代码。。。。。。。
register();
// 省略其它代码。。。。。。。
}
}
上述源码中发现最终会调用一个register方法,这个方法就是真正向 Nacos Server 注册了当前实例。
// com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register
@Override
public void register(Registration registration) {
// 省略其它代码。。。。。。。
try {
// 真正调用了Nacos Server接口
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
// 省略其它代码。。。。。。。
}
}
// com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
serverProxy.registerService(groupedServiceName, groupName, instance);
}
// com.alibaba.nacos.client.naming.net.NamingProxy#registerService
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
// 省略其它代码。。。。。。。
// 采用HTTP请求调用 /nacos/v1/ns/instance
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
从源码中可以看出最终调用了reqApi方法,向 Nacos Server /nacos/v1/ns/instance 接口发送了一个POST请求,把当前实例注册进去,到这里整个客户端的核心注册流程就分析完了。
2、客户端服务发现
服务发现的接口地址为:/nacos/v1/ns/instance/list,此接口的源码全路径为:com.alibaba.nacos.naming.controllers.InstanceController#list
@GetMapping("/list")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
public ObjectNode list(HttpServletRequest request) throws Exception {
// 省略其它代码。。。。。。。
// 省略的代码基本上都是在获取request中的参数
return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
healthyOnly);
}
在spring.factories中配置了一个NacosDiscoveryClientConfiguration类,此类向Spring中注入了一个NacosWatch类,这类的类图如下:
从上图可以看出,此类实现了Lifecycle接口,这个接口是Spring设计的生命周期接口,如果实现这个接口,那么就会在Spring加载完所有的Bean并初始化之后就会回调start()方法,在这个方法中完成了服务的拉取并更新到本地缓存,代码如下:
从源码可以看出最后也是调用了serverProxy.queryList方法,这个方法也是发起了一个HTTP的请求,调用了Nacos Server的/nacos/v1/ns/instance/list接口,进行服务拉取。
到这里已经从源码级别分析了Spring Cloud的集成了Nacos客户端关于服务拉取的代码,其实代码还是比较简单的,总结来说就是构造出list接口需要的参数,然后发起HTTP请求,进行服务拉取。
从源码中注意留意一个scheduleUpdateIfAbsent方法的调用,这里提交了一个UpdateTask任务,UpdateTask是一个实现了Runnable接口的类,主要代码如下:
// com.alibaba.nacos.client.naming.core.HostReactor.UpdateTask
@Override
public void run() {
long delayTime = -1;
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null) {
// 从Nacos Server拉取service信息并更新到本地
updateServiceNow(serviceName, clusters);
delayTime = DEFAULT_DELAY;
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
// 从Nacos Server拉取service信息并更新到本地
updateServiceNow(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
} else {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters);
}
lastRefTime = serviceObj.getLastRefTime();
if (!eventDispatcher.isSubscribed(serviceName, clusters) && !futureMap
.containsKey(ServiceInfo.getKey(serviceName, clusters))) {
// abort the update task
NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
return;
}
delayTime = serviceObj.getCacheMillis();
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
} finally {
if (delayTime > 0) {
// 再次提交一个Task,把自己提交给executor,让其在delayTime时间之后执行(Nacos默认设置为10s)
executor.schedule(this, delayTime, TimeUnit.MILLISECONDS);
}
}
}
从源码中可以看出,这段代码相当于定时10s(这个时间是从/nacos/v1/ns/instance/list接口里回传回来的)拉取一次服务,这里有个Nacos Server比较巧妙的设计需要提一下,在updateServiceNow方法中可以看到调用服务端/nacos/v1/ns/instance/list接口的时候传入了一个Udp的端口,这个端口的作用是如果Nacos Server感知到Service的变化,就会把变化信息通知给订阅了这个Service信息的客户端。