Как стать автором
Обновить

SSE, нотификации, Node.js и при чём тут C#?

Время на прочтение4 мин
Количество просмотров8.7K

Вписавшись в очередной, провальный заранее, стартап, мне прилетела задача: нужны уведомления на сайте. Ладно - сказал я себе. Открываю любимую IDE и начинаю писать очередной микросервис.

До этого я никогда не занимался уведомлениями, но был осведомлен, что есть для этого несколько путей: WebSocket, SSE и Long Polling.

https://imgflip.com/i/30mye1 Сначала я был Базом, а потом всё же стал Вуди
https://imgflip.com/i/30mye1 Сначала я был Базом, а потом всё же стал Вуди

Планирование

Изучив статистику по браузерам наших пользователей, я принял решение использовать SSE (Server Sent Events). Приложению всё равно на вряд ли понадобится отправлять по WebSocket данные, для этого есть наш API над HTTP. А Long Polling никак не стандартизирован.

SSE по своей технологии очень прост. Это долговисящий HTTP запрос, но, благодаря заголовку о MIME-типе Content-Type: text/event-stream , сервер и клиент не обрывают соединение по тайм-ауту. И в ответ по этому потоку нам приходят данные о событиях в специальном формате по стандарту. 

event: task-updated
data: {"id":"236c2259-a5f4-4f87-bc5d-2c6d00bd0875","title":"New title"}

Разработка

Написал я этот микросервис, наверное, за часа 2-3, как-никак писать приложения на Node.js получается довольно быстро. По логике всё очень просто.

  1. Принимаем запрос.

  2. Сохраняем соединение в массив, который находится в словаре с ключом, который в данной ситуации является идентификатором пользователя.

  3. Слушаем очередь RabbitMQ, по прибытии сообщения отправляем событие по тем самым сохраненным соединениям пользователя, взяв их из словаря по идентификатору. 

Есть уже куча туториалов о том, как использовать SSE на стороне бэкенда. Поэтому не вижу смысла показывать конкретный код. Не обижайтесь.

Первые проблемы

Всё в проде работало хорошо и не предвещало беды. Но программирование, оно такое - без проблем никогда не получается. Произошёл наплыв пользователей и наш микросервис не мог физически быстрее обрабатывать сообщения из очереди. Вследствие чего, уведомления стали приходить с заметной задержкой. Ладно, давайте масштабировать, сделаем кластер из инстансов - подумал я. Всё было просто до того момента, когда я вспомнил, что так называемые сессии пользователей (сокеты, держащие SSE потоки) хранятся в словаре. И при репликации процессов соединения делятся между инстансами, а как именно - только одному процессору известно. Немного погуглив, понял, что можно через IPC канал отправлять сообщения процессам, чтобы хоть как-то синхронизировать соединения пользователей. Но есть небольшое НО, передавать можно в сообщении только те данные и объекты, которые можно сериализовать.

message
Это может быть любое значение или объект JavaScript, которые может обработать алгоритм структурированного клонирования, поддерживающий циклические ссылки.

MDN Web Docs

А с сокетами такое не прокатит, так как соединения нельзя просто взять и склонировать, чтобы можно было пошарить их между процессами. И я решил, что лучшим способом будет отправлять остальным инстансам то самое сообщение, которое приходит из очереди RabbitMQ.

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

Что мы имеем на данном этапе? В одном инстансе Node.js приложения у нас держатся подключения пользователей (SSE), прослушивается очередь RabbitMQ, и к этому добавилась прослушка IPC канала. Инстансов - N штук. Вроде всё заработало, но ненадолго...

Проблема усиливается

Внимательные читатели поняли, что одно уведомление для пользователя будет обрабатываться всеми инстансами нашего микросервиса. И поэтому в этот же день, когда я выкатил, как думал, решение, всё так и продолжило медленно работать. Я понял, что нужно работать с единой точкой в памяти, где держатся соединения SSE. Обычно первое, что при таком случае приходит на ум - это многопоточность. 

На собеседованиях некоторые компании спрашивают про многопоточность на нашей любимой Node.js. Мы все смело отвечаем, что да, она есть - worker threads. Но многопоточность всё же не заканчивается на параллельности. Помимо неё нам нужна общая память между потоками, хоть это и корень всех бед в работе многопоточных приложений. Но, как назло, в Node.js потоки изолированы друг от друга. Да, мы можем слать сообщения, но как и при реализации с кластером инстансов, там можно отправлять только сериализуемые данные, а нам это не подходит: проходили, знаем, не решает проблему. 

И, наконец, решение

Не долго думая, я просто переписал этот микросервис на C# (ASP.NET Core). Но, на самом деле, вместо C# мог быть любой другой язык программирования, в котором есть возможность работать с HTTP и RabbitMQ, а главное - возможность управления потоками: Java, C++, Go и т.д. C# меня заманил тем, что TPL (Task Parallel Library) использует libuv в потоках, а волшебный ThreadPoolManager сам решает, когда создавать поток, а когда накинуть работу уже созданному. Можно считать это как Node.js на максималках (строгая типизация, нормальная многопоточность и т.д.), да и я очень люблю этот язык, чего таить греха. После перехода на новый ЯП, проблема исчезла, а проблем с конкурентостью вроде не было. В принципе, не так страшно, если на один из открытых браузеров пользователю не придёт уведомление. 

Конечно, даже моё решение, когда-нибудь упрётся в максимум и масштабировать его, например, по серверам не получиться, пока что я с таким highload не сталкивался. Возможно, когда-нибудь в Node.js завезут классические потоки и средства их синхронизации, но бизнес не ждёт, нужно сейчас, при всей моей любви к этой прекрасной платформе.

P.S. Теоретически, проблема бы решилась, если бы я увеличил мощность CPU, но я же писал вам в начале, что стартап провальный.

Теги:
Хабы:
Всего голосов 8: ↑6 и ↓2+8
Комментарии13

Публикации

Истории

Работа

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Innopolis 2025
Иннополис