Config 入门教程 - 高级篇

大纲

大纲

前言

版本说明

在下面的的教程中,使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,特别声明除外。

Config 高可用

对于线上的生产环境,通常对其都是有很高的要求,其中高可用是不可或缺的一部分,必须要保证服务是可用状态,才能保证系统更好地运行,这是业务稳定的保证。

Config 客户端高可用

对于客户端的高可用,这里的方案主要还是用 File 的形式,本质与 “客户端回退” 的思路大体一致。客户端高可用主要是解决当服务端不可用的情况下,客户端依然可以正常启动。从客户端的角度出发,不是增加配置中心的高可用性,而是降低客户端对配置中心的依赖程度,从而提高整个分布式架构的健壮性。客户端加载配置的高可用流程图如下,点击下载完整的案例代码。

config-client-ha

1. 准备工作

由于下面的 Spring Cloud Config 使用 Git 作为存储方式,因此需要提前在 Git 远程仓库(Github、Gitlab)中创建对应的仓库,然后往仓库里 Push 三个配置文件,分别是 config-client-dev.yml、config-client-prod.yml、config-client-test.yml,配置文件的内容如下:

1
2
3
4
5
6
server:
port: 9001

cn:
springcloud:
config: I am the git configuration file from dev environment
1
2
3
4
5
6
server:
port: 9002

cn:
springcloud:
config: I am the git configuration file from prod environment
1
2
3
4
5
6
server:
port: 9003

cn:
springcloud:
config: I am the git configuration file from test environment

2. 创建 Config Client HA AutoConfig 工程

创建 Config Client HA AutoConfig 工程,配置工程里的 pom.xml 文件:

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

创建 Config Client HA AutoConfig 工程里的配置属性加载类:

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
@Component
@ConfigurationProperties(prefix = ConfigSupportProperties.CONFIG_PREFIX)
public class ConfigSupportProperties {

public static final String CONFIG_PREFIX = "spring.cloud.config.backup";
private final String DEFAULT_FILE_NAME = "fallback.properties";
private boolean enable = false;
private String fallbackLocation;

public boolean isEnable() {
return enable;
}

public void setEnable(boolean enable) {
this.enable = enable;
}

public String getFallbackLocation() {
return fallbackLocation;
}

public void setFallbackLocation(String fallbackLocation) {
// 如果只是填写路径, 就添加上一个默认的文件名
if (fallbackLocation.indexOf(".") == -1) {
this.fallbackLocation = fallbackLocation + DEFAULT_FILE_NAME;
return;
}
this.fallbackLocation = fallbackLocation;
}
}

创建 Config Client HA AutoConfig 工程里的自动配置类,该类主要的作用是判断 Config Server 端的配置信息是否可用,如果不能用将读取加载本地备份配置文件进行启动。需要注意的是启动顺序的设置,这是因为 Spring Cloud 使用的 PropertySourceBootstrapConfiguration 启动顺序为 private int order = -2147483638,order 的值越小越先加载,所以下述的 orderNum 只要加上一个整数比其大即可:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
@Configuration
@EnableConfigurationProperties(ConfigSupportProperties.class)
public class ConfigSupportConfiguration implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

private final Logger LOGGER = LoggerFactory.getLogger(ConfigSupportConfiguration.class);
private final Integer orderNum = Ordered.HIGHEST_PRECEDENCE + 11;

@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = Collections.EMPTY_LIST;

@Autowired
private ConfigSupportProperties configSupportProperties;

@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {

if (!isHasCloudConfigLocator(this.propertySourceLocators)) {
LOGGER.info("未启用Config Server管理配置");
return;
}

LOGGER.info("检查Config Service配置资源");
ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
LOGGER.info("加载PropertySources源:" + propertySources.size() + "个");

if (!configSupportProperties.isEnable()) {
LOGGER.warn("未启用配置备份功能,可使用{}.enable打开", ConfigSupportProperties.CONFIG_PREFIX);
return;
}

if (isCloudConfigLoaded(propertySources)) {
PropertySource cloudConfigSource = getLoadedCloudPropertySource(propertySources);
LOGGER.info("成功获取ConfigService配置资源");
Map<String, Object> backupPropertyMap = makeBackupPropertyMap(cloudConfigSource);
doBackup(backupPropertyMap, configSupportProperties.getFallbackLocation());
LOGGER.info("成功备份ConfigService配置资源");
} else {
LOGGER.error("获取ConfigService配置资源失败");
Properties backupProperty = loadBackupProperty(configSupportProperties.getFallbackLocation());
if (backupProperty != null) {
HashMap backupSourceMap = new HashMap<>(backupProperty);

PropertySource backupSource = new MapPropertySource("backupSource", backupSourceMap);
propertySources.addFirst(backupSource);
LOGGER.warn("使用备份的配置启动:{}", configSupportProperties.getFallbackLocation());
}
}
}

