Обновлено: 21 апреля 2026 г. в 16:55

Chat System

средний

Классическая задача: долгоживущие WebSocket-соединения, мгновенная доставка сообщений, онлайн-статус, синхронизация между устройствами и офлайн-уведомления.

Чат становится сложным не на отправке одного сообщения, а там, где нужно удерживать миллионы постоянных соединений, сохранять порядок сообщений и синхронизировать несколько устройств после переподключения.

Кейс помогает собрать WebSocket-шлюзы, маршрутизацию между серверами, хранение истории, онлайн-статус, офлайн-доставку и пуш-уведомления в одну рабочую архитектуру.

В интервью и архитектурных обсуждениях он полезен тем, что заставляет явно определить, что должно доставляться мгновенно, что обязано сохранять порядок, а что можно догрузить после восстановления связи.

Бюджет задержки

Каждый переход от WebSocket-шлюза до получателя должен укладываться в понятный бюджет, иначе чат перестаёт ощущаться мгновенным.

Состояние сессий

Важно знать, на каком сервере живёт активное соединение пользователя и когда система должна считать его офлайн.

Офлайн-доставка

Нужно отдельно проектировать хранение истории, пуш-уведомления и синхронизацию после переподключения, а не смешивать это в один путь.

Групповая раздача

С ростом группы приходится отделять запись сообщения от его доставки и контролировать стоимость массовой раздачи на каждом уровне.

Чат-система становится сложной не на отправке одного сообщения, а там, где нужно держать миллионы постоянных соединений, укладываться в низкую , синхронизировать несколько устройств и восстанавливать историю после переподключения. Поэтому на собеседованиях по системному дизайну этот кейс проверяет не только знание WebSocket, но и умение собрать онлайн-доставку, хранение сообщений, офлайн-сценарии и пуш-уведомления в одну рабочую архитектуру.

Связанная глава

Обзор книги Alex Xu

Подробный разбор чат-системы есть в главе 12 книги Alex Xu.

Читать обзор

Примеры реальных систем

WhatsApp
Telegram
Slack
Discord

1Функциональные требования

Личные чаты между пользователями.

Групповые чаты с ограничением по числу участников.

Отправка текстовых сообщений и медиафайлов.

Онлайн-статус и индикатор набора текста.

Подтверждения прочтения и синхронизация истории между устройствами.

Push-уведомления для пользователей вне активной сессии.

2Нефункциональные требования

Задержка: < 100 мс

Онлайн-пользователи должны получать сообщения почти мгновенно.

Доступность: 99.99% uptime

Мессенджер должен оставаться доступным даже при частичных сбоях.

Консистентность: доставка и порядок

Сообщения не должны теряться и не должны менять порядок внутри одного чата.

Масштабируемость: 50M одновременных соединений

Архитектура должна горизонтально расти вместе с аудиторией.

Пример масштаба системы

DAU:500M
Сообщений в день:100B
Одновременных соединений:50M
Средний размер сообщения:100 bytes

3Выбор протокола коммуникации

Связанная глава

WebSocket Protocol

Подробный разбор WebSocket: handshake, keepalive, reconnect и практические рекомендации.

Читать обзор

Сравнение подходов

ПодходЗадержкаНагрузка на серверЛучший сценарий
HTTP PollingВысокаяОчень высокаяНаследуемый запасной вариант
Long PollingСредняяВысокаяПростые уведомления
WebSocket ✓МинимальнаяОптимальнаяЧаты и совместная работа в реальном времени
Server-Sent EventsМинимальнаяСредняяОднонаправленные уведомления

Почему обычно выбирают WebSocket

  • Двусторонний канал: клиент и сервер могут отправлять сообщения в любой момент, а не ждать нового запроса.
  • Одно соединение на сессию: не нужно поднимать новый HTTP-запрос на каждое событие.
  • Меньше служебных накладных расходов: снижается объём лишнего трафика и нагрузка на серверный слой.
  • Предсказуемая модель доставки: проще держать онлайн-путь, подтверждения и переподключения под одним протоколом.

