nacos source code analysis - service registration (client)

foreword

I have always wanted to write the source code analysis of SpringCloudAlibaba, and finally started the first article. If you want to understand the source code of Nacos, you must at least read the " SpringBoot Automatic " configuration, otherwise you just read the bible. This article will take you to take a look at the source code of the Nacos-Client client service registration part.

basic environment

First of all, you need to download a Nacos-server, which is the registration center. By the way, you also downloaded the source code. We will use it later when analyzing the server. The download address is https://github.com/alibaba/nacos/releases/tag /1.4.3
insert image description here
After next week, decompress and start nacos-server, enter the bin directory, and execute startup.cmd -m standalone in cmd, which means single instance startup. If you find it troublesome, you can change the set MODE="cluster" in startup.cmd to standalone, and then start the startup directly without adding parameters. insert image description here
Next is the client. My project structure is as follows:

insert image description here
The SpringBoot version I use here is 2.2.5; the version of alibaba is 2.2.1; the parent project pom.xml management is as follows

 <parent>
        <groupId> org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>


    <dependencyManagement>
        <dependencies>
            <!--SpringClou-alibaba依赖-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringCloud依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

nacos-client depends on the following

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

The startup class is as follows

//Ncaos客户端应用程序启动
@SpringBootApplication
public class NacosClientStarter {
    
    

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

A service name and nacos registration center address are configured in the configuration file

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: nacos-client

Then start the project, and nacos-client will be registered in the Nacos registration center, as follows:

insert image description here
OK, the basic environment is ready here, I believe it is relatively easy for you.

Service registration source code analysis

Both Nacos and Eureka have functions such as service registration, service discovery, and heartbeat renewal. This article mainly discusses service registration, that is, when the client starts, it will actively register itself with the server, and then a service registry will be formed on the server ( In fact, it is a Map) Let's study the process of service registration.

Students who have done some research on SpringBoot are aware of SpringBoot's automatic configuration. SpringCloud or SpringCloudAlibaba is based on SpringBoot's automatic configuration to complete the loading of related components. Find our imported dependency spring-cloud-starter-alibaba-nacos-discovery in External libraries

insert image description here
Then expand to find the META-INF/spring.factories file, the content is as follows:

insert image description here
NacosServerRegistryAutoConfiguration is an automatic configuration class for service registration. The configuration in this class takes effect automatically when the program starts. In addition to this configuration class, the file also includes a configuration class for service discovery and an automatic configuration class for config.

NacosServerRegistryAutoConfiguration service registration automatic configuration

The following is the source code of NacosServerRegistryAutoConfiguration

@Configuration(
    proxyBeanMethods = false
)
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
//条件开关
@ConditionalOnProperty(
    value = {
    
    "spring.cloud.service-registry.auto-registration.enabled"},
    matchIfMissing = true
)
@AutoConfigureAfter({
    
    AutoServiceRegistrationConfiguration.class, AutoServiceRegistrationAutoConfiguration.class, NacosDiscoveryAutoConfiguration.class})
public class NacosServiceRegistryAutoConfiguration {
    
    
    public 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);
    }
}

  • spring.cloud.service-registry.auto-registration.enabled: automatic registration condition switch, the default is true

  • NacosServiceRegistry: Nacos service registrar, through which the client sends a registration request to the server. It is implemented in the ServiceRegistry interface (EurekaClient is also available). Override the register service registration method.

  • NacosRegistration: It can be regarded as the level of the service to be registered, including the instance ID of the service, the registration address, etc.

  • NacosAutoServiceRegistration : Automatic registrar, service registration is triggered by it

  • NacosDiscoveryProperties : configuration property object for Nacos

  • NacosRegistration: The registration object of the registered service created according to the nacos configuration, which contains the serviceId, host, port, etc. of the registered service

NacosAutoServiceRegistration automatic registration

NacosAutoServiceRegistration inherits AbstractAutoServiceRegistration, and calls NacosAutoServiceRegistration#register to trigger automatic registration by listening to the WebServerInitializedEvent web service initialization event. In this method, the register-enabled: true switch will be judged to decide whether to register. If automatic registration is enabled, the NacosServiceRegistry#register method will eventually be called to trigger service registration.
insert image description here
The start() method will call the NacosAutoServiceRegistration #register method internally,
insert image description here
and after NacosAutoServiceRegistration #register has done whether to enable the registration configuration (spring.cloud.nacos.discovery.register-enabled=true), it will call the super registration method to register. The following is AbstractAutoServiceRegistration #registersource

