Although Spring Boot has greatly simplified the configuration, it is still difficult to manage the configuration for many environments and restart the service after each configuration change. Nacos Config has solved these problems. It puts the configuration in the service for unified management, and the client side can obtain the required configuration, and modify and publish the configuration in real time

How do I use Nacos Config

The nacOS Config JAR package needs to be imported first

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> < version > 2.2.1. RELEASE < / version > < / dependency >Copy the code

Configure the required configuration files in advance on the NACOS console

The configuration file format can be Text, JSON, XML, YAML, HTML, and Properties. Note that the configuration file format supported by Spring Boot can only be YAML or Properties. You need to write your own code to obtain configuration files in other formats

Db.properties is also a database configuration

Data ID is the corresponding configuration file ID, group is the group, and the configuration content is in the properties format

How does bootstrap.properties refer to this configuration file

Name =nacos-config server.port=20200 # namespace Spring. Cloud. Nacos. Config. The namespace = ${ca74337 nacos_register_namingspace: 0 to 8 f42-49 c3 - aec9-32 f268a937c4} # group name Spring. Cloud. Nacos. Config. Group = ${spring. Application. The name} # file format spring. Cloud. Nacos. Config. The file - the extension = properties # nacos server address spring. Cloud. Nacos. Config. The server - addr = localhost: 8848 # to load the configuration file spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties spring.cloud.nacos.config.ext-config[1].data-id=db.properties spring.cloud.nacos.config.ext-config[2].data-id=mybatis-plus.propertiesCopy the code

Notice The group name for loading the configuration file is DEFAULT_GROUP by default. If you want to specify a group, you need to specify it again

spring.cloud.nacos.config.ext-config[0].data-id=nacos.properties Spring. Cloud. Nacos. Config. Ext - config [0]. Group = ${spring. Cloud. Nacos. Config. Group} # or spring.cloud.nacos.config.ext-config[1].data-id=undertow.properties spring.cloud.nacos.config.ext-config[1].group=MY_DEFAULTCopy the code

To explain the concepts of namespace and group, namespace can be used to solve problems in different environments, while group is used to manage configuration groups. Their relationship is shown in the following figure

How do spring Boot boot containers load nacOS Config configuration files

This configuration is enabled when Spring prepares the context between startup to import nacOS-related configuration files in preparation for subsequent container startup

To see NacosConfigBootstrapConfiguration the configuration class

NacosConfigProperties: corresponds to the configuration information we had in bootstrap.properties above

NacosConfigManager: Holds NacosConfigProperties and ConfigService, which is used to query interfaces for publishing configurations

NacosPropertySourceLocator: it implements PropertySourceLocator, spring boot startup called PropertySourceLocator. Locate (env) is used to load configuration information, see below related source code

