阅读 646

面试被问SpringCloud Alibaba Nacos,一问三不知?看这里!

服务注册与发现

随着业务的发展,用户量和业务复杂度逐渐增加,系统为了支撑更大的流量需要做很多优化,比如升级服务器配置提升性能。在软件方面,我们会采用微服务架构,对业务服务进行微服务化拆分,水平扩容等来提升系统性能,以及解决业务的复杂性问题。

在微服务架构下,一个业务服务会拆分多个微服务,各个服务之间相互通信完成整体的功能。另外,为了避免单点故障,微服务都会采取集群方式的高可用部署,集群规模越大,性能也会越高 服务消费者要去调用多个服务提供者组成的集群。首先,服务消费者需要在本地配置文件中维护一个服务提供者集群的每个节点的请求地址。其次,服务提供者集群中如果某个节点下线或者宕机,服务消费者本地配置中需要同步删除这个节点的请求地址,防止请求发送到已经宕机的节点上造成请求失败,为了解决这类问题,就需要引入服务注册中心,它主要有以下功能:

  • 服务地址的管理
  • 服务注册
  • 服务动态感知

为什么会出现Nacos?

Nacos是阿里巴巴开源的一个对微服务架构中服务发现,配置管理和服务管理平台,由于第一代SpringCloud也就是Springcloud Netflix很多组件已经进入停更维护模式,所以迫使我们必须要找到一个可以代替Netflix的第二代产品,这时候SpringCloud Alibaba出现了

Nacos就是注册中心+配置中心的结合体,在SpringCloud第一代中=Eureka+Config+Bus

Nacos功能特性

  • 服务发现与健康监测
  • 动态配置管理
  • 动态DNS服务
  • 服务和元数据管理(管理平台的角度,nacos也有一个ui页面,可以看到注册的服务以及实例信息(元数据信息等),动态的服务权重调整,动态服务优雅下线,都可以去做)

Nacos单例服务部署

nacos.io/zh-cn/docs/… 先来点开Nacos官网快速开始看一下官网是如何教大家安装的

从官网看到,官网上说了两种编译方式

从 Github 上下载源码方式

git clone https://github.com/alibaba/nacos.git
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U  
ls -al distribution/target/

// change the $version to your actual path
cd distribution/target/nacos-server-$version/nacos/bin
复制代码

下载编译后压缩包方式

您可以从 github.com/alibaba/nac… 下载 nacos-server-$version.zip 包。

unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz
cd nacos/bin
复制代码

启动服务器

Linux/Unix/Mac

启动命令(standalone代表着单机模式运行,非集群模式):

sh startup.sh -m standalone

如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:

bash startup.sh -m standalone

Windows

启动命令(standalone代表着单机模式运行,非集群模式):

cmd startup.cmd -m standalone

这里我们刚刚下好了一个最新版的nacos-server 解压,进入nacos目录下,进入bin目录

sh-3.2# ls
shutdown.cmd	shutdown.sh	startup.cmd	startup.sh
复制代码

因为我电脑是mac所以需要执行sh startup.sh -m standalone命令

如果没有报错,访问http://127.0.0.1:8848/nacos/#/login

账号密码都是nacos 进入到首页

Nacos数据模型(领域模型)

Namespace命名空间,Group分组,集群这些都是为了进行归类管理,把服务和配置文件进行归类,归类之后就可以实现一定的效果,比如隔离

比如,对于服务来说,不同命名空间中的服务不能够相互访问调用 Namespace:命名空间,对不同的环境进行隔离,比如隔离开发环境,测试环境和生产环境

Group:分组,将若干个服务或者若干个配置集归为一组,通常习惯一个系统归为一个组

Service:某一个服务

DataId:配置集或者可以认为是一个配置文件

Namespace + Group + Service 如同 Maven 中的GAV坐标,GAV坐标是为了锁定 Jar,二这里是为了锁定服务

Namespace + Group + DataId 如同 Maven 中的GAV坐标,GAV坐标是为了锁定 Jar,二这里是为了锁定配置文件

