前言:

虽然强烈推荐选择使用国内开源的配置中心,如携程开源的 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 节点迁移引起的问题,请格外注意这一点!的更多相关文章

  1. 为Spring Cloud Config Server配置远程git仓库

    简介 虽然在开发过程,在本地创建git仓库操作起来非常方便,但是在实际项目应用中,多个项目组需要通过一个中心服务器来共享配置,所以Spring Cloud配置中心支持远程git仓库,以使分散的项目组更 ...

  2. ubuntu14.04 spring cloud config server + gradle搭建

    Server端:在eclipse上,创建Java Project项目.自带的src包删掉手动建文件夹.基础的目录文件都创建上 |--ZSpringCloud|--build.gradle|----sr ...

  3. Spring Cloud Config 自动刷新所有节点 架构改造

    详细参考:<Sprin Cloud 与 Docker 微服务架构实战>p162-9.9.4节 要做的改动是: 1.在spring cloud config server 服务端加入 spr ...

  4. Spring Cloud config之三:config-server因为server端和client端的健康检查导致服务超时阻塞问题

    springcloud线上一个问题,当config-server连不上git时,微服务集群慢慢的都挂掉. 在入口层增加了日志跟踪问题: org.springframework.cloud.config ...

  5. Spring Cloud官方文档中文版-Spring Cloud Config(上)

    官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#spring-cloud-feign 文中例子我做了一些测试在:http ...

  6. Spring Cloud config之一:分布式配置中心入门介绍

    Spring Cloud Config为服务端和客户端提供了分布式系统的外部化配置支持.配置服务器为各应用的所有环境提供了一个中心化的外部配置.它实现了对服务端和客户端对Spring Environm ...

  7. Spring Cloud官方文档中文版-Spring Cloud Config(上)-服务端(配置中心)

    官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#spring-cloud-feign 文中例子我做了一些测试在:http ...

  8. Spring Cloud(八)高可用的分布式配置中心 Spring Cloud Config

    在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件.在Spring Cloud中,有分布式配置中心组件spring cloud config,它支持配 ...

  9. 9.Spring Cloud Config统一管理微服务配置

    Spring Cloud Config统一管理微服务配置 9.1. 为什么要统一管理微服务配置 9.2. Spring Cloud Config简介 Spring Cloud Config为分布式系统 ...

随机推荐

  1. 共价大爷游长沙 lct 维护子树信息

    这个题目的关键就是判断 大爷所有可能会走的路 会不会经过询问的边. 某一条路径经过其中的一条边, 那么2个端点是在这条边的2测的. 现在我们要判断所有的路径是不是都经过 u -> v 我们以u为 ...

  2. yzoj P2343 & 洛谷 P1437 [HNOI2004]敲砖块

    题意 在一个凹槽中放置了N层砖块,最上面的一层油N块砖,从上到下每层一次减少一块砖.每块砖都有一个分值,敲掉这块砖就能得到相应的分值,如图所示. 如果你想敲掉第i层的第j块砖的话,若i=1,你可以直接 ...

  3. Oracle sqlldr 在DOS窗口导入多列数据到数据库表

    sqlldr 用法详见:https://www.cnblogs.com/rootq/archive/2009/03/01/1401061.html 测试内容: 1.创建数据库表: create tab ...

  4. mysql 查询参数尾部有空格时被忽略

    最近再使用mysql时,无意见发现,当我查询参数尾部输入若干个空格时,依然可以查出和不加空格相同的结果,像这样 select * from wa where name='be ' 等同于 select ...

  5. Optional和Stream的map与flatMap

    Optional的map和flatMap Optional存在map和flatMap方法.map源码如下 public<U> Optional<U> map(Function& ...

  6. postman--请求以及变量设置的实例练习

    我们可以在2个地方添加需要执行的js脚本,一个是Pre-request Script,还有一个tests,我们先看请求之前的 1 在请求被发送到服务器之前:就是在“Pre-request Script ...

  7. 固定定位下的div水平居中

    发现了一个之前未留意的知识点,做个笔记. 当一个块级元素的父元素开启了flex布局后,我们可以很轻松的将这个元素居中对齐,可以在父元素上加 justify-content: center; align ...

  8. 行内元素有哪些?块级元素有哪些?空(void)元素有哪些?

    CSS规范规定,每个元素都有display属性,确定该元素的类型.每个元素都有默认的display值,如div的display默认值为“block”,则为“块级”元素:span默认display属性值 ...

  9. 自荐RedisViewer有情怀的跨平台Redis可视化客户端工具

    # **自荐一个有情怀的跨平台Redis可视化客户端工具——RedisViewer**[转载自 最美分享Coder 2019-09-17 06:31:00](https://www.toutiao.c ...

  10. php将图片存储在阿里云oss存储上

    创建两个方法 1.上传方法 use OSS\OssClient; use think\Config; use OSS\Core\OssException; /** * 存储文件 * * @param ...