/******************************************NacosPropertySourceLocator******************************************/ public PropertySource<? > locate(Environment env) { ConfigService configService = this.nacosConfigProperties.configServiceInstance(); if (null == configService) { log.warn("no instance of config service found, can't load config from nacos"); return null; } else { long timeout = (long)this.nacosConfigProperties.getTimeout(); this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout); String name = this.nacosConfigProperties.getName(); String dataIdPrefix = this.nacosConfigProperties.getPrefix(); if (StringUtils.isEmpty(dataIdPrefix)) { dataIdPrefix = name; } if (StringUtils.isEmpty(dataIdPrefix)) { dataIdPrefix = env.getProperty("spring.application.name"); } CompositePropertySource composite = new CompositePropertySource("NACOS"); // Load shared configuration files to specify different groups by default DEFAULT_GROUP, The corresponding configuration spring. Cloud. Nacos. Config. SharedDataids = shared_1. Properties, shared_2. Properties this.loadSharedConfiguration(composite); Corresponding spring. / / cloud. Nacos. Config. Ext - config [0]. Data - id = nacos. The properties of configuration enclosing loadExtConfiguration (composite); / / load current application configuration. This loadApplicationConfiguration (composite, dataIdPrefix, enclosing nacosConfigProperties, env); return composite; }} / / see a loading implementation process about the same Specific implementation in NacosPropertySourceBuilder. LoadNacosData () method / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * specific implementation in NacosPropertySourceBuilder * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / private Properties loadNacosData(String dataId, String group, String fileExtension) { String data = null; Try {/ / to nacos server pull configuration file data = this. ConfigService. GetConfig (dataId group, enclosing the timeout); if (! StringUtils.isEmpty(data)) { log.info(String.format("Loading nacos data, dataId: '%s', group: '%s'", dataId, group)); / / spring boot configuration, of course, only support the properties and the yaml file format if (fileExtension. EqualsIgnoreCase (" properties ")) {properties properties = new  Properties(); properties.load(new StringReader(data)); return properties; } if (fileExtension.equalsIgnoreCase("yaml") || fileExtension.equalsIgnoreCase("yml")) { YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean(); yamlFactory.setResources(new Resource[]{new ByteArrayResource(data.getBytes())}); return yamlFactory.getObject(); } } } catch (NacosException var6) { log.error("get data from Nacos error,dataId:{}, ", dataId, var6); } catch (Exception var7) { log.error("parse data from Nacos error,dataId:{},data:{},", new Object[]{dataId, data, var7}); } return EMPTY_PROPERTIES; }Copy the code

At this point we’re nacos configuration properties and yaml files are loaded on to the spring configuration file, back through the context. The Environment. The getProperty (propertyName) to obtain the relevant configuration information

How does the configuration load in with Spring Boot

Nacos config is dynamically refreshed

When the nacos Config is updated, whether to refresh the configuration is determined according to the refresh attribute in the configuration. The configuration is as follows

spring.cloud.nacos.config.ext-config[0].refresh=true
Copy the code

The first sprin. Factories configuration EnableAutoConfiguration = NacosConfigAutoConfiguration, NacosConfigAutoConfiguration configuration class will inject a NacosContextRefresher, it first listening ApplicationReadyEvent, Then register a NacOS Listener to listen for nacOS Config configuration changes and issue a Spring refreshEvent to refresh the configuration and application

public class NacosContextRefresher implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware public void onApplicationEvent(ApplicationReadyEvent Event) {// Only register if once (this.ready.compareAndSet(false, true)) { this.registerNacosListenersForApplications(); } } private void registerNacosListenersForApplications() { if (this.refreshProperties.isEnabled()) { Iterator var1 = NacosPropertySourceRepository.getAll().iterator(); while(var1.hasNext()) { NacosPropertySource nacosPropertySource = (NacosPropertySource)var1.next(); / / whether the corresponding configuration of just said need configuration files need to refresh the if (nacosPropertySource. IsRefreshable ()) {String dataId = nacosPropertySource. GetDataId (); Registration / / nacos listeners enclosing registerNacosListener (nacosPropertySource. GetGroup (), dataId); } } } } private void registerNacosListener(final String group, final String dataId) { Listener listener = (Listener)this.listenerMap.computeIfAbsent(dataId, (i) -> { return new Listener() { public void receiveConfigInfo(String configInfo) { NacosContextRefresher.refreshCountIncrement(); String md5 = ""; if (! StringUtils.isEmpty(configInfo)) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md5 = (new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))).toString(16); } catch (UnsupportedEncodingException | NoSuchAlgorithmException var4) { NacosContextRefresher.log.warn("[Nacos] unable to get md5 for dataId: " + dataId, var4); }} / / adds refresh record NacosContextRefresher enclosing refreshHistory. Add (dataId, md5); // Issue a Spring refreshEvent event corresponding to a RefreshEventListener which completes the configured update application NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config")); if (NacosContextRefresher.log.isDebugEnabled()) { NacosContextRefresher.log.debug("Refresh Nacos config group " + group + ",dataId" + dataId); } } public Executor getExecutor() { return null; }}; }); try { this.configService.addListener(dataId, group, listener); } catch (NacosException var5) { var5.printStackTrace(); }}Copy the code

We said that the nacos Config dynamic refresh, then there must be a corresponding dynamic listening, nacos Config will listen for configuration updates on the NacOS Server status

Nacos Config dynamically listens

Generally speaking, there are only two ways for client and server data to interact

Pull: The client actively pulls data from the server

Push: The server actively pushes data to the client

The advantages and disadvantages of the two modes are different. The pull mode needs to consider when to pull data from the server, which may cause data delay, while the push mode requires the client and the server to maintain a long connection. If there are more clients, the server will be under pressure, but its real-time performance will be better

Nacos uses pull mode, but it is optimized to be regarded as pull+push. The client will poll to send a long connection request to the server. The long connection will time out in 30 seconds at most

If no server will “hold” the request 29.5s to queue, and finally 0.5s to check whether the configuration file is updated or not, it will return normally, but during 29.5s to wait for configuration update can end early and return, the following will be explained in the source code specific how to deal with

Nacos client processing

Dynamic listening is initiated in the constructor of ConfigService’s implementation class, NacosConfigService, which is the external NacOS Config API interface obtained or created in the previous loading configuration file and NacosContextRefresher constructor

ConfigServer is created, and if not, a NacosConfigService is instantiated to see its constructor

/***************************************** NacosConfigService *****************************************/ public NacosConfigService(Properties properties) throws NacosException { String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE); if (StringUtils.isBlank(encodeTmp)) { encode = Constants.ENCODE; } else { encode = encodeTmp.trim(); } initNamespace(properties); Agent = new MetricsHttpAgent(new ServerHttpAgent(properties)); agent.start(); / / client a working class, the structure of the agent as its parameter Will certainly do some remote call in can guess the worker = new ClientWorker (agent, configFilterChainManager, properties); } /***************************************** ClientWorker *****************************************/ public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) { this.agent = agent; this.configFilterChainManager = configFilterChainManager; // Initialize the timeout parameter init(properties); / / the thread pool is only one core thread Used to perform checkConfigInfo () method of the 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; }}); / / other places need the execution thread to this thread pool to process the executorService = Executors. NewScheduledThreadPool (Runtime. The 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; }}); // Execute a periodic task that calls the checkConfigInfo() method every 10ms, First executed after 1 ms delay executor. ScheduleWithFixedDelay (new Runnable () {@ Override public void the run () {try {checkConfigInfo (); } catch (Throwable e) { LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e); } } }, 1L, 10L, TimeUnit.MILLISECONDS); }Copy the code

The NacosConfigService constructor creates an agent that makes a request to the NacOS Server, and then a ClientWoker constructor that creates two thread pools. The first pool has only one core thread. It performs a periodic task that only calls the checkConfigInfo () method, and the second thread is later handed over to it wherever the thread needs to be executed, focusing on the checkConfigInfo () method

Public void checkConfigInfo() {// Subtask int listenerSize = cachemap.get ().size(); / / rounded up for batch number int longingTaskCount = (int) Math. Ceil (listenerSize/ParamUtil getPerTaskConfigSize ()); if (longingTaskCount > currentLongingTaskCount) { for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) { executorService.execute(new LongPollingRunnable(i)); } currentLongingTaskCount = longingTaskCount; } } AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>( new HashMap<String, CacheData>());Copy the code

CacheMap: Caches configurations that need to be refreshed. It is added when you call ConfigService to add listeners. You can customize the listening configuration refresh

// Add a config listener to listen for dataId as ErrorCode, Group for DEFAULT_GROUP config configService. AddListener (" ErrorCode ", "DEFAULT_GROUP", new Listener () {@ Override public Executor getExecutor() { return null; } @override public void receiveConfigInfo(String s) {// Listeners are called when the configuration is updated. String>> map = JSON.parseObject(s, Map.class); // According to their own business needs}});Copy the code

The strategy used here is to create a LongPollingRunnable that listens for configuration updates by grouping the cacheMap population in groups of 3000. The LongPollingRunnable is the long-connect task we talked about earlier

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); CheckLocalConfig (cacheData); if (cacheData.isUseLocalConfigInfo()) { cacheData.checkListenerMd5(); } } catch (Exception e) { LOGGER.error("get local config info error", e); }} // 2, send a long connection to the nacOS server for 30 seconds. Nacos Server dataIds List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList); LOGGER.info("get changedGroupKeys:" + changedGroupKeys); 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]; String[] ct = getServerConfig(dataId, group, tenant, 3000L); CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant)); cache.setContent(ct[0]); if (null ! = ct[1]) { cache.setType(ct[1]); For (CacheData CacheData: cacheDatas) {if (! cacheData.isInitializing() || inInitializingCacheList .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) { cacheData.checkListenerMd5(); cacheData.setInitializing(false); } } inInitializingCacheList.clear(); Execute (this); // Continue polling executorService.execute(this); } catch (Throwable e) {executorService.schedule(this, taskPenaltyTime, timeunit.milliseconds); }}}Copy the code

