Nacos 提供用于存储配置和其他元数据的 key/value 存储,为分布式系统中的外部化配置提供服务器端
和客户端支持。使用 Spring Cloud Alibaba Nacos Config,您可以在 Nacos Server 集中管理你 Spring
Cloud 应用的外部属性配置。
Spring Cloud Alibaba Nacos Config 是 Config Server 和 Client 的替代方案,客户端和服务器上的概念
与 Spring Environment 和 PropertySource 有着一致的抽象,在特殊的 bootstrap 阶段,配置被加载
到 Spring 环境中。当应用程序通过部署管道从开发到测试再到生产时,您可以管理这些环境之间的配
置,并确保应用程序具有迁移时需要运行的所有内容。

配置管理

可以在Nacos控制台配置项目的配置数据,先打开Nacos控制台,在 命名空间 中添加 新建命名空
间 ,如下图
image.png
在配置列表中展示在 配置管理>配置列表 中添加,如下图:
image.png
再将项目中的配置内容拷贝到如下表单中,比如我们可以把原来在代码里面的application.properties中的配置填
写到下面表单中,如下图:
image.png
注意 Data ID 和服务名字保持一致,作为程序默认加载配置。
工程中先引入依赖包

  1. <properties>
  2. <java.version>11</java.version>
  3. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  4. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  5. <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
  6. <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
  7. </properties>
  8. <dependencies>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter</artifactId>
  12. </dependency>
  13. <!--配置中心-->
  14. <dependency>
  15. <groupId>com.alibaba.cloud</groupId>
  16. <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  17. </dependency>
  18. <dependency>
  19. <groupId>com.alibaba.cloud</groupId>
  20. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>com.alibaba.cloud</groupId>
  24. <artifactId>spring-cloud-starter-dubbo</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-test</artifactId>
  29. <scope>test</scope>
  30. <exclusions>
  31. <exclusion>
  32. <groupId>org.junit.vintage</groupId>
  33. <artifactId>junit-vintage-engine</artifactId>
  34. </exclusion>
  35. </exclusions>
  36. </dependency>
  37. </dependencies>
  38. <dependencyManagement>
  39. <dependencies>
  40. <dependency>
  41. <groupId>org.springframework.boot</groupId>
  42. <artifactId>spring-boot-dependencies</artifactId>
  43. <version>${spring-boot.version}</version>
  44. <type>pom</type>
  45. <scope>import</scope>
  46. </dependency>
  47. <dependency>
  48. <groupId>com.alibaba.cloud</groupId>
  49. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  50. <version>${spring-cloud-alibaba.version}</version>
  51. <type>pom</type>
  52. <scope>import</scope>
  53. </dependency>
  54. </dependencies>
  55. </dependencyManagement>
  56. <build>
  57. <plugins>
  58. <plugin>
  59. <groupId>org.apache.maven.plugins</groupId>
  60. <artifactId>maven-compiler-plugin</artifactId>
  61. <version>3.8.1</version>
  62. <configuration>
  63. <source>11</source>
  64. <target>11</target>
  65. <encoding>UTF-8</encoding>
  66. </configuration>
  67. </plugin>
  68. <plugin>
  69. <groupId>org.springframework.boot</groupId>
  70. <artifactId>spring-boot-maven-plugin</artifactId>
  71. <version>2.3.7.RELEASE</version>
  72. <configuration>
  73. <mainClass>com.yehui.springcloudnacosdemo.SpringCloudNacosDemoApplication</mainClass>
  74. </configuration>
  75. <executions>
  76. <execution>
  77. <id>repackage</id>
  78. <goals>
  79. <goal>repackage</goal>
  80. </goals>
  81. </execution>
  82. </executions>
  83. </plugin>
  84. </plugins>
  85. </build>

配置文件中添加配置中心地址,在项目中 的 bootstrap.properties. 中添加如下配置:

  1. # nacos设置配置中心服务端地址
  2. spring.cloud.nacos.config.server-addr=localhost:8848
  3. #Nacos中命名空间的ID
  4. spring.cloud.nacos.config.namespace=6f7b53f3-c083-4230-82a2-26d4e7c85a5d
  5. # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
  6. # spring.cloud.nacos.config.namespace=
  7. spring.cloud.nacos.discovery.server-addr=localhost:8848
  8. #Nacos中命名空间的ID
  9. spring.cloud.nacos.discovery.namespace=6f7b53f3-c083-4230-82a2-26d4e7c85a5d
  10. #Nacos中配置文件的后缀为yaml
  11. spring.cloud.nacos.config.file-extension=properties
  1. @RestController
  2. public class NacosControllerTest {
  3. @Value("${name}")
  4. private String name;
  5. @RequestMapping("/index")
  6. public String index(){
  7. return name+"nacos配置中心测试";
  8. }
  9. }

启动 hailtaxi-driver 服务,默认加载 ${spring.application.name}.${fileextension:properties} 配置,加载完成后,配置数据会生效,并访问
http://localhost:8082/index 测试,效果如下:
image.png
如果此时配置文件名字如果和当前服务名字不一致,可以使用 name 属性来指定配置文件名字:

  1. # nacos设置配置中心服务端地址
  2. spring.cloud.nacos.config.server-addr=localhost:8848
  3. #Nacos中命名空间的ID
  4. spring.cloud.nacos.config.namespace=6f7b53f3-c083-4230-82a2-26d4e7c85a5d
  5. # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
  6. # spring.cloud.nacos.config.namespace=
  7. spring.cloud.nacos.discovery.server-addr=localhost:8848
  8. #Nacos中命名空间的ID
  9. spring.cloud.nacos.discovery.namespace=6f7b53f3-c083-4230-82a2-26d4e7c85a5d
  10. #Nacos中配置文件的后缀为yaml
  11. spring.cloud.nacos.config.file-extension=properties
  12. ##指定配置文件名字
  13. spring.cloud.nacos.config.name=dataIdTest.properties

Nacos 中的Namespace和Group

在nacos中提供了namespace和group命名空间和分组的机制。,它是Nacos提供的一种数据模型,也
就是我们要去定位到一个配置,需要基于namespace- > group ->dataid来实现。
namespace可以解决多环境以及多租户数据的隔离问题。比如在多套环境下,可以根据指定环境创建
不同的namespace,实现多环境隔离。或者在多租户的场景中,每个用户可以维护自己的
namespace,实现每个用户的配置数据和注册数据的隔离。

group是分组机制,它的纬度是实现服务注册信息或者DataId的分组管理机制,对于group的用法,没
有固定的规则,它也可以实现不同环境下的分组,也可以实现同一个应用下不同配置类型或者不同业务
类型的分组。

官方建议是,namespace用来区分不同环境,group可以专注在业务层面的数据分组。实际上在
使用过程中,最重要的是提前定要统一的口径和规定,避免不同的项目团队混用导致后期维护混
乱的问题。

自定义namespace

在没有明确指定 ${spring.cloud.nacos.config.namespace} 配置的情况下, 默认使用的是 Nacos
上 Public 这个namespae。如果需要使用自定义的命名空间,可以通过以下配置来实现:

  1. spring.cloud.nacos.config.namespace=b3404bc0-d7dc-4855-b519-570ed34b62d7

该配置必须放在 bootstrap.properties 文件中。此外
spring.cloud.nacos.config.namespace 的值是 namespace 对应的 id,id 值可以在 Nacos
的控制台获取。并且在添加配置时注意不要选择其他的 namespae,否则将会导致读取不到正确
的配置。

自定义group

在没有明确指定 ${spring.cloud.nacos.config.group} 配置的情况下, 默认使用的是DEFAULT_GROUP 。如果需要自定义自己的 Group,可以通过以下配置来实现:

  1. spring.cloud.nacos.config.group=DEVELOP_GROUP

该配置必须放在 bootstrap.properties 文件中。并且在添加配置时 Group 的值一定要和
spring.cloud.nacos.config.group 的配置值一致

自定义扩展的DataId

Spring Cloud Alibaba Nacos Config 从 0.2.1 版本后,可支持自定义 Data Id 的配置。关于这部分详细
的设计可参考 这里。 一个完整的配置案例如下所示:

  1. spring.application.name=opensource-service-provider
  2. spring.cloud.nacos.config.server-addr=127.0.0.1:8848
  3. # config external configuration
  4. # 1、Data Id 在默认的组 DEFAULT_GROUP,不支持配置的动态刷新
  5. spring.cloud.nacos.config.extension-configs[0].data-id=ext-configcommon01.properties
  6. # 2、Data Id 不在默认的组,不支持动态刷新
  7. spring.cloud.nacos.config.extension-configs[1].data-id=ext-configcommon02.properties
  8. spring.cloud.nacos.config.extension-configs[1].group=GLOBALE_GROUP
  9. # 3、Data Id 既不在默认的组,也支持动态刷新
  10. spring.cloud.nacos.config.extension-configs[2].data-id=ext-configcommon03.properties
  11. spring.cloud.nacos.config.extension-configs[2].group=REFRESH_GROUP
  12. spring.cloud.nacos.config.extension-configs[2].refresh=true

可以看到:

  1. 通过 spring.cloud.nacos.config.extension-configs[n].data-id 的配置方式来支持多个Data Id 的配置。
  2. 通过 spring.cloud.nacos.config.extension-configs[n].group 的配置方式自定义 Data Id所在的组,不明确配置的话,默认是 DEFAULT_GROUP。
  3. 通过 spring.cloud.nacos.config.extension-configs[n].refresh 的配置方式来控制该Data Id 在配置变更时,是否支持应用中可动态刷新, 感知到最新的配置值。默认是不支持的。

多个 Data Id 同时配置时,他的优先级关系是 spring.cloud.nacos.config.extensionconfigs[n].data-id 其中 n 的值越大,优先级越高。
Note spring.cloud.nacos.config.extension-configs[n].data-id 的值必须带文件扩展名,文件扩展名既可支持 properties,又可以支持 yaml/yml。 此时spring.cloud.nacos.config.file-extension 的配置对自定义扩展配置的 Data Id 文件扩展名没有影响。

通过自定义扩展的 Data Id 配置,既可以解决多个应用间配置共享的问题,又可以支持一个应用有多个
配置文件。
为了更加清晰的在多个应用间配置共享的 Data Id ,你可以通过以下的方式来配置:
通过自定义扩展的 Data Id 配置,既可以解决多个应用间配置共享的问题,又可以支持一个应用有多个
配置文件。
为了更加清晰的在多个应用间配置共享的 Data Id ,你可以通过以下的方式来配置:

  1. # 配置支持共享的 Data Id
  2. spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml
  3. # 配置 Data Id 所在分组,缺省默认 DEFAULT_GROUP
  4. spring.cloud.nacos.config.shared-configs[0].group=GROUP_APP1
  5. # 配置Data Id 在配置变更时,是否动态刷新,缺省默认 false
  6. spring.cloud.nacos.config.shared-configs[0].refresh=true

可以看到:

  1. 通过 spring.cloud.nacos.config.shared-configs[n].data-id 来支持多个共享 Data Id 的配置。
  2. 通过 spring.cloud.nacos.config.shared-configs[n].group 来配置自定义 Data Id 所在的

组,不明确配置的话,默认是 DEFAULT_GROUP。

  1. 通过 spring.cloud.nacos.config.shared-configs[n].refresh 来控制该Data Id在配置变更

时,是否支持应用中动态刷新,默认false。

配置的优先级

Spring Cloud Alibaba Nacos Config 目前提供了三种配置能力从 Nacos 拉取相关的配置。

  1. A: 通过 spring.cloud.nacos.config.shared-configs[n].data-id 支持多个共享 Data Id 的配置
  2. B: 通过 spring.cloud.nacos.config.extension-configs[n].data-id 的方式支持多个扩展Data Id 的配置
  3. C: 通过内部相关规则(应用名、应用名+ Profile )自动生成相关的 Data Id 配置

当三种方式共同使用时,他们的一个优先级关系是:A < B < C

多环境切换