@Override
public int getOrder() {
return orderNum;
}

/**
* 是否启用了Spring Cloud Config获取配置资源
*
* @param propertySourceLocators
* @return
*/
private boolean isHasCloudConfigLocator(List<PropertySourceLocator> propertySourceLocators) {
for (PropertySourceLocator sourceLocator : propertySourceLocators) {
if (sourceLocator instanceof ConfigServicePropertySourceLocator) {
return true;
}
}
return false;
}

/**
* 是否启用Cloud Config
*
* @param propertySources
* @return
*/
private boolean isCloudConfigLoaded(MutablePropertySources propertySources) {
if (getLoadedCloudPropertySource(propertySources) == null) {
return false;
}
return true;
}

/**
* 获取加载的Cloud Config配置项
*
* @param propertySources
* @return
*/
private PropertySource getLoadedCloudPropertySource(MutablePropertySources propertySources) {
if (!propertySources.contains(PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return null;
}

PropertySource propertySource = propertySources.get(PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME);
if (propertySource instanceof CompositePropertySource) {
for (PropertySource<?> source : ((CompositePropertySource) propertySource).getPropertySources()) {
if (source.getName().equals("configService")) {
return source;
}
}
}
return null;
}

/**
* 生成备份的配置数据
*
* @param propertySource
* @return
*/
private Map<String, Object> makeBackupPropertyMap(PropertySource propertySource) {
Map<String, Object> backupSourceMap = new HashMap<>();
if (propertySource instanceof CompositePropertySource) {
CompositePropertySource composite = (CompositePropertySource) propertySource;
for (PropertySource<?> source : composite.getPropertySources()) {
if (source instanceof MapPropertySource) {
MapPropertySource mapSource = (MapPropertySource) source;
for (String propertyName : mapSource.getPropertyNames()) {
// 前面的配置覆盖后面的配置
if (!backupSourceMap.containsKey(propertyName)) {
backupSourceMap.put(propertyName, mapSource.getProperty(propertyName));
}
}
}
}
}
return backupSourceMap;
}

/**
* 生成备份文件
*
* @param backupPropertyMap
* @param filePath
*/
private void doBackup(Map<String, Object> backupPropertyMap, String filePath) {
FileSystemResource fileSystemResource = new FileSystemResource(filePath);
File backupFile = fileSystemResource.getFile();
try {
if (!backupFile.exists()) {
backupFile.createNewFile();
}
if (!backupFile.canWrite()) {
LOGGER.error("无法读写文件:{}", fileSystemResource.getPath());
}

Properties properties = new Properties();
Iterator<String> keyIterator = backupPropertyMap.keySet().iterator();
while (keyIterator.hasNext()) {
String key = keyIterator.next();
properties.setProperty(key, String.valueOf(backupPropertyMap.get(key)));
}

FileOutputStream fos = new FileOutputStream(fileSystemResource.getFile());
properties.store(fos, "Backup Cloud Config");
} catch (IOException e) {
LOGGER.error("文件操作失败:{}", fileSystemResource.getPath());
e.printStackTrace();
}
}

/**
* 加载本地文件
*
* @param filePath
* @return
*/
private Properties loadBackupProperty(String filePath) {
PropertiesFactoryBean propertiesFactory = new PropertiesFactoryBean();
Properties props = new Properties();
try {
FileSystemResource fileSystemResource = new FileSystemResource(filePath);
propertiesFactory.setLocation(fileSystemResource);

propertiesFactory.afterPropertiesSet();
props = propertiesFactory.getObject();

} catch (IOException e) {
e.printStackTrace();
return null;
}
return props;
}
}

创建 Config Client HA AutoConfig 工程里 /src/main/resources/META-INF/spring.factories 配置文件,添加上面的自动配置类:

1
2
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.springcloud.study.config.ConfigSupportConfiguration

