Real-Time Backend

Message Persistence

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

  • Slack хранит сообщения в MySQL через Vitess (шардирование) + S3 для архива. До 2022 года Slack обрабатывал более 1 млрд сообщений в день при SLA 99.99%.
  • Discord в 2023 мигрировал с Cassandra (177 узлов) на ScyllaDB (72 узла) для хранения сообщений. Результат: P99 latency упала вдвое, стоимость инфраструктуры снизилась на 30%.
  • Facebook Messenger использует inbox model с fan-out on write: каждое сообщение физически копируется в inbox каждого участника. При группе из 10 человек одно сообщение создаёт 10 строк в БД.
  • WhatsApp до 2021 года хранил сообщения только на устройстве, сервер был relay на базе XMPP/Mnesia. Смена подхода на серверное хранение потребовала полной перестройки архитектуры.

Persist Or Not

Когда сообщение отправлено - оно либо записывается на диск, либо живёт только в памяти и умирает вместе с процессом. Это первое архитектурное решение в любом чат-сервисе: **persist or not**.

Slack хранит каждое сообщение навсегда (по умолчанию) - потому что команды используют историю как базу знаний. Discord хранит сообщения, но не гарантирует вечное хранение без Nitro. WhatsApp до 2021 хранил сообщения только на устройстве - сервер был relay, а не storage.

Два ключевых вопроса перед выбором: нужна ли получателю история после reconnect? Нужен ли аудит / compliance? Если оба «нет» - persistence только добавляет latency и стоимость.

  • **Ephemeral messaging** - сообщение живёт в памяти, доставляется online-получателям, не пишется на диск. Пример: live cursor positions в Figma.
  • **Persistent messaging** - сообщение записывается в БД до подтверждения доставки. Пример: Slack DM, Discord channel message.
  • **Hybrid** - пишется в fast log (Redis Streams / Kafka), потом асинхронно архивируется в cold storage. Пример: Facebook Messenger inbox model.

WhatsApp до 2021 года использовал подход, при котором сервер не хранил сообщения постоянно, а выступал только relay. Какой главный недостаток этого подхода?

Message Ttl

Даже если сообщения хранятся - они не обязаны жить вечно. TTL (Time To Live) задаёт срок жизни записи: после него данные автоматически удаляются движком БД или отдельным cron-процессом.

Telegram's «исчезающие сообщения» (Secret Chats) - это TTL на уровне клиента: устройство получателя само удаляет запись через N секунд. Kafka по умолчанию хранит события 7 дней (`retention.ms=604800000`), потом segment файлы удаляются автоматически. Redis ставит TTL на каждый ключ и удаляет в фоне через lazy + periodic eviction.

  • **DB-level TTL** - Cassandra и ScyllaDB поддерживают `TTL` на column-уровне: `INSERT ... USING TTL 86400`. Движок помечает запись tombstone и очищает при compaction.
  • **Kafka retention** - задаётся `retention.ms` или `retention.bytes` на topic. Старые сегменты удаляются целиком, что эффективнее row-by-row DELETE.
  • **Application TTL** - cron-job делает `DELETE WHERE created_at < NOW() - INTERVAL`. Гибко, но создаёт нагрузку на БД и реплики в момент запуска.
  • **Soft delete + archival pipeline** - вместо физического удаления ставится флаг `archived=true`, данные переносятся в cold storage асинхронно.

PostgreSQL partitioning по времени позволяет удалять старые сообщения через `DROP TABLE partition` - это O(1) операция без нагрузки на WAL и реплики, в отличие от `DELETE` по миллионам строк.

Slack хранит историю сообщений без TTL по умолчанию. Команда хочет автоматически удалять сообщения старше 90 дней из PostgreSQL. Какой подход создаёт наименьшую нагрузку на продакшн БД?

Archival

Архивация - это перенос «холодных» данных из дорогого быстрого хранилища в дешёвое медленное. В контексте сообщений: hot tier (последние N дней) живёт в ScyllaDB / PostgreSQL, старые сообщения переезжают в S3 или Glacier.

Discord в 2023 году мигрировал хранилище сообщений с Cassandra на ScyllaDB (Rust-реализация Cassandra-compatible БД). Главная причина - Cassandra требовала 177 узлов для обслуживания 4 млрд сообщений в день, а ScyllaDB справилась с 72 узлами при меньшей P99 latency. Архивные данные (сообщения старше года) при этом хранятся в S3 в Parquet-формате.

  1. **Tiered storage** - данные автоматически мигрируют между hot / warm / cold по возрасту. ClickHouse TTL MOVE поддерживает это нативно.
  2. **Export pipeline** - Kafka consumer читает события и пишет в S3 в Parquet-формате (через Flink / Spark Structured Streaming). Parquet сжимает текстовые сообщения в 5-10x.
  3. **Tombstone + lazy archival** - при TTL-удалении из горячей БД запись сначала попадает в archival queue, а уже потом физически удаляется.
  4. **Index separation** - в архивном S3 хранится отдельный индекс (Elasticsearch / OpenSearch) для full-text поиска по старым сообщениям.

