SpringCloud配置重新整理機制的簡單分析[nacos為例子]

OhOutOfMemoryError發表於2021-01-30

SpringCloud Nacos

  1. 本文主要分為SpringCloud Nacos的設計思路
  2. 簡單分析一下觸發重新整理事件後發生的過程以及一些踩坑經驗

org.springframework.cloud.bootstrap.config.PropertySourceLocator

  1. 這是一個SpringCloud提供的啟動器載入配置類,實現locate,注入到上下文中即可發現配置
/**
 * @param environment The current Environment.
 * @return A PropertySource, or null if there is none.
 * @throws IllegalStateException if there is a fail-fast condition.
 */
PropertySource<?> locate(Environment environment);
  1. com.alibaba.cloud.nacos.client.NacosPropertySourceLocator
  • 該類為nacos實現的配置發現類
  1. org.springframework.core.env.PropertySource
  • 改類為springcloud抽象出來表達屬性源的類
  • com.alibaba.cloud.nacos.client.NacosPropertySource / nacos實現了這個類,並賦予了其他屬性
/**
 * Nacos Group.
 */
private final String group;

/**
 * Nacos dataID.
 */
private final String dataId;

/**
 * timestamp the property get.
 */
private final Date timestamp;

/**
 * Whether to support dynamic refresh for this Property Source.
 */
private final boolean isRefreshable;

大概講解com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#locate

  1. 原始碼解析
@Override
public PropertySource<?> locate(Environment env) {
	nacosConfigProperties.setEnvironment(env);
	// 獲取nacos配置的服務類,http協議,訪問nacos的api介面獲得配置
	ConfigService configService = nacosConfigManager.getConfigService();

	if (null == configService) {
		log.warn("no instance of config service found, can't load config from nacos");
		return null;
	}
	long timeout = nacosConfigProperties.getTimeout();
	// 構建一個builder
	nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
			timeout);
	String name = nacosConfigProperties.getName();

	String dataIdPrefix = nacosConfigProperties.getPrefix();
	if (StringUtils.isEmpty(dataIdPrefix)) {
		dataIdPrefix = name;
	}

	if (StringUtils.isEmpty(dataIdPrefix)) {
		dataIdPrefix = env.getProperty("spring.application.name");
	}
    // 構建一個複合資料來源
	CompositePropertySource composite = new CompositePropertySource(
			NACOS_PROPERTY_SOURCE_NAME);
    // 載入共享的配置
	loadSharedConfiguration(composite);
	// 載入擴充套件配置
	loadExtConfiguration(composite);
	// 載入應用配置,應用配置的優先順序是最高,所以這裡放在最後面來做,是因為新增配置的地方都是addFirst,所以最先的反而優先順序最後
	loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

	return composite;
}
  1. 每次nacos檢查到配置更新的時候就會觸發上下文配置重新整理,就會調取locate這個方法

org.springframework.cloud.endpoint.event.RefreshEvent

  1. 該事件為spring cloud內建的事件,用於重新整理配置

com.alibaba.cloud.nacos.refresh.NacosRefreshHistory

  1. 該類用於nacos重新整理歷史的存放,用來儲存每次拉取的配置的md5值,用於比較配置是否需要重新整理

com.alibaba.cloud.nacos.refresh.NacosContextRefresher

  1. 該類是Nacos用來管理一些內部監聽器的,主要是配置重新整理的時候可以出發回撥,並且發出spring cloud上下文的配置重新整理事件

com.alibaba.cloud.nacos.NacosPropertySourceRepository

  1. 該類是nacos用來儲存拉取到的資料的
  2. 流程:
  • 重新整理器檢查到配置更新,儲存到NacosPropertySourceRepository
  • 發起重新整理事件
  • locate執行,直接讀取NacosPropertySourceRepository

com.alibaba.nacos.client.config.NacosConfigService

  1. 該類是nacos的主要重新整理配置服務類
  2. com.alibaba.nacos.client.config.impl.ClientWorker
  • 該類是服務類裡主要的客戶端,協議是HTTP
  • clientWorker啟動的時候會初始化2個執行緒池,1個用於定時檢查配置,1個用於輔助檢查
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;
            }
        });

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;
    }
});

