Давайте представим, что у нас есть простое чат-приложение, в котором фронтенд взаимодействует с бэкендом посредством rest и WebSocket’ов, используемых для общения в чате. Мы замечаем, что один инстанс приложения уже не справляется с нагрузкой.

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

Пользовательский балансировщик нагрузки

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

Здесь возможно несколько проблем:

  1. Если пользователи общаются со многими людьми одновременно, то для каждого чата необходимо открывать новое WebSocket соединение.

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

Брокер сообщений

Давайте попробуем пойти другим путем. К счастью, Spring, помимо встроенного в память брокера для WebSocket’ов, также имеет BrokerRelay, который делегирует обработку очередей стороннему брокеру.

Самое время выбрать брокера. Вариантов много, но мы будем рассматривать только самые популярные:

  1. Apache Kafka не совсем подходит в данном случае, потому что не рассчитан на большое количество динамически генерируемых очередей.

  2. Redis PUB/SUB лучше всего справляется с такого рода нагрузкой, но вы не можете получить ее из коробки для Spring.

  3. Альтернатива — RabbitMQ и ActiveMQ.

@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);
    }
}

Настройка узла брокера

Нейминг маршрутов

Выбирая брокер RabbitMQ, будьте готовы к проблемам с именованием маршрутов. Если мы используем стандартные пути со слешами, то мы можем увидеть сообщения:

2021-07-16 10:54:44.765 [error] <0.983.0> STOMP error frame sent:

Message: "Invalid destination"

Detail: "'/clusters/list-userc3f44bd2-bbff-c237-04de-4cd9375fe344' is not a valid topic destination\n"

Дело в том, что RabbitMQ не допускает слеши после стандартных маршрутов. Таким образом, если мы отправляем сообщение по маршрутам /topic/ или /queue/, то в наименовании не должно быть других слешей. Самый простой способ — это переименовать все пути фронтенда и бэкенда, заменив слеши точками.

Ситуация может усложниться, если у фронтенда (или другого приложения, подключающегося через WebSocket) другой цикл релиза. Или если вам нужно поддерживать совместимость с другими версиями. В этом случае вы можете написать интерсептор (Interceptor), который будет менять пути в сообщениях.

Но вам следует помнить, что Spring сохраняет подписки перед их отправкой в ​​MessageChannel, поэтому вам придется заменить адрес получателя на этапе отправки самому брокеру и ожидания отклика от него. Полную схему коммуникации можно увидеть в документации Spring.

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

SubscribeMapping

Если ваше приложение использует аннотацию @SubscribeMapping в контроллерах для маршрутов, делегированных внешнему брокеру (/topic/, /queue/), то это также может вызвать некоторые проблемы.

Когда Spring получает сообщение с такими путями, он сохраняет подписку, а затем отправляет сообщение брокеру, минуя контроллеры. Таким образом, метод, снабженный аннотацией @SubscribeMapping, никогда не будет вызван.

Есть два решения этой проблемы. Первое решение не требует изменения бизнес-логики, в то время как второе — требует:

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

2. Вы можете заменить @SubscribeMapping в контроллерах на REST аннотацию @GetMapping. Таким образом, данные об исходном состоянии будут получать не WebSocket’ы, а REST. Такое решение также потребует изменений на стороне фронтенда.

Заключение

Масштабирование микросервисов, использующих WebSocket’ы, может быть очень сложным, если вы не делаете оглядку на него при создании приложения. Основное решение сводится к использованию внешнего брокера сообщений. Во время первичного подключении брокера сообщений приложение может работать некорректно. Но есть только две основные проблемы (нейминг маршрутов, @SubscribeMapping), обе из которых можно решить.


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

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