public void run() {
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null) {
updateServiceNow(serviceName, clusters);
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
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;
}
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
}
}# 前言
作为Spring Cloud Alibaba的重要组件之一,Nacos的到底是如何工作的呢?接下来走进Nacos的神秘世界
Nacos整合Spring Cloud的服务注册
Nacos作为Spring Cloud Alibaba 的核心组件之一,必然也会遵循cloud的一些规范,在Spring Cloud中存在一个ServiceRegistry的接口,顾名思义,该接口就是用于服务注册的,所以在Nacos在Spring Cloud的环境下,也存在一个实现了该接口的类NacosServiceRegistry
,该方法实现了ServiceRegistry的所有方法。如下:
public interface ServiceRegistry<R extends Registration> {
void register(R registration);
void deregister(R registration);
void close();
void setStatus(R registration, String status);
<T> T getStatus(R registration);
当前主题之讨论服务的注册方法register
在讨论该方法之前,我们还是先简单说明一下NacosServiceRegistry
是怎么装配到spring 的环境中的
这里采用的是spring boot的自动装配的原理。我们可以在spring-cloud-starter-alibaba-nacos-discovery-2.2.1.RELEASE.jar
中的META-INF/spring.factories文件中存在一个NacosServiceRegistryAutoConfiguration
的类,该类就是Nacos整合Spring Cloud的自动装配的类,进入到该类
public class NacosServiceRegistryAutoConfiguration {
@Bean
public NacosServiceRegistry nacosServiceRegistry(
NacosDiscoveryProperties nacosDiscoveryProperties) {
return new NacosServiceRegistry(nacosDiscoveryProperties);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosRegistration nacosRegistration(
NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
return new NacosRegistration(nacosDiscoveryProperties, context);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}
该类是一个配置类,注入了NacosServiceRegistry
,NacosRegistration
,NacosAutoServiceRegistration
等bean。
其中
NacosServiceRegistry
: serviceRegistry接口,作为nacos服务注册的实现NacosRegistration
: 实现了Registration
和ServiceInstance
,用户存储服务实例信息,如端口,ip等等NacosAutoServiceRegistration
:继承自AbstractAutoServiceRegistration
类,用于通过事件的机制触发服务的自动注册。
所以入口我们基本上已经找到了,会在NacosAutoServiceRegistration
中触发服务的注册。
进入到AbstractAutoServiceRegistration
类我们可以看到该类实现了ApplicationListener<WebServerInitializedEvent>
接口,所以会监听到WebServerInitializedEvent
的事件,如果使用dubbo作为注册中心且没有web的环境时,将会走DubboServiceRegistrationNonWebApplicationAutoConfiguration
类的自动装配,该类中会监听ApplicationStartedEvent
事件,然后调用serviceRegistry.register()
方法。这里我们从web环境的入口进入。
进入到AbstractAutoServiceRegistration
类的监听方法onApplicationEvent
。该方法会调用一个bind方法
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(((ConfigurableWebServerApplicationContext) context)
.getServerNamespace())) {
return;
}
}
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();
}
- 为port赋值,获取web项目监听的端口
- 调用start()方法开始注册服务
进入到start()方法
该方法最终会调用子类的register
方法,对端口进行校验,校验之后调用父类的register
方法,在父类的register
方法中,调用注入的serviceRegistry
的register
方法,并传入注入的Registration
.如下:
protected void register() {
this.serviceRegistry.register(getRegistration());
}
现在进入到serviceRegistry.register
方法中。因为他的实现是NacosServiceRegistry
,所以我们先进入到NacosServiceRegistry
的构造方法中看看该类初始化时做了什么
public NacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.namingService = nacosDiscoveryProperties.namingServiceInstance();
}
该类在注入时,会传入一个nacosDiscoveryProperties,同时可以通过nacosDiscoveryProperties.namingServiceInstance();方法获取到一个namingService,该对象是nacos提供的服务注册的API。在namingService对象的初始化时初始化的是NacosNamingService
类,将会初始化如下信息
private void init(Properties properties) {
this.namespace = InitUtils.initNamespaceForNaming(properties);
this.initServerAddr(properties);
InitUtils.initWebRootContext();
this.initCacheDir();
this.initLogName(properties);
this.eventDispatcher = new EventDispatcher();
this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
this.beatReactor = new BeatReactor(this.serverProxy, this.initClientBeatThreadCount(properties));
this.hostReactor = new HostReactor(this.eventDispatcher, this.serverProxy, this.cacheDir, this.isLoadCacheAtStart(properties), this.initPollingThreadCount(properties));
}
然后回到NacosServiceRegistry
中的register方法中
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
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);
}
}
-
参数校验
-
获取
serviceId
,作为服务名,默认会获取spring.application.name
配置的名字 -
获取group,可配置,默认为Default,
-
调用
getNacosInstanceFromRegistration
组装一个Instance,表示一个服务的具体的是一个实例对象instance.该方法中主要携带了post,ip,权重,元数据信息,集群名称等。如下Instance instance = new Instance(); instance.setIp(registration.getHost()); instance.setPort(registration.getPort()); instance.setWeight(nacosDiscoveryProperties.getWeight()); instance.setClusterName(nacosDiscoveryProperties.getClusterName()); instance.setMetadata(registration.getMetadata());
-
调用
namingService.registerInstance(serviceId, group, instance);
进行具体的服务注册
进入到namingService.registerInstance(serviceId, group, instance);
方法,该对象中有很多的重载方法,最终都是构建一个instance的实例,所以我们直接进入到该方法
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
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);
beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
如果实例是一个临时节点,需要尽心心跳检测,配置心跳信息对象BeatInfo
,调用this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);方法将心跳信息传入。该方法会通过线程池创建一个scheduler.发送心跳,这里就不具体分析了。
进入到this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
方法,看看具体的服务注册
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<String, String> params = new HashMap<String, String>(9);
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);
}
封装发起请求的参数及注册的api:这里传入的是/nacos/v1/ns/instance,因为在nacos中的服务注册其实是通过http请求进行的。接下来调用callServer方法
public String callServer(String api, Map<String, String> params, String body, String curServer, String method)
throws NacosException {
long start = System.currentTimeMillis();
long end = 0;
injectSecurityInfo(params);
List<String> headers = builderHeaders();
String url;
if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
url = curServer + api;
} else {
if (!curServer.contains(UtilAndComs.SERVER_ADDR_IP_SPLITER)) {
curServer = curServer + UtilAndComs.SERVER_ADDR_IP_SPLITER + serverPort;
}
url = HttpClient.getPrefix() + curServer + api;
}
HttpClient.HttpResult result = HttpClient.request(url, headers, params, body, UtilAndComs.ENCODING, method);
end = System.currentTimeMillis();
MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code))
.observe(end - start);
if (HttpURLConnection.HTTP_OK == result.code) {
return result.content;
}
if (HttpURLConnection.HTTP_NOT_MODIFIED == result.code) {
return StringUtils.EMPTY;
}
throw new NacosException(result.code, result.content);
}
使用HttpClient发起一个http接口的调用。到这里,关于服务注册的client已经完成了。
Nacos 整合Spring Cloud 的服务发现
在Spring Cloud中同样存在一个服务发现的接口DiscoveryClient
,作为服务的发现接口,而在nacos中同样存在实现了该接口的类,NacosDiscoveryClient
,它的注入依然是通过spring boot的自动装配实现的,进入到该类的getInstances
方法中
public List<ServiceInstance> getInstances(String serviceId) {
try {
return serviceDiscovery.getInstances(serviceId);
}
catch (Exception e) {
throw new RuntimeException(
"Can not get hosts from nacos server. serviceId: " + serviceId, e);
}
}
该方法中会调用serviceDiscovery
该方法根据服务名返回该服务的所有实例。服务名的获取方式有很多,比如restTemplate拦截指定的服务名,feign客户端指定等等。
现在直接进入到serviceDiscovery.getInstances(serviceId);
方法中。
public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
String group = discoveryProperties.getGroup();
List<Instance> instances = discoveryProperties.namingServiceInstance()
.selectInstances(serviceId, group, true);
return hostToServiceInstanceList(instances, serviceId);
}
从配置discoveryProperties
获取分组信息,并通过配置初始化一个namingServiceInstance
的对象,通过该对象调用selectInstances
方法获取所有实例。namingService的获取方式跟服务注册一样,这里就不再详述。
进入到selectInstances
方法。该方法会根据是否订阅通过不同的方式获取到ServiceInfo.SerivceInfo中包含了我们服务的所有信息,如集群名称,实例list,分组名称,服务名等。selectInstances如下
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
if (subscribe) {
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
}
return selectInstances(serviceInfo, healthy);
}
如果没有订阅时,将调用hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
直接从远程服务端拉取服务。最终调用serverProxy.queryList(serviceName, clusters, 0, false);
方法获取服务。在queryList
中将会组装参数和远程调用的接口api/v1/ns/instance/list
接口,然后远程调用接口。返回ServiceInfo。
我们重点来看看当订阅时,做了些什么东西。
进入到hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
方法中。
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
- 现根据服务名和集群名获取ServiceInfo.如果不null,则如果服务需要根性,就根性,否则就返回该服务对应的ServiceInfo.
- 如果为null,则立刻拉取服务
进入updateServiceNow
方法中。
public void updateServiceNow(String serviceName, String clusters) {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
if (StringUtils.isNotEmpty(result)) {
processServiceJSON(result);
}
} catch (Exception e) {
NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
该方法将会立刻调用serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
方法查询最新的服务。如果获取到,就进入到processServiceJSON(result);
进行处理。我们注意到,这里传入了一个UDP的端口,该端口会在hostReactor初始化时初始化一个PushReceiver
,该对象中初始化一个DatagramSocket
,并使用线程池处理接收到的请求,当注册中心服务提供者出现变化时,会主动回调该接口,发送变动的服务提供者实例,这里接到请求后在进行处理。
该方法比较长,主要的功能就是做一些判断,然后将新获取到的ServiceInfo组装起来。缓存到serviceInfoMap
集合中。
回到getServiceInfo
方法,此时serviceInfoMap
中已经存在ServiceInfo对象了,进入到scheduleUpdateIfAbsent
方法
public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
}
}
该方法会添加一个调度任务,定时的去更新远程服务.因为添加的是一个任务,所以我们直接去看UpdateTask
中的run方法
public void run() {
try {
ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null) {
updateServiceNow(serviceName, clusters);
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
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;
}
executor.schedule(this, serviceObj.getCacheMillis(), TimeUnit.MILLISECONDS);
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
}
}
定时的去远程拉取服务。每隔10s执行一次,根据获取到的服务进行判断,是否更新本地缓存。
到这里,服务发现就讲完了,同时客户端从注册中心更新服务的两种方式都已经得到了体现。