protected void register() {
    
    
	this.serviceRegistry.register(getRegistration());
}

The serviceRegistry here is the NacosServiceRegistry mentioned above, and the core service registration process is in it.

NacosServiceRegistry service registration process

The following is the source code of NacosServiceRegistry#register

@Override
public void register(Registration registration) {
    
    
	//判断是否有服务ID,也就是spring.application.name配置不能为空
	if (StringUtils.isEmpty(registration.getServiceId())) {
    
    
		log.warn("No service to register for nacos client...");
		return;
	}
	//拿到服务ID
	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);
	}
}

This method will first determine the service serviceId, that is, the service name cannot be empty, then get the group and Instance (service instance object), and call NamingService#registerInstance to register the service. The following is the source code of NacosNamingService#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);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
            this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

        this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

Here it is clearer. Here, the service ip, port, service name and other information will be encapsulated into the BeatInfo object. beatReactor.addBeatInfo is to add the current service instance to the heartbeat mechanism (heartbeat renewal), and then register through serverProxy.registerService . The following is the source code of NamingProxy#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, "POST");
    }
	...省略...
	public String reqAPI(String api, Map<String, String> params, String body, String method) throws NacosException {
    
    
        return this.reqAPI(api, params, body, this.getServerList(), method);
    }

Finally, the relevant configuration is encapsulated with a Map, and then handed over to the reqAPI method to send a POST request to the server.

  • /nacos/v1/ns/instance : It is the interface registered by nacos, splicing the nacos address in yaml is the completed registration address
  • this.getServerList() : Get the addresses of all nacos servers, considering the situation of the nacos-server cluster

The code ends up in the reqAPI method

public String reqAPI(String api, Map<String, String> params, String body, List<String> servers, String method) throws NacosException {
    
    
        params.put("namespaceId", this.getNamespaceId());
        if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(this.nacosDomain)) {
    
    
            throw new NacosException(400, "no server available");
        } else {
    
    
            NacosException exception = new NacosException();
            if (servers != null && !servers.isEmpty()) {
    
    
            	//使用随机数随机取一个nacos-server
                Random random = new Random(System.currentTimeMillis());
                int index = random.nextInt(servers.size());
                int i = 0;

                while(i < servers.size()) {
    
    
                    String server = (String)servers.get(index);

                    try {
    
    
                    	//发送请求了
                        return this.callServer(api, params, body, server, method);
                    } catch (NacosException var13) {
    
    
                        exception = var13;
                        if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
    
    
                            LogUtils.NAMING_LOGGER.debug("request {} failed.", server, var13);
                        }

                        index = (index + 1) % servers.size();
                        ++i;
                    }
                }
            }

            if (StringUtils.isNotBlank(this.nacosDomain)) {
    
    
                int i = 0;

                while(i < 3) {
    
    
                    try {
    
    
                        return this.callServer(api, params, body, this.nacosDomain, method);
                    } catch (NacosException var12) {
    
    
                        exception = var12;
                        if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
    
    
                            LogUtils.NAMING_LOGGER.debug("request {} failed.", this.nacosDomain, var12);
                        }

                        ++i;
                    }
                }
            }

            LogUtils.NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", new Object[]{
    
    api, servers, exception.getErrCode(), exception.getErrMsg()});
            throw new NacosException(exception.getErrCode(), "failed to req API:/api/" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
        }
    }

The code is not difficult to understand. In the reqAPI method, one will be randomly selected from multiple Nacos server addresses, and then a registration request will be initiated and completed through the callServer method.

public String callServer(String api, Map<String, String> params, String body, String curServer, String method) throws NacosException {
    
    
        long start = System.currentTimeMillis();
        long end = 0L;
        this.injectSecurityInfo(params);
        List<String> headers = this.builderHeaders();
        String url;
        //拼接完整的URL: http://127.0.0.1:8848/nacos/v1/ns/instance
        if (!curServer.startsWith("https://") && !curServer.startsWith("http://")) {
    
    
            if (!curServer.contains(":")) {
    
    
                curServer = curServer + ":" + this.serverPort;
            }

            url = HttpClient.getPrefix() + curServer + api;
        } else {
    
    
            url = curServer + api;
        }
		//调用nacos的http客户端执行请求
        HttpClient.HttpResult result = HttpClient.request(url, headers, params, body, "UTF-8", method);
        end = System.currentTimeMillis();
        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code)).observe((double)(end - start));
        if (200 == result.code) {
    
    
            return result.content;
        } else if (304 == result.code) {
    
    
            return "";
        } else {
    
    
            throw new NacosException(result.code, result.content);
        }
    }

