SpringCloudConfigServer配置刷新优化方案

前一文章《SpringCloudConfigServer配置中心使用与刷新详解》 介绍了Spring Cloud原生配置中心的部署方案,以及配置变更时的刷新方案。
通过该文可以看到:

  • 第一种方案无法同时刷新单个服务的所有实例
  • 第二种方案依赖于消息中间件(RabbitMQ或kafka)比较重,我测试中还碰到客户端反序列化异常导致故障的情况

基于如上2个原因,希望有一个比较轻量的配置刷新方案,思考了一下,是否可以由Client定时轮询,如果配置有更新,就自己调用自己的/actuator/refresh接口,完成配置刷新呢?
验证了一下可行性,是没有问题的:

  • Config-Server支持添加@RestController接口
  • Client调用自己的/actuator/refresh接口,就是http发请求给自己,调用localhost:端口就可以
    根据/actuator/refresh接口调用日志,在对应的类上打断点,跟踪了一下,发现该接口定义在org.springframework.cloud.endpoint.RefreshEndpoint类里,调用的是org.springframework.cloud.context.refresh.ConfigDataContextRefresher类的refresh()方法,并且该类也注册为Bean了;
    那就更简单了,直接定义一个Job,调用这个Bean的refresh()就好了

自定义方案原理介绍

自定义实现的轮询机制,大致步骤:

  • Config-Server端:
    • 提供管理API,用于开发人员更改 最近配置刷新时间
    • 提供客户端API,用于客户端定时拉取最近的配置刷新时间,并判断是否需要重新加载配置和刷新
  • Config-Client端:
    • 定时轮询Config-Server端的API,获取自己的最近的配置刷新时间;
    • 如果比上一次刷新时间大,则进行配置刷新

整体的刷新流程图如下:
在这里插入图片描述

代码实现

Config-Server端

1、定义全局时间和每个应用的时间,并提供读写方法,核心代码如下:

@Service
public class RefreshService implements InitializingBean {
    
    
    private LocalDateTime globalLastUpdateTime = LocalDateTime.now();

    private Map<String, LocalDateTime> mapAppLastUpdateTimes = new HashMap<>();

    private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-DD HH:mm:ss.SSS");

    /**
     * 客户端使用:获取指定app的配置更新时间
     */
    public String getLastUpdateTime(String appName) {
    
    
        appName = formatAppName(appName);

        LocalDateTime time = null;
        if (StringUtils.hasLength(appName)) {
    
    
            time = mapAppLastUpdateTimes.get(appName);
        }
        if (time == null) {
    
    
            time = globalLastUpdateTime;
        }
        return time.format(formatter);
    }

    /**
     * 管理端使用:设置指定app的配置更新时间,以触发该app更新配置
     */
    public void setLastUpdateTime(String appName, LocalDateTime time) {
    
    
        appName = formatAppName(appName);

        if (!StringUtils.hasLength(appName))
            throw new RuntimeException("应用名不能为空");
        if (time == null)
            time = LocalDateTime.now();

        mapAppLastUpdateTimes.put(appName, time);
        log.info("{}最后配置更新时间修改为:{}", appName, time);
    }

    /**
     * 管理端使用:设置全局配置更新时间,触发所有app的配置更新
     */
    public void setGlobalLastUpdateTime(LocalDateTime time) {
    
    
        if (time == null)
            time = LocalDateTime.now();
        globalLastUpdateTime = time;
        mapAppLastUpdateTimes.clear();
        log.info("全局最后配置更新时间修改为:{}", time);
    }

    private String formatAppName(String appName) {
    
    
        if (appName == null)
            return "";
        return appName.trim().toLowerCase();
    }

2、提供对外API

@RestController
public class DefaultController {
    
    
    private final RefreshService refreshService;

    public DefaultController(RefreshService refreshService) {
    
    
        this.refreshService = refreshService;
    }

    /**
     * 查看指定应用的的最近配置更新时间
     *
     * @param appName 应用
     * @return 时间
     */
    @GetMapping("lastTime")
    public String getTime(@RequestParam(required = false) String appName) {
    
    
        return refreshService.getLastUpdateTime(appName);
    }

    /**
     * 修改指定应用的最近配置更新时间为now
     *
     * @param appName 应用
     * @return 更新后时间
     */
    @PostMapping("lastTime")
    public String setTime(@RequestParam(required = false) String appName) {
    
    
        refreshService.setLastUpdateTime(appName, LocalDateTime.now());
        return getTime(appName);
    }

