Как масштабировать микросервис чат при большом количестве подключений и сообщений
Что проверяют
Понимаете ли вы, где у чата узкое место: не в пропускной способности HTTP, а в долгоживущих соединениях и доставке сообщения получателю, подключённому к другому экземпляру. Следом обычно спрашивают, как сообщение с пода A попадёт к собеседнику на поде B и что будет при падении пода.
Как отвечать
Разделите систему на слои:
- шлюз соединений: сервис на Go держит WebSocket (
gorilla/websocketилиcoder/websocket). На соединение одна горутина чтения и одна записи, исходящие идут через буферизованный канал с лимитом, медленный клиент при переполнении отключается. Горутина стоит несколько килобайт стека, поэтому сотни тысяч соединений на узел упираются в буферы, лимит дескрипторов (ulimit -n) и настройки ядра; - маршрутизация: реестр «какой пользователь на каком узле» в Redis и шина между узлами. Для фан-аута подойдут Redis Pub/Sub или NATS, для надёжной истории Kafka с партиционированием по
chat_id, что сохраняет порядок внутри чата; - хранение: база под интенсивную запись с ключом
(chat_id, message_id), например Cassandra, ScyllaDB или партиционированный PostgreSQL. По последнемуmessage_idклиент дозапрашивает пропущенное после переподключения; - доставка: подтверждение от клиента, повтор без подтверждения, дедупликация по идентификатору. Офлайн-пользователь получает пуш.
При деплое узел закрывает соединения постепенно (graceful shutdown), а клиент переподключается со случайной задержкой, иначе все разом устроят шторм. В Prometheus стоит вывести число соединений на узел, задержку доставки и размер очередей.
Где спотыкаются
Говорят «поставим балансировщик и добавим подов», не объясняя, как узлы узнают друг о друге. Без шины сообщение не дойдёт до собеседника на другом узле.
Держат сессии в памяти узла без учёта переподключений и не думают о дозапросе истории.
Не ограничивают буфер медленного клиента. Один зависший мобильный клиент копит сообщения, и узел падает по памяти.