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

1,773 阅读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 CloudBootstrap 阶段的概念:

Bootstrap 阶段会构造 ApplicationContext,这个ApplicationContext 加载配置的过程会基于 bootstrap.propertiesbootstrap.yaml 文件(spring.config.namebootstrap)去加载文件。 在加载文件的过程中,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 会加载 keyorg.springframework.cloud.bootstrap.BootstrapConfigurationNacosConfigBootstrapConfiguration 配置类。

该配置类内部会构造 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;
        }
    }
    ... ...
}