自己动手玩玩Nacos

pom.xml:

<dependencies>
        <dependency>
            <artifactId>nacos-dubbo-provider-api</artifactId>
            <groupId>com.gupaoedu.example</groupId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
    </dependencies>
复制代码

NacosDubboProviderApplication.java

@SpringBootApplication
public class NacosDubboProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(NacosDubboProviderApplication.class, args);
    }

}
复制代码

启动项目,访问nacos控制台 http://127.0.0.1:8848/nacos/#/serviceManagement?dataId=&group=&appName=&namespace=&pageSize=&pageNo=

Nacos源码分析

git clone github.com/alibaba/nac… 下载nacos源码,使用idea打开

Nacos主要源码主要分三个部分:

  1. 服务注册
  2. 服务地址的获取
  3. 服务地址变化的感知

服务注册

首先,搞过SpringBoot的都知道,自动装配

那么,上面实战中,我们在maven中导入的spring-cloud-starer-alibaba-nacos这个包,我们在idea中项目目录中External Libraries下找到这个包

在META-INF目录下找到spring.factories文件

这里在SpringBoot启动的时候,就会自动装配以下几个类,这里直接找com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration

这里跟大家说一下,自动装配了这么多的类,我们如果一个一个点进去看,真的,累死,废时间,那么,我们可以根据类名猜一下。NacosDiscoveryAutoConfiguration(Nacos自动发现配置)看翻译, 像这么点意思,后面的就是RibbonNacosAutoConfiguration这个一看就是跟ribbon相关的,所以先不用管之后就是NacosDiscoveryEndpointAutoConfiguration,这个endpoint一看就不是主线代码,所以也先不用管,在之后NacosDiscoveryClientAutoConfiguration看着也有点那么意思,所以暂时先保留,再看最后一个NacosConfigServerAutoConfiguration,带Config的就肯定是就是把properties文件或者yml文件中的配置信息加载的类,所以,我们找NacosDiscoveryAutoConfiguration类来看一下

@Configuration
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(
    value = {"spring.cloud.service-registry.auto-registration.enabled"},
    matchIfMissing = true
)
@AutoConfigureAfter({AutoServiceRegistrationConfiguration.class, AutoServiceRegistrationAutoConfiguration.class})
public class NacosDiscoveryAutoConfiguration {
    public NacosDiscoveryAutoConfiguration() {
    }

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

复制代码

看到@Configuration这个注解,就说明这个类是一个配置类,所以我们就需要看里面的三个@Bean就行了。首先第一个是NacosServiceRegistry,也就是nacos服务注册的意思,后面看到服务注册回看这里,先跳过。第二个是NacosRegistration,nacos注册,好像也跟注册有关。这里我们看到第三个@Bean下面的类是NacosAutoServiceRegistration,Auto是自动的意思,自动装配,这里我们点进去看一下

public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
			AutoServiceRegistrationProperties autoServiceRegistrationProperties,
			NacosRegistration registration) {
		super(serviceRegistry, autoServiceRegistrationProperties);
		this.registration = registration;
	}
复制代码

到这里我们看一下这个类有没有继承什么父类,如果有,那么,我们还需要先看一下它的父类

public class NacosAutoServiceRegistration
		extends AbstractAutoServiceRegistration<Registration> 
 
复制代码

这里我们点进父类看一下

public abstract class AbstractAutoServiceRegistration<R extends Registration> implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> 
复制代码

这里我们看到它实现了ApplicationListener这个接口。对Spring了解的人应该知道这个接口是做事件监听的。这里会调用事件处理方法onApplicationEvent方法

public void onApplicationEvent(WebServerInitializedEvent event) {
        this.bind(event);
    }
复制代码

这个onApplicationEvent方法调用了bind方法,我们点进bind方法看一下

@Deprecated
    public void bind(WebServerInitializedEvent event) {
        ApplicationContext context = event.getApplicationContext();
        if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
            this.port.compareAndSet(0, event.getWebServer().getPort());
            this.start();
        }
    }
复制代码

