使用Spring Cloud Config统一管理微服务配置

449 阅读7分钟

一、为什么要统一管理微服务配置

对于传统的单体应用,常使用配置文件管理所有配置。例如一个SpringBoot开发的单体应用,可将配置内容放在application.yml文件中。如果需要切换环境,可设置多个Profile并在启动应用时指定spring.profiles.active={profile}.也可以借助maven的Profile实现环境切换。

然而在微服务架构中,微服务的配置管理一般有以下需求:

  • 集中管理配置。一个使用微服务架构的应用系统可能会包含成百上千个微服务,因此集中管理配置是非常有必要的。
  • 不同环境,不同配置。例如,数据源配置在不同的环境(开发、测试、预发布、生产等)中是不同的。
  • 运行期间不可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小或熔断阈值,并且在调整配置时不停止微服务。
  • 配置修改后可自动更新。如配置内容发生变化,微服务能够自动更新配置。

综上所述,对于微服务架构而言,一个通用的配置管理机制是必不可少的,常见做法是使用配置服务器管理配置。

二 SpringCloud Config简介

Spring Cloud Config为分布式系统外部化配置提供了服务器端和客户端的支持,它包括Config Server 和Config Client两部分。由于Config Server和Config Client都实现了对Spring Environment 和 PropertySource抽象的映射,因此,Spring Cloud Config非常适合Spring应用程序,当然也可以与任何其他语言编写的应用程序配合使用。

Config Server是一个可横向扩展、集中式的配置服务器,它用于集中管理应用程序各个环境下的配置,默认使用git存储配置内容(也可以使用Subversion、本地文件系统或Vault存储配置),因此可以很方便地实现配置的版本控制与内容审计。

Config Client是Config Server的客户端,用于操作存储在Config Server中的配置属性。引入Spring Cloud Config后的架构如下:

服务端:

pom.xml文件
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId></groupId>
    <artifactId>config-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>config-server</name>
    <description>配置中心</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <package.environment>dev</package.environment>
                <dockerHost></dockerHost>
                <imageName></imageName>
            </properties>
            <!-- 是否默认 true表示默认-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>

        <profile>
            <!-- 本地环境 -->
            <id>local</id>
            <properties>
                <package.environment>local</package.environment>
                <dockerHost></dockerHost>
                <imageName></imageName>
            </properties>
        </profile>

        <profile>
            <id>test</id>
            <properties>
                <package.environment>test</package.environment>
                <dockerHost></dockerHost>
                <imageName></imageName>
            </properties>
        </profile>

        <profile>
            <!-- 环境 -->
            <id>prod</id>
            <properties>
                <package.environment>prod</package.environment>
                <imageName></imageName>
                <dockerHost></dockerHost>
            </properties>
        </profile>
    </profiles>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml 配置文件

server:
  port: 1004
spring:
  profiles:
    active: local
  application:
    name: config-server  
  cloud:
    config:
      server:
        git:
          uri: http://gitlab。。。。。。。。。。。。。。。
          username: *********
          password: **********
    bus:
      enabled: true
      trace:
        enabled: true
  rabbitmq:                       #rabbitmq配置
    host: *****
    port: 5672
    username: admin
    password: *****
management:
  endpoints:
    web:
      exposure:
        include: bus-refresh
---
spring:
  profiles: local
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: 注册中心

---
spring:
  profiles: dev
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: 注册中心

---
spring:
  profiles: test
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: 注册中心

启动类

加注解:@EnableConfigServer

@EnableDiscoveryClient
@EnableConfigServer
@SpringBootApplication
public class SpringConfigServer {
    public static void main(String[] args) {
        SpringApplication.run(SpringConfigServer.class, args);
    }
}

路径规则

Spring Cloud Config Server提供了RESTful API,可用来访问存放在Git仓库中的配置文件。

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

其中的{appliation}、{profile}、{label} 都是占位符。

客户端 pom文件:

<?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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>config-client</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- spring cloud config 客户端包 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>
        <!-- spring cloud config 客户端包 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <package.environment>dev</package.environment>
                <dockerHost></dockerHost>
                <imageName></imageName>
            </properties>
            <!-- 是否默认 true表示默认-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>

        <profile>
            <!-- 本地环境 -->
            <id>local</id>
            <properties>
                <package.environment>local</package.environment>
                <dockerHost></dockerHost>
                <imageName></imageName>
            </properties>
        </profile>

        <profile>
            <id>test</id>
            <properties>
                <package.environment>test</package.environment>
                <dockerHost></dockerHost>
                <imageName></imageName>
            </properties>
        </profile>

        <profile>
            <!-- 环境 -->
            <id>prod</id>
            <properties>
                <package.environment>prod</package.environment>
                <imageName>}</imageName>
                <dockerHost></dockerHost>
            </properties>
        </profile>
    </profiles>
</project>

bootstrap.yml 文件

server:
  port: 8001
spring:
  application:
    name: adicon-config-client
  cloud:
    config:
      uri: http://127.0.0.1:1004 # 对应config-server的地址,默认:http://localhost:8888
      name: lis-config-client  #git文件名
      profile: local  # 对应config-server所获取的配置文件中的{profile}
      label: master # 对应config-server所获取的配置文件中的{lable}
      discovery:
        enabled: true
        service-id: config-server # 对应config-server所获取的配置文件中的{application}
  rabbitmq: #rabbitmq配置
    host: *****
    port: 5672
    username: *****
    password: *****
    #bus:
      #enabled: true
      #trace:
      #  enabled: true
