Проектирование системы мгновенных сообщений (chat system) — одна из классических задач на System Design интервью. Это комплексная задача, которая затрагивает real-time коммуникации, масштабирование stateful-соединений, консистентность сообщений и офлайн-доставку.
Связанная глава
Обзор книги Alex Xu
Подробный разбор чат-систем есть в главе 12 книги Alex Xu.
Примеры реальных систем
1Функциональные требования
Core Features
- •1-on-1 чаты между пользователями
- •Групповые чаты (до N участников)
- •Отправка текстовых сообщений
- •Индикатор онлайн-статуса
- •Уведомления о прочтении (read receipts)
Extended Features
- •Отправка медиа-файлов (изображения, видео)
- •Push-уведомления для офлайн-пользователей
- •End-to-end шифрование
- •История сообщений и синхронизация
- •Typing indicators ("печатает...")
2Нефункциональные требования
Low Latency
Доставка сообщений < 100ms для онлайн-пользователей. Real-time experience критичен.
High Availability
99.99% uptime. Мессенджер должен работать всегда — это критическая инфраструктура.
Consistency
Гарантия доставки и порядка сообщений. Сообщения не должны теряться.
Масштаб системы (пример)
3Выбор протокола коммуникации
Связанная глава
WebSocket Protocol
Подробный разбор WebSocket: handshake, keepalive, reconnect и best practices.
Сравнение подходов
| Подход | Latency | Server Load | Use Case |
|---|---|---|---|
| HTTP Polling | Высокий (интервал) | Очень высокий | Legacy fallback |
| Long Polling | Средний | Высокий | Простые уведомления |
| WebSocket ✓ | Минимальный | Оптимальный | Real-time чаты |
| Server-Sent Events | Минимальный | Средний | Однонаправленный поток |
Почему WebSocket?
- ✓Bidirectional: Клиент и сервер могут отправлять сообщения в любой момент
- ✓Persistent connection: Одно соединение на сессию, минимум overhead
- ✓Low latency: Нет HTTP handshake на каждое сообщение
- ✓Efficient: Меньше трафика и нагрузки на сервер
4High-Level архитектура
Chat System: High-Level Map
realtime routing + durable storage + offline push deliveryRealtime Plane
Durable + Offline Plane
Базовая topology chat system: realtime WebSocket-контур, durable storage и отдельный offline push pipeline.
Online delivery path
- User A отправляет сообщение через WebSocket-соединение.
- WS Gateway передаёт payload в Chat Router.
- Session Registry находит целевой chat server для User B.
- При активной сессии сообщение мгновенно доставляется получателю.
Offline delivery path
- Сообщение сначала подтверждается и сохраняется в Message Store.
- Delivery Queue формирует задачу push-уведомления с retry-политикой.
- Push Service отправляет событие в APNS/FCM.
- При reconnection клиент синхронизируется по last-seen message ID.
5Хранение сообщений
Хранение данных
Database Internals
Выбор между SQL и NoSQL зависит от паттернов доступа.
Сравнение подходов к хранению
| Database | Плюсы | Минусы | Use Case |
|---|---|---|---|
| PostgreSQL | ACID, знакомый | Шардинг сложен | Малый масштаб |
| Cassandra ✓ | Горизонтальное масштабирование, отличная запись | Eventual consistency | Чаты Facebook/Discord |
| HBase | Wide-column, интеграция с Hadoop | Сложность операций | Facebook Messenger |
Схема данных (Cassandra)
-- Таблица сообщений (partitioned by chat_id)
CREATE TABLE messages (
chat_id UUID,
message_id TIMEUUID, -- Snowflake ID или TIMEUUID
sender_id UUID,
content TEXT,
created_at TIMESTAMP,
PRIMARY KEY ((chat_id), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
-- Быстрый доступ к последним сообщениям чата
SELECT * FROM messages
WHERE chat_id = ?
ORDER BY message_id DESC
LIMIT 50;Почему message_id важен?
- •Ordering: TIMEUUID или Snowflake ID гарантируют порядок
- •Pagination: "Загрузить сообщения до message_id X"
- •Deduplication: Idempotent операции при retry
- •Sync: "Дай все сообщения после message_id Y"
6Presence Service (Онлайн-статус)
Heartbeat механизм
Клиент периодически отправляет heartbeat (каждые 5-30 секунд). Если heartbeat не получен — пользователь считается офлайн.
// Redis хранение статуса
SET user:{user_id}:last_active {timestamp}
EXPIRE user:{user_id}:last_active 30
// Проверка онлайн-статуса
GET user:{user_id}:last_active
// Если ключ существует — онлайнFanout проблема
Если у пользователя 500 друзей, каждое изменение статуса требует 500 уведомлений. Решения:
- •Lazy loading: статус запрашивается при открытии чата
- •Batch updates: отправка изменений раз в N секунд
- •Selective push: только для активных чатов
7Групповые чаты
Масштабирование групп
Direct fanout через WebSocket. Каждый получает сообщение напрямую.
Message Queue + async workers. Batch доставка.
Pub/Sub модель. Подписка на канал, не на индивидуальные сообщения.
Схема данных для групп
-- Группы
CREATE TABLE groups (
group_id UUID PRIMARY KEY,
name TEXT,
created_by UUID,
created_at TIMESTAMP
);
-- Участники группы (для быстрого lookup)
CREATE TABLE group_members (
group_id UUID,
user_id UUID,
joined_at TIMESTAMP,
role TEXT, -- admin, member
PRIMARY KEY ((group_id), user_id)
);
-- Группы пользователя (обратный индекс)
CREATE TABLE user_groups (
user_id UUID,
group_id UUID,
last_read TIMEUUID, -- для unread count
PRIMARY KEY ((user_id), group_id)
);8Синхронизация и офлайн-доставка
Синхронизация
Last-seen message ID
Паттерн last-seen message ID — ключ к эффективной синхронизации.
Sync Protocol
Каждое устройство хранит last_synced_message_id. При подключении:
- 1Клиент отправляет свой last_synced_message_id
- 2Сервер возвращает все сообщения после этого ID
- 3Клиент применяет изменения и обновляет last_synced_message_id
Offline Message Queue
Для офлайн-пользователей сообщения накапливаются:
-- Очередь непрочитанных сообщений
CREATE TABLE offline_messages (
user_id UUID,
message_id TIMEUUID,
chat_id UUID,
sender_id UUID,
content TEXT,
PRIMARY KEY ((user_id), message_id)
) WITH default_time_to_live = 2592000; -- 30 дней TTL
-- При подключении пользователя
SELECT * FROM offline_messages WHERE user_id = ?;
-- После синхронизации — удаляем доставленные9Масштабирование WebSocket серверов
⚠️ Главная сложность
WebSocket соединения stateful. Нельзя просто добавить серверов за Load Balancer — нужно знать, на каком сервере находится конкретный пользователь.
Session Registry
Централизованный реестр (Redis) хранит маппинг user → server:
// При подключении пользователя
HSET user_sessions user_123 server_5
// При отправке сообщения
target_server = HGET user_sessions user_456
// При отключении
HDEL user_sessions user_123Pub/Sub между серверами
Серверы общаются через Redis Pub/Sub или Kafka:
// Server 1 публикует сообщение
PUBLISH chat_server_5 {
"type": "message",
"to": "user_456",
"content": "Hello!"
}
// Server 5 получает и доставляет
// через локальное WebSocket соединениеSticky Sessions как альтернатива
Load Balancer может использовать sticky sessions (привязка пользователя к серверу по IP или cookie). Но это усложняет failover и перебалансировку. Рекомендуется Session Registry.
10Ключевые моменты для интервью
✓ Обязательно обсудить
- •Выбор протокола (WebSocket vs alternatives)
- •Как роутить сообщения между серверами
- •Message ordering и delivery guarantees
- •Offline sync и push notifications
- •Scaling stateful connections
💡 Дополнительные темы
- •End-to-end encryption (Signal Protocol)
- •Read receipts и typing indicators
- •Media storage (S3 + CDN)
- •Rate limiting для spam prevention
- •Multi-device sync
Типичные ошибки на интервью
- ✗Забыть про stateful природу WebSocket и сложности масштабирования
- ✗Не продумать offline сценарий и push notifications
- ✗Игнорировать message ordering при распределённой доставке
- ✗Не обсудить fanout проблему для групповых чатов