spring-cloud-starter-alibaba-nacos-config 在加载配置的时候,不仅仅加载了以 dataid 为
${spring.application.name}.${file-extension:properties} 为前缀的基础配置,还加载了dataid为 ${spring.application.name}-${profile}.${file-extension:properties} 的基础配置。在日常开发中如果遇到多套环境下的不同配置,可以通过Spring 提供的${spring.profiles.active} 这个配置项来配置。
比如开发环境我们可以在nacos中创建 spring-cloud-nacos-demo-dev.properties ,测试环境可以在配置中创建
spring-cloud-nacos-demo-test.properties ,创建如下:
spring-cloud-nacos-demo-test.properties
image.png
spring-cloud-nacos-demo-dev.properties
image.png
修改bootstrap.properties配置文件,如下

  1. # nacos设置配置中心服务端地址
  2. spring.cloud.nacos.config.server-addr=localhost:8848
  3. #Nacos中命名空间的ID
  4. spring.cloud.nacos.config.namespace=6f7b53f3-c083-4230-82a2-26d4e7c85a5d
  5. # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
  6. # spring.cloud.nacos.config.namespace=
  7. spring.cloud.nacos.discovery.server-addr=localhost:8848
  8. #Nacos中命名空间的ID
  9. spring.cloud.nacos.discovery.namespace=6f7b53f3-c083-4230-82a2-26d4e7c85a5d
  10. #Nacos中配置文件的后缀为yaml
  11. spring.cloud.nacos.config.file-extension=properties
  12. #Nacos激活配置 对应spring-cloud-nacos-demo-dev.properties
  13. spring.profiles.active=dev
  14. server.port=8082

测试代码

  1. @RestController
  2. public class NacosControllerTest {
  3. @Value("${ip}")
  4. private String ip;
  5. @RequestMapping("/index")
  6. public String index(){
  7. return ip+"nacos配置中心测试";
  8. }
  9. }

测试http://localhost:8082/index 效果如下:
image.png
将 active 换成test,效果如下:
image.png

多/共享配置

在实际的业务场景中应用和共享配置间的关系可能, Spring Cloud Alibaba Nacos Config 从 0.2.1
版本后,可支持自定义 Data Id 的配置,通过它可以解决配置共享问题。
我们可以先创建一个配置 datasource.properties用于模拟配置数据库连接,如下图:
image.png

在 bootstrap.properties中引入配置需要使用 extension-configs 属性,配置如下:
image.png
这里 extension-configs[n] 中n值越大,优先级越高,它既能解决一个应用多个配置,同时还能解决
配置共享问题。

测试代码

  1. @RestController
  2. public class NacosControllerTest {
  3. @Value("${ip}")
  4. private String ip;
  5. @RequestMapping("/index")
  6. public String index(){
  7. return ip+"nacos配置中心测试";
  8. }
  9. }

测试http://localhost:8082/index 效果如下:
image.png

配置中心刷新

配置自动刷新对程序来说非常重要,Nacos支持配置自动刷新,并且提供了多种刷新机制。

Environment自动刷新

spring-cloud-starter-alibaba-nacos-config 支持配置的动态更新,Environment能实时更新到最
新的配置信息,启动 Spring Boot 应用测试的代码如下:

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. public class SpringCloudNacosDemoApplication {
  4. public static void main(String[] args) throws InterruptedException {
  5. ApplicationContext applicationContext = SpringApplication.run(SpringCloudNacosDemoApplication.class, args);
  6. while (true){
  7. //当动态刷新时,会更新到Environment中,因此这里每隔5s中从Enviroment中获取配置
  8. String ip = applicationContext.getEnvironment().getProperty("ip");
  9. System.out.println("ip:"+ip);
  10. TimeUnit.SECONDS.sleep(5);
  11. }
  12. }
  13. }

测试数据如下:

  1. ip:192.168.211.112
  2. ip:192.168.211.113

注意
image.png

@Value刷新

程序中如果写了 @Value 注解,可以采用 @RefreshScope 实现刷新,只需要在指定类上添加该注解即
可,如下代码:

  1. @RestController
  2. @RefreshScope
  3. public class NacosControllerTest {
  4. @Value("${ip}")
  5. private String ip;
  6. @RequestMapping("/index")
  7. public String index(){
  8. return ip+"nacos配置中心测试";
  9. }
  10. }

灰度发布

灰度配置指的是指定部分客户端IP进行新配置的下发,其余客户端配置保持不变,用以验证新配置对客
户端的影响,保证配置的平稳发布。灰度配置是生产环境中一个比较重要的功能,对于保证生产环境的
稳定性非常重要。在1.1.0中,Nacos支持了以IP为粒度的灰度配置,具体使用步骤如下:
在配置列表页面,点击某个配置的“编辑配置”按钮,勾选“Beta发布”,在文本框里填入要下发配置配置的
IP,多个IP用逗号分隔,操作如下:
image.png
修改配置内容,点击“发布Beta”按钮,即可完成灰度配置的发布,点击“发布Beta”后,“发布Beta”按钮
变灰,此时可以选择“停止Beta”或者“发布”。“停止Beta”表示取消停止灰度发布,当前灰度发布配置的IP
列表和配置内容都会删除,页面回到正常发布的样式。“发布”表示将灰度配置在所有客户端生效,之前
的配置也会被覆盖,同时页面回到正常发布的样式:
image.png

源码分析

源码入口

在jar下面spring-cloud-starter-alibaba-nacos-config-2.2.6.RELEASE.jar下的spring.factories文件下面找到自动配置类
image.png

NacosConfigBootstrapConfiguration

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
  3. public class NacosConfigBootstrapConfiguration {
  4. @Bean
  5. @ConditionalOnMissingBean
  6. public NacosConfigProperties nacosConfigProperties() {
  7. return new NacosConfigProperties();
  8. }
  9. @Bean
  10. @ConditionalOnMissingBean
  11. public NacosConfigManager nacosConfigManager(
  12. NacosConfigProperties nacosConfigProperties) {
  13. return new NacosConfigManager(nacosConfigProperties);
  14. }
  15. @Bean
  16. public NacosPropertySourceLocator nacosPropertySourceLocator(
  17. NacosConfigManager nacosConfigManager) {
  18. return new NacosPropertySourceLocator(nacosConfigManager);
  19. }
  20. }

springcloud 整合nacos文件加载流程

NacosPropertySourceLocator

  1. public class NacosPropertySourceLocator implements PropertySourceLocator {}

有代码可以看到NacosPropertySourceLocator实现了PropertySourceLocator接口,PropertySourceLocator接口的代码如下:
image.png
在看一下类的关系图
image.png
此时要关注一下NacosPropertySourceLocator在哪里进行初始化的,在NacosPropertySourceLocator.locate方法入口进行的debug,启动spring boot 观察一下调用链路

image.png

SpringApplication.run

由调用链路可以知道,在spring boot项目启动时,有一个prepareContext的方法,它会回调所有实现了
ApplicationContextInitializer的实例,来做一些初始化工作。

ApplicationContextInitializer是Spring?框架原有的东西,它的主要作用就是在,ConfigurableApplicationContext类型(或者子类型)的ApplicationContext做refresh之前,允许我们对ConfiurableApplicationContext的实例做进一步的设置和处理。 它可以用在需要对应用程序上下文进行编程初始化的wb应用程序中,比如根据上下文环境来注册propertySource,或者配置文件。而Config的这个配置中心的需求合好需要这样一个机制来完成。

  1. public ConfigurableApplicationContext run(String... args) {
  2. //省略代码...
  3. this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
  4. //省略代码
  5. }

PropertySourceBootstrapConfiguration.initialize

其中,PropertySourceBootstrapConfiguration就实现了ApplicationContextInitializer,initialize方法代a码如下。

  1. public void initialize(ConfigurableApplicationContext applicationContext) {
  2. List<PropertySource<?>> composite = new ArrayList();
  3. //对propertySourceLocators数组进行排序,根据默认的AnnotationAwareOrderComparator
  4. AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
  5. boolean empty = true;
  6. //获取运行的环境上下文
  7. ConfigurableEnvironment environment = applicationContext.getEnvironment();
  8. Iterator var5 = this.propertySourceLocators.iterator();
  9. while(true) {
  10. Collection source;
  11. do {
  12. do {
  13. if (!var5.hasNext()) {
  14. if (!empty) {
  15. //获取当前Environment中的所有PropertySources.
  16. MutablePropertySources propertySources = environment.getPropertySources();
  17. String logConfig = environment.resolvePlaceholders("${logging.config:}");
  18. LogFile logFile = LogFile.get(environment);
  19. // 遍历移除bootstrapProperty的相关属性
  20. Iterator var15 = environment.getPropertySources().iterator();
  21. while(var15.hasNext()) {
  22. PropertySource<?> p = (PropertySource)var15.next();
  23. if (p.getName().startsWith("bootstrapProperties")) {
  24. propertySources.remove(p.getName());
  25. }
  26. }
  27. //把前面获取到的PropertySource,插入到Environment中的PropertySources中。
  28. this.insertPropertySources(propertySources, composite);
  29. this.reinitializeLoggingSystem(environment, logConfig, logFile);
  30. this.setLogLevels(applicationContext, environment);
  31. this.handleIncludedProfiles(environment);
  32. }
  33. return;
  34. }
  35. //回调所有实现PropertySourceLocator接口实例的locate方法,并收集到source这个集合中。
  36. PropertySourceLocator locator = (PropertySourceLocator)var5.next();
  37. source = locator.locateCollection(environment);
  38. } while(source == null);
  39. } while(source.size() == 0);
  40. //遍历source,把PropertySource包装成BootstrapPropertySource加入到sourceList中。
  41. List<PropertySource<?>> sourceList = new ArrayList();
  42. Iterator var9 = source.iterator();
  43. while(var9.hasNext()) {
  44. PropertySource<?> p = (PropertySource)var9.next();
  45. if (p instanceof EnumerablePropertySource) {
  46. EnumerablePropertySource<?> enumerable = (EnumerablePropertySource)p;
  47. sourceList.add(new BootstrapPropertySource(enumerable));
  48. } else {
  49. sourceList.add(new SimpleBootstrapPropertySource(p));
  50. }
  51. }
  52. logger.info("Located property source: " + sourceList);
  53. //将source添加到数组
  54. composite.addAll(sourceList);
  55. //表示propertysource不为空
  56. empty = false;
  57. }
  58. }

上述代码逻辑说明如下。

  1. 首先this.propertySourceLocators,表示所有实现了PropertySourceLocators接口的实现类,其中就包括我们前面自定义的GpJsonPropertySourceLocator。
  2. 根据默认的 AnnotationAwareOrderComparator 排序规则对 propertySourceLocators数组进行排序。
  3. 获取运行的环境上下文ConfigurableEnvironment
  4. 遍历propertySourceLocators时
    1. 调用 locate 方法,传入获取的上下文environment
    2. 将source添加到PropertySource的链表中
    3. 设置source是否为空的标识标量empty
  5. source不为空的情况,才会设置到environment中
    1. 返回Environment的可变形式,可进行的操作如addFirst、addLast
    2. 移除propertySources中的bootstrapProperties
    3. 根据config server覆写的规则,设置propertySources
    4. 处理多个active profiles的配置信息

注意:this.propertySourceLocatorsi这个集合中的PropertySourceLocator,是通过自动装配机制完成注入的,具体的实现在BootstrapImportSelector这个类中。

NacosPropertySourceLocator

理解了上述基本原理后,再来思考第二个问题。如何从远程服务器上加载配置到Spring的Environment中。 顺着前面的分析思路,很自然的去找PropertySourceLocator的实现类,发现除了 自定义的GpJsonPropertySourceLocator以外,还有另外一个实现类 NacosPropertySourceLocator . 于是,直接来看NacosPropertySourceLocator中的locate方法,代码如下。

  1. public PropertySource<?> locate(Environment env) {
  2. nacosConfigProperties.setEnvironment(env);
  3. //nacosConfigManager是NacosConfigBootstrapConfiguration配置类里面实例化
  4. ConfigService configService = nacosConfigManager.getConfigService();
  5. if (null == configService) {
  6. log.warn("no instance of config service found, can't load config from nacos");
  7. return null;
  8. }
  9. //获取客户端配置的超时时间
  10. long timeout = nacosConfigProperties.getTimeout();
  11. nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
  12. timeout);
  13. //获取name属性,
  14. String name = nacosConfigProperties.getName();
  15. //在Spring Cloud中,默认的name=spring.application.name。
  16. String dataIdPrefix = nacosConfigProperties.getPrefix();
  17. if (StringUtils.isEmpty(dataIdPrefix)) {
  18. dataIdPrefix = name;
  19. }
  20. if (StringUtils.isEmpty(dataIdPrefix)) {
  21. //获取spring.application.name赋值给dataIdPrefix
  22. dataIdPrefix = env.getProperty("spring.application.name");
  23. }
  24. //创建composite属性源,可以包含多个PropertySource
  25. CompositePropertySource composite = new CompositePropertySource(
  26. NACOS_PROPERTY_SOURCE_NAME);
  27. //加载共享配置
  28. loadSharedConfiguration(composite);
  29. //加载扩展配置
  30. loadExtConfiguration(composite);
  31. //加载自身的配置
  32. loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
  33. return composite;
  34. }

