Давайте представим, что мы уже написали наше Spring Boot приложение, и оно успешно работает на протяжении некоторого времени. И теперь мы понимаем, что для того, чтобы справиться с возросшей нагрузкой и повысить доступность, нам необходимо запустить несколько новых инстансов сервиса. Но мы не подумали об этом заранее, на этапе разработки.

Так что же может помешать нам просто взять и запустить еще несколько инстансов?

  1. Использование планировщиков.

  2. Использование WebSocket.

  3. Пользовательские сессии, хранящиеся в памяти.

  4. Кэш приложения - это может быть либо простой параллельный hashmap в компонентах, либо spring кэш.

Что ж, все это можно легко адаптировать с помощью соответствующих инструментов.

Планировщики

Использование распределенных планировщиков не самая тривиальная задача, которая, к тому же, требует дополнительное время для настройки и правки кода. Наша цель - как можно быстрее реализовать перехода от 1 до N одновременно работающих инстансов.

К счастью, существует библиотека Shedlock с интеграцией Spring, которая позволяет осуществить наш замысел с помощью всего лишь пары аннотаций.

Shedlock создает таблицу в вашей базе данных (поддерживается почти любое хранилище) и использует ее для координации инстансов так, что единовременно только один инстанс может использовать конкретный планировщик.

Нам необходимо включить Shedlock с блокировкой по умолчанию на 5 минут (максимум).

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "5m")
public class ApplicationConfiguration {
}

И добавить аннотацию ShedLock'а с указанием названия конкретного планировщика.

@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "myTask")
public void run() {
   // do something
}

Таким образом, в следующий цикл планировщика каждый инстанс будет пытаться записать строку о себе в таблицу блокировок, и первый, кто успеет это сделать, продолжит выполнение задачи. Все остальные пропускают цикл.

Блокировка задачи в нашем случае будет висеть максимум 5 минут. По завершении задачи инстанс снимает блокировку в таблице. Если по истечении 5 минут блокировка не была снята, другие инстансы будут считать, что этот инстанс завис во время выполнения задачи.

Websocket

Если пользователи нашего приложения могут отправлять друг другу сообщения и для реализации этой функции мы используем WebSocket, возможна такая ситуация, когда пользователь Вова подключен к инстансу A, а пользователь Алиса подключена к инстансу B. Мы оказываемся в ситуации, когда инстансы должны каким-то образом самостоятельно понимать, куда им отправлять сообщения и обмениваться ими между собой.

Написание такого уровня - это кропотливая работа, которая значительно замедляет масштабирование приложения. Вместо этого мы можем использовать готовую реализацию Spring BrokerRelay, которая использует внешнего брокера (RabbitMQ, ActiveMQ и т. д.) для обработки WebSocket, управления подписками и т. д.

@Configuration
public class WebSocketConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue")
            .setClientLogin("guest")
            .setClientPasscode("guest")
            .setSystemLogin("guest")
            .setSystemPasscode("guest")
            .setRelayHost("127.0.0.1")
            .setRelayPort(61613);
    }
}

В этом случае все сообщения в /topic и /queue будут пересылаться внешнему брокеру.  С детальной схемой взаимодействия можно ознакомиться в документации Spring.

Пользовательские сессии

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

Но для JDBC, достаточно добавить всего одну зависимость.

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>

И установить свойство в application.properties:

spring.session.store-type=jdbc

Однако, в данном случае, не стоит забывать, что данные сессии будут храниться в виде блобов, и иногда во время обновления Spring, вам необходимо будет чистить сессии из-за проблем с десериализацией старых объектов.

Кэш приложения

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

Чтобы все инстансы использовали общий кэш, стоит подключить внешнее хранилище, например Redis, Apache Ignite, Hazelcast и т.д.

В случае использования Spring-Cache или Redis достаточно добавить одну зависимость.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

После этого с аннотацией @EnableCaching, Spring Boot автоматически активирует Redis в качестве конфигурации по умолчанию.

Например, его можно настроить так, чтобы установить разное время работы для каждого модуля.

@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
    return builder -> builder
      .withCacheConfiguration(“AccountCache",
        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(60)))
      .withCacheConfiguration(“ProductCache",
        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(120)));
}

Заключение

Таким образом, даже если вы не задумывались о масштабировании при создании приложения, существует набор инструментов, которые помогут сделать возможным переход от 1 к N одновременно запущенных инстансов без переписывания бизнес-логики самого сервиса.


Материал подготовлен в рамках курса «Microservice Architecture».

Всех желающих приглашаем на открытый урок «Паттерны аутентификации и авторизации». На занятии будет рассказано про различные паттерны аутентификации и авторизации. Рассмотрена сессионная аутентификация на основе кук и на основе токенов (jwt), работа identity провайдеров.
>> РЕГИСТРАЦИЯ

Комментарии (1)