在看源码的时候,为了可以快速的了解整体流程,这种if return是可以过掉的,因为实际上这种代码并没有太大的作用 后面是一个cas,cas之后调用了this.start方法,这个start方法,看名字就知道应该是个非常重要的方法,我们点进去看一下

public void start() {
        if (!this.isEnabled()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Discovery Lifecycle disabled. Not starting");
            }

        } else {
            if (!this.running.get()) {
                this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
                this.register();
                if (this.shouldRegisterManagement()) {
                    this.registerManagement();
                }

                this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
                this.running.compareAndSet(false, true);
            }

        }
    }
复制代码

这个start方法里面的逻辑大概看一下,if里面就不用看了,直接看else,else里面,我们第一眼就看到有个方法,叫register,看到这个方法,我们大概猜一下就知道,这个方法应该是用来注册的,我们点进去看一下

protected void register() {
        this.serviceRegistry.register(this.getRegistration());
    }
复制代码

发现它就调用了register方法,我们继续点进去看

void register(R registration);
复制代码

发现这个实际上是一个接口方法,这里我们向下找它具体的实现

@Override
	public void register(Registration registration) {

		if (StringUtils.isEmpty(registration.getServiceId())) {
			log.warn("No service to register for nacos client...");
			return;
		}

		String serviceId = registration.getServiceId();

		Instance instance = getNacosInstanceFromRegistration(registration);

		try {
			namingService.registerInstance(serviceId, instance);
			log.info("nacos registry, {} {}:{} register finished", serviceId,
					instance.getIp(), instance.getPort());
		}
		catch (Exception e) {
			log.error("nacos registry, {} register failed...{},", serviceId,
					registration.toString(), e);
		}
	}
复制代码

这里if也是不用看的,首先就是得到一个serviceId,之后调用getNacosInstanceFromRegistration方法,得到一个Instance实体类,我们点进getNacosInstanceFromRegistration方法看一下

private Instance getNacosInstanceFromRegistration(Registration registration) {
		Instance instance = new Instance();
		instance.setIp(registration.getHost());
		instance.setPort(registration.getPort());
		instance.setWeight(nacosDiscoveryProperties.getWeight());
		instance.setClusterName(nacosDiscoveryProperties.getClusterName());
		instance.setMetadata(registration.getMetadata());
		return instance;
}
复制代码

这里就是简单的拼装了一下信息,而这些信息实际上就是你在yml文件中写的端口号,ip等信息,然后返回

我们继续回到register方法,后面调用了registerInstance方法,看名字,可以猜到注册实例,所以我们继续点进去看一下

void registerInstance(String var1, Instance var2) throws NacosException;
复制代码

这里还是一个接口,我们点击找到这个接口的实现方法

public void registerInstance(String serviceName, Instance instance) throws NacosException {
        this.registerInstance(serviceName, "DEFAULT_GROUP", instance);
    }
复制代码

进入registerInstance方法

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);
            long instanceInterval = instance.getInstanceHeartBeatInterval();
            beatInfo.setPeriod(instanceInterval == 0L ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
            this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

        this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }
复制代码

看到这里,也算是真正的找到最下面的具体是如何注册实例的了

registerInstance方法的实现,主要逻辑如下

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

我们点进addBeatInfo方法看一下

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        LogUtils.NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        this.dom2Beat.put(this.buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()), beatInfo);
        //定时发送心跳包
        this.executorService.schedule(new BeatReactor.BeatTask(beatInfo), 0L, TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set((double)this.dom2Beat.size());
    }
复制代码

从上述代码看,所谓心跳机制就是客户端通过schedule定时向服务端发送一个数据包,然后启动一个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则认为服务器出现了故障,Nacos服务端不会根据客户端的心跳包不断更新服务状态了

这里我们点进schedule方法看一下

public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

复制代码

我们往上找,看了一下类

public interface ScheduledExecutorService extends ExecutorService
复制代码

这里学过并发编程的朋友都知道,其实这个就是一个线程池

