1.概述
我们知道,微服务将单体应用拆分成一个个的子服务,每个子服务都需要必要的配置信息,随着子服务数量的增加,与之相关的配置文件数量也会随之增加,如果有上百个配置文件,那么管理起来便十分困难。因此,一套集中式的、动态的配置管理中心是必不可少的,Spring Cloud 提供了 ConfigServer 来解决这个问题。
Spring Cloud Config 在微服务分布式系统中,采用 Server 服务端和 Client 客户端的方式来提供可扩展的配置服务。服务端提供配置文件的存储,以接口的形式将配置文件的内容提供出去;客户端通过接口获取数据、并依据此数据初始化自己的应用。
Spring Cloud Config 默认使用 Git 来存储配置文件(也有其他方式,比如SVN、本地文件,但最推荐的还是 Git),而且使用的是 http/https 访问的形式。
2.基本使用
2.1 准备配置文件
准备一个 Git 仓库,在 Github 上面创建了一个仓库用来存放配置文件。为了模拟生产环境,我们创建以下三个配置文件:
config-client-dev.yml
server:
port: 7778 # 端口
spring:
application:
name: config-client # 应用名称
# 自定义配置
name: config-client-dev
config-client-test.yml
server:
port: 7779 # 端口
spring:
application:
name: config-client # 应用名称
# 自定义配置
name: config-client-test
2.2 Server 端
添加依赖
只需要在 pom.xml 中 加入 spring-cloud-config-server 即可
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
配置文件
在 application.yml 中添加配置服务的基本信息以及 Git 仓库的相关信息
server:
port: 7010 # 端口
spring:
application:
name: config-server # 应用名称
cloud:
config:
server:
git:
uri: https://github.com/Error4/SpringCloudConfig-repo # 配置文件所在仓库地址
#username: # Github 等产品的登录账号
#password: # Github 等产品的登录密码
#default-label: master # 配置文件分支
#search-paths: # 配置文件所在根目录
Spring Cloud Config 也提供本地存储配置的方式。我们只需要设置属性 spring.profiles.active=native,Config Server 会默认从应用的 src/main/resource 目录下检索配置文件。也可以通过 spring.cloud.config.server.native.searchLocations=file:E:/properties/ 属性来指定配置文件的位置。
启动类
启动类添加 @EnableConfigServer,激活对配置中心的支持
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class,args);
}
}
测试
仓库中的配置文件会被转换成 Web 接口,访问可以参照以下的规则:
- /{application}/{profile}[/{label}]
- /{application}-{profile}.yml
- /{label}/{application}-{profile}.yml
- /{application}-{profile}.properties
- /{label}/{application}-{profile}.properties
上面的 URL 会映射 {application}-{profile}.yml 对应的配置文件,其中 {label} 对应 Git 上不同的分支,默认为 master。以 config-client-dev.yml 为例子,它的 application 是 config-client,profile 是 dev。
例如直接访问 http://localhost:7010/config-client-test.yml, 返回信息如下,说明配置中心服务端一切正常。
name: config-client-test
server:
port: 7779
spring:
application:
name: config-client
2.3 Client端
添加依赖
<!-- 项目依赖 -->
<dependencies>
<!-- spring cloud starter config 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- spring boot web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring boot test 依赖 -->
<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>
配置文件
bootstrap.yml
spring:
cloud:
config:
uri: http://localhost:7010 # 配置中心的具体地址,即 config-server
name: config-client # 对应 {application} 部分,需要从git上读取的资源名称,资源名一般为application.profile.yml
profile: test # 对应 {profile} 部分
label: master # 对应 {label} 部分,即 Git 的分支。如果配置中心使用的是本地存储,则该参数无用
特别注意:上面这些与 Spring Cloud Config 相关的属性必须配置在 bootstrap.yml 中,config 部分内容才能被正确加载。因为 config 的相关配置会先于 application.yml,而 bootstrap.yml 的加载也是先于 application.yml。
启动类
启动类不用修改,在 Controller 中使用 @Value 注解来获取 Server 端参数的值
@RestController
public class ConfigController {
@Value("${name}")
private String name;
@GetMapping("/name")
public String getName() {
return name;
}
}
测试
访问 http://localhost:7779/name, 返回信息如下,说明client正确加载了对应的config-client-test.yml配置文件。
config-client-test
3.高可用
传统作法
通常在生产环境,Config Server 与服务注册中心一样,我们也需要将其扩展为高可用的集群。在之前实现的 config-server 基础上来实现高可用非常简单,不需要我们为这些服务端做任何额外的配置,只需要遵守一个配置规则:将所有的 Config Server 都指向同一个 Git 仓库,这样所有的配置内容就通过统一的共享文件系统来维护,而客户端在指定 Config Server 位置时,只要配置 Config Server 外的均衡负载
注册为服务
虽然通过服务端负载均衡已经能够实现,但是作为架构内的配置管理,本身其实也是可以看作架构中的一个微服务。所以,另外一种方式更为简单的方法就是把 config-server 也注册为服务,这样所有客户端就能以服务的方式进行访问。通过这种方法,只需要启动多个指向同一 Git 仓库位置的 config-server 就能实现高可用了。
服务端改造
额外添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
在 application.yml 里新增 Eureka 的配置
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
客户端改造
额外添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
bootstrap.yml
spring:
cloud:
config:
name: config-client
profile: test
label: master
discovery:
enabled: true
service-id: config-server
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
主要是去掉了 spring.cloud.config.uri 直接指向 Server 端地址的配置,增加了最后的三个配置:
spring.cloud.config.discovery.enabled:开启 Config 服务发现支持spring.cloud.config.discovery.serviceId:指定 Server 端的 name, 也就是 Server 端spring.application.name的值eureka.client.service-url.defaultZone:指向配置中心的地址
访问 http://localhost:7779/name, 返回信息如下,说明client正确加载了对应的config-client-test.yml配置文件。
config-client-test
4.客户端动态刷新
当配置中心的配置文件内容发生改动,服务端可以动态的获取,客户端不能!
因为服务端直接从配置中心获取,而客户端是从上下文环境中获取已加载的属性,配置中心修改后,由于服务没有重启,获取的仍然是之前的属性。
所幸,它提供了一个刷新机制,但是需要我们主动触发。那就是 @RefreshScope 注解并结合 Actuator,注意要引入 spring-boot-starter-actuator。
添加依赖
Config Client 额外添加依赖
<!-- spring boot actuator 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置文件
spring:
cloud:
config:
name: config-client
profile: test
label: master
discovery:
enabled: true
service-id: config-server
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka/
# 度量指标监控与健康检查
management:
endpoints:
web:
base-path: /actuator # 访问端点根路径,默认为 /actuator
exposure:
include: '*' # 需要开启的端点,这里主要用到的是 refresh 这个端点
控制层
在需要读取配置的类上增加 @RefreshScope 注解。
@RefreshScope
@RestController
public class ConfigController {
@Value("${name}")
private String name;
@GetMapping("/name")
public String getName() {
return name;
}
}
以我的程序为例,再修改配置文件后,发送 POST 请求到 http://localhost:7779/actuator/refresh 这个接口,访问 http://localhost:7779/name,就会看到修改后的name属性。
5.在 Github 中配置 Webhook
在第四节中,尽管实现了客户端的动态刷新,但是弊端也很明显,总不能每次改了配置后,就重新访问一下 refresh 接口吧?
Github 提供了一种 Webhook 的方式,当有代码变更的时候,会调用我们设置的地址,来实现我们想达到的目的。
填写回调的地址,也就是上面提到的 actuator/refresh 这个地址,但是必须保证这个地址是可以被 Github 访问的。
6.仍然存在的问题
通过Webhook,我们进一步简化了动态刷新的复杂程度,但当存在大量客户端时,仍然显得有一些复杂,有没有更为简便灵活的方式呢?
第一种方法,我们可以借助 Spring Cloud Bus 的广播功能,让 Config Client 都订阅配置更新事件,当配置更新时,触发其中一个端的更新事件。
参考文章:SPRING CLOUD 系列之 BUS 消息总线
第二种是我更推荐的,我们可以使用其他更合适的配置中心,比如Consul、Apollo等
上文相关代码已提交至github仓库,有兴趣的朋友可以自行对比查看