前言:

虽然强烈推荐选择使用国内开源的配置中心,如携程开源的 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 连接配置中心,需要做如下配置:

  1. spring.cloud.config.discovery.service-id=config-server
  2. spring.cloud.config.discovery.enabled=true

这里的关键是 spring.cloud.config.discovery.enabled 配置,默认值是 false,设置为 true 表示激活服务发现,最终会由 DiscoveryClientConfigServiceBootstrapConfiguration 启动配置类来查找配置中心服务。

接下来我们看下这个类的源码:

  1. @ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false)
  2. @Configuration
  3. // 引入工具类自动配置类
  4. @Import({ UtilAutoConfiguration.class })
  5. // 开启服务发现
  6. @EnableDiscoveryClient
  7. public class DiscoveryClientConfigServiceBootstrapConfiguration {
  8. @Autowired
  9. private ConfigClientProperties config;
  10. @Autowired
  11. private ConfigServerInstanceProvider instanceProvider;
  12. private HeartbeatMonitor monitor = new HeartbeatMonitor();
  13. @Bean
  14. public ConfigServerInstanceProvider configServerInstanceProvider(
  15. DiscoveryClient discoveryClient) {
  16. return new ConfigServerInstanceProvider(discoveryClient);
  17. }
  18. // 上下文刷新事件监听器,当服务启动或触发 /refresh 或触发消息总线的 /bus/refresh 后都会触发该事件
  19. @EventListener(ContextRefreshedEvent.class)
  20. public void startup(ContextRefreshedEvent event) {
  21. refresh();
  22. }
  23. // 心跳事件监听器,这个监听事件是客户端从Eureka中Fetch注册信息时触发的。
  24. @EventListener(HeartbeatEvent.class)
  25. public void heartbeat(HeartbeatEvent event) {
  26. if (monitor.update(event.getValue())) {
  27. refresh();
  28. }
  29. }
  30. // 该方法从注册中心获取一个配合中心的实例,然后将该实例的url设置到ConfigClientProperties中的uri字段。
  31. private void refresh() {
  32. try {
  33. String serviceId = this.config.getDiscovery().getServiceId();
  34. ServiceInstance server = this.instanceProvider
  35. .getConfigServerInstance(serviceId);
  36. String url = getHomePage(server);
  37. if (server.getMetadata().containsKey("password")) {
  38. String user = server.getMetadata().get("user");
  39. user = user == null ? "user" : user;
  40. this.config.setUsername(user);
  41. String password = server.getMetadata().get("password");
  42. this.config.setPassword(password);
  43. }
  44. if (server.getMetadata().containsKey("configPath")) {
  45. String path = server.getMetadata().get("configPath");
  46. if (url.endsWith("/") && path.startsWith("/")) {
  47. url = url.substring(0, url.length() - 1);
  48. }
  49. url = url + path;
  50. }
  51. this.config.setUri(url);
  52. }
  53. catch (Exception ex) {
  54. if (config.isFailFast()) {
  55. throw ex;
  56. }
  57. else {
  58. logger.warn("Could not locate configserver via discovery", ex);
  59. }
  60. }
  61. }
  62. }

这里会开启一个上下文刷新的事件监听器 @EventListener(ContextRefreshedEvent.class),所以当通过消息总线 /bus/refresh 或者直接请求客户端的 /refresh 刷新配置后,该事件会自动被触发,调用该类中的 refresh() 方法从 Eureka 注册中心获取配置中心实例。

这里的 ConfigServerInstanceProvider 对 DiscoveryClient 接口做了封装,通过如下方法获取实例:

  1. @Retryable(interceptor = "configServerRetryInterceptor")
  2. public ServiceInstance getConfigServerInstance(String serviceId) {
  3. logger.debug("Locating configserver (" + serviceId + ") via discovery");
  4. List<ServiceInstance> instances = this.client.getInstances(serviceId);
  5. if (instances.isEmpty()) {
  6. throw new IllegalStateException(
  7. "No instances found of configserver (" + serviceId + ")");
  8. }
  9. ServiceInstance instance = instances.get(0);
  10. logger.debug(
  11. "Located configserver (" + serviceId + ") via discovery: " + instance);
  12. return instance;
  13. }

以上源码中看到通过 serviceId 也就是 spring.cloud.config.discovery.service-id 配置项获取所有的服务列表, instances.get(0) 从服务列表中得到第一个实例。每次从注册中心得到的服务列表是无序的。

从配置中心获取最新的资源属性是由 ConfigServicePropertySourceLocator 类的 locate() 方法实现的,继续深入到该类的源码看下具体实现:

  1. @Override
  2. @Retryable(interceptor = "configServerRetryInterceptor")
  3. public org.springframework.core.env.PropertySource<?> locate(
  4. org.springframework.core.env.Environment environment) {
  5. // 获取当前的客户端配置属性,override作用是优先使用spring.cloud.config.application、profile、label(如果配置的话)
  6. ConfigClientProperties properties = this.defaultProperties.override(environment);
  7. CompositePropertySource composite = new CompositePropertySource("configService”);
  8. // resetTemplate 可以自定义,开放了公共的 setRestTemplate(RestTemplate restTemplate) 方法。如果未设置,则使用默认的 getSecureRestTemplate(properties) 中的定义的resetTemplate。该方法中的默认超时时间是 3分5秒,相对来说较长,如果需要缩短这个时间只能自定义 resetTemplate 来实现。
  9. RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties)
  10. : this.restTemplate;
  11. Exception error = null;
  12. String errorBody = null;
  13. logger.info("Fetching config from server at: " + properties.getRawUri());
  14. try {
  15. String[] labels = new String[] { "" };
  16. if (StringUtils.hasText(properties.getLabel())) {
  17. labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
  18. }
  19. String state = ConfigClientStateHolder.getState();
  20. // Try all the labels until one works
  21. for (String label : labels) {
  22. // 循环labels分支,根据restTemplate模板请求config属性配置中的uri,具体方法可以看下面。
  23. Environment result = getRemoteEnvironment(restTemplate,
  24. properties, label.trim(), state);
  25. if (result != null) {
  26. logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
  27. result.getName(),
  28. result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()),
  29. result.getLabel(), result.getVersion(), result.getState()));
  30. ……
  31. if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
  32. HashMap<String, Object> map = new HashMap<>();
  33. putValue(map, "config.client.state", result.getState());
  34. putValue(map, "config.client.version", result.getVersion());
  35. // 设置到当前环境中的Git仓库最新版本号。
  36. composite.addFirstPropertySource(new MapPropertySource("configClient", map));
  37. }
  38. return composite;
  39. }
  40. }
  41. }
  42. …… // 忽略部分源码
  43. }

