Tear the Nacos source code by hand (tear the client source code first)

Nacos source code analysis

Nacos source code has a lot to learn. In order to deeply understand Nacos, we analyze the source code and analyze the following two knowledge points:

1:Nacos对注册中心的访问原理

2:Nacos注册服务处理流程

Source code environment construction

1. Clone from the official project, check out version 1.4.1, and import idea. The nacos source code environment is relatively easy to build, and almost no errors will be reported. After importing, it can be compiled and installed in the local environment.

file

2. Find it in the configmodule, \resources\META-INF\nacos-db.sqlcreate a database in the local mysql, nacos-configimport the script to execute and create a table.

3. Find consolethe configuration file under the module application.propertiesand modify the relevant configuration

#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=nacos
db.password.0=nacos

4. Find consolethe startup class under the module, start the nacos server, add startup parameters at startup, and specify the startup mode as non-cluster startup

-Dnacos.standalone=true

file

5. Visit the local nacos: http://localhost:8848/nacos

So far, the source code environment has been successfully built!

nacos client

First of all, we need to make it clear: the nacos client is actually in our own service. We have introduced the relevant coordinate dependencies of nacos. The nacos client works in our service in the form of a jar package.

file

For the nacos client, what function does it have to help us achieve?

Its main work includes: service registration, service discovery, service offline operation, service subscription operation and other related operations.

The interaction between the client and the registry server mainly focuses on service registration, service offline, service discovery, and subscribing to a service. In fact, the most used are service registration and service discovery. I will analyze these four from the perspective of source code. Function.

In the Nacos source code, nacos-examplethe com.alibaba.nacos.example.NamingExampleclasses demonstrate the operation of these four functions respectively. We can use it as the entry. The code is as follows:

public class NamingExample {

    public static void main(String[] args) throws NacosException {

        Properties properties = new Properties();
        properties.setProperty("serverAddr", System.getProperty("serverAddr"));
        properties.setProperty("namespace", System.getProperty("namespace"));

        NamingService naming = NamingFactory.createNamingService(properties);
        //服务注册
        naming.registerInstance("nacos.test.3", "11.11.11.11", 8888, "TEST1");
        naming.registerInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT");
        //服务发现
        System.out.println(naming.getAllInstances("nacos.test.3"));
        //服务下线
        naming.deregisterInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT");
        System.out.println(naming.getAllInstances("nacos.test.3"));
        //服务订阅
        Executor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r);
                        thread.setName("test-thread");
                        return thread;
                    }
                });

        naming.subscribe("nacos.test.3", new AbstractEventListener() {

            //EventListener onEvent is sync to handle, If process too low in onEvent, maybe block other onEvent callback.
            //So you can override getExecutor() to async handle event.
            @Override
            public Executor getExecutor() {
                return executor;
            }

            @Override
            public void onEvent(Event event) {
                System.out.println(((NamingEvent) event).getServiceName());
                System.out.println(((NamingEvent) event).getInstances());
            }
        });
    }
}

1.1.1 Service Registration

We follow the service registration method call nacos-apiin the case to find NamingService.registerInstance()and find its implementation class and method com.alibaba.nacos.client.naming.NacosNamingService, the code is as follows:

/***
 * 服务注册
 * @param serviceName 服务名字
 * @param ip          服务IP
 * @param port        服务端口
 * @param clusterName 集群名字
 * @throws NacosException
 */
@Override
public void registerInstance(String serviceName, String ip, int port, String clusterName) throws NacosException {
    registerInstance(serviceName, Constants.DEFAULT_GROUP, ip, port, clusterName);
}

@Override
public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
        throws NacosException {
    //设置实例IP:Port,默认权重为1.0
    Instance instance = new Instance();
    instance.setIp(ip);
    instance.setPort(port);
    instance.setWeight(1.0);
    instance.setClusterName(clusterName);
    //注册实例
    registerInstance(serviceName, groupName, instance);
}

@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
    registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}