上述代码的实现不难理解

  1. 获取nacos客户端的配置属性,并生成dataId(这个很重要,要定位nacos的配置)
  2. 分别调用三个方法从加载配置属性源,保存到composite组合属性源中

loadApplicationConfiguration

可以先不管加载共享配置、扩展配置的方法,最终本质上都是去远程服务上读取配置,只是传入的参数不一样。

  • fileExtension,表示配置文件的扩展名
  • nacosGroup表示分组。
  • 加载dataid=项目名称的配置。
  • 加载dataid=项目名称+扩展名的配置遍历当前配置的激活点(profile),分别循环加载带有profile的dataid配置

    1. private void loadApplicationConfiguration(
    2. CompositePropertySource compositePropertySource, String dataIdPrefix,
    3. NacosConfigProperties properties, Environment environment) {
    4. //默认的扩展名为: properties
    5. String fileExtension = properties.getFileExtension();
    6. //获取group
    7. String nacosGroup = properties.getGroup();
    8. //加载`dataid=项目名称`的配置
    9. loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
    10. fileExtension, true);
    11. //加载`dataid=项目名称+扩展名`的配置
    12. loadNacosDataIfPresent(compositePropertySource,
    13. dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
    14. // 遍历profile(可以有多个),根据profile加载配置
    15. for (String profile : environment.getActiveProfiles()) {
    16. String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
    17. loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
    18. fileExtension, true);
    19. }
    20. }

    loadNacosDataIfPresent

    调用loadNacosPropertySource加载存在的配置信息。把加载之后的配置属性保存CompositePropertySourcer中。

    1. private void loadNacosDataIfPresent(final CompositePropertySource composite,
    2. final String dataId, final String group, String fileExtension,
    3. boolean isRefreshable) {
    4. //如果dataId为空,或者group为空,则直接跳过
    5. if (null == dataId || dataId.trim().length() < 1) {
    6. return;
    7. }
    8. if (null == group || group.trim().length() < 1) {
    9. return;
    10. }
    11. //从nacos中获取属性源
    12. NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
    13. fileExtension, isRefreshable);
    14. //把属性源保存到compositePropertySource中
    15. this.addFirstPropertySource(composite, propertySource, false);
    16. }

    loadNacosPropertySource

    1. private NacosPropertySource loadNacosPropertySource(final String dataId,
    2. final String group, String fileExtension, boolean isRefreshable) {
    3. if (NacosContextRefresher.getRefreshCount() != 0) {
    4. //是否支持自动刷新,// 如果不支持自动刷新配置则自动从缓存获取返回(不从远程服务器加载)
    5. if (!isRefreshable) {
    6. return NacosPropertySourceRepository.getNacosPropertySource(dataId,
    7. group);
    8. }
    9. }
    10. //构造器从配置中心获取数据
    11. return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
    12. isRefreshable);
    13. }

    上述代码,是Spring Cloud 集成Nacos实现远程配置加载并保存到NacosPropertySource 中的。 其中,会根据NacosContextRefresher.getRefreshCount来判断是否要从本地读取配置。

  1. NacosContextRefresher.getRefreshCount() 这个表示Nacos上下文中设置的动态刷 新的监听数量,如果大于0,说明有配置发生了变更,则需要进一步判断是否需要获取最 新配置项
  2. isRefreshable , 这个是对应properties文件中的 spring.cloud.nacos.config.refresh-enabled=false这个属性。如果设置为 false,表示不会从远程服务去获取最新的值。

    动态刷新到本地

    NacosContextRefresher

    在Spring Cloud Nacos中,设置了一个本地缓存 NacosPropertySourceRepository,它会缓存所有配置项对应的NacosPropertySource实 例。那么这个缓存是在哪里更新的呢? 在com.alibaba.cloud.nacos.refresh这个包中,有一个NacosContextRefresher 上下 文刷新对象,它在启动时,会监听ApplicationReadyEvent事件

    1. public class NacosContextRefresher
    2. implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {

    NacosContextRefresher.onApplicationEvent

    1. public void onApplicationEvent(ApplicationReadyEvent event) {
    2. // many Spring context
    3. if (this.ready.compareAndSet(false, true)) {//cas占位
    4. this.registerNacosListenersForApplications(); //针对应用列表建立Nacos监听
    5. }
    6. }

    registerNacosListenersForApplications

    1. private void registerNacosListenersForApplications() {
    2. if (isRefreshEnabled()) {//开启自动配置
    3. for (NacosPropertySource propertySource : NacosPropertySourceRepository
    4. .getAll()) {
    5. if (!propertySource.isRefreshable()) {
    6. continue;
    7. }
    8. String dataId = propertySource.getDataId();
    9. registerNacosListener(propertySource.getGroup(), dataId);
    10. }
    11. }
    12. }

    �该方法是注册应用监听的具体实现。

  3. 如果当前的自动刷新机制是开启状态,则遍历本地缓存中的所有NacosPropertySource

  4. 针对每个PropertySource,判断是否开启了自动刷新,如果没有则退出
  5. 否则,针对该dataId注册监听

registerNacosListener

  1. private void registerNacosListener(final String groupKey, final String dataKey) {
  2. String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
  3. Listener listener = listenerMap.computeIfAbsent(key,
  4. lst -> new AbstractSharedListener() {
  5. @Override
  6. public void innerReceive(String dataId, String group,
  7. String configInfo) {//针对该key绑定监听事件
  8. refreshCountIncrement(); //递增刷新数量的原子变量(表示当前有数据变更)
  9. nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); //添加
  10. 到刷新的历史记录中,提供了EndPoint访问
  11. //发布一个刷新事件,用来同步@Value注解的值
  12. applicationContext.publishEvent(
  13. new RefreshEvent(this, null, "Refresh Nacos config"));
  14. if (log.isDebugEnabled()) {
  15. log.debug(String.format(
  16. "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
  17. group, dataId, configInfo));
  18. }
  19. }
  20. });
  21. try {
  22. //注册事件监听
  23. configService.addListener(dataKey, groupKey, listener);
  24. }
  25. catch (NacosException e) {
  26. log.warn(String.format(
  27. "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
  28. groupKey), e);
  29. }
  30. }

RefreshEventListener

当收到Nacos数据变更事件后,在spring-cloud-context这个包下的 RefreshEventListener就会收到RefreshEvent事件。 这个事件不用猜测也能知道,一定是要修改Environment中的PropertySource属性值,以及 刷新@Value注解的值

  1. public class RefreshEventListener implements SmartApplicationListener {
  2. public void onApplicationEvent(ApplicationEvent event) {
  3. if (event instanceof ApplicationReadyEvent) {
  4. this.handle((ApplicationReadyEvent)event);
  5. } else if (event instanceof RefreshEvent) {
  6. this.handle((RefreshEvent)event);
  7. }
  8. }
  9. }

开始处理RefreshEvent事件

  1. public void handle(RefreshEvent event) {
  2. if (this.ready.get()) {
  3. log.debug("Event received " + event.getEventDesc());
  4. Set<String> keys = this.refresh.refresh();
  5. log.info("Refresh keys changed: " + keys);
  6. }
  7. }

ContextRefresher.refresh

ContextRefresher刷新方通过构建上下文context获取最新的环境配置和处理@RefreshScope

  1. 这个方法需要做两个事情
    1. 刷新Environment中的上下文属性配置。
    2. 刷新bean中属性对应的@Value值
  1. public synchronized Set<String> refresh() {
  2. // 1.构建一个上下文Context刷新Environment配置数据
  3. Set<String> keys = this.refreshEnvironment();
  4. // 2.调用RefreshScope 处理注解@RefreshScope
  5. this.scope.refreshAll();
  6. return keys;
  7. }

Context刷新Environment配置数据

SpringBoot项目启动的时候会创建一个上下文Context,同时NacosConfig的配置也会从远程服务读取加载到本地。
所以这里模拟了SpringBoot的启动,通过SpringApplicationBuilder构建一个上下文帮助获取最新的配置
执行链:refreshEnvironment()->this.addConfigFilesToEnvironment()

addConfigFilesToEnvironment

  1. ConfigurableApplicationContext addConfigFilesToEnvironment() {
  2. // 创建一个新的上下文
  3. ConfigurableApplicationContext capture = null;
  4. boolean var15 = false;
  5. try {
  6. var15 = true;
  7. // 拷贝现有的Environment用来构建新的上下文
  8. StandardEnvironment environment = this.copyEnvironment(this.context.getEnvironment());
  9. // 上下文构建器
  10. SpringApplicationBuilder builder = (new SpringApplicationBuilder(new Class[]{ContextRefresher.Empty.class})).bannerMode(Mode.OFF).web(WebApplicationType.NONE).environment(environment);
  11. // 添加启动监听器
  12. builder.application().setListeners(Arrays.asList(new BootstrapApplicationListener(), new ConfigFileApplicationListener()));
  13. // 启动上下文
  14. capture = builder.run(new String[0]);
  15. if (environment.getPropertySources().contains("refreshArgs")) {
  16. environment.getPropertySources().remove("refreshArgs");
  17. }
  18. // 获取最新的配置信息
  19. MutablePropertySources target = this.context.getEnvironment().getPropertySources();
  20. } finally {
  21. // 最终关闭临时的上下文
  22. if (var15) {
  23. for(ConfigurableApplicationContext closeable = capture; closeable != null; closeable = (ConfigurableApplicationContext)closeable.getParent()) {
  24. try {
  25. closeable.close();
  26. } catch (Exception var16) {
  27. }
  28. if (!(closeable.getParent() instanceof ConfigurableApplicationContext)) {
  29. break;
  30. }
  31. }
  32. }
  33. }
  34. for(ConfigurableApplicationContext closeable = capture; closeable != null; closeable = (ConfigurableApplicationContext)closeable.getParent()) {
  35. try {
  36. closeable.close();
  37. } catch (Exception var17) {
  38. }
  39. if (!(closeable.getParent() instanceof ConfigurableApplicationContext)) {
  40. break;
  41. }
  42. }
  43. return capture;
  44. }

通过新的上下文capture获取Environment再合并到现有的Environment,并关闭capture。

ConfigurationPropertiesRebinder

EnvironmentChangeEvent事件监听的实现,在ConfigurationPropertiesRebinder中,代码如下

  1. public class ConfigurationPropertiesRebinder implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
  2. public void onApplicationEvent(EnvironmentChangeEvent event) {
  3. if (this.applicationContext.equals(event.getSource()) || event.getKeys().equals(event.getSource())) {
  4. this.rebind();
  5. }
  6. }
  7. }

rebind

遍历所有的属性类,重新绑定所有的配置属性对象的属性值

  1. public void rebind() {
  2. this.errors.clear();
  3. Iterator var1 = this.beans.getBeanNames().iterator();
  4. while(var1.hasNext()) {
  5. String name = (String)var1.next();
  6. this.rebind(name);
  7. }
  8. }

rebind

可以看到它的处理逻辑,就是把其内部存储的 ConfigurationPropertiesBeans 依次执行销毁逻辑,再执行初始化逻辑实现属性的重新绑定。

  1. public boolean rebind(String name) {
  2. if (!this.beans.getBeanNames().contains(name)) {
  3. return false;
  4. } else {
  5. if (this.applicationContext != null) {
  6. try {
  7. Object bean = this.applicationContext.getBean(name);
  8. if (AopUtils.isAopProxy(bean)) {
  9. bean = ProxyUtils.getTargetObject(bean);
  10. }
  11. if (bean != null) {
  12. if (this.getNeverRefreshable().contains(bean.getClass().getName())) {
  13. return false;
  14. }
  15. this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);//执行销毁
  16. this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
  17. //初始化bean,重新绑定新的属性
  18. return true;
  19. }
  20. } catch (RuntimeException var3) {
  21. this.errors.put(name, var3);
  22. throw var3;
  23. } catch (Exception var4) {
  24. this.errors.put(name, var4);
  25. throw new IllegalStateException("Cannot rebind to " + name, var4);
  26. }
  27. }
  28. return false;
  29. }
  30. }

RefreshAll

  1. public void refreshAll() {
  2. super.destroy();
  3. this.context.publishEvent(new RefreshScopeRefreshedEvent());
  4. }

销毁所有@RefreshScope注解修饰的bean对象。

  1. public void destroy() {
  2. List<Throwable> errors = new ArrayList();
  3. Collection<GenericScope.BeanLifecycleWrapper> wrappers = this.cache.clear();
  4. Iterator var3 = wrappers.iterator();
  5. while(var3.hasNext()) {
  6. GenericScope.BeanLifecycleWrapper wrapper = (GenericScope.BeanLifecycleWrapper)var3.next();
  7. try {
  8. Lock lock = ((ReadWriteLock)this.locks.get(wrapper.getName())).writeLock();
  9. lock.lock();
  10. try {
  11. wrapper.destroy();
  12. } finally {
  13. lock.unlock();
  14. }
  15. } catch (RuntimeException var10) {
  16. errors.add(var10);
  17. }
  18. }
  19. if (!errors.isEmpty()) {
  20. throw wrapIfNecessary((Throwable)errors.get(0));
  21. } else {
  22. this.errors.clear();
  23. }
  24. }

NacosPropertySourceBuilder.build

  1. NacosPropertySource build(String dataId, String group, String fileExtension,
  2. boolean isRefreshable) {
  3. //调用loadNacosData加载远程数据
  4. List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
  5. fileExtension);
  6. //构造NacosPropertySource(这个是Nacos自定义扩展的PropertySource,和前面演示的自定义
  7. PropertySource类似)。
  8. // 相当于把从远程服务器获取的数据保存到NacosPropertySource中。
  9. NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
  10. group, dataId, new Date(), isRefreshable);
  11. //把属性缓存到本地缓存
  12. NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
  13. return nacosPropertySource;
  14. }

NacosPropertySourceBuilder.loadNacosData

这个方法,就是连接远程服务器去获取配置数据的实现,关键代码是 configService.getConfig

  1. private List<PropertySource<?>> loadNacosData(String dataId, String group,
  2. String fileExtension) {
  3. String data = null;
  4. try {
  5. data = configService.getConfig(dataId, group, timeout);
  6. if (StringUtils.isEmpty(data)) {
  7. log.warn(
  8. "Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
  9. dataId, group);
  10. return Collections.emptyList();
  11. }
  12. if (log.isDebugEnabled()) {
  13. log.debug(String.format(
  14. "Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
  15. group, data));
  16. }
  17. //对加载的数据进行解析,保存到List<PropertySource>集合。
  18. return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
  19. fileExtension);
  20. }
  21. catch (NacosException e) {
  22. log.error("get data from Nacos error,dataId:{} ", dataId, e);
  23. }
  24. catch (Exception e) {
  25. log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
  26. }
  27. return Collections.emptyList();
  28. }

阶段性总结

通过上述分析,知道了Spring Cloud集成Nacos时的关键路径,并且知道在启动时, Spring Cloud会从Nacos Server中加载动态数据保存到Environment集合。 从而实现动态配置的自动注入。

Nacos客户端的数据的加载流程

配置数据的最终加载,是基于configService.getConfig,Nacos提供的SDK来实现的。

  1. public String getConfig(String dataId, String group, long timeoutMs) throws
  2. NacosException

关于Nacos SDK的使用教程: https://nacos.io/zh-cn/docs/sdk.html

也就是说,接下来我们的源码分析,直接进入到Nacos这个范畴。

NacosConfigService.getConfig

  1. public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
  2. return getConfigInner(namespace, dataId, group, timeoutMs);
  3. }

NacosConfigService.getConfigInner

  1. private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
  2. //获取group,如果为空,则为default-group
  3. group = this.blank2defaultGroup(group);
  4. //校验参数
  5. ParamUtils.checkKeyParam(dataId, group);
  6. //设置响应结果
  7. ConfigResponse cr = new ConfigResponse();
  8. cr.setDataId(dataId);
  9. cr.setTenant(tenant);
  10. cr.setGroup(group);
  11. //使用本地缓存
  12. String content = LocalConfigInfoProcessor.getFailover(this.agent.getName(), dataId, group, tenant);
  13. //如果本地缓存中的内容不为空
  14. String encryptedDataKey;
  15. if (content != null) {
  16. LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", new Object[]{this.agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content)});
  17. cr.setContent(content); //把内容设置到cr中。
  18. //获取容灾配置的encryptedDataKey
  19. encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeyFailover(this.agent.getName(), dataId, group, tenant);
  20. cr.setEncryptedDataKey(encryptedDataKey); //保存到cr
  21. this.configFilterChainManager.doFilter((IConfigRequest)null, cr); //执行过滤(目前好像没有实现)
  22. content = cr.getContent();//返回文件content
  23. return content;
  24. } else {
  25. try {
  26. //如果本地文件中不存在相关内容,则发起远程调用
  27. ConfigResponse response = this.worker.getServerConfig(dataId, group, tenant, timeoutMs);
  28. //把响应内容返回
  29. cr.setContent(response.getContent());
  30. cr.setEncryptedDataKey(response.getEncryptedDataKey());
  31. this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
  32. content = cr.getContent();
  33. return content;
  34. } catch (NacosException var9) {
  35. if (403 == var9.getErrCode()) {
  36. throw var9;
  37. } else {
  38. //如果出现NacosException,且不是403异常,则尝试通过本地的快照文件去获取配置进行返回。
  39. LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}", new Object[]{this.agent.getName(), dataId, group, tenant, var9.toString()});
  40. LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", new Object[]{this.agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content)});
  41. content = LocalConfigInfoProcessor.getSnapshot(this.agent.getName(), dataId, group, tenant);
  42. cr.setContent(content);
  43. encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeyFailover(this.agent.getName(), dataId, group, tenant);
  44. cr.setEncryptedDataKey(encryptedDataKey);
  45. this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
  46. content = cr.getContent();
  47. return content;
  48. }
  49. }
  50. }
  51. }

getConfigInner方法,主要有几个逻辑

  1. 先从本地磁盘中加载配置,因为应用在启动时,会加载远程配置缓存到本地,如果本地文件的内容

不为空,直接返回。

  1. 如果本地文件的内容为空,则调用worker.getServerConfig加载远程配置
  2. 如果出现异常,则调用本地快照文件加载配置

    本地文件缓存读取配置

    默认情况下,nacos先从本地缓存的配置中读取文件:
    C:\Users\mayn\nacos\config\fixed-192.168.8.133_8848-6a382560-ed4c-414c-
    a5e2-9d72c48f1a0e_nacos
    如果本地缓存内容存在,则返回内容数据,否则返回空值。
  1. public static String getFailover(String serverName, String dataId, String group, String tenant) {
  2. File localPath = getFailoverFile(serverName, dataId, group, tenant);
  3. if (localPath.exists() && localPath.isFile()) {
  4. try {
  5. return readFile(localPath);
  6. } catch (IOException var6) {
  7. LOGGER.error("[" + serverName + "] get failover error, " + localPath, var6);
  8. return null;
  9. }
  10. } else {
  11. return null;
  12. }
  13. }

从指定文件目录下读取文件内容。

  1. static File getFailoverFile(String serverName, String dataId, String group, String tenant) {
  2. File tmp = new File(LOCAL_SNAPSHOT_PATH, serverName + "_nacos");
  3. tmp = new File(tmp, "data");
  4. if (StringUtils.isBlank(tenant)) {
  5. tmp = new File(tmp, "config-data");
  6. } else {
  7. tmp = new File(tmp, "config-data-tenant");
  8. tmp = new File(tmp, tenant);
  9. }
  10. return new File(new File(tmp, group), dataId);
  11. }

ClientWorker.getServerConfig


