目录

spirngcloud配置中心config

概述

Spring Cloug Config官网

写在开头

微服务在 服务配置 + 服务总线 这块进行选型,共有三套方案:1.Spring Cloud Config + Spring Cloud Bus2.Spring Cloud Alibaba Nacos (Nacos 官网)、3.携程 Apollo(Github 地址:Apollo)

Spring Cloud ConfigSpring Cloud Bus 这两哥们,倒是谈不上 停更进维,在开发中还在使用。但是在接下来将会慢慢的被 后起之秀 Alibaba Nacos 所替代。

Nacos 可以替代 EurekaSpring Cloud ConfigSpring Cloud Bus 。一代三减少更多组件的使用,这样我们就可以在工作中将更多的经历放在业务逻辑上。Spring Cloud CofigSpring Cloud Bus 这两个组件还是比较重要,很多公司都有在使用。

http://img.cana.space/picStore/20201111171807.png

分布式系统面临的配置问题

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。

Config是什么

http://img.cana.space/picStore/20201111172214.png

SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为 各个不同微服务应用 的所有环境提供了一个 中心化的外部配置

Config如何使用

三个角色:配置服务器,服务端,客户端

SpringCloud Config分为 服务端和客户端两部分 。

服务端也称为 分布式配置中心,它是一个独立的微服务应用 ,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口

客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

Config主要功能

  • 集中管理配置文件
  • 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
  • 运行期间动态调整配置,不再需要在每个服务部署的机器上(Config客户端)编写配置文件,Config客户端会向配置中心统一拉取配置自己的信息,当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  • 将配置信息以REST接口的形式暴露,post、curl访问刷新均可

Config服务端配置与测试

由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式,本例中我们使用github作为我们的配置服务器。

使用步骤

  1. github上新建名为microservice-cloud-config的仓库,作为配置服务器

  2. 提交配置文件到远程仓库

    http://img.cana.space/picStore/20201111182050.png

  3. 新建配置中心moudle,cloud-config-center3344

  4. pom

    新引入依赖

    1
    2
    3
    4
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    

    完整内容

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>cloud2020</artifactId>
            <groupId>org.eh</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
       
        <artifactId>cloud-config-config</artifactId>
       
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>
            <dependency>
                <groupId>org.eh</groupId>
                <artifactId>cloud-common</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
       
    </project>
    
  5. yml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    server:
      port: 3344
       
    spring:
      application:
        name: cloud-config-config
      cloud:
        config:
          server:
            git:
              uri: git@github.com:lienhui68/microservice-cloud-config.git
              # 搜索目录
              search-paths: microservice-cloud-config
              # 注意github默认主分支是main,但是config默认分支是master,所以需要修改,master->main
              default-label: main
          ####读取分支
          label: main
    
  6. 主启动类,添加注解@EnableConfigServer

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    package com.eh.cloud2020.config;
       
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.config.server.EnableConfigServer;
       
    @EnableConfigServer
    @SpringBootApplication
    public class ConfigMain3344 {
        public static void main(String[] args) {
            SpringApplication.run(ConfigMain3344.class, args);
        }
    }
    
  7. 测试通过Config微服务是否可以从Github上获取配置内容

    1
    2
    3
    4
    
    GET http://localhost:3344/config-dev.yml
       
    config:
      info: dev
    

配置读取规则

官网说明

The HTTP service has resources in the following form:

1
2
3
4
5
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

where application is injected as the spring.config.name in the SpringApplication (what is normally application in a regular Spring Boot app), profile is an active profile (or comma-separated list of properties), and label is an optional git label (defaults to master.)

  • label:分支名,不写默认使用yml里配置的读取分支
  • name:服务名,也就是文件名-{profile}前面的部分,起一个有意义的名字就可以
  • profile:环境(dev/test/prod)

Spring Cloud Config Server pulls configuration for remote clients from various sources. The following example gets configuration from a git repository (which must be provided), as shown in the following example:

1
2
3
4
5
6
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/spring-cloud-samples/config-repo

Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems.

Config客户端配置与测试

