Кастомное автоматическое обновление конфигураций клиентов Spring Cloud Config Server. Часть 2: настройка сервера

от автора

Продолжим описание нашего кейса и перейдем к настройкам сервера. Первую часть статьи с настройками клиента вы можете посмотреть здесь

Это будет самый обычный Spring Cloud Config Server, поэтому начнем с зависимостей:

        <dependency>             <groupId>org.springframework.cloud</groupId>             <artifactId>spring-cloud-config-server</artifactId>         </dependency> 

остальные зависимости специфично зависят от вашего приложения.

Сервер необходимо подключить в стартовом классе:

@EnableConfigServer @SpringBootApplication public class ConfigServerApplication {     public static void main(String[] args) {         SpringApplication.run(ConfigServerApplication.class, args);     } } 

Далее мы планируем, что сервер будет забирать конфигурации клиентов, в которую мы добавим параметр для версионирования конфига, проверять версию на сервере и на клиенте, и если версия изменилась — отправлять новую конфигурацию клиенту принудительно с помощью встроенного в клиент Spring Actuator. Подход сильно упрощенный, при желании его можно всячески усовершенствовать. Сейчас для нас главное — показать, как это можно сделать без дополнительных посредников.

Для этого мы добавим в конфигурацию самого сервера отдельный bean для этого, который будет создавать простейший однопоточный экзекьютор, работающий по расписанию:

@Configuration public class ApplicationConfig {     .....      @Bean     public Executor forceRefreshExecutor() {         return Executors.newSingleThreadScheduledExecutor();     } } 

Опять, здесь вы видите точку для возможных в будущем усовершенствований — никто не мешает использовать более совершенный экзекьютор сообразно задачам вашего приложения. Однако, вполне достаточно и однопоточного, это упрощает разработку, да я как-то и не вижу потребности именно в этом месте кода использовать что-то более продвинутое.

Экзекьютор будет запускать по расписанию единственный метод из класса RefereshService, который и будет выполнять для нас всю полезную работу:

@Service @EnableScheduling @Slf4j public class SchedulerService implements SchedulingConfigurer {     private final RefreshService refreshService;      @Value("${application.refreshDelayInMs}")     private int delay;      private final Executor taskExecutor;      @Autowired     public SchedulerService(RefreshService refreshService, @Qualifier("forceRefreshExecutor") Executor taskExecutor) {         this.refreshService = refreshService;         this.taskExecutor = taskExecutor;     }      @Override     public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {         taskRegistrar.setScheduler(taskExecutor);         // триггер отсчитывает время следующего запуска обновления клиентов config-server через delay ms после         // окончания предыдущего успешного обновления или от текущего времени при указанной ошибке, простая аннотация         // @Scheduled в методе refreshOnCheckConfigVersion в RefreshService считала бы время от начала предыдущего         // запуска независимо от успешности его завершения,что может привести к утечке памяти при постоянной         // ошибке запуска задачи по расписанию и медленной обработке обновления по какой-то причине         taskRegistrar.addTriggerTask(                 refreshService::refreshOnCheckConfigVersion,                 triggerContext -> {                     Optional<Date> lastCompletionTime = Optional.ofNullable(triggerContext.lastCompletionTime());                     if (lastCompletionTime.equals(Optional.empty())) {                         log.info("Не удалось получить предыдущее время запуска обновления клиентов config-server.\n" +                                 "Новый запуск обновления будет выполнен через {} ms от текущего времени.", delay);                     }                     Instant nextExecutionTime =                             lastCompletionTime.orElseGet(Date::new).toInstant()                                     .plusMillis(delay);                     Date nextRefreshDate = Date.from(nextExecutionTime);                     log.info("Следующее обновление клиентов config-server запланировано на дату {}", nextRefreshDate);                     return nextRefreshDate;                 }         );     } } 

Прошу также обратить особое внимание на комментарий в коде — он объясняет, почему мне потребовалось кастомизировать триггер для запуска задачи. Очень не рекомендую заменять кастомный триггер задачи на простой запуск по аннотации @Scheduled — можно наступить на очень неудачно кем-то брошенные в травке грабельки.

Теперь перейдем непосредственно к самому полезному методу в классе RefereshService:

@Service @Slf4j public class RefreshService {      private final Clients clients;     private final ExternalConfigService externalConfigService;     private final InternalConfigService internalConfigService;      @Autowired     public RefreshService(Clients clients, ExternalConfigService externalConfigService, InternalConfigService internalConfigService) {         this.clients = clients;         this.externalConfigService = externalConfigService;         this.internalConfigService = internalConfigService;     }      public void refreshOnCheckConfigVersion() {         for (Clients.Client client : clients.getClients()) {             try {                 InternalConfigEntity internalConfigEntity = internalConfigService.getInternalConfigByClient(client);                 ExternalConfigEntity externalConfigEntity = externalConfigService.getExternalConfigByClient(client);                  if (internalConfigEntity.isNotValid()) {                     log.info("Значение версии клиента {} на сервере не задано, будет выполнено принудительное " +                             "обновление конфигурации клиента", client.getName());                     externalConfigService.forceRefresh(client);                 } else if (externalConfigEntity.isNotValid()) {                     log.info("Значение версии клиента {} на клиенте не задано, будет выполнено принудительное " +                             "обновление конфигурации клиента", client.getName());                     externalConfigService.forceRefresh(client);                 } else if (!internalConfigEntity.getConfigVersion().equals(externalConfigEntity.getExternalConfigProperty().getValue())) {                     log.info("Значение версии клиента {} на клиенте и на сервере не совпадают, будет выполнено " +                             "принудительное обновление конфигурации клиента", client.getName());                     externalConfigService.forceRefresh(client);                 } else {                     log.info("Значение версии клиента {} на клиенте и на сервере совпадают, " +                             "конфигурация клиента не нуждается в обновлении", client.getName());                 }              } catch (InternalConfigServiceGenericException e) {                 log.error(e.getMessage());                 //TODO подключить кастомную метрику                 //На самом деле последующие catch не будут просить схлопываться с предыдущим, когда будут добавлены                 //различные метрики в каждом блоке catch             } catch (ExternalConfigServiceGenericException e) {                 log.error(e.getMessage());                 //TODO подключить кастомную метрику             } catch (Throwable e) {                 log.error(e.getMessage());                 //TODO подключить кастомную метрику, здесь отлавливаются только неучтенные в сервисах остальные ошибки             }         }     }   }  

Здесь описана вся главная логика — считываем версию конфигурации с очередного клиента, сравниваем ее с версией для него, хранящейся на сервере, и если они совпадают, запускаем обновление конфигурации на клиенте + еще несколько дополнительных вариантов, менее значимых, но тоже полезных. Разумеется, если вам нужна другая бизнес-логика реакции на версионность, просто сделайте собственную.

Дополнительные сервисы, использованные в этом классе, как раз и служат для всяких вспомогательных целей в рамках этой логики. Вы можете посмотреть их код на github по ссылке, приведенной в конце статьи.

Хранение конфигураций клиентов на сервере вы можете настроить самостоятельно где угодно в приложении, я сделал это в отдельном каталоге в корне приложения:

Соответственно, нам понадобится также файл applicaton.properties, где мы будем хранить в том числе и список клиентов централизованно (можно было сделать и посложнее, и хранить их где-то в enum-ах, но для демо приложения вполне достаточно и этого), а также класс для описания дополнительных проперти:

@Component @ConfigurationProperties(prefix = "application") @Data public class Clients {      String refreshDelayInMs;      List<Client> clients;      @Data     public static class Client {         private String name;         private String protocol;         private String url;         private String user;         private String password;          .....     } } 
spring:     application:         name: config-server     profiles:         active: native     cloud:         config:             server:                 native:                     searchLocations: file:./cfg/ #здесь нельзя менять searchLocations на другое имя, это часть спецификации config server     security:         user:             name: configserver             password: configserver             roles: ACTUATOR_ADMIN application:     refreshDelayInMs: 60000     clients:         -             name: pyramid-local.yml             protocol: http             url: localhost:8081             user: actuator             password: actuator ..... 

Обратите внимание, что параметры clients здесь — это список, и перечислять их нужно именно в таком виде — с дефисами. Имя конфига клиента, хранимого на сервере, должно совпадать с именем, под которым вы его поместили в каталог cfg, авторизация на клиенте описывалась в предыдущей статье. Авторизация на сервере также присутствует в упрощенном виде, ее можно посмотреть в файлах проекта на github.

В конце каждого конфигурационного файла (в данном случае это pyramid-local.xml) добавьте такой параметр:

В демо приложении не поддерживается необходимость в последовательной нумерации версии и возможности хранения и отката на старые версии конфигураций клиентов, всего лишь сравнивается совпадение текущей версии на клиенте и текущей версии на сервере, то есть все упрощено до предела. Версия конфигурации на клиенте получается путем обращения и парсинга ответа от Spring Actuator клиента по адресу "/actuator/env/configVersion", а само обновление конфигурации, обращением по адресу "/actuator/refresh" для соответствующего клиента. Клиентов может быть сколько угодно, главное — не забыть добавить их в список в файле application.properties. Однако, если вы не добавите его туда, обновление конфигурации, разумеется, все равно будет работать — но только не по факту изменения версии на сервере, а при очередном рестарте клиента или если вы вручную обновите конфигурацию с помощью актуатора, автоматического обновления не будет. Существенным недостатком демо реализации является примитивное версионирование — а именно, если забыть вручную поставить новую версию в новом конфиге, то автообновления не произойдет. Это тоже можно решить, если реализовать версионирование не по кастомному параметру, а просто сохранять где-нибудь в хранилище хэш нового конфига при любом его изменении и редеплое. Но это уже существенное усовершенствование, которое выходит за рамки поставленной задачи.

Код сервера можно посмотреть в github по адресу https://github.com/yamangulov/project14-config-server В нем вы также увидите код, не относящийся непосредственно к теме статьи, но все, что я здесь описал, в ней тоже имеется.

Если вам необходимо посмотреть что-то в коде клиента из предыдущей статьи, напомню, что он находится здесь.

Также хочу пригласить всех желающих на бесплатный урок от OTUS, в рамках которого будет рассмотрено, что такое REST, как пишутся REST-сервисы с использованием Spring MVC. Также будут рассмотрены вопросы применения Spring Session.

Регистрация доступна по ссылке.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/682594/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *