Nacos get configuration source code interpretation (detailed explanation)

Posted by hassank1 on Tue, 21 Jan 2020 13:42:03 +0100

 

Background:

Today's network services are mostly distributed cluster deployment, and the adopted architecture is also service-oriented architecture (SOA). Everyone is striving in the field of service governance. No, today's Nacos is also a product of the field of service governance. Nacos is an excellent registration and configuration center. Recommended by Nacos, borrow a picture: https://www.jianshu.com/p/39ade28c150d

Introduction:

This article mainly introduces the clients of Nacos. How can Nacos get the configuration of the server and update it in real time.

Example:

	@Test
	public void testNacos() throws NacosException {
		String serverAddr = "10.199.150.216:8848";
		String namespace = "8ba3b4f6-1c6a-4c7d-99b0-f444002d526d";
		String dataId = "cxtj.properties";
		String group = "cxtj";
		Properties properties = new Properties();
		properties.put("serverAddr",serverAddr);
		properties.put("namespace",namespace);
                //1. Create Nacos configuration service
		ConfigService configService = NacosFactory.createConfigService(properties);
                //2. Get configuration information from service
		String content = configService.getConfig(dataId,group,3000);
		System.out.println("receive" + content);
	}

Print out profile information:

Start reading the source code:

//1. Create Nacos configuration service
ConfigService configService = NacosFactory.createConfigService(properties);
//2. Get configuration information from service
String content = configService.getConfig(dataId,group,3000);

First, create the configuration service through the previous configuration properties:

    public static ConfigService createConfigService(Properties properties) throws NacosException {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
            return vendorImpl;
    }

It can be seen that the object is instantiated through reflection by obtaining the construction method of the NacosConfigService class. (question 1: is it not good to directly new an object? Finally)

Take a look at the construction method:

    public NacosConfigService(Properties properties) throws NacosException {
        //Initialize encoding format        
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            encode = Constants.ENCODE;
        } else {
            encode = encodeTmp.trim();
        }
        //Initialize namespace
        initNamespace(properties);
        //Create a proxy for the server
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        //Start agent
        agent.start();
        //Create client working
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }

The previous coding and namespace initialization are skipped. MetricsHttpAgent is based on ServerHttpAgent, which adds monitoring function and records the requested time and other information. It's mainly ServerHttpAgent. Let's see the construction method of ServerHttpAgent.

    public ServerHttpAgent(Properties properties) throws NacosException {
        //Create service list manager
        serverListMgr = new ServerListManager(properties);
        //Initialization
        init(properties);
    }

The service list in ServerListManager generally maintains these two concepts, one is isStarted (whether the service is started), the other is isFixed (whether the service address is fixed, in the example, String serverAddr = "10.199.150.216:8848"; that is, fixed address). The service list ServerListManager holds some basic information of the server, such as service address, server name endpoint, etc.

    public ServerListManager(Properties properties) throws NacosException {
        isStarted = false;
        serverAddrsStr = properties.getProperty(PropertyKeyConst.SERVER_ADDR);
        String namespace = properties.getProperty(PropertyKeyConst.NAMESPACE);
        //Initialize endpoint, contextPath, clusterName
        initParam(properties);
        //If serverAddrsStr has a value, the server address is fixed
        if (StringUtils.isNotEmpty(serverAddrsStr)) {
            isFixed = true;
            List<String> serverAddrs = new ArrayList<String>();
            //Get service address
            String[] serverAddrsArr = serverAddrsStr.split(",");
            for (String serverAddr: serverAddrsArr) {
                if (serverAddr.startsWith(HTTPS) || serverAddr.startsWith(HTTP)) {
                    serverAddrs.add(serverAddr);
                } else {
                    String[] serverAddrArr = serverAddr.split(":");
                    if (serverAddrArr.length == 1) {
                        serverAddrs.add(HTTP + serverAddrArr[0] + ":" + ParamUtil.getDefaultServerPort());
                    } else {
                        serverAddrs.add(HTTP + serverAddr);
                    }
                }
            }
            serverUrls = serverAddrs;
            //Namespace selection
            if (StringUtils.isBlank(namespace)) {
                name = FIXED_NAME + "-" + getFixedNameSuffix(serverUrls.toArray(new String[serverUrls.size()]));
            } else {
                this.namespace = namespace;
                this.tenant = namespace;
                name = FIXED_NAME + "-" + getFixedNameSuffix(serverUrls.toArray(new String[serverUrls.size()])) + "-"
                    + namespace;
            }
        //If it's an endpoint connection
        } else {
            if (StringUtils.isBlank(endpoint)) {
                throw new NacosException(NacosException.CLIENT_INVALID_PARAM, "endpoint is blank");
            }
            isFixed = false;
            if (StringUtils.isBlank(namespace)) {
                name = endpoint;
                addressServerUrl = String.format("http://%s:%d/%s/%s", endpoint, endpointPort, contentPath,
                    serverListName);
            } else {
                this.namespace = namespace;
                this.tenant = namespace;
                name = endpoint + "-" + namespace;
                addressServerUrl = String.format("http://%s:%d/%s/%s?namespace=%s", endpoint, endpointPort,
                    contentPath, serverListName, namespace);
            }
        }

    }