bootstrap.yml

  • applicaiton.yml是用户级的资源配置项
  • bootstrap.yml是系统级的资源配置项, 优先级更高

Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context的 父上下文 。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的EnvironmentBootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap contextApplication Context有着不同的约定, 所以新增了个bootstrap.yml文件,保证Bootstrap ContextApplication Context配置的分离。

客户端使用配置中心步骤

  1. 新建客户端moudle,cloud-config-client3355

  2. pom

    引入配置中心客户端依赖和web依赖(测试访问用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>
       
    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    

    完整内容:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>cloud2020</artifactId>
            <groupId>org.eh</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
       
        <artifactId>cloud-config-client</artifactId>
       
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.eh</groupId>
                <artifactId>cloud-common</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
       
    </project>
    
  3. bootstrap.yml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    server:
      port: 335
    spring:
      application:
        name: cloud-config-client
      cloud:
        # 下面四项拼起来就是http://localhost:3344/main/config-dev.yml
        config:
          # 分支名称
          label: main
          # 配置文件名称
          name: config
          # 环境profile
          profile: dev
          # 配置中心地址
          uri: http://localhost:3344
    
  4. 主启动类

    不需要额外加注解

    1
    2
    3
    4
    5
    6
    
    @SpringBootApplication
    public class ConfigClientMain3355 {
        public static void main(String[] args) {
            SpringApplication.run(ConfigClientMain3355.class, args);
        }
    }
    
  5. 新建Controller类,验证是否能从配置中心读取Github上的配置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package com.eh.cloud2020.config.client.controller;
       
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
       
    @RestController
    public class ClientController {
       
        @Value("${config.info}")
        private String configInfo;
       
        @GetMapping("/info")
        public String getConfigInfo() {
            return "从配置中心获取配置信息=========>" + configInfo;
        }
    }
    
  6. 测试

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    GET http://localhost:3355/info
       
    HTTP/1.1 200 
    Content-Type: text/plain;charset=UTF-8
    Content-Length: 46
    Date: Wed, 11 Nov 2020 11:57:37 GMT
    Keep-Alive: timeout=60
    Connection: keep-alive
       
    从配置中心获取配置信息=========>dev
       
    Response code: 200; Time: 25ms; Content length: 24 bytes
    

Config客户端之动态刷新

动态刷新也叫热刷新

问题

经过上面的配置成功从客户端访问到了github上的配置,但是问题也随之而来

分布式配置的动态刷新问题,假设运维工程师在github上修改了配置文件内容,如下

http://img.cana.space/picStore/20201111200555.png

刷新3344,发现ConfigServer配置中心立刻响应

http://img.cana.space/picStore/20201111200622.png

刷新3355,发现ConfigClient客户端还是保持原样,除非自己重启或者重新加载

http://img.cana.space/picStore/20201111200658.png

难道每次运维修改配置文件客户端都需要重启??噩梦

解决

避免每次更新配置都要重启客户端微服务3355,可以使用@RefreshScope注解,修改客户端3355模块,具体步骤如下

  1. POM引入actuator监控

    1
    2
    3
    4
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
  2. 修改YML,暴露监控endpoint

    1
    2
    3
    4
    5
    6
    
    # 暴露监控endpoint
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  3. 在Controller类上添加@RefreshScope注解

    1
    2
    3
    
    @RefreshScope
    @RestController
    public class ClientController {
    
  4. 此时修改github -> 3344变化 ->3355没有变化

  5. 使用post请求刷新3355

    1
    2
    
    $ curl -X POST http://localhost:3355/actuator/refresh
    ["config.client.version","config.info"]%
    
  6. 再次刷新3355访问,ok,成功实现了客户端3355刷新,读取到最新配置内容

原理

要说清楚RefreshScope,先要了解Scope

  • Scope(org.springframework.beans.factory.config.Scope)是Spring 2.0开始就有的核心的概念

  • RefreshScope(org.springframework.cloud.context.scope.refresh)是spring cloud提供的一种特殊的scope实现,用来实现配置、实例热加载。

  • RefreshScope类结构图如下:

    20201112172739

Scope与ApplicationContext生命周期

AbstractBeanFactory#doGetBean创建Bean实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 protected <T> T doGetBean(...){
  final RootBeanDefinition mbd = ...
  if (mbd.isSingleton()) {
    ...
  } else if (mbd.isPrototype())
    ...
  } else {
     String scopeName = mbd.getScope();
     final Scope scope = this.scopes.get(scopeName);
     Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {...});
     ...
  }
  ...
 }