/***
 * 实例注册
 * @param serviceName name of service
 * @param groupName   group of service
 * @param instance    instance to register
 * @throws NacosException
 */
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    //该字段表示注册的实例是否是临时实例还是持久化实例。
    // 如果是临时实例,则不会在 Nacos 服务端持久化存储,需要通过上报心跳的方式进行包活,
    // 如果一段时间内没有上报心跳,则会被 Nacos 服务端摘除。
    if (instance.isEphemeral()) {
        //为注册服务设置一个定时任务获取心跳信息,默认为5s汇报一次
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    //注册到服务端
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

注册主要做了两件事,第一件事:为注册的服务设置一个定时心跳任务。 第二件事:将服务注册到服务端。

1:启动一个定时心跳任务,时间间隔为5s,如果服务正常,不做处理,如果不正常,重新注册

2:发送http请求给注册中心服务端,调用服务注册接口,注册服务

上面代码我们可以看到定时任务添加,但并未完全看到远程请求,serverProxy.registerService()方法如下,会先封装请求参数,接下来调用reqApi()reqApi()最后会调用callServer(),代码如下:

public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
            instance);
    //封装Http请求参数
    final Map<String, String> params = new HashMap<String, String>(16);
    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", JacksonUtils.toJson(instance.getMetadata()));
    //执行Http请求
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}


/***
*执行远程调用
**/
public String callServer(String api, Map<String, String> params, Map<String, String> body, String curServer,
        String method) throws NacosException {
    long start = System.currentTimeMillis();
    long end = 0;
    injectSecurityInfo(params);
    //封装请求头部
    Header header = builderHeader();
    
    //请求是Http还是Https协议
    String url;
    if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
        url = curServer + api;
    } else {
        if (!IPUtil.containsPort(curServer)) {
            curServer = curServer + IPUtil.IP_PORT_SPLITER + serverPort;
        }
        url = NamingHttpClientManager.getInstance().getPrefix() + curServer + api;
    }

    try {
        //执行远程请求,并获取结果集
        HttpRestResult<String> restResult = nacosRestTemplate
                .exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);
        end = System.currentTimeMillis();
        
        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(restResult.getCode()))
                .observe(end - start);
        //结果集解析
        if (restResult.ok()) {
            return restResult.getData();
        }
        if (HttpStatus.SC_NOT_MODIFIED == restResult.getCode()) {
            return StringUtils.EMPTY;
        }
        throw new NacosException(restResult.getCode(), restResult.getMessage());
    } catch (Exception e) {
        NAMING_LOGGER.error("[NA] failed to request", e);
        throw new NacosException(NacosException.SERVER_ERROR, e);
    }
}

执行远程Http请求的对象是NacosRestTemplate,该对象就是封装了普通的Http请求,大家可以自己查阅一下。

1.1.2 服务发现

两个入口:

1、NamingService.getAllInstances(serviceName)

我们沿着案例中的服务发现方法调用找到nacos-api中的NamingService.getAllInstances()并找到它的实现类和方法com.alibaba.nacos.client.naming.NacosNamingService.getAllInstances(),代码如下:

@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
        boolean subscribe) throws NacosException {

    ServiceInfo serviceInfo;
    /*默认true->获取服务实例*/
    if (subscribe) {
        //从本地缓存中获取,如果本地缓存不存在从服务端拉取
        //本地缓存会存储在HostReactor.serviceInfoMap中,它是一个Map对象
        serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                StringUtils.join(clusters, ","));
    } else {
        serviceInfo = hostReactor
                .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
                        StringUtils.join(clusters, ","));
    }
    List<Instance> list;
    if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
        return new ArrayList<Instance>();
    }
    return list;
}

上面的代码调用了hostReactor.getServiceInfo()方法,该方法会先调用getServiceInfo0()方法从本地缓存获取数据,缓存没有数据,就构建实例更新到Nacos,并从Nacos中获取最新数据,getServiceInfo0()方法源码如下:

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);
    }
    /*1。先从本地缓存中获取服务对象,因为启动是第一次进来,所以缓存暂不存在*/
    ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);

    if (null == serviceObj) {
        /*构建服务实例*/
        serviceObj = new ServiceInfo(serviceName, clusters);
        /*将服务实例存放到缓存中*/
        serviceInfoMap.put(serviceObj.getKey(), serviceObj);
        /*更新nacos-上的服务*/
        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);
                }
            }
        }
    }
    /*2.开启定时任务*/
    scheduleUpdateIfAbsent(serviceName, clusters);
    return serviceInfoMap.get(serviceObj.getKey());
}