executor.scheduleWithFixedDelay(new Runnable() {
    @Override
    public void run() {
        try {
            checkConfigInfo();
        } catch (Throwable e) {
            LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
        }
    }
}, 1L, 10L, TimeUnit.MILLISECONDS);
  1. com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable
  • 該類用於長輪詢任務
  • com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5比對MD5之後開始重新整理配置

com.alibaba.cloud.nacos.parser

  1. 該包提供了很多檔案型別的轉換器
  2. 載入資料的時候會根據副檔名去查詢一個轉換器例項
// com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#loadNacosData
private Map<String, Object> loadNacosData(String dataId, String group,
			String fileExtension) {
	String data = null;
	try {
		data = configService.getConfig(dataId, group, timeout);
		if (StringUtils.isEmpty(data)) {
			log.warn(
					"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
					dataId, group);
			return EMPTY_MAP;
		}
		if (log.isDebugEnabled()) {
			log.debug(String.format(
					"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
					group, data));
		}
		Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
				.parseNacosData(data, fileExtension);
		return dataMap == null ? EMPTY_MAP : dataMap;
	}
	catch (NacosException e) {
		log.error("get data from Nacos error,dataId:{}, ", dataId, e);
	}
	catch (Exception e) {
		log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
	}
	return EMPTY_MAP;
}
  1. 資料會變成key value的形式,然後轉換成PropertySource

如何配置一個啟動配置類

  1. 由於配置上下文是屬於SpringCloud管理的,所以本次的注入跟以往SpringBoot不一樣
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
  1. 如何在SpringCloud和SpringBoot共享一個bean呢(舉個例子)
@Bean
public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
	if (context.getParent() != null
			&& BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
					context.getParent(), NacosConfigProperties.class).length > 0) {
		return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
				NacosConfigProperties.class);
	}
	return new NacosConfigProperties();
}

關於重新整理機制的流程

org.springframework.cloud.endpoint.event.RefreshEventListener
// 外層方法
public synchronized Set<String> refresh() {
	Set<String> keys = refreshEnvironment();
	this.scope.refreshAll();
	return keys;
}

// 
public synchronized Set<String> refreshEnvironment() {
	Map<String, Object> before = extract(
			this.context.getEnvironment().getPropertySources());
	addConfigFilesToEnvironment();
	Set<String> keys = changes(before,
			extract(this.context.getEnvironment().getPropertySources())).keySet();
	this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
	return keys;
}

  1. 該類是對RefreshEvent監聽的處理
  2. 直接定位到org.springframework.cloud.context.refresh.ContextRefresher#refreshEnvironment,這個方法是主要的重新整理配置的方法,具體做的事:
  • 歸併得到重新整理之前的配置key value
  • org.springframework.cloud.context.refresh.ContextRefresher#addConfigFilesToEnvironment 模擬一個新的SpringApplication,觸發大部分的SpringBoot啟動流程,因此也會觸發讀取配置,於是就會觸發上文所講的Locator,然後得到一個新的Spring應用,從中獲取新的聚合配置源,與舊的Spring應用配置源進行比較,並且把本次變更的配置放置到舊的去,然後把新的Spring應用關閉
  • 比較新舊配置,把配置拿出來,觸發一個事件org.springframework.cloud.context.environment.EnvironmentChangeEvent
  • 跳出該方法棧後,執行org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll
簡單分析 EnvironmentChangeEvent
  1. org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind()
  • 程式碼如下:
