Microservice architecture * 2.5 source code analysis of Nacos long polling timing mechanism

Posted by whit3fir3 on Mon, 24 Jan 2022 05:10:54 +0100

preface

reference material:
<Spring Microservices in Action>
Principles and practice of Spring Cloud Alibaba microservice
"Spring cloud framework development tutorial in Silicon Valley of station B" Zhou Yang

In order to facilitate understanding and expression, the Nacos console and the Nacos registry are called the Nacos server (that is, the web interface), and the business service we write is called the Nacso client;

Due to the limited space, the source code analysis is divided into two parts. The first part focuses on the acquisition configuration and event subscription mechanism, and the second part focuses on the long polling timing mechanism; stay Microservice architecture | 2.2 unified configuration management of Alibaba Nacos A schematic diagram of long polling mechanism for Nacos dynamic monitoring is mentioned in. This paper will analyze the principle of long polling timing mechanism around this diagram;

Part I Microservice architecture * 2.4 source code analysis of Nacos configuration center (acquisition configuration and event subscription mechanism) As mentioned in 1.1 in, ConfigService is a class provided by Nacos client to access and implement the basic operations of the configuration center. We will start the source code journey of long polling timing mechanism from the instantiation of ConfigService;

1. Long polling timing mechanism of client

  • Let's start from here in the last article [breakpoint entry];
  • NacosPropertySourceLocator.locate();

1.1 instantiate the NacosConfigService object using reflection mechanism

  • The long polling timing task of the client is in the nacosfactory In the createconfigservice () method, it is started when the ConfigService object instance is built. We then follow the source code at 1.1;
  • Enter the nacosfactory createConfigService():
public static ConfigService createConfigService(Properties properties) throws NacosException {
    //[breakpoint] create ConfigService
    return ConfigFactory.createConfigService(properties);
}
  • Enter configfactory Createconfigservice(), it is found that it instantiates the NacosConfigService object using the reflection mechanism;
public static ConfigService createConfigService(Properties properties) throws NacosException {
    try {
        //Pass class Forname to load the NacosConfigService
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
        //Creates an object constructor (reflection) based on an object's properties
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        //[1.2] instantiate an object (reflection) through a constructor
        ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable var4) {
        throw new NacosException(-400, var4);
    }
}

1.2 start the long polling timing task in the construction method of nacosconfigservice

  • Enter nacosconfigservice Nacosconfigservice() construction method, which sets some properties related to more remote tasks;
public NacosConfigService(Properties properties) throws NacosException {
    String encodeTmp = properties.getProperty("encode");
    if (StringUtils.isBlank(encodeTmp)) {
        this.encode = "UTF-8";
    } else {
        this.encode = encodeTmp.trim();
    }
    //Initialize namespace
    this.initNamespace(properties);
    //[1.2.1] initializing HttpAgent uses decorator mode. The actual working class is ServerHttpAgent
    this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
    this.agent.start();
    //[1.2.2] ClientWorker is a working class of the client, and agent is passed into ClientWorker as a parameter
    this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}

1.2.1 initialize HttpAgent

  • The MetricsHttpAgent class is designed as follows:
  • The ServerHttpAgent class is designed as follows:

1.2.2 initializing ClientWorker

  • Enter clientworker The clientworker () construction method mainly creates two scheduled thread pools and starts a scheduled task;
public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
    this.agent = agent;
    this.configFilterChainManager = configFilterChainManager;
    this.init(properties);
    //Create an executor thread pool with only one core thread, and execute the checkConfiglnfo() method every 10ms to check the configuration information
    this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
            t.setDaemon(true);
            return t;
        }
    });
    //The executorService thread pool is only initialized and will be used later. It is mainly used to realize the timed long polling function of the client
    this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
        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;
        }
    });
    //Use the executor to start a scheduled task that is executed every 10s
    this.executor.scheduleWithFixedDelay(new Runnable() {
        public void run() {
            try {
                //[breakpoint] check whether the configuration has changed
                ClientWorker.this.checkConfigInfo();
            } catch (Throwable var2) {
                ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
            }
        }
    }, 1L, 10L, TimeUnit.MILLISECONDS);
}
  • Enter clientworker Checkconfiginfo(), check whether the configuration changes every 10s;
    • cacheMap: an atomicreference < map < string, cachedata > > object used to store the cache collection listening for changes. The key is the value spliced according to datalD/group/tenant (tenant). Value is the content of the corresponding configuration file stored on the Nacos server;
    • Long polling task splitting: by default, each long polling LongPollingRunnable task handles 3000 listening configuration sets. If there are more than 3000, you need to start multiple LongPollingRunnable to execute;