The method will splice the registered address, such as: http://127.0.0.1:8848/nacos/v1/ns/instance, and then call HttpClient.request to execute the request with lower parameters; the following is: com.alibaba.nacos From the source code of .client.naming.net.HttpClient#request, it can be seen that the bottom layer still sends requests through the HttpURLConnection that comes with JDK

public static HttpResult request(String url, List<String> headers, Map<String, String> paramValues, String body, String encoding, String method) {
    
    
        HttpURLConnection conn = null;

        HttpResult var8;
        try {
    
    
            String encodedContent = encodingParams(paramValues, encoding);
            url = url + (StringUtils.isEmpty(encodedContent) ? "" : "?" + encodedContent);
            conn = (HttpURLConnection)(new URL(url)).openConnection();
            setHeaders(conn, headers, encoding);
            conn.setConnectTimeout(CON_TIME_OUT_MILLIS);
            conn.setReadTimeout(TIME_OUT_MILLIS);
            conn.setRequestMethod(method);
            conn.setDoOutput(true);
            if (StringUtils.isNotBlank(body)) {
    
    
                byte[] b = body.getBytes();
                conn.setRequestProperty("Content-Length", String.valueOf(b.length));
                conn.getOutputStream().write(b, 0, b.length);
                conn.getOutputStream().flush();
                conn.getOutputStream().close();
            }

            conn.connect();
            if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
    
    
                LogUtils.NAMING_LOGGER.debug("Request from server: " + url);
            }

            var8 = getResult(conn);
            return var8;
        } catch (Exception var14) {
    
    
            try {
    
    
                if (conn != null) {
    
    
                    LogUtils.NAMING_LOGGER.warn("failed to request " + conn.getURL() + " from " + InetAddress.getByName(conn.getURL().getHost()).getHostAddress());
                }
            } catch (Exception var13) {
    
    
                LogUtils.NAMING_LOGGER.error("[NA] failed to request ", var13);
            }

            LogUtils.NAMING_LOGGER.error("[NA] failed to request ", var14);
            var8 = new HttpResult(500, var14.toString(), Collections.emptyMap());
        } finally {
    
    
            IoUtils.closeQuietly(conn);
        }

        return var8;
    }

BeatReactor #addBeatInfo Heartbeat renewal

Encapsulate the service information as a BeatInfo in the NacosNamingService#registerInstance method, and then add this.beatReactor.addBeatInfo heartbeat mechanism. Let's take a look at how the heartbeat is done. The following is the source code of beatReactor.addBeatInfo

 public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
    
    
        LogUtils.NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = this.buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        if ((existBeat = (BeatInfo)this.dom2Beat.remove(key)) != null) {
    
    
            existBeat.setStopped(true);
        }

        this.dom2Beat.put(key, beatInfo);
        //线程池,定时任务,5000毫秒发送一次心跳。beatInfo.getPeriod()是定时任务执行的频率
        this.executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set((double)this.dom2Beat.size());
    }

   //心跳任务
   class BeatTask implements Runnable {
    
    
        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
    
    
            this.beatInfo = beatInfo;
        }

       public void run() {
    
    
            if (!this.beatInfo.isStopped()) {
    
    
                long nextTime = this.beatInfo.getPeriod();

                try {
    
    
                    JSONObject result = BeatReactor.this.serverProxy.sendBeat(this.beatInfo, BeatReactor.this.lightBeatEnabled);
                    long interval = (long)result.getIntValue("clientBeatInterval");
                    boolean lightBeatEnabled = false;
                    if (result.containsKey("lightBeatEnabled")) {
    
    
                        lightBeatEnabled = result.getBooleanValue("lightBeatEnabled");
                    }

                    BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                    if (interval > 0L) {
    
    
                        nextTime = interval;
                    }

                    int code = 10200;
                    if (result.containsKey("code")) {
    
    
                        code = result.getIntValue("code");
                    }

                    if (code == 20404) {
    
    
                        Instance instance = new Instance();
                        instance.setPort(this.beatInfo.getPort());
                        instance.setIp(this.beatInfo.getIp());
                        instance.setWeight(this.beatInfo.getWeight());
                        instance.setMetadata(this.beatInfo.getMetadata());
                        instance.setClusterName(this.beatInfo.getCluster());
                        instance.setServiceName(this.beatInfo.getServiceName());
                        instance.setInstanceId(instance.getInstanceId());
                        instance.setEphemeral(true);

                        try {
    
    
                            BeatReactor.this.serverProxy.registerService(this.beatInfo.getServiceName(), NamingUtils.getGroupName(this.beatInfo.getServiceName()), instance);
                        } catch (Exception var10) {
    
    
                        }
                    }
                } catch (NacosException var11) {
    
    
                    LogUtils.NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", new Object[]{
    
    JSON.toJSONString(this.beatInfo), var11.getErrCode(), var11.getErrMsg()});
                }

                BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
            }
        }
   }

