Коротко - да. Мы сделали это, и это работает очень хорошо.
Хитрость в том, что вы должны думать глубже, чем просто рабочие приложения API, когда дело доходит до горизонтального масштабирования. Если вам нужна архитектура push-уведомлений, она должна быть асинхронной с самого начала.
Для этого мы использовали системы очередей, а именно RabbitMQ.
Представьте себе такой сценарий формирования отчета, который может занять до 10 минут:
- Клиент подключается к нашему GraphQL API (экземпляр 1) через WebSocket
- Клиент отправляет команду для создания отчета через WebSocket
- API генерирует токен для команды и помещает команду для создания отчета в CommandQueue (в RabbitMQ), возвращая токен клиенту.
- Клиент подписывается на события результата своей команды, используя токен
- Какой-то backend Worker подхватывает команду и выполняет процедуру формирования отчета
- За это время GraphQL API (экземпляр 1) умирает
- Клиент автоматически повторно подключается к GraphQL API (экземпляр 2)
- Клиент продлевает подписку с ранее приобретенным токеном
- Рабочий процесс выполнен, результаты в EventsQueue (RabbitMQ)
- ВСЕ наши экземпляры GraphQL получают информацию о
ReportGenerationDoneEvent
и проверяют, прослушивает ли кто-нибудь его токен.
- GraphQL API (экземпляр 2) видит, что клиент ожидает результатов. Отправляет результаты через веб-сокеты.
- GraphQL API (экземпляры 3-100) игнорирует
ReportGenerationDoneEvent
.
Это довольно обширно, но с простыми абстракциями вам не нужно думать обо всей этой сложности и писать ~ 30 строк кода для нескольких сервисов для нового процесса, использующего этот маршрут.
И что в этом гениального, вы получаете хорошее горизонтальное масштабирование, воспроизводимость событий (повторные попытки), разделение задач (клиент, API, рабочие процессы), как можно быстрее отправляете данные клиенту, и, как вы упомянули, вы делаете не тратить пропускную способность на запросы are we done yet?
.
Еще одна интересная вещь заключается в том, что всякий раз, когда пользователь открывает список отчетов на нашей панели, он видит текущие генерируемые отчеты и может подписаться на их изменения, поэтому им не нужно обновлять список вручную.
Хорошая мысль о SocketCluster. Это оптимизировало бы шаг 10 в приведенном выше сценарии, но на данный момент мы не видим никаких проблем с производительностью при трансляции ReportGenerationDoneEvent
на весь кластер API. При большем количестве экземпляров или многорегиональной архитектуре это было бы обязательно, так как это позволило бы лучше масштабировать и сегментировать.
Важно понимать, что SocketCluster работает на уровне связи (WebSockets), но уровень логического API (GraphQL) выше этого. Чтобы сделать подписку GraphQL, вам просто нужно использовать протокол связи, который позволяет вам передавать информацию пользователю, и WebSockets позволяют это.
Я думаю, что использование SocketCluster — хороший выбор дизайна, но не забывайте повторять реализацию. Используйте SocketCluster только в том случае, если вы планируете открывать много сокетов в любой момент времени. Кроме того, вы должны подписываться только при необходимости, потому что WebSocket отслеживает состояние и требует управления и тактов.
Если вас больше интересует асинхронная внутренняя архитектура, которую я использовал выше, прочитайте шаблоны CQRS и Event Sourcing.
person
Marcin Natanek
schedule
02.05.2018