public void checkConfigInfo() {
    //Subtask
    int listenerSize = ((Map)this.cacheMap.get()).size();
    //Round up to batches
    int longingTaskCount = (int)Math.ceil((double)listenerSize / ParamUtil.getPerTaskConfigSize());
    //If the listening configuration set exceeds 3000, multiple LongPollingRunnable threads are created
    if ((double)longingTaskCount > this.currentLongingTaskCount) {
        for(int i = (int)this.currentLongingTaskCount; i < longingTaskCount; ++i) {
            //[click in] LongPollingRunnable is actually a thread
            this.executorService.execute(new ClientWorker.LongPollingRunnable(i));
        }
        this.currentLongingTaskCount = (double)longingTaskCount;
    }
}

1.3 check the configuration change and read the changed configuration longpollingrunnable run()

  • Because we don't have so many configuration items, debug won't go in, so we directly find longpollingrunnable Run() method. The main logic of this method is:
    • Segment the cacheMap according to taskld;
    • Then compare whether the data in the local configuration file (in ${user}\nacos\config \) has changed through the checkLocalConfig() method. If there is any change, the notification will be triggered directly;
public void run() {
    List<CacheData> cacheDatas = new ArrayList();
    ArrayList inInitializingCacheList = new ArrayList();
    try {
        //Traverse CacheData and check local configuration
        Iterator var3 = ((Map)ClientWorker.this.cacheMap.get()).values().iterator();
        while(var3.hasNext()) {
            CacheData cacheData = (CacheData)var3.next();
            if (cacheData.getTaskId() == this.taskId) {
                cacheDatas.add(cacheData);
                try {
                    //Check local configuration
                    ClientWorker.this.checkLocalConfig(cacheData);
                    if (cacheData.isUseLocalConfigInfo()) {
                        cacheData.checkListenerMd5();
                    }
                } catch (Exception var13) {
                    ClientWorker.LOGGER.error("get local config info error", var13);
                }
            }
        }
        //[1.3.1] check whether the corresponding configuration of the server has changed through a long polling request
        List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
        //Traverse the changed groupKey and reload the latest data
        Iterator var16 = changedGroupKeys.iterator();
        while(var16.hasNext()) {
            String groupKey = (String)var16.next();
            String[] key = GroupKey.parseKey(groupKey);
            String dataId = key[0];
            String group = key[1];
            String tenant = null;
            if (key.length == 3) {
                tenant = key[2];
            }
            try {
                //[1.3.2] read the change configuration. The dataId, group and tenant here are obtained in [1.3.1]
                String content = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
                CacheData cache = (CacheData)((Map)ClientWorker.this.cacheMap.get()).get(GroupKey.getKeyTenant(dataId, group, tenant));
                cache.setContent(content);
                ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(content)});
            } catch (NacosException var12) {
                String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
                ClientWorker.LOGGER.error(message, var12);
            }
        }
        //Trigger event notification
        var16 = cacheDatas.iterator();
        while(true) {
            CacheData cacheDatax;
            do {
                if (!var16.hasNext()) {
                    inInitializingCacheList.clear();
                    //Continue scheduled execution of the current thread
                    ClientWorker.this.executorService.execute(this);
                    return;
                }
                cacheDatax = (CacheData)var16.next();
            } while(cacheDatax.isInitializing() && !inInitializingCacheList.contains(GroupKey.getKeyTenant(cacheDatax.dataId, cacheDatax.group, cacheDatax.tenant)));
            cacheDatax.checkListenerMd5();
            cacheDatax.setInitializing(false);
        }
    } catch (Throwable var14) {
        ClientWorker.LOGGER.error("longPolling error : ", var14);
        ClientWorker.this.executorService.schedule(this, (long)ClientWorker.this.taskPenaltyTime, TimeUnit.MILLISECONDS);
    }
}
  • Note: the breakpoints here need to be modified on the Nacos server (the interval is greater than 30s) before they can be understood;