那既然这个类是线程池,说明new BeatReactor.BeatTask(beatInfo)这个应该是一个线程,那我们点进BeatTask方法看一下

class BeatTask implements Runnable {
        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }
 ...
}
复制代码

这里我们发现,BeatTask这个类其实是实现了Runnable的类

那这里我们看一下它的run方法

public void run() {
            if (!this.beatInfo.isStopped()) {
                long result = BeatReactor.this.serverProxy.sendBeat(this.beatInfo);
                long nextTime = result > 0L ? result : this.beatInfo.getPeriod();
                BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
            }
}
复制代码

这里可以看到,其中方法叫sendBeat(发送心跳)。所以这里我们点进这个方法看一下

public long sendBeat(BeatInfo beatInfo) {
        try {
            if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
                LogUtils.NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", this.namespaceId, beatInfo.toString());
            }

            Map<String, String> params = new HashMap(4);
            params.put("beat", JSON.toJSONString(beatInfo));
            params.put("namespaceId", this.namespaceId);
            params.put("serviceName", beatInfo.getServiceName());
            String result = this.reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, (String)"PUT");
            JSONObject jsonObject = JSON.parseObject(result);
            if (jsonObject != null) {
                return jsonObject.getLong("clientBeatInterval");
            }
        } catch (Exception var5) {
            LogUtils.NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: " + JSON.toJSONString(beatInfo), var5);
        }

        return 0L;
  }
复制代码

这里if也不用看,params是在组装数据,看看中间,我们看到一个this.reqAPI方法,reqAPI,看名字应该是请求api,再看一眼后面参数,应该是在拼装路径

nacos.io/zh-cn/docs/… 在官网的open api中,有一个是发送心跳

我们点击NACOS_URL_BASE发现NACOS_URL_BASE属性为空,所以我们把整个类看一下,看是在哪赋值的

static {
        NACOS_URL_BASE = WEB_CONTEXT + "/v1/ns";
        NACOS_URL_INSTANCE = NACOS_URL_BASE + "/instance";
        NACOS_URL_SERVICE = NACOS_URL_BASE + "/service";
        DEFAULT_CLIENT_BEAT_THREAD_COUNT = Runtime.getRuntime().availableProcessors() > 1 ? Runtime.getRuntime().availableProcessors() / 2 : 1;
        DEFAULT_POLLING_THREAD_COUNT = Runtime.getRuntime().availableProcessors() > 1 ? Runtime.getRuntime().availableProcessors() / 2 : 1;
    }
复制代码

原来是在static代码块中赋值的,这样就跟官网的发送实例心跳的请求路径是一样的了,所以这里我们更确定,是发送心跳方法了

这里我们点进去看一下

public String reqAPI(String api, Map<String, String> params, String method) throws NacosException {
        List<String> snapshot = this.serversFromEndpoint;
        if (!CollectionUtils.isEmpty(this.serverList)) {
            snapshot = this.serverList;
        }

        return this.reqAPI(api, params, snapshot, method);
    }
复制代码

这里我们再回到run方法中

 BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
复制代码

最后还有一个调用schedule方法,说明这里是周期性的发送心跳,时间为5s,关于秒数前面参数传了,感兴趣的可以自己点回去看一下

这里我们在回到run方法之前的registerInstance方法中看最后一个方法this.serverProxy.registerService,这个方法应该是最核心的方法了,我们点进去看一下

public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
        Map<String, String> params = new HashMap(9);
        params.put("namespaceId", this.namespaceId);
        params.put("serviceName", serviceName);
        params.put("groupName", groupName);
        params.put("clusterName", 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()));
        this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, (String)"POST");
}
复制代码

前面又是拼装了一堆参数,最后又看到了这个熟悉的方法,reqAPI。这里我们点开NACOS_URL_INSTANCE看一下

public static String NACOS_URL_INSTANCE;
复制代码

发现还是没赋值的 我们又找到static代码块