Slack использует Vitess (MySQL sharding layer) для горячего хранилища сообщений + S3 для архивных данных. Vitess позволяет горизонтально шардировать MySQL без изменения SQL-кода приложения.

Discord перешёл с Cassandra на ScyllaDB в 2023. Что стало главным выигрышем от этой миграции при том же объёме данных?

Replay

Replay - это способность воспроизвести поток сообщений с произвольной точки в прошлом. Kafka делает это нативно: каждый consumer group хранит offset, и `--from-beginning` буквально перечитывает все события с нуля.

Facebook Messenger использует inbox model: каждое сообщение записывается в "инбокс" отправителя и каждого получателя отдельными строками (fan-out on write). При reconnect клиент запрашивает все сообщения с `last_seen_id` - это и есть локальный replay. Twitter DM реализован аналогично: каждая копия сообщения в inbox пользователя имеет собственный курсор.

  • **Kafka offset replay** - consumer сбрасывает offset назад и перечитывает события. Работает в пределах `retention.ms`. Используется для перезапуска упавших сервисов.
  • **Inbox cursor replay** - клиент хранит `last_message_id` и запрашивает `WHERE id > last_message_id ORDER BY id`. Пагинация по курсору, а не по offset.
  • **Event sourcing replay** - весь state приложения восстанавливается из лога событий. Каждый новый микросервис может построить собственную проекцию из тех же событий.
  • **Archive replay** - воспроизведение из S3/Glacier через Athena или Spark job. Медленно, но позволяет анализировать данные за годы.

Fan-out on write (копия сообщения в каждый inbox при отправке) vs fan-out on read (одна запись, читается всеми получателями) - компромисс write amplification vs read amplification. Мессенджеры с небольшими группами (Messenger, WhatsApp) используют fan-out on write; Reddit/Twitter feed с миллионами подписчиков - гибрид.

Replay и повторная доставка - одно и то же

Replay - это чтение исторических данных с произвольного offset/cursor для нового consumer или восстановления. Повторная доставка - это retry failed delivery конкретного сообщения конкретному получателю.

Replay оперирует на уровне storage (Kafka topic, inbox table), а retry - на уровне delivery pipeline (dead letter queue, acknowledgement timeout). Смешение приводит к неверным решениям: например, retry через DLQ не поможет новому микросервису построить проекцию из исторических событий.

Сервис упал и пропустил 2 часа событий из Kafka. Команда хочет обработать пропущенные события без потерь. Какой механизм это обеспечивает?

Итоги

  • **Persist or not** - первое решение: ephemeral (relay, нет истории), persistent (запись до ACK), hybrid (fast log + async cold storage). Выбор определяется требованиями к offline delivery и compliance.
  • **TTL стратегия** важна с первого дня: Cassandra/ScyllaDB поддерживают column-level TTL нативно; PostgreSQL лучше архивировать через DROP PARTITION, а не DELETE по строкам.
  • **Archival pipeline** позволяет держать hot tier маленьким (только последние 90 дней в быстрой БД) и перекладывать старые данные в S3/Parquet со сжатием 5-10x.
  • **Replay** через Kafka offset или inbox cursor даёт возможность восстановить пропущенные события без потерь - ключевой механизм отказоустойчивости в event-driven архитектурах.
  • **Fan-out on write vs read**: мессенджеры с малыми группами копируют сообщение в каждый inbox при отправке (write amplification), что делает чтение O(1) по cursor.

Связанные темы

Message Persistence строится поверх нескольких фундаментальных концептов распределённых систем:

  • Kafka Internals — Kafka topic - основа replay механизма; offset commit определяет точку восстановления после падения сервиса
  • Database Sharding — Vitess (Slack) и ScyllaDB (Discord) решают шардирование хранилища сообщений горизонтально
  • Fan-out Patterns — Fan-out on write vs read - фундаментальный компромисс inbox model для мессенджеров и social feeds

Вопросы для размышления

  • Slack предлагает платным клиентам «неограниченную историю», а бесплатным - только 90 дней. Какую архитектуру tiered storage поддерживает эту модель, и как это влияет на стоимость инфраструктуры?
  • Discord хранит сообщения в ScyllaDB с TTL, но пользователи жалуются на исчезновение старых сообщений в малопосещаемых каналах. Как tombstone compaction в ScyllaDB может быть причиной этого?
  • WhatsApp Groups поддерживает до 1024 участников. При fan-out on write одно сообщение создаёт 1024 записи. Как это меняет решение о выборе storage engine и стратегию шардирования?

Связанные уроки

  • db-34-lsm
Message Persistence

0

1

Войти