1.3.1 check the configuration change clientworker checkUpdateDataIds()

  • Let's click into clientworker Checkupdatedataids() method, and it is found that the final call is clientworker The implementation logic and source code of the checkupdateconfig str() method are as follows:
    • Through metricshttpagent The httppost () method (mentioned in 1.2.1 above) calls the / v1/cs/configs/listener interface to implement the long polling request;
    • At the implementation level, the long polling request only sets a relatively long timeout, which is 30s by default;
    • If the data of the server is changed, the client will receive an HttpResult, and the server will return the Data ID, Group and Tenant with data change;
    • After obtaining this information, in longpollingrunnable The run () method calls getServerConfig() to read the specific configuration content on the Nacos server.
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
    List<String> params = Arrays.asList("Listening-Configs", probeUpdateString);
    List<String> headers = new ArrayList(2);
    headers.add("Long-Pulling-Timeout");
    headers.add("" + this.timeout);
    if (isInitializingCacheList) {
        headers.add("Long-Pulling-Timeout-No-Hangup");
        headers.add("true");
    }
    if (StringUtils.isBlank(probeUpdateString)) {
        return Collections.emptyList();
    } else {
        try {
            //Call the / v1/cs/configs/listener interface to implement the long polling request. The returned HttpResult contains the Data ID, Group and Tenant with data change
            HttpResult result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), this.timeout);
            if (200 == result.code) {
                this.setHealthServer(true);
                //
                return this.parseUpdateDataIdResponse(result.content);
            }
            this.setHealthServer(false);
            LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.code);
        } catch (IOException var6) {
            this.setHealthServer(false);
            LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var6);
            throw var6;
        }
        return Collections.emptyList();
    }
}

1.3.2 read and change the configuration of clientworker getServerConfig()

  • Enter clientworker Getserverconfig() method;
  • Read the changed configuration on the server;
  • The final call is metricshttpagent Httpget() method (mentioned in 1.2.1 above), call the / V1 / CS / configurations interface to obtain the configuration;
  • Then by calling localconfiginfoprocessor Savesnapshot() saves the changed configuration locally;