Initialization method:

    private void init(Properties properties) {
        //Initialize encoding format, default utf-8
        initEncode(properties);
        //Initialize accessKey and secretKey for asymmetric encryption
        initAkSk(properties);
        //Initialize the maximum number of retries, the default is 3
        initMaxRetry(properties);
    }

Knowledge of AK/SK, refer to https://blog.csdn.net/makenothing/article/details/81158481

When the agent creation is complete, start the agent. The role of this agent is to dynamically obtain the address list of the server.

        //Start agent
        agent.start();

The start of the agent executes the start method of serverListMgr, which is the serverListManager in the agent we created earlier

    @Override
    public synchronized void start() throws NacosException {
        serverListMgr.start();
    }

In the example, if the server address is fixed, start will return directly. There is no need to obtain the server address dynamically, not for now.

public synchronized void start() throws NacosException {
        //If the serverAddr in the example is fixed, return directly
        if (isStarted || isFixed) {
            return;
        }
        //The operation here is to dynamically obtain the server address
        GetServerListTask getServersTask = new GetServerListTask(addressServerUrl);
        for (int i = 0; i < initServerlistRetryTimes && serverUrls.isEmpty(); ++i) {
            getServersTask.run();
            try {
                this.wait((i + 1) * 100L);
            } catch (Exception e) {
                LOGGER.warn("get serverlist fail,url: {}", addressServerUrl);
            }
        }
        TimerService.scheduleWithFixedDelay(getServersTask, 0L, 30L, TimeUnit.SECONDS);
        isStarted = true;
    }

Next is ClientWorker, whose main task is to configure the real-time synchronization server.

//The client gets the configuration of the server in real time        
worker = new ClientWorker(agent, configFilterChainManager, properties);
    public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
        //Assign a value to the agent and configured filter of the server list
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;

        // Initialize the timeout parameter
        init(properties);

        //Create a pool of daemons, a thread, to start (daemons are threads that live and die with user threads)
        executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        //Create another daemons pool with the number of currently available threads to synchronize the configuration information on the server
        executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        //Start the thread once every 10ms
        executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    //Check configuration information
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