eureka:
  client:
    service-url:
      defaultZone: 默认注册中心地址 客户端服务会注册在上面

management:  #actuator
  endpoints:
    web:
      #      base-path: / #默认是/actuator 前缀,可以在这里修改
      exposure:
        include: "*" #打开全部请求端点
  #        include: refresh,health,info #打开部分
  endpoint:
    health:
      show-details: always
    refresh:
      enabled: true

以上属性应均在bootstrap.yml,而不是application.yml,而不是application.yml中。如果配置在application.yml中该部分配置就不能工作。例如Config client连接spring.cloud.config.url的默认值:http://localhost:8888 ,而非配置的http://localhost:8080

SpringCloud 有一个“引导上下文”的概念,这是主应用程序的父上下文。引导上下文负责从配置服务器加载配置属性,以及解密外部配置文件中的属性。和主应用程序加载application.*(yml或properties)中的属性不同,引导上下文加载bootstrap.*中的属性。配置在bootstrap.*中的属性有更高的优先级,因此默认情况下它们不能被本地配置覆盖。

Config server 的GIT仓库配置详解

搜索目录

  • 在很多场景下,可能把配置文件放在了git仓库子目录中,此时可以使用search-path指定,search-path同样支持占位符
search-path :foo,bar* 

config server就会在git仓库根目录、foo子目录以及所有的以bar开始的子目录中查找配置文件。

启动时加载配置文件

默认情况下,在配置被首次请求时,Config Server才会clone Git仓库。也可让Config Server在启动时就clone Git仓库。


spring:
  cloud:
    config:
      server:
        git:
          uri:https://github.com/spring-cloud-samples/config-repo
          repos:
            team-a:
                pattern:  microservice-*
                clone-on-start: true
                uri:http://git.oschina.net/cakin24/spring-cloud-config-repo
logging:
  level:
    org.springframework.cloud: DEBUG
    org.springframework.boot: DEBUG

日志级别设置为DEBUG,可以打印Config-server请求Git仓库的细节,这样可以更好地理解Config-server的Git仓库的配置。

客户端启动类

@SpringBootApplication
public class ConfigClient {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClient.class, args);
    }
}

编写Controller

@RestController
@RefreshScope //开启更新功能
@RequestMapping("api")
public class TestController {

    @Value("${spring.profile}")
    private String profile;

    /**
     * 返回配置文件中的值
     */
    @GetMapping("/from")
    @ResponseBody
    public String returnFormValue(){
        return profile;
    }
}

使用/refresh端点手动刷新配置

  • 依赖中有spring-boot-starter-actuator

  • 添加如下配置,暴露/actuator/refresh 端点:

management:
  endpoints:
    web:
      exposure:
        include: refresh只需向应用的/actuator/refresh 端点发送POST请求,即可刷新该属性

使用Spring Cloud Bus自动刷新配置

讨论了使用/refresh 端点手动刷新配置,但是如果所有微服务节点的配置都需要手动去刷新的话,那必然是一个繁琐的工作,并且随着系统的不断扩张,会变得越来越难以维护。因此,实现配置的自动刷新是很有必要的,本节我们讨论使用Spring Cloud Bus实现配置的自动刷新。

Spring Cloud Bus提供了批量刷新配置的机制,它使用轻量级的消息代理(例如RabbitMQ、Kafka等)连接分布式系统的节点,这样就可以通过Spring Cloud Bus广播配置的变化或者其他的管理指令。使用Spring Cloud Bus后的架构如图9-2所示。

上图使用Spring Cloud Bus的架构图

由图可知,微服务A的所有实例通过消息总线连接到了一起,每个实例都会订阅配置更新事件。当其中一个微服务节点的/bus/refresh端点被请求时,该实例就会向消息总线发送一个配置更新事件,其他实例获得该事件后也会更新配置。

向应用的/actuator/refresh 端点发送POST请求,即可刷新该属性

服务端收到刷新请求

Spring Cloud Config高可用

前文构建的都是单节点的Config Server,本节来讨论如何构建高可用的Config Server集群,包括Config Server的高可用依赖Git仓库的高可用以及RabbitMQ的高可用。

先来讨论Git仓库的高可用。

Git仓库的高可用

由于配置内容存储在Git仓库中,所以要想实现Config Server的高可用,必须有一个高可用的Git仓库。有两种方式可以实现Git仓库的高可用。

使用第三方Git仓库:这种方式非常简单,可使用例如GitHub、BitBucket、Gitee、Coding等提供的仓库托管服务,这些服务本身就已实现了高可用。 自建Git仓库管理系统:使用第三方服务的方式虽然省去了很多烦恼,但是很多场景下,倾向于自建Git仓库管理系统。此时就需要保证自建Git的高可用。

搭建高可用RabbitMQ的资料,读者可详见:www.rabbitmq.com/ha.html。由于比…

Config Server自身的高可用

本节来讨论如何实现Config Server自身的高可用。笔者分两种场景进行讨论。

Config Server未注册到Eureka Server上

对于这种情况,Config Server的高可用可借助一个负载均衡器来实现

各个微服务将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Config Server节点。这样,就可以实现Config Server的高可用。

Config Server注册到Eureka Server上

这种情况下,Config Server的高可用相对简单,只须将多个Config Server节点注册到Eureka Server上,即可实现Config Server的高可用。架构如图

所示。

文章参考:www.itmuch.com/spring-clou…