【Nacos】配置中心获取配置的原理

2,066 阅读5分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」

一、前言

nacos 如何去拉取配置?

大胆猜想的步骤如下:

  1. 容器启动加载 nacos-client(这个发生在容器初始化 Bean 之前)
  2. nacos-client 读取本地配置(例如:服务器地址 server-addr)
  3. nacos-client 发送 HTTP 请求到 nacos-server,请求拉取配置
  4. 将配置缓存在内存和本地文件中

先了解下 nacos 的数据模型:

Nacos 数据模型 Key 由三元组唯一确定, Namespace 默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。

  • namespace: 命名空间
  • group:群
  • service / dataId:数据

数据模型,如图:

data-model.png

bootstrap.properties 文件,该文件配置了 Nacos 配置中心的地址,这个文件会让 Spring Cloud 启动 Bootstrap 阶段在 Nacos 配置中心获取配置。

Spring Cloud 的 Bootstrap 阶段的概念:

在 Bootstrap 阶段会构造 ApplicationContext,这个ApplicationContext 加载配置的过程会基于 bootstrap.properties 或 bootstrap.yaml 文件(spring.config.name 为 bootstrap)去加载文件。 在加载文件的过程中,Spring Cloud 有一套机制(PropertySourceLocator 接口的定义)来u构造数据源 PropertySource,其余跟 Spring Boot 一致的。

BootStrap 阶段构造的 ApplicationContext 会作为正常阶段的 ApplicationContext 的父类(parent),有了这一层父子关系之后,如果不能从子 ApplicationContext 获取配置,就会从父 ApplicationContext 获取。

Spring Cloud Bootstrap 阶段优先级高,会先读取配置中心的配置,这些配置在下一次正常的 ApplicationContext 启动时作用。

spring-cloud-context 模块内部的 META-INF/spring.factories 添加了一个 Bootstrap-ApplicationListener (实现了 ApplicationListener 接口),用于监听 ApplicationEnvironmentPreparedEvent 事件(Environment 刚创建,ApplicationContext 未创建时会触发事件),收到该事件后进入 Bootstrap 阶段,从配置中心加载配置。



二、源码分析

Nacos 版本:1.4.2。 代码来自 nacos 源码工程下的 example 工程。

从这个 demo 入手,可以看到分为几步:

  1. 创建配置服务和获取配置
  2. 推送配置
  3. 删除配置

样例代码如下:

public class ConfigExample {

    public static void main(String[] args) throws NacosException, InterruptedException {
        String serverAddr = "localhost:8848";
        String dataId = "my-provider";
        String group = "DEFAULT_GROUP";
        Properties properties = new Properties();
        properties.put("serverAddr", serverAddr);

        // 创建配置服务
        ConfigService configService = NacosFactory.createConfigService(properties);
        // 获取配置
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);

        // 注册事件监听
        // 监听 nacos 的事件
        configService.addListener(dataId, group, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("receive:" + configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });

        // 推送配置
        boolean isPublishOk = configService.publishConfig(dataId, group, "content");
        System.out.println(isPublishOk);
        Thread.sleep(3000);

        // 获取配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);

        // 移除配置
        boolean isRemoveOk = configService.removeConfig(dataId, group);
        System.out.println(isRemoveOk);
        Thread.sleep(3000);

        // 获取配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
        Thread.sleep(300000);
    }
}

(1)获取配置

从 demo 入手:

public class ConfigExample {

    public static void main(String[] args) throws NacosException, InterruptedException {
        String serverAddr = "localhost";
        String dataId = "my-provider";
        String group = "DEFAULT_GROUP";
        Properties properties = new Properties();
        properties.put("serverAddr", serverAddr);
        
        // 主要通过反射创建 ConfigService,实际 NacosConfigService
        ConfigService configService = NacosFactory.createConfigService(properties);
        
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println(content);
    }
}

运行后,输出: 2022-01-0511-05-15.png

跟实际配置一致: 2022-01-0511-06-39.png

1)创建 ConfigService,实际子类 NacosConfigService

  1. 通过反射创建 ConfigService,实现类 NacosConfigService
public class ConfigFactory {
    
    // 参数包括:配置中心 serverAddr 地址、dataId 和 group
    public static ConfigService createConfigService(Properties properties) throws NacosException {
    
        // 通过反射创建对象
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            // 构造器创建对象
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable e) {
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }
}
  1. 深入 ConfigService , 即 NacosConfigService

主要研究对象。

public class NacosConfigService implements ConfigService {

    ......
    