@ManagedOperation
public boolean rebind(String name) {
	if (!this.beans.getBeanNames().contains(name)) {
		return false;
	}
	if (this.applicationContext != null) {
		try {
			Object bean = this.applicationContext.getBean(name);
			// 獲取source物件
			if (AopUtils.isAopProxy(bean)) {
				bean = ProxyUtils.getTargetObject(bean);
			}
			if (bean != null) {
				// 重新觸發銷燬和初始化的週期方法
				this.applicationContext.getAutowireCapableBeanFactory()
						.destroyBean(bean);
			    // 因為觸發初始化生命週期,就可以觸發
			    // org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization
				this.applicationContext.getAutowireCapableBeanFactory()
						.initializeBean(bean, name);
				return true;
			}
		}
		catch (RuntimeException e) {
			this.errors.put(name, e);
			throw e;
		}
		catch (Exception e) {
			this.errors.put(name, e);
			throw new IllegalStateException("Cannot rebind to " + name, e);
		}
	}
	return false;
}
  • 該方法時接受到事件後,對一些bean進行屬性重繫結,具體哪些Bean呢?
  • org.springframework.cloud.context.properties.ConfigurationPropertiesBeans#postProcessBeforeInitialization 該方法會在Spring refresh上下文時候執行的bean生命後期裡的其中一個後置處理器,它會檢查註解 @ConfigurationProperties,這些bean就是上面第一步講的重繫結的bean
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
		throws BeansException {
	if (isRefreshScoped(beanName)) {
		return bean;
	}
	ConfigurationProperties annotation = AnnotationUtils
			.findAnnotation(bean.getClass(), ConfigurationProperties.class);
	if (annotation != null) {
		this.beans.put(beanName, bean);
	}
	else if (this.metaData != null) {
		annotation = this.metaData.findFactoryAnnotation(beanName,
				ConfigurationProperties.class);
		if (annotation != null) {
			this.beans.put(beanName, bean);
		}
	}
	return bean;
}
簡單分析org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll
@ManagedOperation(description = "Dispose of the current instance of all beans "
			+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
	super.destroy();
	this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

  1. org.springframework.cloud.context.scope.GenericScope#destroy()
  • 對BeanLifecycleWrapper例項集合進行銷燬
  • BeanLifecycleWrapper是什麼?
private static class BeanLifecycleWrapper {
    // bean的名字
	private final String name;
    // 獲取bean
	private final ObjectFactory<?> objectFactory;
    // 真正的例項
	private Object bean;
    // 銷燬函式
	private Runnable callback;
}	
  • BeanLifecycleWrapper是怎麼構造的?
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
	BeanLifecycleWrapper value = this.cache.put(name,
			new BeanLifecycleWrapper(name, objectFactory));
	this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
	try {
		return value.getBean();
	}
	catch (RuntimeException e) {
		this.errors.put(name, e);
		throw e;
	}
}
  • 以上程式碼可以追溯到Spring在建立bean的某一個分支程式碼,org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 347行程式碼
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
	throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
	Object scopedInstance = scope.get(beanName, () -> {
		beforePrototypeCreation(beanName);
		try {
			return createBean(beanName, mbd, args);
		}
		finally {
			afterPrototypeCreation(beanName);
		}
	});
	bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
  • 銷燬完之後呢?其實就是把BeanLifecycleWrapper繫結的bean變成了null,那配置怎麼重新整理呢?@RefreshScope標記的物件一開始就是被初始化為代理物件,然後在執行它的@Value的屬性的get操作的時候,會進入代理方法,代理方法裡會去獲取Target,這裡就會觸發 org.springframework.cloud.context.scope.GenericScope#get
public Object getBean() {
	if (this.bean == null) {
		synchronized (this.name) {
			if (this.bean == null) {
			    // 因為bean為空,所以會觸發一次bean的重新初始化,走了一遍生命週期流程所以配置又回來了
				this.bean = this.objectFactory.getObject();
			}
		}
	}
	return this.bean;
}

踩坑

  1. 上面的分析簡單分析到那裡,那麼在使用這種配置自動重新整理機制有什麼坑呢?
  • 使用@RefreshScople的物件,如果把配置中心的某一行屬性刪掉,那麼對應的bean對應的屬性會變為null,但是使用@ConfigaruationProperties的物件則不會,為什麼呢?因為前者是整個bean重新走了一遍生命流程,但是後者只會執行init方法
  • 不管使用@RefreshScople和@ConfigaruationProperties都不應該在destory和init方法中執行過重的邏輯,前者會影響服務的可用性,在高併發下會阻塞太多數的請求。後者會影響配置重新整理的時延性

最後

  1. 感謝閱讀完這篇文章的大佬們,如果發現文章中有什麼錯誤的話,請留言,不甚感激!

相關文章