This long poll does four main steps

  1. Check the local configuration, if there is a local configuration, and the local configuration version of the cache is not the same, the local configuration content updated to the cache, and trigger events, the source code is relatively simple, readers follow the source code to read the system
  2. Send a long connection to the Nacos server, and after 30 seconds, the Nacos Server will return the changed dataIds
  3. Based on the changing dataId, the latest configuration content is pulled from the server and updated to the cache
  4. Trigger event listeners to handle configuration changes

After the nacOS client processing process, let’s look at how the server side handles this long connection

Nacos server processing

The server interface is /config/listener, and the corresponding source package is config

/****************************************** ConfigController ******************************************/ @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 { clientMd5Map = MD5Util.getClientMd5Map(probeModify); } catch (Throwable e) { throw new IllegalArgumentException("invalid probeModify"); } inner. DoPollingConfig (request, response, clientMd5Map, probemodify.length ()); } /****************************************** ConfigServletInner ******************************************/ public String doPollingConfig(HttpServletRequest request, HttpServletResponse response, Map<String, String> clientMd5Map, Int probeRequestSize) throws IOException {/ / determine whether support long polling the if (LongPollingService. IsSupportLongPolling (request)) {/ / long polling  longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize); return HttpServletResponse.SC_OK + ""; List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map); // Compatible with short polling result. String oldResult = MD5Util.compareMd5OldResult(changedGroups); String newResult = MD5Util.compareMd5ResultString(changedGroups); */ return httpServletresponse.sc_ok + ""; } /****************************************** LongPollingService ******************************************/ public void  addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) { 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"); // The maximum processing time on the server side is 29.5s. In order to avoid the client side timeout int delayTime = SwitchService. GetSwitchInteger (SwitchService FIXED_DELAY_TIME, 500); // Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout. long timeout = Math.max(10000, Long.parseLong(str) - delayTime); if (isFixedPolling()) { timeout = Math.max(10000, getFixedPollingInterval()); Return long start = system.currentTimemillis (); // Do nothing but set fix polling timeout. List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map); if (changedGroups.size() > 0) { generateResponse(req, rsp, changedGroups); // log.... return; } else if (noHangUpFlag ! = null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) { // log.... return; } } String ip = RequestUtil.getRemoteIp(req); 29.5s final AsyncContext AsyncContext = req.startAsync(); // AsyncContext.setTimeout() is incorrect, Control by oneself asyncContext.setTimeout(0L); // Execute the client long connection task.  ConfigExecutor.executeLongPolling( new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag)); } /****************************************** ClientLongPolling ******************************************/ class ClientLongPolling implements Runnable {@override public void run() { Delay 29.5 s executive asyncTimeoutFuture = ConfigExecutor. ScheduleLongPolling (new Runnable () {@ Override public void the run () {try { getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis()); // Delete subsciber's relations. allSubs.remove(ClientLongPolling.this); If (isFixedPolling()) {List<String> changedGroups = MD5Util. CompareMd5 ((HttpServletRequest)) asyncContext.getRequest(), (HttpServletResponse) asyncContext.getResponse(), clientMd5Map); if (changedGroups.size() > 0) { sendResponse(changedGroups); } else { sendResponse(null); } } else { sendResponse(null); } } catch (Throwable t) { LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause()); } } }, timeoutTime, TimeUnit.MILLISECONDS); allSubs.add(this); } } final Queue<ClientLongPolling> allSubsCopy the code