3. 创建 Config Client 工程

创建 Config Client 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入上面的 config-client-ha-autoconfig

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>com.springcloud.study</groupId>
<artifactId>config-client-ha-autoconfig</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

创建 Config Client 的主启动类:

1
2
3
4
5
6
7
@SpringBootApplication
public class ConfigClientApplication {

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

为了更好地观察拉取到的 Git 上面的配置,这里需要创建一个 Controller 用于访问返回配置信息,同时还需要创建一个实体,用于注入远程配置上的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@ConfigurationProperties(prefix = "cn.springcloud")
public class ConfigProperties {

private String config;

public String getConfig() {
return config;
}

public void setConfig(String config) {
this.config = config;
}
}
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ConfigController {

@Autowired
public ConfigProperties configProperties;

@GetMapping("/getConfigInfo")
public String getConfigInfo() {
return configProperties.getConfig();
}
}

添加 Config Client 需要的 application.yml 配置文件到工程中:

1
2
3
spring:
application:
name: config-client

添加 Config Client 需要的 bootstrap.yml 配置文件到工程中,enable 表示是否启动加载远程配置信息进行本地备份,fallbackLocation 表示本地备份的路径,也可以是路径加上文件名:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
config:
name: config-client #需要从远程Git仓库读取的配置文件的名称,注意没有"yml"文件后缀,可以写多个,通过逗号隔开
profile: dev #本次访问的配置项
label: master #Git分支的名称
uri: http://127.0.0.1:8001 #Config Server的地址
backup:
enable: true
fallbackLocation: /tmp/config/config-client-dev/fallback.properties #备份配置文件的路径

4. 创建 Config Server 工程

创建 Config Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-config-server

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

创建 Config Server 的主启动类,增加 @EnableConfigServer 注解:

1
2
3
4
5
6
7
8
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

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

添加 Config Server 需要的 application.yml 配置文件到工程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8001

spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: git@github.com:xxxxx/spring-cloud-config-study-repo.git
search-paths: spring-cloud-config-study-repo/
strictHostKeyChecking: false
private_key_file: /root/.ssh/id_rsa.pub
label: master

5. 测试结果