ClientWorker,表示客户端的一个工作类,它负责和服务端交互
通过agent.httpGet发起http请求,获取远程服务的配置。

  1. public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
  2. ConfigResponse configResponse = new ConfigResponse();
  3. if (StringUtils.isBlank(group)) {//如果group为空,则返回默认group
  4. group = "DEFAULT_GROUP";
  5. }
  6. HttpRestResult result = null;
  7. String encryptedDataKey;
  8. try {
  9. //构建参数
  10. Map<String, String> params = new HashMap(3);
  11. if (StringUtils.isBlank(tenant)) {
  12. params.put("dataId", dataId);
  13. params.put("group", group);
  14. } else {
  15. params.put("dataId", dataId);
  16. params.put("group", group);
  17. params.put("tenant", tenant);
  18. }
  19. //发起远程调用
  20. result = this.agent.httpGet("/v1/cs/configs", (Map)null, params, this.agent.getEncode(), readTimeout);
  21. } catch (Exception var10) {
  22. encryptedDataKey = String.format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", this.agent.getName(), dataId, group, tenant);
  23. LOGGER.error(encryptedDataKey, var10);
  24. throw new NacosException(500, var10);
  25. }
  26. //根据响应结果实现不同的处理
  27. switch(result.getCode()) {
  28. case 200: //如果响应成功,保存快照到本地,并返回响应内容
  29. LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, (String)result.getData());
  30. configResponse.setContent((String)result.getData());
  31. String configType;
  32. //配置文件的类型,如text、json、yaml等
  33. if (result.getHeader().getValue("Config-Type") != null) {
  34. configType = result.getHeader().getValue("Config-Type");
  35. } else {
  36. configType = ConfigType.TEXT.getType();
  37. }
  38. //设置到configResponse中,后续要根据文件类型实现不同解析策略
  39. //获取加密数据的key
  40. configResponse.setConfigType(configType);
  41. encryptedDataKey = result.getHeader().getValue("Encrypted-Data-Key");
  42. //保存
  43. LocalEncryptedDataKeyProcessor.saveEncryptDataKeySnapshot(this.agent.getName(), dataId, group, tenant, encryptedDataKey);
  44. configResponse.setEncryptedDataKey(encryptedDataKey);
  45. return configResponse;
  46. case 403: //如果返回404, 清空本地快照
  47. LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
  48. throw new NacosException(result.getCode(), result.getMessage());
  49. case 404:
  50. LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, (String)null);
  51. LocalEncryptedDataKeyProcessor.saveEncryptDataKeySnapshot(this.agent.getName(), dataId, group, tenant, (String)null);
  52. return configResponse;
  53. case 409:
  54. LOGGER.error("[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
  55. throw new NacosException(409, "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
  56. default:
  57. LOGGER.error("[{}] [sub-server-error] dataId={}, group={}, tenant={}, code={}", new Object[]{this.agent.getName(), dataId, group, tenant, result.getCode()});
  58. throw new NacosException(result.getCode(), "http error, code=" + result.getCode() + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
  59. }
  60. }

在NacosConfigService的构造方法中,当这个类被实例化以后,有做一些事情

  1. 初始化一个HttpAgent,这里又用到了装饰起模式,实际工作的类是ServerHttpAgent,

MetricsHttpAgent内部也是调用了ServerHttpAgent的方法,增加了监控统计的信息

  1. ClientWorker, 客户端的一个工作类,agent作为参数传入到clientworker,可以基本猜测到里面

会用到agent做一些远程相关的事情

  1. public NacosConfigService(Properties properties) throws NacosException {
  2. ValidatorUtils.checkInitParam(properties);
  3. String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
  4. if (StringUtils.isBlank(encodeTmp)) {
  5. this.encode = Constants.ENCODE;
  6. } else {
  7. this.encode = encodeTmp.trim();
  8. }
  9. initNamespace(properties);
  10. //agent远程通信代理
  11. this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
  12. this.agent.start();
  13. this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
  14. }

MetricsHttpAgent.httpGet

�发起远程调用实现

  1. public HttpRestResult<String> httpGet(String path, Map<String, String> headers, Map<String, String> paramValues, String encode, long readTimeoutMs) throws Exception {
  2. Timer timer = MetricsMonitor.getConfigRequestMonitor("GET", path, "NA");
  3. HttpRestResult result;
  4. try {
  5. result = this.httpAgent.httpGet(path, headers, paramValues, encode, readTimeoutMs);
  6. } catch (IOException var13) {
  7. throw var13;
  8. } finally {
  9. timer.observeDuration();
  10. }
  11. return result;
  12. }

ServerHttpAgent.httpGet

  1. public HttpRestResult<String> httpGet(String path, Map<String, String> headers, Map<String, String> paramValues, String encode, long readTimeoutMs) throws Exception {
  2. long endTime = System.currentTimeMillis() + readTimeoutMs;
  3. //注入安全信息
  4. this.injectSecurityInfo(paramValues);
  5. //获取当前服务器地址
  6. String currentServerAddr = this.serverListMgr.getCurrentServerAddr();
  7. //获取最大重试次数,默认3次
  8. int maxRetry = this.maxRetry;
  9. //配置HttpClient的属性,默认的readTimeOut超时时间是3s
  10. HttpClientConfig httpConfig = HttpClientConfig.builder().setReadTimeOutMillis(Long.valueOf(readTimeoutMs).intValue()).setConTimeOutMillis(ConfigHttpClientManager.getInstance().getConnectTimeoutOrDefault(100)).build();
  11. do {
  12. try {
  13. //设置header
  14. Header newHeaders = this.getSpasHeaders(paramValues, encode);
  15. if (headers != null) {
  16. newHeaders.addAll(headers);
  17. }
  18. //构建query查询条件
  19. Query query = Query.newInstance().initParams(paramValues);
  20. //发起http请求,http://192.168.8.133:8848/nacos/v1/cs/configs
  21. HttpRestResult<String> result = NACOS_RESTTEMPLATE.get(this.getUrl(currentServerAddr, path), httpConfig, newHeaders, query, String.class);
  22. if (!this.isFail(result)) { //如果请求失败,
  23. this.serverListMgr.updateCurrentServerAddr(currentServerAddr);
  24. return result;
  25. }
  26. LOGGER.error("[NACOS ConnectException] currentServerAddr: {}, httpCode: {}", this.serverListMgr.getCurrentServerAddr(), result.getCode());
  27. } catch (ConnectException var15) {
  28. LOGGER.error("[NACOS ConnectException httpGet] currentServerAddr:{}, err : {}", this.serverListMgr.getCurrentServerAddr(), var15.getMessage());
  29. } catch (SocketTimeoutException var16) {
  30. LOGGER.error("[NACOS SocketTimeoutException httpGet] currentServerAddr:{}, err : {}", this.serverListMgr.getCurrentServerAddr(), var16.getMessage());
  31. } catch (Exception var17) {
  32. LOGGER.error("[NACOS Exception httpGet] currentServerAddr: " + this.serverListMgr.getCurrentServerAddr(), var17);
  33. throw var17;
  34. }
  35. //如果服务端列表有多个,并且当前请求失败,则尝试用下一个地址进行重试
  36. if (this.serverListMgr.getIterator().hasNext()) {
  37. currentServerAddr = (String)this.serverListMgr.getIterator().next();
  38. } else {
  39. --maxRetry; //重试次数递减
  40. if (maxRetry < 0) {
  41. throw new ConnectException("[NACOS HTTP-GET] The maximum number of tolerable server reconnection errors has been reached");
  42. }
  43. this.serverListMgr.refreshCurrentServerAddr();
  44. }
  45. } while(System.currentTimeMillis() <= endTime);
  46. LOGGER.error("no available server");
  47. throw new ConnectException("no available server");
  48. }

Nacos Server端的配置获取

客户端向服务端加载配置,调用的接口是:/nacos/v1/cs/configs,于是,在Nacos的源码中找到该接口

定位到Nacos源码中的ConfigController.getConfig中的方法,代码如下:

  1. @GetMapping
  2. @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
  3. public void getConfig(HttpServletRequest request, HttpServletResponse response,
  4. @RequestParam("dataId") String dataId, @RequestParam("group") String group,
  5. @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
  6. @RequestParam(value = "tag", required = false) String tag)
  7. throws IOException, ServletException, NacosException {
  8. // check tenant
  9. ParamUtils.checkTenant(tenant);
  10. //租户,也就是namespaceid
  11. tenant = NamespaceUtil.processNamespaceParameter(tenant);
  12. //检查请求参数是否为空
  13. ParamUtils.checkParam(dataId, group, "datumId", "content");
  14. ParamUtils.checkParam(tag);
  15. final String clientIp = RequestUtil.getRemoteIp(request);
  16. //加载配置
  17. inner.doGetConfig(request, response, dataId, group, tenant, tag, clientIp);
  18. }

Inner.doGetConfig

  1. public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group,
  2. String tenant, String tag, String clientIp) throws IOException, ServletException {
  3. final String groupKey = GroupKey2.getKey(dataId, group, tenant);
  4. String autoTag = request.getHeader("Vipserver-Tag");
  5. //获取请求段应用名称
  6. String requestIpApp = RequestUtil.getAppName(request);
  7. //尝试获取当前请求配置的读锁(避免读写冲突)
  8. int lockResult = tryConfigReadLock(groupKey);
  9. //请求端的ip
  10. final String requestIp = RequestUtil.getRemoteIp(request);
  11. boolean isBeta = false;
  12. //lockResult>0 ,表示CacheItem(也就是缓存的配置项)不为空,并且已经加了读锁,意味着这个缓存数据不能被删除。
  13. //lockResult=0 ,表示cacheItem为空,不需要加读锁
  14. //lockResult=01 , 表示加锁失败,存在冲突。
  15. //下面这个if,就是针对这三种情况进行处理。
  16. if (lockResult > 0) {
  17. // LockResult > 0 means cacheItem is not null and other thread can`t delete this cacheItem
  18. FileInputStream fis = null;
  19. try {
  20. String md5 = Constants.NULL;
  21. long lastModified = 0L;
  22. //从本地缓存中,根据groupKey获取CacheItem
  23. CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey);
  24. //判断是否是beta发布,也就是测试版本
  25. if (cacheItem.isBeta() && cacheItem.getIps4Beta().contains(clientIp)) {
  26. isBeta = true;
  27. }
  28. //获取配置文件的类型
  29. final String configType =
  30. (null != cacheItem.getType()) ? cacheItem.getType() : FileTypeEnum.TEXT.getFileType();
  31. response.setHeader("Config-Type", configType);
  32. //返回文件类型的枚举对象
  33. FileTypeEnum fileTypeEnum = FileTypeEnum.getFileTypeEnumByFileExtensionOrFileType(configType);
  34. String contentTypeHeader = fileTypeEnum.getContentType();
  35. response.setHeader(HttpHeaderConsts.CONTENT_TYPE, contentTypeHeader);
  36. File file = null;
  37. ConfigInfoBase configInfoBase = null;
  38. PrintWriter out = null;
  39. //如果是测试配置
  40. if (isBeta) {
  41. md5 = cacheItem.getMd54Beta();
  42. lastModified = cacheItem.getLastModifiedTs4Beta();
  43. if (PropertyUtil.isDirectRead()) {
  44. configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
  45. } else {
  46. //从磁盘中获取文件,得到的是一个完整的File
  47. file = DiskUtil.targetBetaFile(dataId, group, tenant);
  48. }
  49. response.setHeader("isBeta", "true");
  50. } else {
  51. if (StringUtils.isBlank(tag)) { //判断tag标签是否为空,tag对应的是nacos配置中心的标签选项
  52. if (isUseTag(cacheItem, autoTag)) {
  53. if (cacheItem.tagMd5 != null) {
  54. md5 = cacheItem.tagMd5.get(autoTag);
  55. }
  56. if (cacheItem.tagLastModifiedTs != null) {
  57. lastModified = cacheItem.tagLastModifiedTs.get(autoTag);
  58. }
  59. if (PropertyUtil.isDirectRead()) {
  60. configInfoBase = persistService.findConfigInfo4Tag(dataId, group, tenant, autoTag);
  61. } else {
  62. //从磁盘中获取文件,得到的是一个完整的File
  63. file = DiskUtil.targetTagFile(dataId, group, tenant, autoTag);
  64. }
  65. response.setHeader("Vipserver-Tag",
  66. URLEncoder.encode(autoTag, StandardCharsets.UTF_8.displayName()));
  67. } else {//直接走这个逻辑(默认不会配置tag属性)
  68. md5 = cacheItem.getMd5(); //获取缓存的md5
  69. lastModified = cacheItem.getLastModifiedTs(); //获取最后更新时间
  70. if (PropertyUtil.isDirectRead()) {//判断是否是stamdalone模式且
  71. 使用的是derby数据库,如果是,则从derby数据库加载数据
  72. configInfoBase = persistService.findConfigInfo(dataId, group, tenant);
  73. } else {
  74. //否则,如果是数据库或者集群模式,先从本地磁盘得到文件
  75. file = DiskUtil.targetFile(dataId, group, tenant);
  76. }
  77. //如果本地磁盘文件为空,并且configInfoBase为空,则表示配置数据不存在,直接返回null
  78. if (configInfoBase == null && fileNotExist(file)) {
  79. ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1,
  80. ConfigTraceService.PULL_EVENT_NOTFOUND, -1, requestIp);
  81. // pullLog.info("[client-get] clientIp={}, {},
  82. // no data",
  83. // new Object[]{clientIp, groupKey});
  84. response.setStatus(HttpServletResponse.SC_NOT_FOUND);
  85. response.getWriter().println("config data not exist");
  86. return HttpServletResponse.SC_NOT_FOUND + "";
  87. }
  88. }
  89. } else {
  90. //如果tag不为空,说明配置文件设置了tag标签
  91. if (cacheItem.tagMd5 != null) {
  92. md5 = cacheItem.tagMd5.get(tag);
  93. }
  94. if (cacheItem.tagLastModifiedTs != null) {
  95. Long lm = cacheItem.tagLastModifiedTs.get(tag);
  96. if (lm != null) {
  97. lastModified = lm;
  98. }
  99. }
  100. if (PropertyUtil.isDirectRead()) {
  101. configInfoBase = persistService.findConfigInfo4Tag(dataId, group, tenant, tag);
  102. } else {
  103. file = DiskUtil.targetTagFile(dataId, group, tenant, tag);
  104. }
  105. if (configInfoBase == null && fileNotExist(file)) {
  106. // FIXME CacheItem
  107. // No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1.
  108. ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1,
  109. ConfigTraceService.PULL_EVENT_NOTFOUND, -1, requestIp);
  110. // pullLog.info("[client-get] clientIp={}, {},
  111. // no data",
  112. // new Object[]{clientIp, groupKey});
  113. response.setStatus(HttpServletResponse.SC_NOT_FOUND);
  114. response.getWriter().println("config data not exist");
  115. return HttpServletResponse.SC_NOT_FOUND + "";
  116. }
  117. }
  118. }
  119. //把获取的数据结果设置到response中返回
  120. response.setHeader(Constants.CONTENT_MD5, md5);
  121. // Disable cache.
  122. response.setHeader("Pragma", "no-cache");
  123. response.setDateHeader("Expires", 0);
  124. response.setHeader("Cache-Control", "no-cache,no-store");
  125. //如果是单机模式,直接把数据写回到客户端
  126. if (PropertyUtil.isDirectRead()) {
  127. response.setDateHeader("Last-Modified", lastModified);
  128. } else {
  129. //否则,通过trasferTo
  130. fis = new FileInputStream(file);
  131. response.setDateHeader("Last-Modified", file.lastModified());
  132. }
  133. if (PropertyUtil.isDirectRead()) {
  134. out = response.getWriter();
  135. out.print(configInfoBase.getContent());
  136. out.flush();
  137. out.close();
  138. } else {
  139. fis.getChannel()
  140. .transferTo(0L, fis.getChannel().size(), Channels.newChannel(response.getOutputStream()));
  141. }
  142. LogUtil.PULL_CHECK_LOG.warn("{}|{}|{}|{}", groupKey, requestIp, md5, TimeUtils.getCurrentTimeStr());
  143. final long delayed = System.currentTimeMillis() - lastModified;
  144. // TODO distinguish pull-get && push-get
  145. /*
  146. Otherwise, delayed cannot be used as the basis of push delay directly,
  147. because the delayed value of active get requests is very large.
  148. */
  149. ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, lastModified,
  150. ConfigTraceService.PULL_EVENT_OK, delayed, requestIp);
  151. } finally {
  152. //释放锁
  153. releaseConfigReadLock(groupKey);
  154. IoUtils.closeQuietly(fis);
  155. }
  156. } else if (lockResult == 0) {//缓存为空
  157. // FIXME CacheItem No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1.
  158. ConfigTraceService
  159. .logPullEvent(dataId, group, tenant, requestIpApp, -1, ConfigTraceService.PULL_EVENT_NOTFOUND, -1,
  160. requestIp);
  161. response.setStatus(HttpServletResponse.SC_NOT_FOUND);
  162. response.getWriter().println("config data not exist");
  163. return HttpServletResponse.SC_NOT_FOUND + "";
  164. } else {
  165. PULL_LOG.info("[client-get] clientIp={}, {}, get data during dump", clientIp, groupKey);
  166. response.setStatus(HttpServletResponse.SC_CONFLICT);
  167. response.getWriter().println("requested file is being modified, please try later.");
  168. return HttpServletResponse.SC_CONFLICT + "";
  169. }
  170. return HttpServletResponse.SC_OK + "";
  171. }

