一、为什么要统一管理微服务配置
对于传统的单体应用,常使用配置文件管理所有配置。例如一个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的高可用。架构如图
所示。