Кейс: чат и мессенджер

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

Тот же шаблон: требования → оценки → схема → ключевой компромисс → детали. Сердце задачи — как мгновенно доставить сообщение и не потерять его.

1. Требования

Функциональные: отправить сообщение 1-на-1; доставка в реальном времени; история переписки; статус «доставлено/прочитано»; (опц.) групповые чаты и «онлайн». Нефункциональные: низкая задержка доставки (< 1 с при онлайн-получателе); сообщения не теряются; доставка при возвращении из офлайна; порядок сообщений в диалоге сохранён.

2. Оценки масштаба

500 млн DAU, по 40 сообщений/день
Сообщений: 20 млрд / день ≈ 230 000 сообщений/с (пик выше)
Хранилище: ~300 байт/сообщение * 20 млрд * 365 ≈ 2 PB/год
→ огромный поток записей → колоночная NoSQL (Cassandra), шардинг по чату

3. Push против polling

Как получатель узнаёт о новом сообщении? Главный компромисс:

ПодходКакМинус
Pollingклиент спрашивает «есть новое?» каждые N секундзадержка и тонны пустых запросов
Long pollingсервер держит запрос, пока не появится сообщениелучше, но всё ещё накладно
WebSocketпостоянное двунаправленное соединениенадо держать миллионы соединений

Для реального времени берут WebSocket: постоянный канал, по которому сервер сам шлёт сообщение в момент прихода. Никакого опроса — задержка минимальна.

4. Высокоуровневая схема

[клиент A] ⇄ WebSocket ⇄ [gateway-узел] → [сервис чата] → [очередь]
                                              ↓                ↓
                                     [БД сообщений]   [сервис присутствия:
                                     (Cassandra)       кто к какому gateway подключён]
[клиент B] ⇄ WebSocket ⇄ [gateway-узел] ←── доставка ──────────┘

Ключевая проблема: A и B подключены к разным gateway-узлам. Чтобы доставить сообщение B, надо знать, на каком узле висит его соединение. Это решает сервис присутствия — реестр «пользователь → gateway-узел». Сообщение маршрутизируется на нужный узел, а тот шлёт его в WebSocket клиента B.

5. Получатель офлайн

Если B не в сети, доставлять некуда. Сообщение нельзя терять, поэтому:

  • сообщение всегда сначала пишется в БД (durable), потом доставляется;
  • для офлайн-получателя ставится отметка «недоставлено»;
  • при возвращении B онлайн он запрашивает недоставленные сообщения и получает их по порядку;
  • дополнительно — push-уведомление через APNs/FCM, что пришло сообщение.

Это применение идемпотентности и at-least-once: у сообщения есть id, повторная доставка не задваивает его в истории.

6. Модель данных

{
  "chat_id": "u7-u42",
  "message_id": "01HXY...",
  "sender": "user-7",
  "text": "привет!",
  "ts": 1718380800,
  "status": "delivered"
}

Шардируем по chat_id: все сообщения одного диалога — на одном шарде, читаются вместе и по порядку. Монотонный message_id (например, на основе времени) задаёт порядок.

7. Узкие места

Узкое местоРешение
Миллионы WebSocket-соединениймного gateway-узлов; соединения держат stateful-узлы, логика — отдельно
Поток записей 230k/сCassandra, шардинг по chat_id
Найти узел получателясервис присутствия (реестр пользователь → узел)
Офлайн-доставкасначала запись в БД, потом доставка; push-уведомления

Итог

  • Для реального времени берут WebSocket вместо polling: сервер сам шлёт сообщение без опроса.
  • Сервис присутствия знает, на каком gateway-узле висит получатель, и маршрутизирует туда сообщение.
  • Сообщение сначала пишут в БД (не теряем), офлайн-получатель забирает недоставленное при возвращении; шардинг по chat_id хранит порядок.
Проверьте себя
1. Почему для доставки сообщений в реальном времени выбирают WebSocket, а не polling?
AWebSocket проще реализовать
BПостоянное соединение позволяет серверу сразу слать сообщение без опроса, минимизируя задержку
CPolling вообще не работает
DWebSocket не требует серверов
2. Зачем нужен сервис присутствия в архитектуре мессенджера?
AЧтобы считать число сообщений
BЧтобы знать, к какому gateway-узлу подключён получатель, и доставить сообщение именно туда
CЧтобы шифровать сообщения
DЧтобы хранить историю
3. Что делают с сообщением, если получатель офлайн?
AУдаляют его
BСначала сохраняют в БД, помечают недоставленным и отдают при возвращении пользователя онлайн (плюс push-уведомление)
CБесконечно повторяют доставку каждую секунду
DОтправляют отправителю обратно
4. По какому ключу разумно шардировать сообщения мессенджера?
AПо времени отправки
BПо chat_id — тогда вся переписка одного диалога на одном шарде и читается по порядку
CПо длине текста
DПо случайному числу
Поддержать проект