Давайте представим, что у нас есть простое чат-приложение, в котором фронтенд взаимодействует с бэкендом посредством rest и WebSocket’ов, используемых для общения в чате. Мы замечаем, что один инстанс приложения уже не справляется с нагрузкой.
Масштабирование микросервисов, использующих WebSocket’ы, — задача не тривиальная. При простом запуске другого инстанса с циклическим балансировщиком нагрузки по умолчанию возможна такая ситуация, когда один пользователь подключается к инстансу A, а второй пользователь подключается к инстансу B. И теперь наши бэкенды должны каким-то образом самостоятельно понять, куда отправлять входящие сообщения.
Пользовательский балансировщик нагрузки
Первое, что может прийти в голову для решения этой задачи, — это написать интеллектуальный балансировщик нагрузки, который будет перенаправлять пользователей из одного чата в один инстанс.
Здесь возможно несколько проблем:
Если пользователи общаются со многими людьми одновременно, то для каждого чата необходимо открывать новое WebSocket соединение.
Если в чате слишком много людей, то один бэкенд инстанс может не справиться. Конкретно для чата это маловероятно, мы ведь приводим упрощенный пример. В реальной жизни эта проблема не редкость.
Брокер сообщений
Давайте попробуем пойти другим путем. К счастью, Spring, помимо встроенного в память брокера для WebSocket’ов, также имеет BrokerRelay, который делегирует обработку очередей стороннему брокеру.
Самое время выбрать брокера. Вариантов много, но мы будем рассматривать только самые популярные:
Apache Kafka не совсем подходит в данном случае, потому что не рассчитан на большое количество динамически генерируемых очередей.
Redis PUB/SUB лучше всего справляется с такого рода нагрузкой, но вы не можете получить ее из коробки для Spring.
Альтернатива — 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
, никогда не будет вызван.
Есть два решения этой проблемы. Первое решение не требует изменения бизнес-логики, в то время как второе — требует:
Вы можете добавить
BeanPostProcessor
, который будет сканировать все методы с этой аннотации и вызывать их в событииSessionSubscribeEvent
. Основная проблема здесь заключается в том, что событиеSessionSubscribeEvent
возникает до отправления брокеру запроса о подписке. Поэтому возможна ситуация, когда мы вызываем метод, который что-то отправляет в топик, но сам пользователь еще не подписался на этот топик. Решение этой проблемы сложно и требует ожидания события отправки через интерсепторы.
2. Вы можете заменить @SubscribeMapping
в контроллерах на REST аннотацию @GetMapping
. Таким образом, данные об исходном состоянии будут получать не WebSocket’ы, а REST. Такое решение также потребует изменений на стороне фронтенда.
Заключение
Масштабирование микросервисов, использующих WebSocket’ы, может быть очень сложным, если вы не делаете оглядку на него при создании приложения. Основное решение сводится к использованию внешнего брокера сообщений. Во время первичного подключении брокера сообщений приложение может работать некорректно. Но есть только две основные проблемы (нейминг маршрутов, @SubscribeMapping
), обе из которых можно решить.
Материал подготовлен преподавателем OTUS в преддверии старта курса Microservice Architecture.