persistService.findConfigInfo4Beta

�从derby数据库中获取数据内容,这个就是一个基本的数据查询操作。

  1. public ConfigInfo4Beta findConfigInfo4Beta(final String dataId, final String group, final String tenant) {
  2. String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
  3. try {
  4. return this.jt.queryForObject(
  5. "SELECT ID,data_id,group_id,tenant_id,app_name,content,beta_ips FROM config_info_beta WHERE data_id=? AND group_id=? AND tenant_id=?",
  6. new Object[] {dataId, group, tenantTmp}, CONFIG_INFO4BETA_ROW_MAPPER);
  7. } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null.
  8. return null;
  9. } catch (CannotGetJdbcConnectionException e) {
  10. LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
  11. throw e;
  12. }
  13. }

DiskUtil.targetBetaFile

从磁盘目录中获取目标文件,直接根据dataId/group/tenant ,查找指定目录下的文件即可

  1. public static File targetBetaFile(String dataId, String group, String tenant) {
  2. File file = null;
  3. if (StringUtils.isBlank(tenant)) {
  4. file = new File(EnvUtil.getNacosHome(), BETA_DIR);
  5. } else {
  6. file = new File(EnvUtil.getNacosHome(), TENANT_BETA_DIR);
  7. file = new File(file, tenant);
  8. }
  9. file = new File(file, group);
  10. file = new File(file, dataId);
  11. return file;
  12. }

客户端配置的动态感知

Nacos采用长轮训机制来实现数据变更的同步,原理如下!
image.png
image.png
image.png

整体工作流程如下:

  1. 客户端发起长轮训请求
  2. 服务端收到请求以后,先比较服务端缓存中的数据是否相同,如果不通,则直接返回
  3. 如果相同,则通过schedule延迟29.5s之后再执行比较
  4. 为了保证当服务端在29.5s之内发生数据变化能够及时通知给客户端,服务端采用事件订

阅的方式来监听服务端本地数据变化的事件,一旦收到事件,则触发DataChangeTask的 通知,并且遍历allStubs队列中的ClientLongPolling,把结果写回到客户端,就完成 了一次数据的推送

  1. 如果 DataChangeTask 任务完成了数据的 “推送” 之后,ClientLongPolling 中的调 度任务又开始执行了怎么办呢? 很简单,只要在进行 “推送” 操作之前,先将原来等待 执行的调度任务取消掉就可以了,这样就防止了推送操作写完响应数据之后,调度任务又 去写响应数据,这时肯定会报错的。所以,在ClientLongPolling方法中,最开始的一 个步骤就是删除订阅事件

在NacosConfigService的构造方法中,当这个类被实例化以后,有做一些事情

  1. 初始化一个HttpAgent,这里又用到了装饰起模式,实际工作的类是ServerHttpAgent,

MetricsHttpAgent内部也是调用了ServerHttpAgent的方法,增加了监控统计的信息

  1. ClientWorker, 客户端的一个工作类,agent作为参数传入到clientworker,可以基本猜测到里面

会用到agent做一些远程相关的事情

  1. public NacosConfigService(Properties properties) throws NacosException {
  2. ValidatorUtils.checkInitParam(properties);
  3. String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
  4. if (StringUtils.isBlank(encodeTmp)) {
  5. this.encode = Constants.ENCODE;
  6. } else {
  7. this.encode = encodeTmp.trim();
  8. }
  9. initNamespace(properties);
  10. //agent远程通信代理
  11. this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
  12. this.agent.start();
  13. this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
  14. }

ClientWorker

    public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
            final Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;

        // Initialize the timeout parameter
        //初始化配置
        init(properties);
        //初始化一个定时调度的线程池,重写了threadfactory方法
        this.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;
            }
        });
        //初始化一个定时调度的线程池,从里面的name名字来看,似乎和长轮训有关系。而这个长轮训应该是和nacos服务端的长轮训
        this.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;
                    }
                });
        //设置定时任务的执行频率,并且调用checkConfigInfo这个方法,猜测是定时去检测配置是否发生了变化
        //首次执行延迟时间为1毫秒、延迟时间为10毫秒
        this.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);
    }

初始化一个HttpAgent,这里又用到了装饰起模式,实际工作的类是ServerHttpAgent,MetricsHttpAgent内部也是调用了ServerHttpAgent的方法,增加了监控统计的信息可以看到 ClientWorker 除了将 HttpAgent 维持在自己内部,还创建了两个线程池:

  1. 第一个线程池是只拥有一个线程用来执行定时任务的 executor,executor 每隔 10ms 就会执行一次

checkConfigInfo() 方法,从方法名上可以知道是每 10 ms 检查一次配置信息。

  1. 第二个线程池是一个普通的线程池,从 ThreadFactory 的名称可以看到这个线程池是做长轮询的。

    checkConfigInfo

    这个方法主要的目的是用来检查服务端的配置信息是否发生了变化。如果有变化,则触发listener通知

  2. cacheMap: AtomicReference> cacheMap 用来存储监听变更的缓存集合。key是根据dataID/group/tenant(租户) 拼接的值。

  3. Value是对应存储在nacos服务器上的配置文件的内容。默认情况下,每个长轮训LongPullingRunnable任务默认处理3000个监听配置集。
  4. 如果超过3000, 则需要启动多个LongPollingRunnable去执行。currentLongingTaskCount保存已启动的LongPullingRunnable任务数

    public void checkConfigInfo() {
         //分发任务
         int listenerSize = cacheMap.size();
         //向上取整为批数,监听的配置数量除以3000,得到一个整数,代表长轮训任务的数量
         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;
         }
     }
    

    LongPollingRunnable.run

    初始化new LongPollingRunnable()丢给 executorService线程池来处理,所以我们可以找到LongPollingRunnable里面的run方法这个方法传递了一个taskid, tasked用来区分cacheMap中的任务批次, 保存到cacheDatas这个集合中cacheData.isUseLocalConfigInfo 这个值的变化来自于checkLocalConfig这个方法

    public void run() {
    
             List<CacheData> cacheDatas = new ArrayList<CacheData>();
             List<String> inInitializingCacheList = new ArrayList<String>();
             try {
                 // tasked用来区分cacheMap中的任务批次, 保存到cacheDatas这个集合中
                 for (CacheData cacheData : cacheMap.values()) {
                     if (cacheData.getTaskId() == taskId) {
                         cacheDatas.add(cacheData);
                         try {
                             //通过本地文件中缓存的数据和cacheData集合中的数据进行比对,判断是否出现数据变化
                             checkLocalConfig(cacheData);
                             if (cacheData.isUseLocalConfigInfo()) {//这里表示数据有变化,需要通知监听器
                                 cacheData.checkListenerMd5();
                             }
                         } catch (Exception e) {
                             LOGGER.error("get local config info error", e);
                         }
                     }
                 }
    
                 // check server config
                 List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                 if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                     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];
                     }
                     try {
                         String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                         CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                         cache.setContent(ct[0]);
                         if (null != ct[1]) {
                             cache.setType(ct[1]);
                         }
                         LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                                 agent.getName(), dataId, group, tenant, cache.getMd5(),
                                 ContentUtils.truncateContent(ct[0]), ct[1]);
                     } catch (NacosException ioe) {
                         String message = String
                                 .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                         agent.getName(), dataId, group, tenant);
                         LOGGER.error(message, ioe);
                     }
                 }
                 for (CacheData cacheData : cacheDatas) {
                     if (!cacheData.isInitializing() || inInitializingCacheList
                             .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                         cacheData.checkListenerMd5();
                         cacheData.setInitializing(false);
                     }
                 }
                 inInitializingCacheList.clear();
    
                 executorService.execute(this);
    
             } catch (Throwable e) {
    
                 // If the rotation training task is abnormal, the next execution time of the task will be punished
                 LOGGER.error("longPolling error : ", e);
                 executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
             }
         }
     }
    

    checkLocalConfig

    检查本地配置,这里面有三种情况

  5. 如果isUseLocalConfigInfo为false,但是本地缓存路径的文件是存在的,那么把isUseLocalConfigInfo设置为true,并且更新cacheData的内容以及文件的更新时间

  6. 如果isUseLocalCOnfigInfo为true,但是本地缓存文件不存在,则设置为false,不通知监听器
  7. isUseLocalConfigInfo为true,并且本地缓存文件也存在,但是缓存的的时间和文件的更新时间不

一致,则更新cacheData中的内容,并且isUseLocalConfigInfo设置为true

    private void checkLocalConfig(CacheData cacheData) {
        final String dataId = cacheData.dataId;
        final String group = cacheData.group;
        final String tenant = cacheData.tenant;
        File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);
        //没有->有
        if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
            String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
            final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
            cacheData.setUseLocalConfigInfo(true);
            cacheData.setLocalConfigInfoVersion(path.lastModified());
            cacheData.setContent(content);

            LOGGER.warn(
                    "[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
                    agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
            return;
        }

        // 有 -> 没有。不通知业务监听器,从server拿到配置后通知。
        if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
            cacheData.setUseLocalConfigInfo(false);
            LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
                    dataId, group, tenant);
            return;
        }

        // 有变更
        if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
                .lastModified()) {
            String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
            final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
            cacheData.setUseLocalConfigInfo(true);
            cacheData.setLocalConfigInfoVersion(path.lastModified());
            cacheData.setContent(content);
            LOGGER.warn(
                    "[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
                    agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        }
    }

CacheData.checkListenerMd5

遍历用户自己添加的监听器,如果发现数据的md5值不同,则发送通知

    void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, type, md5, wrap);
            }
        }
    }

检查服务端配置

在LongPollingRunnable.run中,先通过本地配置的读取和检查来判断数据是否发生变化从而实现变化
的通知接着,当前的线程还需要去远程服务器上获得最新的数据,检查哪些数据发生了变化

  1. 通过checkUpdateDataIds获取远程服务器上数据变更的dataid
  2. 遍历这些变化的集合,然后调用getServerConfig从远程服务器获得对应的内容
  3. 更新本地的cache,设置为服务器端返回的内容
  4. 最后遍历cacheDatas,找到变化的数据进行通知
   //从服务端获取发生变化的数据的DataID列表,保存在List<String>集合中
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                    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];
                    }
                    try {
                        //遍历有变换的groupkey,发起远程请求获得指定groupkey的内容
                        String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                        //把获取到的内容设置到CacheData中
                        CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(ct[0]);
                        if (null != ct[1]) {
                            cache.setType(ct[1]);
                        }
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                                agent.getName(), dataId, group, tenant, cache.getMd5(),
                                ContentUtils.truncateContent(ct[0]), ct[1]);
                    } catch (NacosException ioe) {
                        String message = String
                                .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                        agent.getName(), dataId, group, tenant);
                        LOGGER.error(message, ioe);
                    }
                }
                //再遍历CacheData这个集合,找到发生变化的数据进行通知
                for (CacheData cacheData : cacheDatas) {
                    if (!cacheData.isInitializing() || inInitializingCacheList
                            .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                        cacheData.checkListenerMd5();
                        cacheData.setInitializing(false);
                    }
                }
                inInitializingCacheList.clear();
                //继续传递当前线程进行轮询
                executorService.execute(this);