public String getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
    if (StringUtils.isBlank(group)) {
        group = "DEFAULT_GROUP";
    }
    HttpResult result = null;
    try {
        List<String> params = null;
        if (StringUtils.isBlank(tenant)) {
            params = Arrays.asList("dataId", dataId, "group", group);
        } else {
            params = Arrays.asList("dataId", dataId, "group", group, "tenant", tenant);
        }
        //Get the interface call to change the configuration
        result = this.agent.httpGet("/v1/cs/configs", (List)null, params, this.agent.getEncode(), readTimeout);
    } catch (IOException var9) {
        String message = String.format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", this.agent.getName(), dataId, group, tenant);
        LOGGER.error(message, var9);
        throw new NacosException(500, var9);
    }
    switch(result.code) {
    //The changed configuration is successfully obtained and added to the cache
    case 200:
        LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, result.content);
        //result.content is our changed configuration information
        return result.content;
    case 403:
        LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
        throw new NacosException(result.code, result.content);
    case 404:
        LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, (String)null);
        return null;
    case 409:
        LOGGER.error("[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
        throw new NacosException(409, "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
    default:
        LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", new Object[]{this.agent.getName(), dataId, group, tenant, result.code});
        throw new NacosException(result.code, "http error, code=" + result.code + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
    }
}


2. Long polling timing mechanism of server

2.1 the server receives the request configcontroller listener()

  • The Nacos client communicates with the server through HTTP protocol, so there must be the implementation of the corresponding interface in the server source code;
  • The controller package under the Nacos config module provides a ConfigController class to process requests, including a / listener interface, which is the interface for the client to initiate data listening. Its main logic and source code are as follows:
    • Obtain the configuration that may change that the client needs to listen to, and calculate the MD5 value;
    • ConfigServletInner.doPollingConfig() starts executing the long polling request;
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void listener(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
    String probeModify = request.getParameter("Listening-Configs");
    if (StringUtils.isBlank(probeModify)) {
        throw new IllegalArgumentException("invalid probeModify");
    }
    
    probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
    
    Map<String, String> clientMd5Map;
    try {
        //Calculate MD5 value
        clientMd5Map = MD5Util.getClientMd5Map(probeModify);
    } catch (Throwable e) {
        throw new IllegalArgumentException("invalid probeModify");
    }
    
    //[2.2] execute long polling request
    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}

2.2 execute long polling request configservletinner doPollingConfig()

  • Enter configservletinner Dopollingconfig() method, which encapsulates the implementation logic of long polling and is compatible with short polling logic;
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response, Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
    //Long polling
    if (LongPollingService.isSupportLongPolling(request)) {
        //[breakpoint] long polling logic
        longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
        return HttpServletResponse.SC_OK + "";
    }
    //Compatible with short polling logic
    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);
    //Compatible short polling result
    String oldResult = MD5Util.compareMd5OldResult(changedGroups);
    String newResult = MD5Util.compareMd5ResultString(changedGroups);
    String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
    if (version == null) {
        version = "2.0.0";
    }
    int versionNum = Protocol.getVersionNumber(version);
    
    //Before version 2.0.4, the return value was put into the header
    if (versionNum < START_LONG_POLLING_VERSION_NUM) {
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
        response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
    } else {
        request.setAttribute("content", newResult);
    }
    Loggers.AUTH.info("new content:" + newResult);
    //disable cache 
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", 0);
    response.setHeader("Cache-Control", "no-cache,no-store");
    response.setStatus(HttpServletResponse.SC_OK);
    return HttpServletResponse.SC_OK + "";
}
  • Enter longpollingservice Addlongpollingclient() method, which is the core processing logic of long polling. Its main function is to encapsulate the client's long polling request into ClientPolling and submit it to the scheduler for execution;
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) {
    //Gets the request timeout set by the client
    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
    String tag = req.getHeader("Vipserver-Tag");
    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
    
    //Add a delay time for LoadBalance and return a response 500ms in advance to avoid client timeout (that is, assign the timeout value to the timeout variable after the timeout time is reduced by 500ms)
    long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
    //Judge whether it is fixed polling. If yes, execute it after 30s; Otherwise, execute after 29.5s
    if (isFixedPolling()) {
        timeout = Math.max(10000, getFixedPollingInterval());
        // Do nothing but set fix polling timeout.
    } else {
        long start = System.currentTimeMillis();
        //Compare MD5 with the data of the server, and return it directly if there is no change
        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
        if (changedGroups.size() > 0) {
            generateResponse(req, rsp, changedGroups);
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant", RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize, changedGroups.size());
            return;
        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
            LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup", RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize, changedGroups.size());
            return;
        }
    }
    String ip = RequestUtil.getRemoteIp(req);
    
    //It must be called by the HTTP thread, or the response will be sent immediately after leaving the container
    final AsyncContext asyncContext = req.startAsync();
    
    //AsyncContext. The timeout of settimeout() is not allowed, so you can control it yourself
    asyncContext.setTimeout(0L);
    
    //Click enter to call scheduler Execute execute ClientLongPolling thread
    ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

2.3 create a thread to execute a scheduled task clientlongpolling run()

  • We found clientlongpolling The run () method can reflect the core principle of the long polling timing mechanism. Generally speaking, it is:
    • After receiving the request, the server will not return immediately. If there is no change, the server will return the request result to the client within (30-0.5)s delay;
    • This makes the client and the server always connected when the data has not changed within 30s;
@Override
public void run() {
    //Start the scheduled task with a delay time of 29.5s
    asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
        @Override
        public void run() {
            try {
                //Add the ClientLongPolling instance itself to the allSubs queue, which mainly maintains a long polling subscription relationship
                getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                //After the scheduled task is executed, first remove the ClientLongPolling instance itself from the allSubs queue
                allSubs.remove(ClientLongPolling.this);
                //Determine whether it is fixed polling
                if (isFixedPolling()) {
                    LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix", RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),"polling", clientMd5Map.size(), probeRequestSize);
                    //Compare the MD5 value of the data to determine whether there is a change
                    List<String> changedGroups = MD5Util.compareMd5((HttpServletRequest) asyncContext.getRequest(), (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                    if (changedGroups.size() > 0) {
                        //And return the change result to the client through response
                        sendResponse(changedGroups);
                    } else {
                        sendResponse(null);
                    }
                } else {
                    LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout", RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),"polling", clientMd5Map.size(), probeRequestSize);
                    sendResponse(null);
                }
            } catch (Throwable t) {
                LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
            }
        }
    }, timeoutTime, TimeUnit.MILLISECONDS);
    allSubs.add(this);
}

2.4 listening for configuration change events

2.4.1 implementation of monitoring LocalDataChangeEvent event

  • When we change the configuration on the Nacos server or through API, we will publish a LocalDataChangeEvent event, which will be monitored by LongPollingService;
  • Here is why LongPollingService has the listening function. There are some changes after version 1.3.1:
    • 1.3.1 front: longpollingservice onEvent();
    • 1.3.1 post: subscriber onEvent();
  • Before Nacos version 1.3.1, the AbstractEventListener was inherited through LongPollingService to realize listening and override onEvent() method;
  • Click to view the source code of version 1.3.1 on github
@Service
public class LongPollingService extends AbstractEventListener {

   //Omit other codes

    @Override
    public void onEvent(Event event) {
        if (isFixedPolling()) {
            // Ignore.
        } else {
            if (event instanceof LocalDataChangeEvent) {
                LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                //[click 2.4.2] execute the DataChangeTask task through the thread pool
                scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
            }
        }
    }
}    
NotifyCenter.registerSubscriber(new Subscriber() {
    
    @Override
    public void onEvent(Event event) {
        if (isFixedPolling()) {
            // Ignore.
        } else {
            if (event instanceof LocalDataChangeEvent) {
                LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                //[click 2.4.2] execute the DataChangeTask task through the thread pool
                ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
            }
        }
    }
    @Override
    public Class<? extends Event> subscribeType() {
        return LocalDataChangeEvent.class;
    }
});
  • The effect is the same. It monitors the LocalDataChangeEvent event and executes the DataChangeTask through the thread pool;

2.4.2 processing logic after listening to events: datachangetask run()

  • We found datachangetask Run () method, this thread task implements
@Override
public void run() {
    try {
        ConfigCacheService.getContentBetaMd5(groupKey);
        //Traverse client long polling requests in allSubs
        for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
            ClientLongPolling clientSub = iter.next();
            //Compare the groupKey carried by each client long polling request. If the configuration changed by the server is consistent with the configuration concerned by the client request, it will be returned directly
            if (clientSub.clientMd5Map.containsKey(groupKey)) {
                //If the beta is released and not in the beta list, skip it directly
                if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                    continue;
                }
                //If the tag is published and is not in the tag list, skip it directly
                if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                    continue;
                }
                getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                iter.remove(); //Delete subscription relationship
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance", RequestUtil.getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()), "polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                //Send response
                clientSub.sendResponse(Arrays.asList(groupKey));
            }
        }
    } catch (Throwable t) {
        LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
    }
}