Singleton和Prototype是硬编码的,并不是Scope子类。 Scope实际上是自定义扩展的接口

Scope Bean实例交由Scope自己创建,例如Session对象是使用一开始注册到SessionScope的ObjectFactory创建实例的,而RefreshScope是在内建缓存中获取的。

@Scope 对象的实例化

@RefreshScope 是scopeName=“refresh"的 @Scope

1
2
3
4
5
...
@Scope("refresh")
public @interface RefreshScope {
  ...
}

@Scope 的注册 AnnotatedBeanDefinitionReader#registerBean

1
2
3
4
5
6
7
public void registerBean(...){
 ...
 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
  abd.setScope(scopeMetadata.getScopeName());
 ...
  definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
}

读取@Scope元数据, AnnotationScopeMetadataResolver#resolveScopeMetadata

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
     AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
         annDef.getMetadata(), Scope.class);
     if (attributes != null) {
       metadata.setScopeName(attributes.getString("value"));
       ScopedProxyMode proxyMode = attributes.getEnum("proxyMode");
       if (proxyMode == null || proxyMode == ScopedProxyMode.DEFAULT) {
         proxyMode = this.defaultProxyMode;
       }
       metadata.setScopedProxyMode(proxyMode);
     }
}

Scope实例对象通过ScopedProxyFactoryBean创建,其中通过AOP使其实现ScopedObject接口,这里不再展开

现在来说说RefreshScope是如何实现配置和实例刷新的

RefreshScope注册

RefreshAutoConfiguration#RefreshScopeConfiguration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component
// 当RefreshScope实例不存在时创建RefreshScope实例
@ConditionalOnMissingBean(RefreshScope.class)
protected static class RefreshScopeConfiguration implements BeanDefinitionRegistryPostProcessor{
...
  registry.registerBeanDefinition("refreshScope",
  BeanDefinitionBuilder.genericBeanDefinition(RefreshScope.class)
            .setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
            .getBeanDefinition());
...
}

RefreshScope extends GenericScope, 大部分逻辑在 GenericScope 中

GenericScope#postProcessBeanFactory 中向AbstractBeanFactory注册自己

1
2
3
4
5
6
7
8
public class GenericScope implements Scope, BeanFactoryPostProcessor...{
   @Override
   public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
     throws BeansException {
     beanFactory.registerScope(this.name/*refresh*/, this/*RefreshScope*/);
     ...
   }
}

RefreshScope 刷新过程

入口在org.springframework.cloud.context.refresh.ContextRefresher#refresh

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

refreshEnvironment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public synchronized Set<String> refreshEnvironment() {
  // 1. 提取标准参数(SYSTEM,JNDI,SERVLET)之外所有参数变量
   Map<String, Object> before = extract(
         this.context.getEnvironment().getPropertySources());
  // 2. 把原来的Environment里的参数放到一个新建的Spring Context容器下重新加载,完事之后关闭新容器
  // 这一步读取到了更新后的属性值
   addConfigFilesToEnvironment();
  // 4. 比较出变更项
   Set<String> keys = changes(before,
                              // 3. 提取更新过的参数(排除标准参数)
         extract(this.context.getEnvironment().getPropertySources())).keySet();
  // 5. 发布环境变更事件,接受:EnvironmentChangeListener/LoggingRebinder
   this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
   return keys;
}

refreshAll

1
2
3
4
5
6
// 6. RefreshScope用新的环境参数重新生成Bean
public void refreshAll() {
  	// 销毁Bean并清除refreshScope缓存
   super.destroy();
   this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

由于设置了@ConditionalOnMissingBean(RefreshScope.class),下次就会重新从BeanFactory获取一个新的实例(该实例使用新的配置)

问题more

假设有多个微服务3355、3366、3377…,每个微服务都要执行一次post请求手动刷新?

可否广播,一次通知,处处生效?或者98%生效,留2%继续使用原有配置?

这些问题都可以通过下一篇 spirngcloud消息总线bus

参考

Spring Cloud @RefreshScope 原理及使用