static {
        NACOS_URL_BASE = WEB_CONTEXT + "/v1/ns";
        NACOS_URL_INSTANCE = NACOS_URL_BASE + "/instance";
        NACOS_URL_SERVICE = NACOS_URL_BASE + "/service";
        DEFAULT_CLIENT_BEAT_THREAD_COUNT = Runtime.getRuntime().availableProcessors() > 1 ? Runtime.getRuntime().availableProcessors() / 2 : 1;
        DEFAULT_POLLING_THREAD_COUNT = Runtime.getRuntime().availableProcessors() > 1 ? Runtime.getRuntime().availableProcessors() / 2 : 1;
 }
复制代码

发现NACOS_URL_INSTANCE=NACOS_URL_BASE+/insetance

在官网上,我们找到了这个api,注册一个实例到服务 ok,看到这里大家就知道了,这句代码实际上就是给服务端发送注册实例的请求,那这时候,我们就应该看一下服务端这个代码具体是如何实现的了

这里我们在官网上下载好nacos源码,直接用idea打开,等待包下载完毕就可以了

我们大概看一下目录,发现了naming这个文件夹,点进去找到controllers文件夹看到了InstanceController,点开看一下代码

@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance")
复制代码

看到这个,就说明找对了。我们往下看

/**
     * Register new instance.
     *
     * @param request http request
     * @return 'ok' if success
     * @throws Exception any error during 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";
    }
复制代码

register正是我们服务端要调的接口,我们看到里面有一个registerInstance方法,我们点进去看一下

public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        
        createEmptyService(namespaceId, serviceName, instance.isEphemeral());
        
        Service service = getService(namespaceId, serviceName);
        
        if (service == null) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "service not found, namespace: " + namespaceId + ", service: " + serviceName);
        }
        
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
    }
复制代码

这里第一句代码是createEmptyService,创建一个空的service,我们前面说Nacos数据模型的时候也说过一个词叫Service是包含在Namespace和Group之中的。我们在写springboot客户端的时候,都会去yml或者properties文件中,写一个spring.application.name,这个就对应着一个service

这里我们点进createEmptyService方法看一下

public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
        createServiceIfAbsent(namespaceId, serviceName, local, null);
    }
复制代码

这里调用了createServiceIfAbsent(大概意思就是如果是没有的,那么就创建)。所以我们点进这个方法看一下

public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
            throws NacosException {
        Service service = getService(namespaceId, serviceName);
        if (service == null) {
            
            Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
            service = new Service();
            service.setName(serviceName);
            service.setNamespaceId(namespaceId);
            service.setGroupName(NamingUtils.getGroupName(serviceName));
            // now validate the service. if failed, exception will be thrown
            service.setLastModifiedMillis(System.currentTimeMillis());
            service.recalculateChecksum();
            if (cluster != null) {
                cluster.setService(service);
                service.getClusterMap().put(cluster.getName(), cluster);
            }
            service.validate();
            
            putServiceAndInit(service);
            if (!local) {
                addOrReplaceService(service);
            }
        }
    }
复制代码

这里首先是调用getService判断service是否为空,我们看一下getService方法

public Service getService(String namespaceId, String serviceName) {
        if (serviceMap.get(namespaceId) == null) {
            return null;
        }
        return chooseServiceMap(namespaceId).get(serviceName);
    }
复制代码

这里看出来有一个serviceMap

/**
  * Map(namespace, Map(group::serviceName, Service)).
  */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
复制代码

这个map是由namespace和group+serviceName进行分离的

如果不是空,就直接返回map里面具体的值,这里我们点开Service源码看一下

private Map<String, Cluster> clusterMap = new HashMap<>();
复制代码

可以看到Service里面有一个clusterMap,这里就跟一开始的图关联起来了,这里Cluster是用来真正存放实例的

点开Cluster看一下

@JsonIgnore
private Set<Instance> persistentInstances = new HashSet<>();
    
@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>();
复制代码

Cluster中包含了两个,一个是持久的实例一个临时实例

回到createServiceIfAbsent方法中

因为第一次一定是空,所以我们看if中的逻辑。最后有一个putServiceAndInit,看名字大概是添加Service和初始化,我们点进去看一下

