Nacos源码解析 -- 服务注册源码以及服务发现最全分析

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);
   }

该类是一个配置类,注入了NacosServiceRegistryNacosRegistrationNacosAutoServiceRegistration等bean。

其中

  • NacosServiceRegistry: serviceRegistry接口,作为nacos服务注册的实现
  • NacosRegistration: 实现了RegistrationServiceInstance,用户存储服务实例信息,如端口,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方法中,调用注入的serviceRegistryregister方法,并传入注入的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执行一次,根据获取到的服务进行判断,是否更新本地缓存。

到这里,服务发现就讲完了,同时客户端从注册中心更新服务的两种方式都已经得到了体现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

carl的分享笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值