  1. 依次启动 config-server、config-client 应用
  2. 访问 http://127.0.0.1:9001/getConfigInfo 后观察返回的配置信息,与此同时查看是否在目录 /tmp/config/config-client-dev/ 下成功创建了备份文件 fallback.properties
  3. 关闭 config-server、config-client 应用,然后单独启动 config-client 应用;观察在不启动 config-server 的情况下,config-client 应用是否能正常启动
  4. 若 config-client 应用单独启动成功,config-client 应用会先尝试去连接 config-server,当连接失败后,会加载本地的备份文件,此时控制台输出的日志信息如下:
1
2
3
4
5
6
7
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://127.0.0.1:8001
c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://127.0.0.1:8001. Will be trying the next url if available
c.c.c.ConfigServicePropertySourceLocator : Could not locate PropertySource: I/O error on GET request for "http://127.0.0.1:8001/config-client/dev/master": 拒绝连接; nested exception is java.net.ConnectException: 拒绝连接
c.s.s.config.ConfigSupportConfiguration : 检查Config Service配置资源
c.s.s.config.ConfigSupportConfiguration : 加载PropertySources源:10个
c.s.s.config.ConfigSupportConfiguration : 获取ConfigService配置资源失败
c.s.s.config.ConfigSupportConfiguration : 使用备份的配置启动:/tmp/config/config-client-dev/fallback.properties

Config 服务端高可用

Config Server 一样需要在生成环境下保证高可用的,这里将通过结合 Eureka 注册中心的方式搭建 Config Server 的高可用,即通过 Ribbon 的客户端负载均衡选择一个 Config Server 进行连接来获取配置信息,具体的流程如下,点击下载完整的案例代码。对于 Eureka 的高可用这里也不进行详解,详细关于 Eureka 的高可用可参考 Eureka 集群配置

config-server-ha

1. 准备说明

本示例用到上面的 “客户端高可用” 示例中 Git 仓库里的配置文件,包括 config-client-dev.yml、config-client-prod.yml、config-client-test.yml。

2. 创建 Eureka Server 工程

创建 Eureka Server 的 Maven 工程,配置工程里的 pom.xml 文件,引入 spring-cloud-starter-netflix-eureka-server

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

创建 Eureka Server 的启动主类,这里添加相应注解,作为程序的入口:

1
2
3
4
5
6
7
8
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

添加 Eureka Server 需要的 application.yml 配置文件到工程的 src/main/resources 目录下:

1
2
3
4
5
6
7
8
9
10
11
server:
port: 7001

eureka:
instance:
hostname: localhost #Eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己
fetch-registry: false #false表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3. 创建 Config Server 工程

创建 Config Server 的 Maven 工程,配置工程里的 pom.xml 文件,由于 Config Sever 需要注册到 Eureka Server,所以需要另外添加 Eureka Client 的依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

创建 Config Server 的主启动类,增加 @EnableConfigServer@EnableDiscoveryClient 注解:

1
2
3
4
5
6
7
8
9
@EnableConfigServer
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigServerApplication {

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

添加 Config Server 需要的 application.yml 配置文件到工程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
port: 8001

spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: git@github.com:xxxxx/spring-cloud-config-study-repo.git
search-paths: spring-cloud-config-study-repo/
strictHostKeyChecking: false
private_key_file: /root/.ssh/id_rsa.pub
label: master

eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: ${spring.application.name}-${server.port} #自定义服务名称
prefer-ip-address: true #将IP注册到Eureka Server上,若不配置默认使用机器的主机名

4. 创建 Config Client 工程

创建 Config Client 的 Maven 工程,配置工程里的 pom.xml 文件,由于 Config Client 需要注册到 Eureka Server,所以需要另外添加 Eureka Client 的依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

创建 Config Client 的主启动类,增加 @EnableDiscoveryClient 注解:

1
2
3
4
5
6
7
8
9
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigClientApplication {

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

}

为了更好地观察拉取到的 Git 上面的配置,这里需要创建一个 Controller 用于访问返回配置信息,同时还需要创建一个实体,用于注入远程配置上的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@ConfigurationProperties(prefix = "cn.springcloud")
public class ConfigProperties {

private String config;

public String getConfig() {
return config;
}

public void setConfig(String config) {
this.config = config;
}
}
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ConfigController {

@Autowired
public ConfigProperties configProperties;

@GetMapping("/getConfigInfo")
public String getConfigInfo() {
return configProperties.getConfig();
}
}

添加 Config Client 需要的 application.yml 配置文件到工程中:

1
2
3
spring:
application:
name: config-client

添加 Config Client 需要的 bootstrap.yml 配置文件到工程中,这里不再使用 spring.cloud.config.uri 参数直接指向 Config Server 端的连接地址,而是增加了下述三个参数:

  • spring.cloud.config.discovery.enabled:开启 Config Client 的服务发现支持
  • spring.cloud.config.discovery.service-id:指定 Config Server 端的 serviceId,也就是 Config Server 端的 spring.application.name 参数值
  • eureka.client.service-url.defaultZone: 指向 Eureka 注册中心的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
config:
name: config-client #需要从远程Git仓库读取的配置文件的名称,注意没有"yml"文件后缀,可以写多个,通过逗号隔开
profile: dev #本次访问的配置项
label: master #Git分支的名称
discovery:
enabled: true
service-id: config-server

eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: config-client-${server.port} #自定义服务名称
prefer-ip-address: true #将IP注册到Eureka Server上,若不配置默认使用机器的主机名

5. 测试

  1. 通过 maven install 命令将各个应用安装到本地,然后再使用命令行启动各个应用,当然也可以直接在 IDEA、Eclipse 里启动,具体的命令如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 启动Eureka
java -jar eureka-server-1.0-SNAPSHOT.jar

# 启动Config Server(默认端口:8001)
java -jar config-server-1.0-SNAPSHOT.jar

# 启动Config Server
java -jar config-server-1.0-SNAPSHOT.jar --server.port=8002

# 启动Config Config(默认端口:9001)
java -jar config-client-1.0-SNAPSHOT.jar

# 启动Config Config
java -jar config-client-1.0-SNAPSHOT.jar --server.port=9002
  1. 当两个 Config Client 应用启动完成后,查看控制台输出的日志信息,看看是否已经负载了;如果没有负载到也没关系,可以启动多个 Config Client 实例再试试
  2. 浏览器访问 http://127.0.0.1:9001/getConfigInfohttp://127.0.0.1:9002/getConfigInfo,观察是否可以正确返回配置信息

Config 源码解析(待续)