Spring Cloud Config Server 节点迁移引起的问题,请格外注意这一点!
前言:
虽然强烈推荐选择使用国内开源的配置中心,如携程开源的 Apollo 配置中心、阿里开源的 Nacos 注册&配置中心。
但实际架构选型时,根据实际项目规模、业务复杂性等因素,有的项目还是会选择 Spring Cloud Config,也是 Spring Cloud 官网推荐的。特别是对性能要求也不是很高的场景,Spring Cloud Config 还算是好用的,基本能够满足需求,通过 Git 天然支持版本控制方式管理配置。
而且,目前 github 社区也有小伙伴针对 Spring Cloud Config 一些「缺陷」,开发了简易的配置管理界面,并且也已开源,如 spring-cloud-config-admin,也是超哥(程序员DD)杰作,该项目地址:https://dyc87112.github.io/spring-cloud-config-admin-doc/
本文所使用的 Spring Cloud 版本:Edgware.SR3,Spring Boot 版本:1.5.10.RELEASE
问题分析:
个人认为这个问题是有代表性的,也能基于该问题,了解到官网是如何改进的。使用 Spring Cloud Config 过程中,如果遇到配置中心服务器迁移,可能会遇到 DD 这篇博客所描述的问题:
http://blog.didispace.com/Spring-Cloud-Config-Server-ip-change-problem/
我这里大概简述下该文章中提到的问题:
当使用的 Spring Cloud Config 配置中心节点迁移或容器化方式部署(IP 是变化的),Config Server 端会因为健康检查失败报错,检查失败是因为使用的还是迁移之前的节点 IP 导致。
本文结合这个问题作为切入点,继续延伸下,并结合源码探究下原因以及改进措施。
前提条件是使用了 DiscoveryClient 服务注册发现,如果我们使用了 Eureka 作为注册中心,其实现类是 EurekaDiscoveryClient
客户端通过 Eureka 连接配置中心,需要做如下配置:
spring.cloud.config.discovery.service-id=config-server
spring.cloud.config.discovery.enabled=true
这里的关键是 spring.cloud.config.discovery.enabled 配置,默认值是 false,设置为 true 表示激活服务发现,最终会由 DiscoveryClientConfigServiceBootstrapConfiguration 启动配置类来查找配置中心服务。
接下来我们看下这个类的源码:
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false)
@Configuration
// 引入工具类自动配置类
@Import({ UtilAutoConfiguration.class })
// 开启服务发现
@EnableDiscoveryClient
public class DiscoveryClientConfigServiceBootstrapConfiguration {
@Autowired
private ConfigClientProperties config;
@Autowired
private ConfigServerInstanceProvider instanceProvider;
private HeartbeatMonitor monitor = new HeartbeatMonitor();
@Bean
public ConfigServerInstanceProvider configServerInstanceProvider(
DiscoveryClient discoveryClient) {
return new ConfigServerInstanceProvider(discoveryClient);
}
// 上下文刷新事件监听器,当服务启动或触发 /refresh 或触发消息总线的 /bus/refresh 后都会触发该事件
@EventListener(ContextRefreshedEvent.class)
public void startup(ContextRefreshedEvent event) {
refresh();
}
// 心跳事件监听器,这个监听事件是客户端从Eureka中Fetch注册信息时触发的。
@EventListener(HeartbeatEvent.class)
public void heartbeat(HeartbeatEvent event) {
if (monitor.update(event.getValue())) {
refresh();
}
}
// 该方法从注册中心获取一个配合中心的实例,然后将该实例的url设置到ConfigClientProperties中的uri字段。
private void refresh() {
try {
String serviceId = this.config.getDiscovery().getServiceId();
ServiceInstance server = this.instanceProvider
.getConfigServerInstance(serviceId);
String url = getHomePage(server);
if (server.getMetadata().containsKey("password")) {
String user = server.getMetadata().get("user");
user = user == null ? "user" : user;
this.config.setUsername(user);
String password = server.getMetadata().get("password");
this.config.setPassword(password);
}
if (server.getMetadata().containsKey("configPath")) {
String path = server.getMetadata().get("configPath");
if (url.endsWith("/") && path.startsWith("/")) {
url = url.substring(0, url.length() - 1);
}
url = url + path;
}
this.config.setUri(url);
}
catch (Exception ex) {
if (config.isFailFast()) {
throw ex;
}
else {
logger.warn("Could not locate configserver via discovery", ex);
}
}
}
}
这里会开启一个上下文刷新的事件监听器 @EventListener(ContextRefreshedEvent.class),所以当通过消息总线 /bus/refresh 或者直接请求客户端的 /refresh 刷新配置后,该事件会自动被触发,调用该类中的 refresh() 方法从 Eureka 注册中心获取配置中心实例。
这里的 ConfigServerInstanceProvider 对 DiscoveryClient 接口做了封装,通过如下方法获取实例:
@Retryable(interceptor = "configServerRetryInterceptor")
public ServiceInstance getConfigServerInstance(String serviceId) {
logger.debug("Locating configserver (" + serviceId + ") via discovery");
List<ServiceInstance> instances = this.client.getInstances(serviceId);
if (instances.isEmpty()) {
throw new IllegalStateException(
"No instances found of configserver (" + serviceId + ")");
}
ServiceInstance instance = instances.get(0);
logger.debug(
"Located configserver (" + serviceId + ") via discovery: " + instance);
return instance;
}
以上源码中看到通过 serviceId 也就是 spring.cloud.config.discovery.service-id 配置项获取所有的服务列表, instances.get(0) 从服务列表中得到第一个实例。每次从注册中心得到的服务列表是无序的。
从配置中心获取最新的资源属性是由 ConfigServicePropertySourceLocator 类的 locate() 方法实现的,继续深入到该类的源码看下具体实现:
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
// 获取当前的客户端配置属性,override作用是优先使用spring.cloud.config.application、profile、label(如果配置的话)
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService”);
// resetTemplate 可以自定义,开放了公共的 setRestTemplate(RestTemplate restTemplate) 方法。如果未设置,则使用默认的 getSecureRestTemplate(properties) 中的定义的resetTemplate。该方法中的默认超时时间是 3分5秒,相对来说较长,如果需要缩短这个时间只能自定义 resetTemplate 来实现。
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties)
: this.restTemplate;
Exception error = null;
String errorBody = null;
logger.info("Fetching config from server at: " + properties.getRawUri());
try {
String[] labels = new String[] { "" };
if (StringUtils.hasText(properties.getLabel())) {
labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
}
String state = ConfigClientStateHolder.getState();
// Try all the labels until one works
for (String label : labels) {
// 循环labels分支,根据restTemplate模板请求config属性配置中的uri,具体方法可以看下面。
Environment result = getRemoteEnvironment(restTemplate,
properties, label.trim(), state);
if (result != null) {
logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
result.getName(),
result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()),
result.getLabel(), result.getVersion(), result.getState()));
……
if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
// 设置到当前环境中的Git仓库最新版本号。
composite.addFirstPropertySource(new MapPropertySource("configClient", map));
}
return composite;
}
}
}
…… // 忽略部分源码
}
根据方法内的 uri 来源看到是从 properties.getRawUri() 获取的。
从配置中心服务端获取 Environment 方法:
private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties,
String label, String state) {
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
String token = properties.getToken();
String uri = properties.getRawUri();
……// 忽略部分源码
response = restTemplate.exchange(uri + path, HttpMethod.GET,
entity, Environment.class, args);
}
…...
Environment result = response.getBody();
return result;
}
上述分析看到从远端配置中心根据 properties.getRawUri(); 获取的固定 uri,通过 restTemplate 完成请求得到最新的资源属性。
源码中看到的 properties.getRawUri() 是一个固化的值,当配置中心迁移或者使用容器动态获取 IP 时为什么会有问题呢?
原因是当配置中心迁移后,当超过了注册中心的服务续约失效时间(Eureka 注册中心默认是 90 秒,其实这个值也并不准确,官网源码中也已注明是个 bug,这个可以后续单独文章再说)会从注册中心被踢掉,当我们通过 /refresh 或 /bus/refresh 触发这个事件的刷新,那么这个 uri 会更新为可用的配置中心实例,此时 ConfigServicePropertySourceLocator 是新创建的实例对象,所以会通过最新的 uri 得到属性资源。
但因为健康检查 ConfigServerHealthIndicator 对象以及其所依赖的ConfigServicePropertySourceLocator 对象都没有被重新实例化,还是使用服务启动时初始化的对象,所以 properties.getRawUri() 中的属性值也没有变化。
这里也就是 Spring Cloud Config 的设计缺陷,因为即使刷新配置后能够获取其中一个实例,但是并不代表一定请求该实例是成功的,比如遇到网络不可达等问题时,应该通过负载均衡方式,重试其他机器获取数据,保障最新环境配置数据一致性。
解决姿势:
github 上 spring cloud config 的 2.x.x 版本中已经在修正这个问题。实现方式也并没有使用类似 Ribbon 软负载均衡的方式,猜测可能考虑到减少框架的耦合。
在这个版本中 ConfigClientProperties 类中配置客户端属性中的 uri 字段由 String 字符串类型修改为 String[] 数组类型,通过 DiscoveryClient 获取到所有的可用的配置中心实例 URI 列表设置到 uri 属性上。
然后 ConfigServicePropertySourceLocator.locate() 方法中循环该数组,当 uri 请求不成功,会抛出 ResourceAccessException 异常,捕获此异常后在 catch 中重试下一个节点,如果所有节点重试完成仍然不成功,则将异常直接抛出,运行结束。
同时,也将请求超时时间 requestReadTimeout 提取到 ConfigClientProperties 作为可配置项。
部分源码实现如下:
private Environment getRemoteEnvironment(RestTemplate restTemplate,
ConfigClientProperties properties, String label, String state) {
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
String token = properties.getToken();
int noOfUrls = properties.getUri().length;
if (noOfUrls > 1) {
logger.info("Multiple Config Server Urls found listed.");
}
for (int i = 0; i < noOfUrls; i++) {
Credentials credentials = properties.getCredentials(i);
String uri = credentials.getUri();
String username = credentials.getUsername();
String password = credentials.getPassword();
logger.info("Fetching config from server at : " + uri);
try {
......
response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
Environment.class, args);
}
catch (HttpClientErrorException e) {
if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
throw e;
}
}
catch (ResourceAccessException e) {
logger.info("Connect Timeout Exception on Url - " + uri
+ ". Will be trying the next url if available");
if (i == noOfUrls - 1)
throw e;
else
continue;
}
if (response == null || response.getStatusCode() != HttpStatus.OK) {
return null;
}
Environment result = response.getBody();
return result;
}
return null;
}
总结:
本文主要从 Spring Cloud Config Server 源码层面,对 Config Server 节点迁移后遇到的问题,以及对此问题过程进行剖析。同时,也进一步结合源码,了解到 Spring Cloud Config 官网中是如何修复这个问题的。
当然,现在一般也都使用最新版的 Spring Cloud,默认引入的 Spring Cloud Config 2.x.x 版本,也就不会存在本文所描述的问题了。
如果你选择了 Spring Cloud Config 作为配置中心,建议你在正式上线到生产环境前,按照 「CAP理论模型」做下相关测试,确保不会出现不可预知的问题。
大家感兴趣可进一步参考 github 最新源码实现:
https://github.com/spring-cloud/spring-cloud-config
欢迎关注我的公众号,扫二维码关注获得更多精彩文章,与你一同成长~