private void putServiceAndInit(Service service) throws NacosException {
        putService(service);
        service.init();
        consistencyService
                .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
        consistencyService
                .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
        Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
    }
复制代码

这里调用了putService,我们继续点进去看一下

/**
     * Put service into manager.
     *
     * @param service service
     */
    public void putService(Service service) {
        if (!serviceMap.containsKey(service.getNamespaceId())) {
            synchronized (putServiceLock) {
                if (!serviceMap.containsKey(service.getNamespaceId())) {
                    serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
                }
            }
        }
        serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
    }
复制代码

这里首先是判断了一下是否存在,如果不存在就加锁,紧接着又进行了一次判断,这里进行两次判断就很nice。有点单例模式双重锁的感觉,这里可能有小伙伴不太清楚为啥要判断两次,我简单说明一下 这里第一次判断,是为了减少锁等待时间,假如,有很多个线程同时请求进来了,假如serviceMap是有返回值的,那么,这里还是上锁了,还是需要串行的执行,消耗时间。

synchronized里面的判断是,假如有两个线程第一个if判断是空了,走到synchronized了,线程A加锁了,线程B等待,线程A put了,线程B就需要判断再判断一下,要不然就重复put了,也消耗时间

这里把外层的put进去了之后,紧接着就获取外层的,在put里面的map

这里我们再回到putServiceAndInit方法中,putService方法看完了,下面有一个init方法,我们点进去看一下

/**
     * Init service.
     */
    public void init() {
        HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
        for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
            entry.getValue().setService(this);
            entry.getValue().init();
        }
    }
复制代码

我们看到有个类叫HealthCheckReactor,可以看出这个类是用来健康检查的,我们点进scheduleCheck方法看一下

/**
     * Schedule client beat check task with a delay.
     *
     * @param task client beat check task
     */
    public static void scheduleCheck(ClientBeatCheckTask task) {
        futureMap.putIfAbsent(task.taskKey(), GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
    }
复制代码

这里有几个参数,第一个5000是过5s执行,第二个5000是每隔5s执行一次,这个task,跟之前客户端一样,肯定是一个线程,所以我们点开ClientBeatCheckTask类来看一下

public class ClientBeatCheckTask implements Runnable
复制代码

实现了Runnable接口,所以我们找到run方法

public void run() {
        try {
            if (!getDistroMapper().responsible(service.getName())) {
                return;
            }
            
            if (!getSwitchDomain().isHealthCheckEnabled()) {
                return;
            }
            
            List<Instance> instances = service.allIPs(true);
            
            // first set health status of instances:
            for (Instance instance : instances) {
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
                    if (!instance.isMarked()) {
                        if (instance.isHealthy()) {
                            instance.setHealthy(false);
                            Loggers.EVT_LOG
                                    .info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
                                            instance.getIp(), instance.getPort(), instance.getClusterName(),
                                            service.getName(), UtilsAndCommons.LOCALHOST_SITE,
                                            instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
                            getPushService().serviceChanged(service);
                            ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
                        }
                    }
                }
            }
            
            if (!getGlobalConfig().isExpireInstance()) {
                return;
            }
            
            // then remove obsolete instances:
            for (Instance instance : instances) {
                
                if (instance.isMarked()) {
                    continue;
                }
                
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
                    // delete instance
                    Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),
                            JacksonUtils.toJson(instance));
                    deleteIp(instance);
                }
            }
            
        } catch (Exception e) {
            Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
        }
        
    }
复制代码

这里我们回到registerInstance方法中,我们之前看完了createEmptyService方法内容,它只是new了一个空的service并没有往service中放具体的实例所以我们往下看addInstance方法

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
            throws NacosException {
        
        String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        
        Service service = getService(namespaceId, serviceName);
        
        synchronized (service) {
            List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
            
            Instances instances = new Instances();
            instances.setInstanceList(instanceList);
            
            consistencyService.put(key, instances);
        }
    }
复制代码

剩下的下一篇来讲,这一篇写的有点多了,哈哈哈!关注一下我,期待下一篇

文章分类
后端