    public NacosConfigService(Properties properties) throws NacosException {
        // 1. 检查配置中的 `contextPath` 
        ValidatorUtils.checkInitParam(properties);
        
        // 2. 从配置中获取配置编码,默认 UTF-8
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE); // encode
        if (StringUtils.isBlank(encodeTmp)) {
            this.encode = Constants.ENCODE; // UTF-8
        } else {
            this.encode = encodeTmp.trim();
        }
        
        // 3. 设置命名空间
        initNamespace(properties);
        
        // 4. 创建网络连接代理,构造函数传入了ServerHttpAgent, 采用装饰器模式
        this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));

        // 5. 去拉取服务配置
        // 实际上调用 ServerHttpAgent.start() -> ServerListManager.start()
        this.agent.start();

        // 6. 客户端工作线程池取获取配置中心的配置
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }
    ......
}

2)获取服务器中配置

从这行代码入手:

String content = configService.getConfig(dataId, group, 5000);

还是看源码:NacosConfigService

public class NacosConfigService implements ConfigService {

    // ... ...

    @Override
    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
        return getConfigInner(namespace, dataId, group, timeoutMs);
    }
    
    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = null2defaultGroup(group);
        ParamUtils.checkKeyParam(dataId, group);
        ConfigResponse cr = new ConfigResponse();
        
        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);
        
        // 1. 优先使用本地配置
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        if (content != null) {
            LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
                    dataId, group, tenant, ContentUtils.truncateContent(content));
            cr.setContent(content);
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        }
        
        // 2. 从远端服务器从拉取配置
        try {
            String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
            cr.setContent(ct[0]);
            
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            
            return content;
        } catch (NacosException ioe) {
            if (NacosException.NO_RIGHT == ioe.getErrCode()) {
                throw ioe;
            }
            LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
                    agent.getName(), dataId, group, tenant, ioe.toString());
        }
        
        // 3. 从快照中获取配置
        content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
        
        // ... ...
        
        return content;
    }
    
    // ... ...
}

小结,获取配置步骤:

  1. 本地配置:对应目录例如:/home/donald/nacos/config/fixed-localhost_8848_nacos/data/config-data
  2. 远端配置
  3. 本地快照配置 快照配置对应目录例如:
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot$ ll
total 12
drwxrwxr-x 3 donald donald 4096 Jan  5 10:59 ./
drwxrwxr-x 3 donald donald 4096 Jan  5 10:59 ../
drwxrwxr-x 2 donald donald 4096 Jan  5 10:59 DEFAULT_GROUP/
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot$ cd DEFAULT_GROUP/
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot/DEFAULT_GROUP$ ll
total 12
drwxrwxr-x 2 donald donald 4096 Jan  5 10:59 ./
drwxrwxr-x 3 donald donald 4096 Jan  5 10:59 ../
-rw-rw-r-- 1 donald donald   39 Jan  5 11:22 my-provider
donald@donald-pro:~/nacos/config/fixed-localhost_8848_nacos/snapshot/DEFAULT_GROUP$ cat my-provider 
book.author=haha

(2)推送配置

这块只是封装请求,然后发送 HTTP 请求到 nacos。

略。


(3)移除配置

这块只是封装请求,然后发送 HTTP 请求到 nacos。

略。



三、Spring 装配 Nacos 配置

若使用 Alibaba Nacos 配置中心,那么 spring-cloud-alibaba-nacos-config 模块中对应的 spring.factories 会加载 key 为 org.springframework.cloud.bootstrap.BootstrapConfiguration 的 NacosConfigBootstrapConfiguration 配置类。

该配置类内部会构造 NacosPropertySourceLocator 这个 PropertySourceLocator Bean。

NacosPropertySourceLocator 的作用就是从 Nacos 配置中心获取配置,定义如下:

@Order(0)
public class NacosPropertySourceLocator implements PropertySourceLocator {
    ... ...
    public PropertySource<?> locate(Environment env) {
        this.nacosConfigProperties.setEnvironment(env);
        
        // 1. 得到 ConfigService, 用于从 Nacos 配置中心获取配置。
        ConfigService configService = this.nacosConfigManager.getConfigService();
        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");
            }

            // 2. 构造 CompositePropertySource, 
            // 后续 Nacos 配置中心加载的配置都会构造成 PropertySource
            // 并添加到该 CompositePropertySource
            CompositePropertySource composite = new CompositePropertySource("NACOS");
            
            // 加载共享配置
            this.loadSharedConfiguration(composite);
            
            // 加载扩展配置
            this.loadExtConfiguration(composite);
            
            // 加载应用配置
            this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);
            return composite;
        }
    }
    ... ...
}