    /**
     * 修改所有应用的最近配置更新时间为now
     *
     * @return 更新后时间
     */
    @PostMapping("lastAllTime")
    public String setGlobalTime() {
    
    
        refreshService.setGlobalLastUpdateTime(LocalDateTime.now());
        return getTime(null);
    }
}

Config-Client端

1、定义feign类以获取配置更新时间

// url就是上面的Config-Server的url地址,可以写入yml配置
@FeignClient(name = "config-server", url = "http://localhost:8999")
public interface FeignConfigServer {
    
    
    @GetMapping("/lastTime")
    String getTime(@RequestParam String appName);
}

2、定时job轮询Config-Server端API,并判断是否刷新配置

@Component
@ConditionalOnProperty(value = "spring.cloud.config.refresh.enabled", havingValue = "true", matchIfMissing = true)
@RequiredArgsConstructor
@Slf4j
public class ConfigsRefresh implements InitializingBean {
    
    

    private final FeignConfigServer feignConfigServer;
    private final org.springframework.cloud.context.refresh.ConfigDataContextRefresher refresher;

    @Value("${spring.application.name:}")
    private String appName;

    private LocalDateTime lastUpdateTime = LocalDateTime.now();
    private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-DD HH:mm:ss.SSS");
    private static final Random RANDOM = new Random();

    /**
     * 每分钟检查并刷新
     */
    @Scheduled(cron = "0 * * * * *")
    public void doRefresh() {
    
    
        LocalDateTime serverUpdateTime = getServerLastUpdateTime(appName);
        if (serverUpdateTime.compareTo(lastUpdateTime) <= 0) {
    
    
            return;
        }
        log.info("本地更新时间较小:{} < {} 需要刷新配置", lastUpdateTime, serverUpdateTime);
        sleep();

        Set<String> keys = refresher.refresh();

        lastUpdateTime = serverUpdateTime;
        log.info("更新列表 {}", keys);
    }

    // 获取当前应用,配置中心的配置最近刷新时间
    private LocalDateTime getServerLastUpdateTime(String appName) {
    
    
        try {
    
    
            String serverUpdateTime = feignConfigServer.getTime(appName);
            // log.info("配置中心最近更新: {}", serverUpdateTime);
            if (StringUtils.hasLength(serverUpdateTime)) {
    
    
                return LocalDateTime.parse(serverUpdateTime, formatter);
            }
        } catch (Exception exp) {
    
    
            log.error("取配置中心更新时间出错:", exp);
        }
        return LocalDateTime.MIN;
    }

    private void sleep() {
    
    
        // 随机休眠,避免所有pod同时刷新配置
        int selectSecond = RANDOM.nextInt(10);
        try {
    
    
            Thread.sleep(selectSecond * 1000L);
        } catch (InterruptedException e) {
    
    
            log.error("休眠出错", e);
        }
    }

    @Override
    public void afterPropertiesSet() {
    
    
        lastUpdateTime = LocalDateTime.now();
        log.info("配置本地更新时间:{}", lastUpdateTime);
    }
}

Demo代码参考:

Config-Server端Demo | Config-Client端Demo

实际工作中使用步骤简述

假设Config-Server端url为 http://localhost:8999

单个应用刷新

假设需要刷新配置的应用,spring.application.name=cb-admin
1、修改配置,并提交到git(注意提交到正确的分支)
2、调用配置中心API,通知刷新,调用方式:
curl -X POST http://localhost:8999/lastTime?appName=cb-admin
3、OK,1分钟左右,该应用就会完成配置刷新

所有应用刷新

如果希望所有应用同时刷新,需要调用另一个API:
curl -X POST http://localhost:8999/lastAllTime

查看配置最近更新时间

浏览器里访问:
http://localhost:8999/lastAllTime

未解决的问题

在测试过程中,Config-Server使用了https,结果发现/actuator/refresh无法刷新配置,因为启动能正常加载配置,只是无法刷新,搞了1,2天,后面新建了一个空项目才发现是刷新时报ssl证书无法验证的问题。
可是启动时也是同样的配置加载过程,就比较奇怪。
在跟踪过程中,发现Config-Client使用的是自己内部new的RestTemplate,而不是Spring里的Bean,所以也无法通过重定义RestTemplate的Bean方式来忽略ssl证书校验,
我知道在Config-Server里,可以添加配置spring.cloud.config.server.git.skip-ssl-validation: true来忽略git的ssl证书认证,但是没找到client的类似配置,这个问题就先忽略了,没有进一步去跟踪代码。
后面有时间再研究吧,先用http方式处理了,内部调用也节约一些性能。

猜你喜欢

转载自blog.csdn.net/youbl/article/details/130888134