Like Eureka, the heartbeat is also implemented through the thread pool ScheduledExecutorService, and the default time frequency is once every 5 seconds.

  • BeatInfo: The object of heartbeat renewal, including service IP, port, service name, weight, etc.

  • executorService.schedule : timed task, beatInfo.getPeriod() is the execution frequency of the timed task, the default is 5000 milliseconds to send a heartbeat renewal request to NacosServer

  • BeatTask : It is a Runnable thread, and the run method will call BeatReactor.this.serverProxy.sendBeat to send a heartbeat request.

  • Send a heartbeat request to the nacos server through an http request. The interface is: /nacos/v1/ns/instance/beat.
    Parse the lightBeatEnabled and interval parameters from the returned result. lightBeatEnabled indicates whether the heartbeat needs to carry the heartbeat information and send it to the nacos server. interval indicates the heartbeat interval.

  • If the return code is RESOURCE_NOT_FOUND, create an instance and perform service registration.
    Finally trigger the next heartbeat request task.

BeatTask is the thread object for heartbeat renewal. In its run method, it sends heartbeat through BeatReactor.this.serverProxy.sendBeat. If it finds that the service is not registered, it will register the service through BeatReactor.this.serverProxy.registerService.

The following is com.alibaba.nacos.client.naming.net.NamingProxy#sendBeat method to send heartbeat

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

        Map<String, String> params = new HashMap(8);
        String body = "";
        if (!lightBeatEnabled) {
    
    
            try {
    
    
                body = "beat=" + URLEncoder.encode(JSON.toJSONString(beatInfo), "UTF-8");
            } catch (UnsupportedEncodingException var6) {
    
    
                throw new NacosException(500, "encode beatInfo error", var6);
            }
        }

        params.put("namespaceId", this.namespaceId);
        params.put("serviceName", beatInfo.getServiceName());
        params.put("clusterName", beatInfo.getCluster());
        params.put("ip", beatInfo.getIp());
        params.put("port", String.valueOf(beatInfo.getPort()));
        String result = this.reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, body, "PUT");
        return JSON.parseObject(result);
    }

Here is also the address that will splice the heartbeat: 127.0.0.1:8848/nacos/v1/ns/instance/beat, the parameters include namespaceId namespace ID; serviceName service ing; clusterName cluster name; ip service IP; port port. Then send a PUT request. The bottom layer still randomly selects one from multiple NacosServers to initiate a heartbeat request.

The client will analyze here first. In order to sort out the process, I have drawn a picture here.

insert image description here

Summarize

  1. NACOS uses SpringBoot automatic assembly to complete the configuration of related components. The service registry (NacosServiceRegistry ) and automatic registry (NacosAutoServiceRegistration ), the registered service registration object (NacosRegistration ), etc. are registered in the Nacos automatic configuration class (NacosServerRegistryAutoConfiguration ).
  2. The Nacos automatic registration device triggers automatic registration by monitoring the application startup time, and it will determine an automatic registration switch (spring.cloud.service-registry.auto-registration.enabled). If the automatic registration is satisfied, the register method of NacosServiceRegistry will be called to register the service
  3. NacosServiceRegistry handed over the registration work to NacosNamingService, and did two things in NacosNamingService: 1. Encapsulate the service as BeatInfo and add the heartbeat mechanism; 2. Complete the registration of the service
  4. For the heartbeat mechanism, it is done through BeatReactor, and the bottom layer uses a thread pool with scheduled tasks to send a heartbeat to the server every 5 seconds.
  5. For service registration, it is done through the NamingProxy proxy. Like the heartbeat, a NacosServer is randomly selected, and then the registration address is stitched together, and then sent to the HttpClient to send the http request.

Well, it's really over here, if the article is helpful to you, please like it and add praise. Your encouragement is my biggest motivation

Guess you like

Origin blog.csdn.net/u014494148/article/details/127931250