checkUpdateDataIds

  1. 首先从cacheDatas集合中找到isUseLocalConfigInfo为false的缓存
  2. 调用checkUpdateConfigStr

    /**
      * 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回
      NULL。
      */
     List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
         StringBuilder sb = new StringBuilder();
         for (CacheData cacheData : cacheDatas) {
             if (!cacheData.isUseLocalConfigInfo()) {
                 sb.append(cacheData.dataId).append(WORD_SEPARATOR);
                 sb.append(cacheData.group).append(WORD_SEPARATOR);
                 if (StringUtils.isBlank(cacheData.tenant)) {
                     sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
                 } else {
                     sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
                     sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
                 }
                 // cacheData 首次出现在cacheMap中&首次check更新
                 if (cacheData.isInitializing()) {
                     // It updates when cacheData occours in cacheMap by first time.
                     inInitializingCacheList
                             .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
                 }
             }
         }
         boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
         return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
     }
    

    通过长轮训的方式,从远程服务器获得变化的数据进行返回

    checkUpdateConfigStr

     /**
      * 从Server获取值变化了的DataID列表。返回的对象里只有dataId和group是有效的。 保证不返回NULL。
      */
     List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
    
         Map<String, String> params = new HashMap<String, String>(2);
         params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
         Map<String, String> headers = new HashMap<String, String>(2);
         headers.put("Long-Pulling-Timeout", "" + timeout);
    
         // told server do not hang me up if new initializing cacheData added in
         if (isInitializingCacheList) {
             headers.put("Long-Pulling-Timeout-No-Hangup", "true");
         }
    
         if (StringUtils.isBlank(probeUpdateString)) {
             return Collections.emptyList();
         }
    
         try {
             long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
             HttpRestResult<String> result = agent
                     .httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
                             readTimeoutMs);
    
             if (result.ok()) {
                 setHealthServer(true);
                 return parseUpdateDataIdResponse(result.getData());
             } else {
                 setHealthServer(false);
                 LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(),
                         result.getCode());
             }
         } catch (Exception e) {
             setHealthServer(false);
             LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
             throw e;
         }
         return Collections.emptyList();
     }
    

    getServerConfig

    根据dataId、group、tenant等信息,使用http请求从远程服务器上获得配置信息,读取到数据之后缓
    存到本地文件中

     public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
             throws NacosException {
         String[] ct = new String[2];
         if (StringUtils.isBlank(group)) {
             group = Constants.DEFAULT_GROUP;
         }
    
         HttpRestResult<String> result = null;
         try {
             Map<String, String> params = new HashMap<String, String>(3);
             if (StringUtils.isBlank(tenant)) {
                 params.put("dataId", dataId);
                 params.put("group", group);
             } else {
                 params.put("dataId", dataId);
                 params.put("group", group);
                 params.put("tenant", tenant);
             }
             //发起远程请求
             result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
         } catch (Exception ex) {
             String message = String
                     .format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s",
                             agent.getName(), dataId, group, tenant);
             LOGGER.error(message, ex);
             throw new NacosException(NacosException.SERVER_ERROR, ex);
         }
    
         switch (result.getCode()) {
             case HttpURLConnection.HTTP_OK:
                 LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
                 ct[0] = result.getData();
                 if (result.getHeader().getValue(CONFIG_TYPE) != null) {
                     ct[1] = result.getHeader().getValue(CONFIG_TYPE);
                 } else {
                     ct[1] = ConfigType.TEXT.getType();
                 }
                 return ct;
             case HttpURLConnection.HTTP_NOT_FOUND:
                 LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
                 return ct;
             case HttpURLConnection.HTTP_CONFLICT: {
                 LOGGER.error(
                         "[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
                                 + "tenant={}", agent.getName(), dataId, group, tenant);
                 throw new NacosException(NacosException.CONFLICT,
                         "data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
             }
             case HttpURLConnection.HTTP_FORBIDDEN: {
                 LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(),
                         dataId, group, tenant);
                 throw new NacosException(result.getCode(), result.getMessage());
             }
             default: {
                 LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", agent.getName(),
                         dataId, group, tenant, result.getCode());
                 throw new NacosException(result.getCode(),
                         "http error, code=" + result.getCode() + ",dataId=" + dataId + ",group=" + group + ",tenant="
                                 + tenant);
             }
         }
     }
    

    客户端缓存配置长轮训机制总结

    整体实现的核心点就一下几个部分的配置

  3. 对本地缓存的配置做任务拆分,每一个批次是3000条

  4. 针对每3000条创建一个线程去执行
  5. 先把每一个批次的缓存和本地磁盘文件中的数据进行比较,
  6. 如果和本地配置不一致,则表示该缓存发生了更新,直接通知客户端监听
  7. 如果本地缓存和磁盘数据一致,则需要发起远程请求检查配置变化
  8. 先以tenent/groupId/dataId拼接成字符串,发送到服务端进行检查,返回发生了变更
  9. 客户端收到变更配置列表,再逐项遍历发送到服务端获取配置内容。


服务端配置更新的推送

分析完客户端之后,随着好奇心的驱使,服务端是如何处理客户端的请求的?那么同样,我们 需要思考几个问题 服务端是如何实现长轮训机制的 客户端的超时时间为什么要设置30s 客户端发起的请求地址是:/v1/cs/configs/listener,于是找到这个接口进行查看,代码 如下。

ConfigController

nacos是使用spring mvc提供的rest api。这里面会调用inner.doPollingConfig进行处理

    @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 {
            //解析客户端传递过来的可能发生变化的配置项目,转化为Map集合(key=dataId,value=md5)
            clientMd5Map = MD5Util.getClientMd5Map(probeModify);
        } catch (Throwable e) {
            throw new IllegalArgumentException("invalid probeModify");
        }

        //开始执行长轮训。
        inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
    }

doPollingConfig

这个方法主要是用来做长轮训和短轮询的判断

  1. 如果是长轮训,直接走addLongPollingClient方法
  2. 如果是短轮询,直接比较服务端的数据,如果存在md5不一致,直接把数据返回。

     /**
      * 轮询接口.
      */
     public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
             Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
    
         //长轮训
         if (LongPollingService.isSupportLongPolling(request)) {
             longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
             return HttpServletResponse.SC_OK + "";
         }
    
         // else 兼容短轮询逻辑
         List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);
    
         // 兼容短轮询result
         String oldResult = MD5Util.compareMd5OldResult(changedGroups);
         String newResult = MD5Util.compareMd5ResultString(changedGroups);
    
         String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
         if (version == null) {
             version = "2.0.0";
         }
         int versionNum = Protocol.getVersionNumber(version);
    
         // Befor 2.0.4 version, return value is put into header.
         if (versionNum < START_LONG_POLLING_VERSION_NUM) {
             response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
             response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
         } else {
             request.setAttribute("content", newResult);
         }
    
         Loggers.AUTH.info("new content:" + newResult);
    
         //禁用缓存
         response.setHeader("Pragma", "no-cache");
         response.setDateHeader("Expires", 0);
         response.setHeader("Cache-Control", "no-cache,no-store");
         response.setStatus(HttpServletResponse.SC_OK);
         return HttpServletResponse.SC_OK + "";
     }
    

    longPollingService.addLongPollingClient

从方法名字上可以推测出,这个方法应该是把客户端的长轮训请求添加到某个任务中去。

  1. 获得客户端传递过来的超时时间,并且进行本地计算,提前500ms返回响应,这就能解释为什么

客户端响应超时时间是29.5+了。当然如果 isFixedPolling=true 的情况下,不会提前返回响应

  1. 根据客户端请求过来的md5和服务器端对应的group下对应内容的md5进行比较,如果不一致,则

通过 generateResponse 将结果返回

  1. 如果配置文件没有发生变化,则通过 scheduler.execute 启动了一个定时任务,将客户端的长轮

询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行

 public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
            int probeRequestSize) {
        //str表示超时时间,也就是timeout
        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");
        int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);

        /**
         * 提前500ms返回响应,为避免客户端超时 @qiaoyi.dingqy 2013.10.22改动 add
         delay time for LoadBalance
         */
        long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
        if (isFixedPolling()) {
            timeout = Math.max(10000, getFixedPollingInterval());
            // Do nothing but set fix polling timeout.
        } else {
            //根据客户端请求过来的md5和服务器端对应的group下对应内容的md5进行比较,如果不一致,则通过`generateResponse`将结果返回
            long start = System.currentTimeMillis();
            List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
            if (changedGroups.size() > 0) {
                generateResponse(req, rsp, changedGroups);
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                return;
            } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
                LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                return;
            }
        }
        String ip = RequestUtil.getRemoteIp(req);

        //一定要由HTTP线程调用,否则离开后容器会立即发送响应
        final AsyncContext asyncContext = req.startAsync();

        // AsyncContext是Servlet3.0中提供的对象,调用startAsync获得AsyncContext对象之后,这个请求的响应会被延后,并释放容器分配的线程。
        // 这样就可以实现长轮询的机制.
        // AsyncContext.setTimeout()的超时时间不准,所以只能自己控制
        asyncContext.setTimeout(0L);

        ConfigExecutor.executeLongPolling(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
    }

ClientLongPolling

接下来我们来分析一下,clientLongPolling到底做了什么操作。或者说我们可以先猜测一下应该会做什
么事情

  1. 这个任务要阻塞29.5s才能执行,因为立马执行没有任何意义,毕竟前面已经执行过一次了
  2. 如果在29.5s+之内,数据发生变化,需要提前通知。需要有一种监控机制

基于这些猜想,我们可以看看它的实现过程
从代码粗粒度来看,它的实现似乎和我们的猜想一致,在run方法中,通过scheduler.schedule实现了
一个定时任务,它的delay时间正好是前面计算的29.5s。在这个任务中,会通过MD5Util.compareMd5
来进行计算那另外一个,当数据发生变化以后,肯定不能等到29.5s之后才通知呀,那怎么办呢?我们发现有一个
allSubs 的东西,它似乎和发布订阅有关系。那是不是有可能当前的clientLongPolling订阅了数据变化的事件呢?

 class ClientLongPolling implements Runnable { 
public void run() {
            asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
                @Override
                public void run() {
                    try {
                        getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());

                        /**
                         * 删除订阅关系
                         */
                        allSubs.remove(ClientLongPolling.this);

                        if (isFixedPolling()) {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            List<String> changedGroups = MD5Util
                                    .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                            (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                            if (changedGroups.size() > 0) {
                                sendResponse(changedGroups);
                            } else {
                                sendResponse(null);
                            }
                        } else {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            sendResponse(null);
                        }
                    } catch (Throwable t) {
                        LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                    }

                }

            }, timeoutTime, TimeUnit.MILLISECONDS);

            allSubs.add(this);
        }
 }

allSubs

allSubs是一个队列,队列里面放了ClientLongPolling这个对象。这个队列似乎和配置变更有某种关联
关系。 那么这里必须要实现的是,当用户在nacos 控制台修改了配置之后,必须要从这个订阅关系中取出关注的客户端长连接,然后把变更的结果返回。于是我们去看LongPollingService的构造方法查找订阅关系

/**
* 长轮询订阅关系
*/
final Queue<ClientLongPolling> allSubs;
allSubs.add(this);

LongPollingService

在LongPollingService的构造方法中,使用了一个NotifyCenter订阅了一个事件,其中不难发现,如果
这个事件的实例是LocalDataChangeEvent,也就是服务端数据发生变更的时间,就会执行一个DataChangeTask 的线程。

  public LongPollingService() {
        allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();

        ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);

        // Register LocalDataChangeEvent to NotifyCenter.
        NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);

        //注册LocalDataChangeEvent订阅事件
        NotifyCenter.registerSubscriber(new Subscriber() {

            @Override
            public void onEvent(Event event) {
                if (isFixedPolling()) {
                    // Ignore.
                } else {
                    if (event instanceof LocalDataChangeEvent) {//如果触发了LocalDataChangeEvent,则执行下面的代码
                        LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                        ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                    }
                }
            }

            @Override
            public Class<? extends Event> subscribeType() {
                return LocalDataChangeEvent.class;
            }
        });

    }