3. Summary of source code structure diagram

3.1 long polling timing mechanism of client

  • NacosPropertySourceLocator.locate(): initialize the ConfigService object and locate the configuration;
    • NacosFactory.createConfigService(): create a configuration server;
    • ConfigFactory.createConfigService(): use reflection mechanism to create configuration server;
      • NacosConfigService.NacosConfigService(): construction method of nacosconfigservice;
        • MetricsHttpAgent.MetricsHttpAgent(): initializes HttpAgent;
        • ClientWorker.ClientWorker(): initializes clientworker;
          • Executors.newScheduledThreadPool(): create the executor thread pool;
          • Executors.newScheduledThreadPool(): create executorService thread pool;
          • ClientWorker.checkConfigInfo(): use the executor thread pool to check whether the configuration has changed;
            • LongPollingRunnable.run(): run the long polling timing thread;
              • ClientWorker.checkLocalConfig(): check the local configuration;
              • ClientWorker. Checkupdatedatids(): check whether the configuration corresponding to the server has changed;
                • ClientWorker. Checkupdateconfig str(): check whether the configuration corresponding to the server has changed;
                  • MetricsHttpAgent.httpPost(): call the / v1/cs/configs/listener interface to implement the long polling request;
              • ClientWorker.getServerConfig(): read change configuration
                • MetricsHttpAgent.httpGet(): call the / V1 / CS / configurations interface to get the configuration;

3.2 long polling timing mechanism of server

  • ConfigController.listener(): the server receives the request;
    • MD5Util.getClientMd5Map(): calculate MD5 value;
    • ConfigServletInner.doPollingConfig(): execute long polling request;
      • LongPollingService.addLongPollingClient(): the core processing logic of long polling, which returns a response 500 ms in advance;
        • HttpServletRequest.getHeader(): get the request timeout set by the client;
        • MD5Util.compareMd5(): MD5 comparison with the data of the server;
        • ConfigExecutor.executeLongPolling(): create a ClientLongPolling thread to execute scheduled tasks;
          • ClientLongPolling.run(): implementation logic of long polling timing mechanism;
            • ConfigExecutor.scheduleLongPolling(): start the scheduled task with a delay of 29.5s;
              • Map.put(): add the ClientLongPolling instance itself to the allSubs queue;
              • Queue.remove(): remove the ClientLongPolling instance itself from the allSubs queue;
              • MD5Util.compareMd5(): MD5 value of comparison data;
                • LongPollingService.sendResponse(): return the change result to the client through response;

3.3 event listening of Nacos server configuration change

  • After the configuration on the Nacos server changes, issue a LocalDataChangeEvent event;
  • Subscriber.onEvent(): listen for LocalDataChangeEvent events (after version 1.3.2);
    • ConfigExecutor.executeLongPolling(): execute DataChangeTask through thread pool;
      • DataChangeTask.run(): returns the configuration according to the groupKey;

last

Newcomer production, if there are mistakes, welcome to point out, thank you very much! Welcome to the official account and share some more everyday things. If you need to reprint, please mark the source!

Topics: Distribution Spring Cloud Microservices config