根据方法内的 uri 来源看到是从 properties.getRawUri() 获取的。

从配置中心服务端获取 Environment 方法:

  1. private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties,
  2. String label, String state) {
  3. String path = "/{name}/{profile}";
  4. String name = properties.getName();
  5. String profile = properties.getProfile();
  6. String token = properties.getToken();
  7. String uri = properties.getRawUri();
  8. ……// 忽略部分源码
  9. response = restTemplate.exchange(uri + path, HttpMethod.GET,
  10. entity, Environment.class, args);
  11. }
  12. …...
  13. Environment result = response.getBody();
  14. return result;
  15. }

上述分析看到从远端配置中心根据 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 作为可配置项。

部分源码实现如下:

  1. private Environment getRemoteEnvironment(RestTemplate restTemplate,
  2. ConfigClientProperties properties, String label, String state) {
  3. String path = "/{name}/{profile}";
  4. String name = properties.getName();
  5. String profile = properties.getProfile();
  6. String token = properties.getToken();
  7. int noOfUrls = properties.getUri().length;
  8. if (noOfUrls > 1) {
  9. logger.info("Multiple Config Server Urls found listed.");
  10. }
  11. for (int i = 0; i < noOfUrls; i++) {
  12. Credentials credentials = properties.getCredentials(i);
  13. String uri = credentials.getUri();
  14. String username = credentials.getUsername();
  15. String password = credentials.getPassword();
  16. logger.info("Fetching config from server at : " + uri);
  17. try {
  18. ......
  19. response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
  20. Environment.class, args);
  21. }
  22. catch (HttpClientErrorException e) {
  23. if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
  24. throw e;
  25. }
  26. }
  27. catch (ResourceAccessException e) {
  28. logger.info("Connect Timeout Exception on Url - " + uri
  29. + ". Will be trying the next url if available");
  30. if (i == noOfUrls - 1)
  31. throw e;
  32. else
  33. continue;
  34. }
  35. if (response == null || response.getStatusCode() != HttpStatus.OK) {
  36. return null;
  37. }
  38. Environment result = response.getBody();
  39. return result;
  40. }
  41. return null;
  42. }

总结:

本文主要从 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. hihocoder [Offer收割]编程练习赛18 C 最美和弦(dp)

    题目链接:http://hihocoder.com/problemset/problem/1532 题解:一道基础的dp,设dp[i][j][k][l]表示处理到第几个数,当前是哪个和弦错了几次初始x ...

  2. Codeforces Round #481 (Div. 3) A. Remove Duplicates

    题目地址:http://codeforces.com/contest/978/problem/A 题解:给一串长度为n的数组,然后删去相同的数字(从右往左). 方法:题目n和数组ai给的范围都很小,所 ...

  3. ACM-ICPC 2017 Asia Urumqi:A. Coins(DP) 组合数学

    Alice and Bob are playing a simple game. They line up a row of nn identical coins, all with the head ...

  4. CodeForces 980 C Posterized

    Posterized 题意:将[0,255] 分成 若干段, 每一段的长度最多为k, 每一个数只能被放进一个段里, 然后每一段的数组都可以被这一段最小的数字表示, 求最小的字典序. 题解:每次一个访问 ...

  5. 【占坑】IDEA从github 导入并运行 SpringBoot + VUE项目

    最近工程实践的项目内容是开发一个类似于博客和bbs论坛的系统,在github上找了一个类似的项目可以照着写一写.所以这里先占着坑,等把后端的数据库连接学完了再来填坑. github项目链接:githu ...

  6. mapper文件中“添加一条新数据并返回此数据的ID(主键)”的方法

    在mapper文件的insert语句前加上<selectKey>标签即可 如下: 添加前测试: 添加后测试:

  7. windows update自启动解决方法

    win+r打开运行,输入services.msc打开服务面板 找到Windows update服务,将常规选项卡的启动类型改为禁用,然后选择恢复选项卡,将三个失败选项都改为无操作 win+r打开运行, ...

  8. Java验证手机号

    在实际开发中我们需要对手机号格式校验,以下是对中国手机号校验的实现. public class PhoneUtils { /** * 中国手机号码 */ private static Pattern ...

  9. Docker入门到实践——简单操作

    1.对比传统虚拟机总结 特性 容器 虚拟机 启动 秒级 分钟级 硬盘使用 一般为MB 一般为GB 性能 接近原生 弱于 系统支持量 单机支持上千个容器 一般几十个 2.基本概念 Docker包括三个基 ...

  10. Winform应用程序简介

    1.winform应用程序是一种智能客户端技术,我们可以使用winform应用程序帮助我们获得信息或传输信息等. WPF技术——XAML美化界面. 2.  F4快速属性 (1)Name:在后台要获得前 ...