SpringCloudConfigServer configuration refresh optimization scheme

The previous article "Detailed Explanation of the Use and Refresh of the SpringCloudConfigServer Configuration Center" introduced the deployment scheme of the Spring Cloud native configuration center and the refresh scheme when the configuration changes.
Through this article you can see:

  • The first option cannot refresh all instances of a single service at the same time
  • The second solution relies heavily on the message middleware (RabbitMQ or Kafka). In my test, I also encountered a situation where the client deserialization exception caused a failure.

Based on the above two reasons, I hope to have a relatively lightweight configuration refresh solution. After thinking about it, is it possible for the client to periodically poll, and if the configuration is updated, call its own interface to complete the configuration refresh /actuator/refresh?
After verifying the feasibility, there is no problem:

  • Config-Server supports adding @RestControllerinterfaces
  • Client calls its own /actuator/refreshinterface, that is, http sends a request to itself, and calls localhost: port to call the log
    according to /actuator/refreshthe interface, break the point on the corresponding class, track it, and find that the interface is defined in org.springframework.cloud.endpoint.RefreshEndpointthe class, and the call is org.springframework.cloud.context.refresh.ConfigDataContextRefresherof the class refresh()method, and the class is also registered as a Bean;
    it is even simpler, just define a Job directly and call this refresh()Bean

Introduction to the principle of custom solutions

The polling mechanism of the custom implementation, the general steps:

  • Config-Server端:
    • Provide management API for developers to change the latest configuration refresh time
    • Provide a client API for the client to regularly pull the latest configuration refresh time and determine whether to reload the configuration and refresh
  • Config-Client端:
    • Regularly poll the API on the Config-Server side to obtain the latest configuration refresh time;
    • If it is greater than the last refresh time, perform a configuration refresh

The overall refresh flow chart is as follows:
insert image description here

Code

Config-Server端

1. Define the global time and the time of each application, and provide read and write methods. The core code is as follows:

@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. Provide external 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. Define the feign class to obtain the configuration update time

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

2. The scheduled job polls the Config-Server API and determines whether to refresh the configuration

@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 code reference:

Config-Server端Demo | Config-Client端Demo

A brief description of the steps used in actual work

Suppose the Config-Server url is http://localhost:8999

Single App Refresh

Suppose the application that needs to refresh the configuration, spring.application.name= cb-admin
1. Modify the configuration and submit it to git (note that it is submitted to the correct branch)
2. Call the API of the configuration center to notify the refresh. Call method:
curl -X POST http://localhost:8999/lastTime?appName= cb-admin
3. OK, in about 1 minute, the application will complete the configuration refresh

All apps refresh

If you want all apps to refresh at the same time, you need to call another API:
curl -X POST http://localhost:8999/lastAllTime

View configuration latest update time

Access in browser:
http://localhost:8999/lastAllTime

unresolved issues

During the test, Config-Server used https, and found that /actuator/refreshthe configuration could not be refreshed, because the configuration could be loaded normally at startup, but it could not be refreshed. After 1 or 2 days, a new empty project was created later and it was found that the ssl certificate could not be refreshed. Validation problem.
But it is also the same configuration loading process at startup, which is quite strange.
During the tracking process, I found that Config-Client uses its own internal new RestTemplate, not the Bean in Spring, so it is impossible to RestTemplateignore the ssl certificate verification by redefining the Bean method.
I know that in Config-Server, you can add Configured spring.cloud.config.server.git.skip-ssl-validation: trueto ignore git's ssl certificate authentication, but did not find a similar configuration of the client, this problem was ignored first, and the code was not further tracked.
Let's study it later when we have time, first use http method to deal with it, and internal calls also save some performance.

Guess you like

Origin blog.csdn.net/youbl/article/details/130888134