Let's look at the checkConfigInfo method:

    public void checkConfigInfo() {
        // The cache map stores all configuration information and is a thread safe AtomicReference.
        int listenerSize = cacheMap.get().size();
        // Every 3000 monitors are in one batch, and if less than 3000, the number of batches is rounded up 
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                //Execution synchronization
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

Update the configuration information cacheData in the cache in batches, and start the LongPollingRunnable thread. There is such a comment in the source code that "to judge whether the task is executing or not, it needs to be considered carefully. The task list is now unordered. There may be problems in the process of change ", because it is necessary to monitor this thread, which cannot be stopped, but at present, it is not possible to monitor these threads.

Let's take a look at how the thread LongPollingRunnable is configured to listen to the server.

class LongPollingRunnable implements Runnable {
        private int taskId;

        public LongPollingRunnable(int taskId) {
            this.taskId = taskId;
        }

        @Override
        public void run() {

            List<CacheData> cacheDatas = new ArrayList<CacheData>();
            List<String> inInitializingCacheList = new ArrayList<String>();
            try {
                // check failover config
                for (CacheData cacheData : cacheMap.get().values()) {
                    if (cacheData.getTaskId() == taskId) {
                        cacheDatas.add(cacheData);
                        try {
                            //Check the local configuration. nacos will create a local copy of the configuration to enhance the disaster tolerance of the system
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }

                //Check the configuration information of the server and return the groupId and dataId of the changed configuration
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);

                //Get the latest configuration from the server and update the changed configuration
                for (String groupKey : changedGroupKeys) {
                    String[] key = GroupKey.parseKey(groupKey);
                    String dataId = key[0];
                    String group = key[1];
                    String tenant = null;
                    if (key.length == 3) {
                        tenant = key[2];
                    }
                    try {
                        String content = getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(content);
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}",
                            agent.getName(), dataId, group, tenant, cache.getMd5(),
                            ContentUtils.truncateContent(content));
                    } catch (NacosException ioe) {
                        String message = String.format(
                            "[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                            agent.getName(), dataId, group, tenant);
                        LOGGER.error(message, ioe);
                    }
                }
                //New configuration appears in synchronization, and the listener needs to be updated at the same time
                for (CacheData cacheData : cacheDatas) {
                    if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                        cacheData.checkListenerMd5();
                        cacheData.setInitializing(false);
                    }
                }
                inInitializingCacheList.clear();
                //After synchronization, the thread is used to keep 3000 configurations consistent with the server
                executorService.execute(this);

            } catch (Throwable e) {

                // If the rotation training task is abnormal, the next execution time of the task will be punished
                LOGGER.error("longPolling error : ", e);
                executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }
    }

nacos will make a local configuration copy, which can greatly enhance the disaster tolerance of the system. When the nacos service goes down, you can also use the local configuration to run.

In the long polling runnable thread, the heartbeat information of the client and the server is also maintained to identify the current health of the server. Specifically, get the groupkey of the server change:

    /**
     * Get a list of dataids with changed values from the Server. Only dataid and group are valid in the returned object. Ensure that NULL is not returned.
     */
    List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws IOException {
        StringBuilder sb = new StringBuilder();
        for (CacheData cacheData : cacheDatas) {
            if (!cacheData.isUseLocalConfigInfo()) {
                sb.append(cacheData.dataId).append(WORD_SEPARATOR);
                sb.append(cacheData.group).append(WORD_SEPARATOR);
                if (StringUtils.isBlank(cacheData.tenant)) {
                    sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
                } else {
                    sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
                    sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
                }
                if (cacheData.isInitializing()) {
                    // cacheData appears in cacheMap for the first time & the first check update
                    inInitializingCacheList
                        .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
                }
            }
        }
        boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
        return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
    }

Enter the checkupdateconfig STR method:

    List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {

        List<String> params = Arrays.asList(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);

        List<String> headers = new ArrayList<String>(2);
        headers.add("Long-Pulling-Timeout");
        headers.add("" + timeout);

        // told server do not hang me up if new initializing cacheData added in
        if (isInitializingCacheList) {
            headers.add("Long-Pulling-Timeout-No-Hangup");
            headers.add("true");
        }

        if (StringUtils.isBlank(probeUpdateString)) {
            return Collections.emptyList();
        }

        try {
            HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
                agent.getEncode(), timeout);
            //Call the server's listener interface, and identify the health status of the server according to the returned http status code
            if (HttpURLConnection.HTTP_OK == result.code) {
                setHealthServer(true);
                return parseUpdateDataIdResponse(result.content);
            } else {
                setHealthServer(false);
                LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
            }
        } catch (IOException e) {
            setHealthServer(false);
            LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
            throw e;
        }
        return Collections.emptyList();
    }

Say what I said before, question 1: is it not good to directly new an object? And use reflection

1. new belongs to static compilation. At the time of compilation, the object to be instantiated has been determined, while the reflection is uncertain

2. Reflection belongs to dynamic compilation, which means that only at run time can we get the instance of the object. Most frameworks use reflection to get the instance of the object, such as Spring

Let 's talk about this for now

79 original articles published, 26 praised, 60000 visitors+
Private letter follow

Topics: encoding network less Spring