Кейс: чат и мессенджер
Мессенджер — задача про доставку сообщения в реальном времени и про то, что делать, когда получатель офлайн.
Тот же шаблон: требования → оценки → схема → ключевой компромисс → детали. Сердце задачи — как мгновенно доставить сообщение и не потерять его.
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 хранит порядок.