简介

nacos (Dynamic Naming And Configuration Service ) 是阿里旗下的一个开源框架 ,前四个字母分别是 Naming 和 Configuration 的前两个字母 最后的 s 指 Service。 根据nacos官网描述 : Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。nacos = Eureka + Config + Bus, 本文章这次主要讲解进行Configuration的配置。

服务端启动

github下载所需要的的service版本, nacos的部署分为三种模式:

  • 单机模式 - 用于测试和单机试用。
  • 集群模式 - 用于生产环境,确保高可用。
  • 多集群模式 - 用于多数据中心场景。

这次采用单机模式, 在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:

  • 1.安装数据库,版本要求:5.6.5+
  • 2.初始化mysql数据库,数据库初始化文件:nacos-mysql.sql
  • 3.修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
#*************** 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?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root

connectTimeout、socketTimeout 、useSSL 等 可根据自身情况进行修改, 进入到bin 目录下以单机模式启动server sh startup.sh -m standalone

在浏览器键入URL, http://localhost:8848/nacos/ 用户名和密码都是nacos, 端口号是珠穆朗玛峰的高度

nacos的前台界面

Java客户端

maven依赖引入

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

配置文件

配置文件的加载的优先级bootstrap的优先级高于application


spring:
  application:
    name: nacos-config
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: properties 
        group: DEV_GROUP
        namespace: 41423500-0b3a-45de-8ee5-08b44f0733d9

server-addr: 就是 nacos的服务端地址

file-extension: 是dateId 组成的一部分, 以前只能指定为properties yaml yml三种 现在又加上json html xml格式

group: 所属的组, 取到对应配置的因素之一 默认为DEFAULT_GROUP

namespace: 命名空间, 取到对应配置的因素之一 默认为"", 前台创建完后将生成的UUID拷贝回来

profiles.active: 进行环境的区分, 比如 生产还是测试

dataId的组成: ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

dataId 优先级:

${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} 全称最高

${spring.application.name}.${spring.cloud.nacos.config.file-extension} 次之

${spring.application.name} 最低

从低到高找, 高优先级覆盖低优先级

直接去nacos service前台创建一个 dataId为: nacos-config-dev.properties的高优先级配置数据

Java代码

@RestController
@RefreshScope
public class SampleController {

    @Value("${user.name}")
    String userName;

    @Value("${user.age}")
    int age;

    @GetMapping("/getUser")
    public String getUserName() {
        return userName + " " + age;
    }
}

@RefreshScope 用来实现环境变量的动态刷新

前台数据配置

需要在命名空间为 dev 下创建一个dataId为 nacos-config-dev.properties group为DEV_GROUP, 包含 user.name 和 user.age 的配置

命名空间在这里进行创建

在dev命名空间下创建对应的数据项

这里选择的配置格式要与配置项配置的一致

创建完成后, 会在config_info 表中新增一条数据, 就是刚刚在界面上创建的数据

his_config_info表存放对配置的操作的历史记录, 进行对比

tenant_info 存放创建的namespace

config_info_beta 存放beta 发布的数据

环境启动时配置项加载流程

config主要起始类NacosConfigAutoConfiguration和NacosConfigBootstrapConfiguration

NacosConfigAutoConfiguration 创建了五个Bean, NacosConfigProperties、NacosRefreshProperties、NacosRefreshHistory、NacosConfigManager、NacosContextRefresher

NacosConfigBootstrapConfiguration主要创建Bean NacosPropertySourceLocator, NacosPropertySourceLocator 实现了 PropertySourceLocator从而实现环境变量的加载 , 主要方法为locate。

public PropertySource<?> locate(Environment env) {
		nacosConfigProperties.setEnvironment(env);
		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();
		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);
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		return composite;
	}

先获取nacos的自身配置信息,

创建NacosPropertySourceBuilder对象

组装dataId

loadSharedConfiguration 需要在nacos配置中配置shared-configs 否则空过

loadExtConfiguration 需要在nacos配置中配置 extension-configs 否则空过

loadApplicationConfiguration 主要方法, 用来获取nacos server 配置的环境变量

private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) {
		String fileExtension = properties.getFileExtension();
		String nacosGroup = properties.getGroup();
		// load directly once by default
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
				fileExtension, true);
		// load with suffix, which have a higher priority than the default
		loadNacosDataIfPresent(compositePropertySource,
				dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		// Loaded with profile, which have a higher priority than the suffix
		for (String profile : environment.getActiveProfiles()) {
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
					fileExtension, true);
		}

	}

获取文件扩展名 fileExtension 和分组 nacos Group

这里 loadNacosDataIfPresent 方法执行了三次, 传入了三次不同的dataID, 这个在上面提到过

第一次 用dataId 为 ${spring.application.name}

第二次 用dataId 为 ${spring.application.name}.${spring.cloud.nacos.config.file-extension}

第三次 用dataId 为 ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

后续调用了 com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadNacosDataIfPresent

com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadNacosPropertySource

com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#build

com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#loadNacosData

private List<PropertySource<?>> 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 Collections.emptyList();
			}
			if (log.isDebugEnabled()) {
				log.debug(String.format(
						"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
						group, data));
			}
			return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
					fileExtension);
		}
		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 Collections.emptyList();
	}

getConfig获取环境变量时会先获取本地配置, 加载保存的快照文件:

  • 第一次启动无快照文件, 会从server端dataId、group、namespace 获取到对应的配置, 拉取到后保存快照文件, 方便下一次使用, 无需每次请求server端
  • 有的话则读取快照文件
  • server端更新过配置后, 会重新修改快照文件, 更新成最新的。如果配置了@RefreshScope 注解, 则会将原先的旧bean销毁, 并在下次使用时创建个新bean, 从而保证bean中的配置为最新的, 下面会详细说到

获取完配置后进行解析配置项com.alibaba.cloud.nacos.parser.NacosDataParserHandler#parseNacosData

根据配置的 fileExtension 来进行配置项的解析, 从而将其装配到Bean的属性中

RefreshScope

先看下RefreshScope注解, 是一个定制化的Scope注解, scope的名字叫做 refresh, 代理模式为 TARGET_CLASS

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {

	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
  • DEFAULT 不使用代理
  • No 不使用代理
  • INTERFACES 使用基于接口的代理 (jdk dymaic proxy)
  • TARGET_CLASS 使用基于类的代理(cglib)

打上注解后, 会将该Bean的创建交给org.springframework.cloud.context.scope.refresh.RefreshScope 来进行创建个代理对象, 该类继承GenericScope

监听Listen的注册

nacos 客户端会根据dataId 和 group 注册一个监听com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener

定时任务启动

客户端启动时会 启动一个延迟定时任务com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable

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

每10毫秒进行config info 的检查, 如果检测到本地snapshot快照文件和缓存中版本不一致, 且MD5值发生了变化, 则从服务端重新拉取最新的配置信息, 然后触发listener, 执行listener中的 applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));

调用 org.springframework.cloud.context.refresh.ContextRefresher#refresh

public synchronized Set<String> refresh() {
		Set<String> keys = refreshEnvironment();
		this.scope.refreshAll();
		return keys;
	}

用来刷新环境变量, 和销毁bean, 在下次使用时重新创建bean 从而实现无重启, 刷新配置项

以上就是本人对于nacos configuration的一点浅显理解, 如有错误请指正!!


和光同尘,与时舒卷;戢鳞潜翼,思属风云