Spring Cloud Config Server 节点迁移引起的问题,请格外注意这一点!的更多相关文章
- 为Spring Cloud Config Server配置远程git仓库
简介 虽然在开发过程,在本地创建git仓库操作起来非常方便,但是在实际项目应用中,多个项目组需要通过一个中心服务器来共享配置,所以Spring Cloud配置中心支持远程git仓库,以使分散的项目组更 ...
- ubuntu14.04 spring cloud config server + gradle搭建
Server端:在eclipse上,创建Java Project项目.自带的src包删掉手动建文件夹.基础的目录文件都创建上 |--ZSpringCloud|--build.gradle|----sr ...
- Spring Cloud Config 自动刷新所有节点 架构改造
详细参考:<Sprin Cloud 与 Docker 微服务架构实战>p162-9.9.4节 要做的改动是: 1.在spring cloud config server 服务端加入 spr ...
- Spring Cloud config之三:config-server因为server端和client端的健康检查导致服务超时阻塞问题
springcloud线上一个问题,当config-server连不上git时,微服务集群慢慢的都挂掉. 在入口层增加了日志跟踪问题: org.springframework.cloud.config ...
- Spring Cloud官方文档中文版-Spring Cloud Config(上)
官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#spring-cloud-feign 文中例子我做了一些测试在:http ...
- Spring Cloud config之一:分布式配置中心入门介绍
Spring Cloud Config为服务端和客户端提供了分布式系统的外部化配置支持.配置服务器为各应用的所有环境提供了一个中心化的外部配置.它实现了对服务端和客户端对Spring Environm ...
- Spring Cloud官方文档中文版-Spring Cloud Config(上)-服务端(配置中心)
官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#spring-cloud-feign 文中例子我做了一些测试在:http ...
- Spring Cloud(八)高可用的分布式配置中心 Spring Cloud Config
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件.在Spring Cloud中,有分布式配置中心组件spring cloud config,它支持配 ...
- 9.Spring Cloud Config统一管理微服务配置
Spring Cloud Config统一管理微服务配置 9.1. 为什么要统一管理微服务配置 9.2. Spring Cloud Config简介 Spring Cloud Config为分布式系统 ...
随机推荐
- js生成玫瑰图
html: <canvas id="c" height="600" width="600"></canvas> js ...
- Spring Boot2 系列教程(二)创建 Spring Boot 项目的三种方式
我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 43W+,如下图: 2017 年由于种种原因,就没有 ...
- 精品自用IDEA插件吐血推荐
Alibaba Java Coding Guidelines 该插件集成了Alibaba代码编程规范,可以帮你找出你的代码怪味道 .gitignore 当你使用Git作为版本工具的时候,想要快速的添加 ...
- mysql之innodb存储引擎介绍
一.Innodb体系架构 1.1.后台线程 后台任务主要负责刷新内存中的数据,保证缓冲池的数据是最近的数据,此外还将修改的数据刷新到文件磁盘,保证在数据库发生异常的情况下Innodb能恢复到正常的运行 ...
- fastjson对象,JSON,字符串,map之间的互转
1.对象与字符串之间的互转 将对象转换成为字符串 String str = JSON.toJSONString(infoDo); 字符串转换成为对象 InfoDo infoDo = JSON.pars ...
- 使用secureCRT进行linux和windows之间的nginx文件夹传输
1.首先进入secureCRT软件,新建一个链接,我现在已经创建好了进入这个页面: 注意:新建链接时里面的hostname是你linux的ip地址,使用ifconfig就可以看到 2.在secureC ...
- Mysql高手系列 - 第9篇:详解分组查询,mysql分组有大坑!
这是Mysql系列第9篇. 环境:mysql5.7.25,cmd命令中进行演示. 本篇内容 分组查询语法 聚合函数 单字段分组 多字段分组 分组前筛选数据 分组后筛选数据 where和having的区 ...
- Winform中设置DevExpress的RadioGroup的items从配置文件中加载
场景 DevExpress的RadioGroup的items选项如果是不确定的话,需要其从配置文件中加载. 实现 在项目目录下新建Config文件夹,文件夹下新建xml配置文件. <?xml v ...
- Winform中使用DevExpress时给控件添加子控件的方法
场景 在WInform中使用DevExpress时经常使用PanelControl控件用来进行布局设计,因此需要在代码中生成控件并添加子控件. 实现 一种是设置要添加的自控件的Parent属性为容器控 ...
- 简单的Socket通信(简单的在线聊天)---winform
注:本博客适合刚开始学习winform程序的初学者,大牛请绕道(跪求大牛指导文中不足) .....10w字废话自动省略,直接开始正题. 首先从最基本的建立winform开始(本项目用的Vs2017) ...