4Высокоуровневая архитектура

В рабочей архитектуре нужно отдельно описать, как связывает пользователя с конкретным WebSocket-сервером, а подсказывает, можно ли доставить сообщение сразу в открытую сессию или надо переключаться на офлайн-путь.

Чат-система: общая схема

маршрутизация соединений, хранение истории и офлайн-доставка

Контур постоянных соединений

Клиенты -> шлюз -> маршрутизатор
основной путь онлайн-доставки
Реестр сессий + статусы
маршрутизация и онлайн-статус

Контур хранения и офлайн-доставки

Хранилище -> очередь доставки
история и асинхронная обработка
Push-сервис -> APNS / FCM
уведомления для офлайн-пользователей

Опорная схема чат-системы: слой постоянных соединений, маршрутизация сообщений, долговременное хранение истории и отдельный путь офлайн-доставки.

Путь онлайн-доставки

  1. Пользователь A отправляет сообщение через WebSocket-соединение.
  2. WebSocket-шлюз передаёт событие в слой маршрутизации чатов.
  3. Реестр сессий показывает, на каком сервере сейчас держится пользователь B.
  4. Если сессия активна, сообщение сразу попадает получателю.

Путь офлайн-доставки

  1. Сообщение подтверждается и сохраняется в основном хранилище.
  2. Очередь доставки создаёт задачу на push-уведомление и повторные попытки.
  3. Push-сервис отправляет событие через APNS или FCM.
  4. После переподключения клиент догружает всё, что появилось после последней подтверждённой точки.

5Хранение сообщений

Хранение данных

Database Internals

Выбор между SQL и NoSQL зависит от паттернов записи, чтения и пагинации.

Читать обзор

Сравнение подходов к хранению

База данныхПлюсыМинусыЛучший сценарий
PostgreSQLТранзакции и знакомый стекГоризонтальное масштабирование даётся трудноНебольшой масштаб
Cassandra ✓Хорошо держит запись и горизонтально растётНужно аккуратно проектировать согласованность чтения и записиКрупные чаты и мессенджеры
HBaseWide-column модель и интеграция с HadoopЭксплуатационно сложнееОчень крупные аналитические платформы

Схема данных в Cassandra