ClientLongPolling (polling) commits a task, responds with or without configuration updates, delays execution by 29.5s, and then adds itself to a queue. The server will find the long connection task waiting for configuration update after configuration update, end this task in advance and return,

So how do we do this step

public LongPollingService() { allSubs = new ConcurrentLinkedQueue<ClientLongPolling>(); ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS); // Register LocalDataChangeEvent to NotifyCenter. NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize); // Register A Subscriber to subscribe LocalDataChangeEvent. NotifyCenter.registerSubscriber(new Subscriber() { @Override  public void onEvent(Event event) { if (isFixedPolling()) { // Ignore. } else { if (event instanceof LocalDataChangeEvent) { LocalDataChangeEvent evt = (LocalDataChangeEvent) event; ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps)); } } } @Override public Class<? extends Event> subscribeType() { return LocalDataChangeEvent.class; }}); } class DataChangeTask implements Runnable { @Override public void run() { try { ConfigCacheService.getContentBetaMd5(groupKey); Iterator<ClientLongPolling> iter = allsubs.iterator (); Iterator<ClientLongPolling> iter = allsubs.iterator (); iter.hasNext(); ) { ClientLongPolling clientSub = iter.next(); if (clientSub.clientMd5Map.containsKey(groupKey)) { // If published tag is not in the beta list, then it skipped. if (isBeta && ! CollectionUtils.contains(betaIps, clientSub.ip)) { continue; } // If published tag is not in the tag list, then it skipped. if (StringUtils.isNotBlank(tag) && ! tag.equals(clientSub.tag)) { continue; } getRetainIps().put(clientSub.ip, System.currentTimeMillis()); iter.remove(); // Delete subscribers' relationships. clientSub.sendResponse(Arrays.asList(groupKey)); } } } catch (Throwable t) { LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t)); }}}Copy the code

In the LongPollingService constructor, a subscription is registered to listen for LocalDataChangeEvent. When this event occurs, a data change task is performed. The task is to find the long connection waiting to be configured and return early

We’re nacos console to modify a configuration file, will call ConfigController. PublishConfig interface, but the interface is released ConfigDataChangeEvent events, careless… LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent LocalDataChangeEvent

So far nacOS Config dynamic listening, refresh will be connected, nacOS related source code is better to understand, follow the source code to catch up with a glance