updateServiceNow(serviceName, clusters);主从从远程服务器获取更新数据,最终会调用updateService()方法,在该方法中完成远程请求和数据处理,源码如下:

public void updateService(String serviceName, String clusters) throws NacosException {
    /*获取本地缓存列表中所存在的服务*/
    ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
    try {
        /*获取服务以及提供者端口信息,端口等*/
        String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);

        if (StringUtils.isNotEmpty(result)) {
            /*对结果进行处理*/
            processServiceJson(result);
        }
    } finally {
        if (oldService != null) {
            synchronized (oldService) {
                oldService.notifyAll();
            }
        }
    }
}

1.1.3 服务下线

我们沿着案例中的服务下线方法调用找到nacos-api中的NamingService.deregisterInstance()并找到它的实现类和方法NacosNamingService.deregisterInstance(),代码如下:

@Override
public void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName)
        throws NacosException {
    //构建实例信息
    Instance instance = new Instance();
    instance.setIp(ip);
    instance.setPort(port);
    instance.setClusterName(clusterName);
    //服务下线操作
    deregisterInstance(serviceName, groupName, instance);
}

@Override
public void deregisterInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    if (instance.isEphemeral()) {
        //移除心跳信息监测的定时任务
        beatReactor.removeBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), instance.getIp(),
                instance.getPort());
    }
    //发送远程请求执行服务下线销毁操作
    serverProxy.deregisterService(NamingUtils.getGroupedName(serviceName, groupName), instance);
}

服务下线方法比较简单,和服务注册做的事情正好相反,也做了两件事,第一件事:不在进行心跳检测。 第二件事:请求服务端服务下线接口。

1.1.4 服务订阅

我们可以查看订阅服务的案例,会先创建一个线程池,接下来会把线程池封装到监听器中,而监听器中可以监听指定实例信息,代码如下:

//服务订阅
Executor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
        new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("test-thread");
                return thread;
            }
        });

naming.subscribe("nacos.test.3", new AbstractEventListener() {

    //EventListener onEvent is sync to handle, If process too low in onEvent, maybe block other onEvent callback.
    //So you can override getExecutor() to async handle event.
    @Override
    public Executor getExecutor() {
        return executor;
    }
    //读取监听到的服务实例
    @Override
    public void onEvent(Event event) {
        System.out.println(((NamingEvent) event).getServiceName());
        System.out.println(((NamingEvent) event).getInstances());
    }
});

我们沿着案例中的服务订阅方法调用找到nacos-api中的NamingService.subscribe()并找到它的实现类和方法NacosNamingService.deregisterInstance(),代码如下:

public void subscribe(String serviceName, String clusters, EventListener eventListener) {
    //注册监听
    notifier.registerListener(serviceName, clusters, eventListener);
    //获取并更新服务实例
    getServiceInfo(serviceName, clusters);
}

At this point, the listener will be registered, and the listener will be registered. Registering the listener is to inject the current listener object information into the listenerMap collection, and the instance information can be read in the specified method onEvent of the listener object. The code is as follows:

public void registerListener(String serviceName, String clusters, EventListener listener) {
    String key = ServiceInfo.getKey(serviceName, clusters);
    ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
    if (eventListeners == null) {
        synchronized (lock) {
            eventListeners = listenerMap.get(key);
            if (eventListeners == null) {
                eventListeners = new ConcurrentHashSet<EventListener>();
                listenerMap.put(key, eventListeners);
            }
        }
    }
    //将当前监听对象放入到集合中,在监听对象的onEvent中可以读出对应的实例对象
    eventListeners.add(listener);
}

Past dry goods:

This article is published by the teaching and research team of Chuanzhi Education Boxue Valley - Wild Architect. If this article is helpful to you, please follow and like it; if you have any suggestions, you can also leave a comment or private letter. Your support is the driving force for me to persist in creating. Please indicate the source!

Guess you like

Origin juejin.im/post/7117517682001494047