-- Таблица сообщений (разделение по 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` так важен

  • Порядок сообщений: TIMEUUID или Snowflake ID помогают однозначно восстановить последовательность.
  • Пагинация: можно читать историю порциями вроде «дай сообщения до `message_id X`».
  • Идемпотентность: повторная доставка или ретрай не создают дубль.
  • Синхронизация: легко запросить все сообщения после последней известной точки.

6Онлайн-статус

Проверка активности

Обычно клиент отправляет каждые 5-30 секунд. Если система перестаёт его видеть, пользователя считают офлайн и больше не рассчитывают на мгновенную доставку в открытую сессию.

// Redis хранит отметку о последней активности
SET user:{user_id}:last_active {timestamp}
EXPIRE user:{user_id}:last_active 30

// Проверка онлайн-статуса
GET user:{user_id}:last_active
// Если ключ существует, пользователь считается онлайн

Проблема массовой раздачи

Даже простое изменение статуса быстро превращается в , если у пользователя сотни контактов или он состоит во многих группах.

  • Подгружать статус только при открытии нужного чата, а не на весь список контактов сразу.
  • Объединять обновления в небольшие пачки вместо отправки каждого события по отдельности.
  • Делать push только для активных диалогов и действительно важных сценариев.

7Групповые чаты

Группы ломают наивную модель «одно сообщение — один получатель». Чем больше участников, тем чаще приходится отделять запись сообщения от его доставки и переходить к более событийной модели раздачи.

Как меняется архитектура с ростом группы

Малые
до 100 участников

Прямая доставка по WebSocket остаётся простой и управляемой.

Средние
100-10K участников

Лучше отделять запись сообщения от доставки и раскладывать её по фоновой обработке.

Очень большие
каналы и сообщества 10K+

Нужна модель подписки на канал, а не персональная отправка каждому участнику.

Схема данных для групп

-- Группы
CREATE TABLE groups (
    group_id    UUID PRIMARY KEY,
    name        TEXT,
    created_by  UUID,
    created_at  TIMESTAMP
);

-- Участники группы (для быстрого поиска)
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,  -- для счётчика непрочитанных
    PRIMARY KEY ((user_id), group_id)
);

8Синхронизация и офлайн-доставка

Синхронизация

Last-seen message ID

Паттерн last-seen message ID помогает быстро синхронизировать историю между устройствами.

Читать обзор

Протокол синхронизации

Каждое устройство хранит `last_synced_message_id`. При новом подключении клиент сообщает эту точку, а сервер отдаёт всё, что появилось после неё.

  1. 1Клиент отправляет свой `last_synced_message_id`.
  2. 2Сервер возвращает все сообщения после этого идентификатора.
  3. 3Клиент применяет изменения и обновляет свою точку синхронизации.

Офлайн-очередь

Для пользователей вне сети полезно отделять долговременное хранение истории от , которая отвечает именно за отложенную доставку и повторные попытки.

-- Очередь непрочитанных сообщений
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 дней
-- При подключении пользователя
SELECT * FROM offline_messages WHERE user_id = ?;
-- После синхронизации доставленные записи удаляются

9Масштабирование WebSocket-серверов

Главная сложность

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

Реестр сессий

Централизованная таблица в Redis хранит соответствие пользователь → сервер, чтобы маршрутизация не зависела от случайного выбора балансировщика.

// При подключении пользователя
HSET user_sessions user_123 server_5

// При отправке сообщения
target_server = HGET user_sessions user_456

// При отключении
HDEL user_sessions user_123

Связь между серверами

Для межсерверной доставки удобно использовать поверх Redis или Kafka, чтобы сервер-получатель сам забирал событие из нужного канала.

// Сервер 1 публикует сообщение
PUBLISH chat_server_5 {
  "type": "message",
  "to": "user_456",
  "content": "Hello!"
}

// Сервер 5 получает событие и доставляет
// его через локальное WebSocket-соединение

Липкие сессии как альтернатива

Балансировщик может использовать модель , то есть стараться возвращать пользователя на тот же сервер по IP или cookie. Но такой подход усложняет failover и перебалансировку, поэтому отдельный реестр сессий обычно надёжнее и понятнее.

10Ключевые моменты для интервью

Что обязательно проговорить

  • Почему здесь нужен WebSocket и где остаются запасные варианты.
  • Как сообщения маршрутизируются между серверами и как находится активная сессия получателя.
  • Как обеспечиваются порядок сообщений и понятные гарантии доставки.
  • Как устроены офлайн-синхронизация и пуш-уведомления.
  • Как масштабируется слой постоянных соединений.

Дополнительные темы

  • Сквозное шифрование и Signal Protocol.
  • Подтверждения прочтения и индикатор набора текста.
  • Хранение медиафайлов отдельно от текстовых сообщений, например через S3 и сеть доставки контента.
  • Лимиты и защита от спама.
  • Синхронизация между несколькими устройствами.

Типичные ошибки на интервью

  • Забыть, что WebSocket-соединения хранят состояние и поэтому не масштабируются «просто как HTTP».
  • Не продумать офлайн-сценарий, синхронизацию после переподключения и пуш-уведомления.
  • Игнорировать порядок сообщений при распределённой доставке между серверами.
  • Не обсудить, как меняется массовая доставка в больших групповых чатах и каналах.

Связанные главы

Чтобы отмечать прохождение, включи трекинг в Настройки