DataChangeTask.run

从名字可以看出来,这个是数据变化的任务,最让人兴奋的应该是,它里面有一个循环迭代器,从
allSubs里面获得ClientLongPolling,最后通过clientSub.sendResponse把数据返回到客户端。所以,这也就能够理解为何数据变化能够实时触发更新了。

   public void run() {
            try {
                ConfigCacheService.getContentBetaMd5(groupKey);
                //遍历所有订阅事件表
                for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                    ClientLongPolling clientSub = iter.next();//得到ClientLongPolling
//判断当前的ClientLongPolling中,请求的key是否包含当前修改的groupKey
                    if (clientSub.clientMd5Map.containsKey(groupKey)) {
                        //如果是beta方式且betaIps不包含当前客户端ip,直接返回
                        if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
                            continue;
                        }

                        //如果配置了tag标签且不包含当前客户端的tag,直接返回
                        if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                            continue;
                        }

                        getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                        iter.remove(); // Delete subscribers' relationships.
                        LogUtil.CLIENT_LOG
                                .info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
                                        RequestUtil
                                                .getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
                                        "polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                        clientSub.sendResponse(Arrays.asList(groupKey));
                    }
                }
            } catch (Throwable t) {
                LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
            }
        }

Nacos配置中心集群原理及源码分析

image.png

Nacos集群工作原理

Nacos作为配置中心的集群结构中,是一种无中心化节点的设计,由于没有主从节点,也没有选举机制,所以为了能够实现热备,就需要增加虚拟IP(VIP)。
Nacos的数据存储分为两部分

  1. Mysql数据库存储,所有Nacos节点共享同一份数据,数据的副本机制由Mysql本身的主从方案来解决,从而保证数据的可靠性。
  2. 每个节点的本地磁盘,会保存一份全量数据,具体路径:/data/program/nacos-1/data/config-data/${GROUP}.在Nacos的设计中,Mysql是一个中心数据仓库,且认为在Mysql中的数据是绝对正确的。 除此之外,Nacos在启动时会把Mysql中的数据写一份到本地磁盘。

    这么设计的好处是可以提高性能,当客户端需要请求某个配置项时,服务端会想Ian从磁盘中读取对应文件返回,而磁盘的读取效率要比数据库效率高。

当配置发生变更时:

  1. Nacos会把变更的配置保存到数据库,然后再写入本地文件。
  2. 接着发送一个HTTP请求,给到集群中的其他节点,其他节点收到事件后,从Mysql中dump刚刚写入的数据到本地文件中。另外,NacosServer启动后,会同步启动一个定时任务,每隔6小时,会dump一次全量数据到本地文件

配置变更同步入口

当配置发生修改、删除、新增操作时,通过发布一个notifyConfigChange事件。

    @DeleteMapping
    @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
    public Boolean deleteConfig(HttpServletRequest request, HttpServletResponse response,
            @RequestParam("dataId") String dataId, //
            @RequestParam("group") String group, //
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
            @RequestParam(value = "tag", required = false) String tag) throws NacosException {
        // check tenant
        ParamUtils.checkTenant(tenant);
        ParamUtils.checkParam(dataId, group, "datumId", "rm");
        ParamUtils.checkParam(tag);
        String clientIp = RequestUtil.getRemoteIp(request);
        String srcUser = RequestUtil.getSrcUserName(request);
        if (StringUtils.isBlank(tag)) {
            persistService.removeConfigInfo(dataId, group, tenant, clientIp, srcUser);
        } else {
            persistService.removeConfigInfoTag(dataId, group, tenant, tag, clientIp, srcUser);
        }
        final Timestamp time = TimeUtils.getCurrentTime();
        ConfigTraceService.logPersistenceEvent(dataId, group, tenant, null, time.getTime(), clientIp,
                ConfigTraceService.PERSISTENCE_EVENT_REMOVE, null);
        ConfigChangePublisher
                .notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));
        return true;
    }

根据ConfigDataChangeEvent全局搜索,找到**AsyncNotifyService监听器

**

AsyncNotifyService

@Autowired
public AsyncNotifyService(ServerMemberManager memberManager) {
    this.memberManager = memberManager;

    // Register ConfigDataChangeEvent to NotifyCenter.
    NotifyCenter.registerToPublisher(ConfigDataChangeEvent.class, NotifyCenter.ringBufferSize);

    // Register A Subscriber to subscribe ConfigDataChangeEvent.
    NotifyCenter.registerSubscriber(new Subscriber() {

        @Override
        public void onEvent(Event event) {
            // Generate ConfigDataChangeEvent concurrently
            if (event instanceof ConfigDataChangeEvent) {
                ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
                long dumpTs = evt.lastModifiedTs;
                String dataId = evt.dataId;
                String group = evt.group;
                String tenant = evt.tenant;
                String tag = evt.tag;
                Collection<Member> ipList = memberManager.allMembers(); //得到集群中的ip列表

                // 构建NotifySingleTask,并添加到队列中。
                Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
                for (Member member : ipList) { //遍历集群中的每个节点
                    queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(),
                            evt.isBeta));
                }
                //异步执行任务 AsyncTask
                ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, queue));
            }
        }

        @Override
        public Class<? extends Event> subscribeType() {
            return ConfigDataChangeEvent.class;
        }
    });
}

AsyncTask

@Override
public void run() {
    executeAsyncInvoke();
}

private void executeAsyncInvoke() {
    while (!queue.isEmpty()) {//遍历队列中的数据,直到数据为空
        NotifySingleTask task = queue.poll(); //获取task
        String targetIp = task.getTargetIP(); //获取目标ip

        if (memberManager.hasMember(targetIp)) { //如果集群中的ip列表包含目标ip
            // start the health check and there are ips that are not monitored, put them directly in the notification queue, otherwise notify
            //判断目标ip的健康状态
            boolean unHealthNeedDelay = memberManager.isUnHealth(targetIp); //
            if (unHealthNeedDelay) { //如果目标服务是非健康,则继续添加到队列中,延后再执行。
                // target ip is unhealthy, then put it in the notification list
                ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,
                        task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,
                        0, task.target);
                // get delay time and set fail count to the task
                asyncTaskExecute(task);
            } else {
                //构建header
                Header header = Header.newInstance();
                header.addParam(NotifyService.NOTIFY_HEADER_LAST_MODIFIED, String.valueOf(task.getLastModified()));
                header.addParam(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP, InetUtils.getSelfIP());
                if (task.isBeta) {
                    header.addParam("isBeta", "true");
                }
                AuthHeaderUtil.addIdentityToHeader(header);
                //通过restTemplate发起远程调用,如果调用成功,则执行AsyncNotifyCallBack的回调方法
                restTemplate.get(task.url, header, Query.EMPTY, String.class, new AsyncNotifyCallBack(task));
            }
        }
    }
}

目标节点接收请求

数据同步的请求地址为,task.url=http://localhost:8848/nacos/v1/cs/communication/dataChange?dataId=log.yaml&group=DEFAULT_GROUP

@GetMapping("/dataChange")
public Boolean notifyConfigInfo(HttpServletRequest request, @RequestParam("dataId") String dataId,
        @RequestParam("group") String group,
        @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
        @RequestParam(value = "tag", required = false) String tag) {
    dataId = dataId.trim();
    group = group.trim();
    String lastModified = request.getHeader(NotifyService.NOTIFY_HEADER_LAST_MODIFIED);
    long lastModifiedTs = StringUtils.isEmpty(lastModified) ? -1 : Long.parseLong(lastModified);
    String handleIp = request.getHeader(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP);
    String isBetaStr = request.getHeader("isBeta");
    if (StringUtils.isNotBlank(isBetaStr) && trueStr.equals(isBetaStr)) {
        dumpService.dump(dataId, group, tenant, lastModifiedTs, handleIp, true);
    } else {
        //
        dumpService.dump(dataId, group, tenant, tag, lastModifiedTs, handleIp);
    }
    return true;
}

dumpService.dump用来实现配置的更新,代码如下
当前任务会被添加到DumpTaskMgr中管理。

public void dump(String dataId, String group, String tenant, String tag, long lastModified, String handleIp,
        boolean isBeta) {
    String groupKey = GroupKey2.getKey(dataId, group, tenant);
    String taskKey = String.join("+", dataId, group, tenant, String.valueOf(isBeta), tag);
    dumpTaskMgr.addTask(taskKey, new DumpTask(groupKey, tag, lastModified, handleIp, isBeta));
    DUMP_LOG.info("[dump-task] add task. groupKey={}, taskKey={}", groupKey, taskKey);
}

TaskManager.addTask, 先调用父类去完成任务添加。

 @Override
    public void addTask(Object key, AbstractDelayTask newTask) {
        super.addTask(key, newTask);
        MetricsMonitor.getDumpTaskMonitor().set(tasks.size());
    }

在这种场景设计中,一般都会采用生产者消费者模式来完成,因此这里不难猜测到,任务会被保存到一个队列中,然后有另外一个线程来执行。

NacosDelayTaskExecuteEngine

TaskManager的父类是NacosDelayTaskExecuteEngine,
这个类中有一个成员属性protected final ConcurrentHashMap tasks;,专门来保存延期执行的任务类型AbstractDelayTask.
在这个类的构造方法中,初始化了一个延期执行的任务,其中具体的任务是ProcessRunnable.

public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger, long processInterval) {
    super(logger);
    tasks = new ConcurrentHashMap<Object, AbstractDelayTask>(initCapacity);
    processingExecutor = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory(name));
    processingExecutor
            .scheduleWithFixedDelay(new ProcessRunnable(), processInterval, processInterval, TimeUnit.MILLISECONDS);
}

ProcessRunnable


    private class ProcessRunnable implements Runnable {

        @Override
        public void run() {
            try {
                processTasks();
            } catch (Throwable e) {
                getEngineLog().error(e.toString(), e);
            }
        }
    }

processTasks

protected void processTasks() {
    //获取所有的任务
    Collection<Object> keys = getAllTaskKeys();
    for (Object taskKey : keys) {
        AbstractDelayTask task = removeTask(taskKey);
        if (null == task) {
            continue;
        }
        //获取任务处理器,这里返回的是DumpProcessor
        NacosTaskProcessor processor = getProcessor(taskKey);
        if (null == processor) {
            getEngineLog().error("processor not found for task, so discarded. " + task);
            continue;
        }
        try {
            // ReAdd task if process failed
            //执行具体任务
            if (!processor.process(task)) {
                retryFailedTask(taskKey, task);
            }
        } catch (Throwable e) {
            getEngineLog().error("Nacos task execute error : " + e.toString(), e);
            retryFailedTask(taskKey, task);
        }
    }
}

DumpProcessor.process

读取数据库的最新数据,然后更新本地缓存和磁盘。

总结

  • Environment -> PropertySource
  • Spring Cloud PropertySourceLocator
  • 自定义了一个外部化配置的加载实现
  • Nacos的外部化配置的实现? NacosPropertySourceLocator
    • Spring Cloud Nacos配置的加载
    • Spring Cloud Nacos配置变更
    • @RefreshScope -Spring Cloud中提供的能力, 在Spring中只提供了@Scope
    • Nacos Confi core
      • 客户端配置的加载
      • 客户端配置的动态刷新
      • 客户端配置的本地快照
      • 服务端配置存储(存储到内存/数据库)
      • 数据动态变更比较(客户端对请求的数据进行分片, 通过本地比较+探测+最终
      • 服务端数据的获取)
      • 数据同步 , 基于广播的方式发送数据变更事件

结构化思维

开放性
请你简述一下最近做的项目?
项目背景
项目采用的技术架构
项目的亮点

applicationContext.publishEvent(
new RefreshEvent(this, null, “Refresh Nacos config”));请你说一下你对Spring Boot的理解? [宽泛的问题]
什么是Spring Boot
Spring Boot有什么好处
Spring Boot的核心组件
请你说一下Spring Boot自动装配的好处 [具体的问题就是一个检索的关键词]
如何设计一个千万级并发的架构?