Проектирование WhatUp
Проектирование WhatsApp
Введение
WhatsApp — самый популярный мессенджер в мире. Масштаб впечатляет:
- ~2.5 млрд активных пользователей в месяц
- ~100 млрд сообщений отправляется ежедневно
- ~7 млрд голосовых сообщений в день
- Поддержка групп до 1024 участников
- End-to-end шифрование по умолчанию для всех сообщений
На первый взгляд — "просто чат". Но за этой простотой скрываются серьёзные инженерные задачи:
- Сообщения должны доставляться мгновенно — задержка ощущается пользователями уже после 200 мс
- Нужно хранить и пересылать сообщения для оффлайн-пользователей
- Группы до 1024 человек создают fan-out проблему — одно сообщение превращается в тысячу доставок
- End-to-end шифрование означает, что серверы не видят содержимое сообщений
Именно эти задачи делают WhatsApp классическим вопросом на System Design интервью.
Требования
Функциональные требования
| # | Требование |
|---|---|
| FR-1 | Пользователь может отправить текстовое сообщение другому пользователю (1-на-1) |
| FR-2 | Пользователь может создать групповой чат и отправить сообщение в группу (до 1024 участников) |
| FR-3 | Сообщения доставляются в реальном времени, если получатель онлайн |
| FR-4 | Сообщения сохраняются и доставляются, когда оффлайн-пользователь выходит в сеть |
| FR-5 | Пользователь видит статусы доставки: ✓ отправлено, ✓✓ доставлено, ✓✓ (синие) прочитано |
| FR-6 | Пользователь видит статус «онлайн» / «был(а) в сети …» |
| FR-7 | Пользователь может отправлять медиафайлы (фото, видео, документы, голосовые) |
| FR-8 | Все сообщения защищены end-to-end шифрованием |
Нефункциональные требования
| # | Требование | Целевое значение |
|---|---|---|
| NFR-1 | Задержка доставки сообщений | < 200 мс (оба онлайн) |
| NFR-2 | Доступность | 99.99% |
| NFR-3 | Упорядоченность сообщений | Гарантия порядка в рамках одного чата |
| NFR-4 | Надёжность | Ни одно сообщение не должно потеряться |
| NFR-5 | Масштабируемость | 2.5 млрд пользователей, 100 млрд msg/day |
| NFR-6 | Безопасность | E2E-шифрование, серверы не видят содержимое |
Оценка масштаба
Сообщения
Пользователей: 2.5 млрд MAU, ~500 млн DAU
Сообщений в день: 100 млрд
Сообщений в секунду: 100 000 000 000 / 86 400 ≈ 1 150 000 msg/sec
Пиковая нагрузка (×3): ~3 500 000 msg/sec
Это колоссальная нагрузка. Один сервер не справится — нужна горизонтально масштабируемая архитектура.
Хранилище сообщений
Средний размер текстового сообщения — ~100 байт (с метаданными — ~200 байт).
Текстовые сообщения в день: 100 млрд × 200 байт = 20 ТБ/день
В год: 20 ТБ × 365 = ~7.3 ПБ/год
Медиафайлы
Около 20% сообщений — медиа. Средний размер — 300 КБ.
Медиа в день: 20 млрд × 300 КБ = 6 ПБ/день
Медиа — это основной объём хранилища. Нужен объектный storage (S3-like) с CDN.
Соединения
Одновременно онлайн: ~500 млн пользователей
Каждый держит постоянное WebSocket/TCP-соединение
500 млн одновременных соединений — это ~5 000 серверов при 100K соединений на каждый.
High-level архитектура
┌──────────────┐ ┌──────────────┐
│ Клиент A │ │ Клиент B │
│ (телефон) │ │ (телефон) │
└──────┬───────┘ └──────▲───────┘
│ │
│ WebSocket │ WebSocket
▼ │
┌───────────────────────────────────────────────────────┐
│ GATEWAY LAYER │
│ (WebSocket-серверы, маршрутизация соединений) │
│ │
│ GW-1 │ GW-2 │ GW-3 │ ... │ GW-N │
└──────┬─────────────────────────────┬──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ MESSAGE │ │ PRESENCE SERVICE │
│ SERVICE │ │ (online/offline, │
│ (валидация, │ │ last seen) │
│ маршрутизация, │ └──────────────────────┘
│ персистенция) │
└──────┬───────────┘
│
├──────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ MESSAGE QUEUE │ │ MESSAGE DB │
│ (Kafka / │ │ (Cassandra / │
│ RabbitMQ) │ │ ScyllaDB) │
└──────┬───────────┘ └──────────────────┘
│
▼
┌──────────────────┐ ┌──────────────────┐
│ PUSH SERVICE │ │ MEDIA SERVICE │
│ (APNs/FCM для │ │ (загрузка, │
│ оффлайн) │ │ S3, CDN) │
└──────────────────┘ └──────────────────┘
Система состоит из пяти ключевых компонентов:
- Gateway Layer — управление WebSocket-соединениями
- Message Service — обработка и маршрутизация сообщений
- Message Storage — персистентное хранение
- Presence Service — отслеживание статусов онлайн/оффлайн
- Media Service — загрузка и доставка медиафайлов
Разберём каждый.
Gateway Layer: управление соединениями
Gateway Layer — это первая линия контакта клиента с серверной инфраструктурой. Каждый клиент устанавливает постоянное WebSocket-соединение с одним из Gateway-серверов.
Почему WebSocket, а не HTTP?
HTTP работает по модели «запрос-ответ» — клиент спрашивает, сервер отвечает. Для мессенджера это неэффективно:
- Сервер не может инициировать отправку сообщения клиенту
- Polling (периодические запросы «есть ли новые сообщения?») создаёт огромный лишний трафик
- Long polling лучше, но всё равно неэффективен при высокой частоте сообщений
WebSocket решает все эти проблемы:
HTTP (polling):
Клиент ──► Сервер: "Есть новые?" (каждые 500ms)
Клиент ──► Сервер: "Есть новые?"
Клиент ──► Сервер: "Есть новые?"
Сервер ──► Клиент: "Да, вот сообщение" ← задержка до 500ms
WebSocket:
Клиент ◄──► Сервер: постоянное двустороннее соединение
Сервер ───► Клиент: "Новое сообщение" ← мгновенно
Session Registry: кто где подключён
Когда клиент устанавливает WebSocket-соединение, Gateway регистрирует его в Session Registry — распределённом хранилище (Redis), которое сопоставляет user_id с конкретным Gateway-сервером.
Session Registry (Redis):
┌───────────────────────────────────────┐
│ user_123 → gateway-eu-west-07 │
│ user_456 → gateway-us-east-03 │
│ user_789 → gateway-ap-south-12 │
└───────────────────────────────────────┘
Когда нужно отправить сообщение пользователю, Message Service:
- Ищет в Session Registry, на каком Gateway подключён получатель
- Отправляет сообщение на этот Gateway через внутреннюю шину (Kafka / gRPC)
- Gateway пересылает сообщение клиенту через WebSocket
Масштабирование Gateway
При 500 млн одновременных соединений и ~100K соединений на один сервер:
Количество Gateway-серверов = 500 000 000 / 100 000 = 5 000 серверов
Серверы распределены географически — клиент подключается к ближайшему (через DNS-based load balancing или Anycast).
Reconnection и Heartbeat
Мобильный интернет нестабилен — соединения рвутся постоянно. Поэтому:
- Клиент отправляет heartbeat каждые 30 секунд
- Если Gateway не получает heartbeat — соединение считается разорванным
- Клиент автоматически переподключается с экспоненциальным backoff (1с, 2с, 4с, 8с…)
- При переподключении клиент запрашивает все непрочитанные сообщения
Message Service: обработка сообщений
Message Service — ядро WhatsApp. Он отвечает за приём, валидацию, маршрутизацию и персистенцию каждого сообщения.
Жизненный цикл сообщения (1-на-1)
Отправитель (Alice) Получатель (Bob)
│ │
│ 1. Шифрует сообщение ключом Bob │
│ │
│ 2. Отправляет через WebSocket │
│──────────► Gateway A ──────────► Message Service │
│ │ │
│ │ 3. Валидация │
│ │ 4. Сохранение в DB │
│ │ 5. Поиск Bob в │
│ │ Session Registry│
│ │ │
│ ┌── Bob ОНЛАЙН? ────────────┤ │
│ │ │ │
│ ДА ▼ НЕТ ▼ │
│ Gateway B ──► Bob Push Service ──► APNs/FCM │
│ │ │ │
│ │ 6. Bob получил │ │
│ │ → ACK "delivered" │ │
│ │ │ │
│ ◄── ✓✓ delivered ─────────────────────────────────────│ │
│ │ │
│ │ 7. Bob прочитал │
│ │ → ACK "read" │
│ │ │
│ ◄── ✓✓ read (синие) ───────────────────────────────────────│
Шаги подробнее:
-
Шифрование на клиенте — сообщение шифруется ключом получателя ещё на устройстве отправителя. Сервер никогда не видит текст.
-
Отправка — зашифрованное сообщение отправляется через WebSocket на Gateway.
-
Валидация — Message Service проверяет:
- Авторизован ли отправитель
- Существует ли получатель
- Не превышен ли rate limit
- Корректен ли формат сообщения
-
Персистенция — сообщение сохраняется в базе данных со статусом
sent. -
Маршрутизация — поиск получателя в Session Registry.
-
Доставка — если получатель онлайн → через Gateway; если оффлайн → push-уведомление (только метаданные, не содержимое — E2E!).
-
ACK (подтверждение) — получатель подтверждает получение и прочтение.
Гарантия доставки: at-least-once
WhatsApp гарантирует, что ни одно сообщение не потеряется. Для этого используется механизм ACK:
┌──────────┐ ┌──────────────┐
│ Клиент │ ── сообщение ───► │ Сервер │
│ │ │ │
│ │ ◄── server_ack ── │ (сохранено │
│ │ │ в DB) │
└──────────┘ └──────────────┘
Если клиент не получил server_ack в течение таймаута →
повторная отправка с тем же message_id (идемпотентность)
Идемпотентность — ключевой принцип. Каждое сообщение имеет уникальный message_id, сгенерированный на клиенте. При повторной отправке сервер распознаёт дубликат и не создаёт копию.
Упорядоченность сообщений
Пользователи ожидают, что сообщения отображаются в том порядке, в котором были отправлены. Это сложнее, чем кажется:
- Сообщения могут приходить через разные Gateway
- Сетевые задержки непредсказуемы
- При reconnection сообщения могут доставляться вне порядка
Решение: монотонные временные метки + sequence number
Каждое сообщение в рамках одного чата получает монотонно возрастающий sequence_number:
Chat: Alice ↔ Bob
seq=1: "Привет!" (Alice, 10:00:01)
seq=2: "Как дела?" (Alice, 10:00:03)
seq=3: "Отлично!" (Bob, 10:00:05)
Клиент сортирует сообщения по sequence_number, а не по времени. Это решает проблему рассинхронизации часов на разных устройствах.
Message Storage: где хранятся сообщения
Выбор базы данных
При 1.15 млн сообщений в секунду и 7.3 ПБ текстовых данных в год нужна база данных, которая:
- Масштабируется горизонтально (добавление серверов, а не upgrade существующих)
- Оптимизирована для записи (write-heavy workload: ~90% операций — запись)
- Поддерживает партиционирование (sharding) из коробки
- Обеспечивает eventual consistency (для мессенджера подходит — нам не нужна строгая согласованность между датацентрами)
Выбор: Apache Cassandra (или её fork — ScyllaDB)
Cassandra идеально подходит:
- LSM-tree storage engine — оптимизирован для записи
- Линейная горизонтальная масштабируемость
- Нет единой точки отказа (peer-to-peer архитектура)
- Настраиваемый уровень согласованности (consistency level)
Схема данных
Таблица: messages
┌──────────────┬──────────────┬─────────────┬──────────────────┬────────────┐
│ chat_id │ message_id │ sender_id │ encrypted_body │ timestamp │
│ (partition │ (clustering │ │ (blob) │ │
│ key) │ key, DESC) │ │ │ │
├──────────────┼──────────────┼─────────────┼──────────────────┼────────────┤
│ chat_abc │ msg_100 │ user_alice │ 0xAE39F... │ 10:00:01 │
│ chat_abc │ msg_099 │ user_bob │ 0x7B21C... │ 09:59:45 │
│ chat_abc │ msg_098 │ user_alice │ 0xF104D... │ 09:58:30 │
└──────────────┴──────────────┴─────────────┴──────────────────┴────────────┘
Partition key = chat_id — все сообщения одного чата хранятся на одной ноде. Это обеспечивает:
- Быструю выборку «последних N сообщений чата» — один запрос на одну ноду
- Упорядоченность внутри партиции (clustering key по
message_idDESC)
Важно: сервер хранит encrypted_body — зашифрованный blob. Сервер физически не может прочитать содержимое сообщений.
Хранение оффлайн-сообщений
Когда получатель оффлайн, сообщения копятся в очереди:
Таблица: pending_messages
┌──────────────┬──────────────┬──────────────┐
│ recipient_id│ message_id │ chat_id │
│ (partition │ (clustering │ │
│ key) │ key) │ │
├──────────────┼──────────────┼──────────────┤
│ user_bob │ msg_100 │ chat_abc │
│ user_bob │ msg_101 │ chat_xyz │
│ user_bob │ msg_102 │ chat_abc │
└──────────────┴──────────────┴──────────────┘
Когда Bob выходит в сеть:
- Gateway сообщает Message Service: «Bob подключился»
- Message Service забирает все pending_messages для Bob
- Отправляет их через Gateway
- После ACK от Bob — удаляет из pending_messages
Хранение медиафайлов
Медиафайлы не хранятся в Cassandra — это был бы кошмар для производительности. Вместо этого:
┌──────────┐ 1. Upload ┌────────────────┐
│ Клиент │────────────────► │ Media Service │
│ │ │ │
│ │ │ 2. Сохраняет │
│ │ │ в S3 │
│ │ │ │
│ │ ◄─ 3. media_url ─│ 4. Возвращает │
│ │ │ URL │
└──────────┘ └────────────────┘
Затем клиент отправляет обычное сообщение,
где encrypted_body содержит зашифрованный media_url + ключ дешифрования
Получатель:
- Получает сообщение с зашифрованным
media_url - Расшифровывает URL
- Скачивает медиа (через CDN)
- Расшифровывает медиа локально
Групповые чаты: проблема fan-out
Групповой чат — одна из самых сложных задач мессенджера. Когда Alice отправляет сообщение в группу из 1024 человек, сообщение должно быть доставлено каждому участнику.
Два подхода: fan-out-on-write vs fan-out-on-read
Fan-out-on-write (push)
- При отправке сообщения оно сразу копируется в очередь каждого участника
- Плюс: чтение instant — клиент забирает готовые сообщения из своей очереди
- Минус: запись дорогая — 1 сообщение × 1024 участника = 1024 записи
Fan-out-on-read (pull)
- Сообщение сохраняется один раз в хранилище группы
- Клиент при открытии чата сам запрашивает новые сообщения
- Плюс: запись дешёвая — всего одна
- Минус: чтение дорогое — каждый клиент делает запрос
WhatsApp использует гибридный подход:
Маленькие группы (< 256): fan-out-on-write
→ Сообщение копируется в inbox каждого участника
→ Мгновенная доставка без лишних запросов
Большие группы (256–1024): fan-out-on-read
→ Сообщение хранится в group_messages
→ Клиенты получают push-уведомление и запрашивают новые сообщения
Архитектура группового чата
Alice отправляет в группу "Работа" (500 участников)
│
▼
┌──────────────────┐
│ Message Service │
│ │
│ 1. Сохраняет │
│ в group_msgs │
│ │
│ 2. Lookup │
│ участников │
│ группы │
│ │
│ 3. Для каждого: │
│ ┌────────────┤
│ │ Онлайн? │
│ │ → Gateway │
│ │ Оффлайн? │
│ │ → Push │
│ │ → pending │
│ └────────────┤
└──────────────────┘
Оптимизация: Kafka для fan-out
Вместо того чтобы Message Service напрямую обрабатывал 1024 доставки, сообщение публикуется в Kafka-топик группы:
Kafka Topic: group_<group_id>
Партиции:
P0: [msg_100, msg_103, msg_107...] → Consumer Group → Gateway-1
P1: [msg_101, msg_104, msg_108...] → Consumer Group → Gateway-2
P2: [msg_102, msg_105, msg_109...] → Consumer Group → Gateway-3
Kafka обеспечивает:
- Порядок сообщений внутри партиции
- Параллельную обработку — разные consumers обрабатывают разные партиции
- Retry — при ошибке доставки сообщение не теряется
Presence Service: кто онлайн
Presence Service отвечает за два состояния:
- Online — пользователь активен прямо сейчас
- Last seen — «был(а) в сети в 14:32»
Heartbeat-механизм
Клиент ──► heartbeat ──► Gateway ──► Presence Service ──► Redis
Каждые 30 секунд клиент шлёт heartbeat.
Presence Service обновляет запись в Redis:
user_123: { status: "online", last_seen: "2026-03-24T14:32:00Z", ttl: 60s }
Если TTL истекает без нового heartbeat → статус = offline
Проблема масштаба: 500 млн онлайн-пользователей
Каждый пользователь подписан на presence-обновления своих контактов. Если у Alice 200 контактов, она должна получать уведомления об изменении статуса каждого из них.
Наивный подход: при каждом изменении статуса — рассылка всем подписчикам. При 500 млн пользователей и 200 контактах в среднем — это было бы ~100 млрд уведомлений в день только для presence.
Оптимизация WhatsApp:
- Lazy presence — статус контакта запрашивается только когда пользователь открывает чат с ним
- Subscribe-on-open — подписка на updates происходит только для видимых на экране контактов
- Batch updates — обновления presence отправляются пачками, а не по одному
Пользователь открывает список чатов:
→ Подписка на presence для 10-15 видимых контактов
→ Остальные 185 контактов не отслеживаются
Пользователь скроллит вниз:
→ Подписка на новые видимые
→ Отписка от скрытых
End-to-End шифрование
WhatsApp использует Signal Protocol — один из самых безопасных протоколов для мессенджеров.
Как это работает (упрощённо)
Регистрация:
Alice генерирует пару ключей:
- Публичный ключ → загружается на сервер WhatsApp
- Приватный ключ → остаётся только на устройстве Alice
Отправка сообщения Alice → Bob:
1. Alice запрашивает публичный ключ Bob с сервера
2. Alice генерирует shared secret (Diffie-Hellman key exchange)
3. Alice шифрует сообщение shared secret-ом
4. Зашифрованное сообщение отправляется через сервер
5. Bob расшифровывает с помощью своего приватного ключа
Сервер видит: 0xAE39F7B21C4D... (бессмысленный blob)
Double Ratchet Algorithm
Signal Protocol использует Double Ratchet — алгоритм, при котором ключ шифрования меняется с каждым сообщением:
Сообщение 1: зашифровано ключом K1
Сообщение 2: зашифровано ключом K2 (производным от K1)
Сообщение 3: зашифровано ключом K3 (производным от K2)
...
Даже если злоумышленник скомпрометирует ключ K5, он не сможет расшифровать предыдущие сообщения (K1-K4) — это свойство называется Forward Secrecy.
Групповое шифрование
В группах используется Sender Keys — вариация протокола:
Alice создаёт группу [Alice, Bob, Carol]:
1. Alice генерирует sender_key для группы
2. Alice шифрует sender_key публичным ключом Bob → отправляет Bob
3. Alice шифрует sender_key публичным ключом Carol → отправляет Carol
Отправка сообщения в группу:
Alice шифрует сообщение sender_key-ом → один раз
Сервер доставляет одинаковый blob всем участникам
Каждый расшифровывает своей копией sender_key
Это эффективнее, чем шифровать сообщение отдельно для каждого участника (1 шифрование вместо N).
Синхронизация и мульти-девайс
Проблема: один аккаунт — несколько устройств
WhatsApp поддерживает подключение до 4 дополнительных устройств (WhatsApp Web, Desktop). Каждое устройство имеет свою пару ключей.
┌─── Телефон (основное устройство)
Alice ────────┤
├─── Desktop
│
└─── Web-браузер
Каждое устройство:
- Имеет свой приватный ключ
- Получает сообщение, зашифрованное своим ключом
Отправитель (Bob) шифрует сообщение для каждого устройства Alice отдельно:
Bob → Alice:
Зашифрованная копия 1 → Alice.phone_key
Зашифрованная копия 2 → Alice.desktop_key
Зашифрованная копия 3 → Alice.web_key
Это увеличивает нагрузку в 3-4 раза, но обеспечивает E2E-шифрование на каждом устройстве.
Синхронизация сообщений между устройствами
Alice печатает на телефоне:
"Привет" → шифруется → отправляется на сервер
Сервер доставляет на все устройства Alice:
Телефон: отмечает как "отправлено мной"
Desktop: показывает в активном чате
Web: показывает в активном чате
Push-уведомления для оффлайн-пользователей
Когда получатель оффлайн, WhatsApp отправляет push через платформенные системы:
┌──────────────────┐
│ Push Service │
│ │
│ iOS → APNs │──► "Новое сообщение от Alice"
│ Android → FCM │──► "Новое сообщение от Alice"
└──────────────────┘
Важно: push-уведомление не содержит текст сообщения (E2E!). Оно содержит только:
- Имя отправителя
- Тип сообщения (текст, фото, видео)
- Зашифрованный payload для пререндера (опционально)
Клиент при получении push:
- Устанавливает WebSocket-соединение (если его нет)
- Забирает pending_messages
- Расшифровывает и показывает
Масштабирование и отказоустойчивость
Шардирование
┌─────────────────────────┐
│ Consistent Hashing │
│ (по user_id) │
└────────────┬────────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ users │ │ users │ │ users │
│ 0-33% │ │ 34-66% │ │ 67-100% │
└─────────┘ └─────────┘ └─────────┘
Consistent hashing — при добавлении/удалении шарда перемещается минимум данных.
Multi-DC развёртывание
┌──────────────────────┐ ┌──────────────────────┐
│ DC: Europe │ │ DC: US East │
│ │ │ │
│ Gateway (×500) │ │ Gateway (×500) │
│ Message Svc (×100) │◄───►│ Message Svc (×100) │
│ Cassandra (×200) │sync │ Cassandra (×200) │
│ Redis (×50) │ │ Redis (×50) │
└──────────────────────┘ └──────────────────────┘
- Active-active — оба датацентра обрабатывают запросы
- Пользователь подключается к ближайшему DC
- Cassandra обеспечивает eventual consistency между DC
- При отказе одного DC — трафик автоматически переключается на другой
Rate Limiting
Для защиты от спама и злоупотреблений:
Rate limits:
- Текстовые сообщения: 100 msg/min на пользователя
- Медиа: 30 uploads/min
- Групповые создания: 10 groups/day
- Добавление участников: 50 adds/day
Реализуется через Token Bucket алгоритм в Redis.
Схема данных: полная картина
┌─────────────────────────────────────────────────────────────┐
│ USERS │
├─────────┬──────────┬──────────┬──────────┬──────────────────┤
│ user_id │ phone │ name │ avatar │ public_keys │
│ (PK) │ │ │ (S3 URL) │ (JSON) │
├─────────┼──────────┼──────────┼──────────┼──────────────────┤
│ u_001 │ +7911... │ Alice │ /av/001 │ {phone: X, ...} │
└─────────┴──────────┴──────────┴──────────┴──────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ CHATS │
├─────────┬──────────┬──────────┬──────────┬──────────────────┤
│ chat_id │ type │ name │ members │ created_at │
│ (PK) │ (1:1/grp)│ │ (list) │ │
├─────────┼──────────┼──────────┼──────────┼──────────────────┤
│ c_abc │ 1:1 │ null │ [u01,u02]│ 2026-01-01 │
│ c_xyz │ group │ "Работа" │ [u01..] │ 2026-01-15 │
└─────────┴──────────┴──────────┴──────────┴──────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MESSAGES (Cassandra) │
├─────────┬──────────┬──────────┬──────────────┬──────────────┤
│ chat_id │ msg_id │ sender │ enc_body │ timestamp │
│ (PK) │ (CK DESC)│ │ (blob) │ │
├─────────┼──────────┼──────────┼──────────────┼──────────────┤
│ c_abc │ m_100 │ u_001 │ 0xAE39F... │ 10:00:01 │
└─────────┴──────────┴──────────┴──────────────┴──────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PENDING MESSAGES │
├──────────────┬──────────────┬──────────────┬────────────────┤
│ recipient_id │ msg_id │ chat_id │ created_at │
│ (PK) │ (CK) │ │ │
└──────────────┴──────────────┴──────────────┴────────────────┘
Итого: ключевые решения
| Решение | Выбор | Обоснование |
|---|---|---|
| Протокол связи | WebSocket | Двусторонний real-time канал |
| БД сообщений | Cassandra | Write-heavy, горизонтальное масштабирование |
| Очередь | Kafka | Надёжность, порядок, fan-out |
| Session Registry | Redis | Быстрый lookup, TTL |
| Хранение медиа | S3 + CDN | Объектный storage, глобальная доставка |
| Шифрование | Signal Protocol | E2E, Forward Secrecy |
| Presence | Redis + Heartbeat | Быстрое обновление, TTL |
| Доставка оффлайн | Push + pending queue | APNs/FCM + Cassandra |
| Шардирование | Consistent hashing | Минимальное перебалансирование |
| Репликация | Multi-DC active-active | Отказоустойчивость, низкая latency |
Типичные вопросы на интервью
Q: Как обеспечить порядок сообщений? A: Sequence number на уровне каждого чата. Клиент дедуплицирует и сортирует по seq_num.
Q: Что будет, если Gateway упадёт? A: Клиент переподключается к другому Gateway. Непрочитанные сообщения хранятся в pending_messages и доставляются при reconnect.
Q: Как масштабировать групповой чат на 1024 человека? A: Для больших групп — fan-out-on-read + Kafka. Вместо 1024 синхронных записей — одна запись + push-уведомления.
Q: Почему Cassandra, а не PostgreSQL? A: PostgreSQL не масштабируется горизонтально без существенных усилий (Citus/sharding). Cassandra обеспечивает линейное масштабирование на 1000+ нод с write-heavy нагрузкой.
Q: Как обрабатывать 3.5 млн msg/sec? A: Gateway Layer (5000+ серверов) → Kafka (буфер, rate limiting) → Message Service (горизонтально масштабируемый, stateless) → Cassandra (шардированная, write-optimized).
Q: Что если два устройства отправили сообщение одновременно? A: Каждое сообщение имеет уникальный client-generated ID. Конфликты разрешаются через seq_num на сервере — сервер назначает порядок.