Real-Time Backend

Design: Slack/Discord

Как одно сообщение в #general с 50K участниками доставляется всем online за 200мс? Не через один WebSocket сервер - через архитектуру с Kafka, distributed Gateway и Redis presence. Это те же паттерны, что используются в Slack и Discord прямо сейчас.

  • Slack шардирует нагрузку по channel_id, не по workspace - это позволяет масштабировать горячие каналы независимо и обслуживать 38M daily active users
  • Discord с переходом с Elixir на Rust для message service снизил p99 latency с 500мс (GC pauses) до 10мс - разница ощутима для 26M concurrent connections
  • Discord отказался от точного presence count для серверов с 100K+ участников - вместо MGET 100K Redis ключей показывает приблизительные данные

Общая архитектура: как устроен Slack изнутри

Slack в 2023: 38 млн daily active users, 26 млрд сообщений в год, 10 млн активных организаций. Под капотом не один монолит, а несколько специализированных сервисов.

Ключевое архитектурное решение Slack: **channel = unit of scale**. Все операции - отправка, подписка, поиск - шардируются по channel_id. Один канал обрабатывается одним набором подов. Это позволяет масштабировать популярные каналы независимо.

Gateway Service держит все WebSocket-соединения. Когда приходит сообщение, Gateway его валидирует, записывает в Kafka, отдаёт acknowledgment клиенту. Kafka consumer читает и фанаутит в нужные Gateway инстансы.

Discord выбрал другой подход: не channel-sharding, а server-sharding. Весь Discord server (гильдия) живёт на одном наборе подов. Это упрощает consistency внутри сервера, но создаёт hotspot для популярных серверов типа Minecraft (868K+ участников).

Почему Slack выбрал channel как unit of scale вместо workspace?

Fan-out: доставка сообщений миллионам подписчиков

Slack #general в большой компании: 50K участников. Одно сообщение нужно доставить всем online-пользователям. Прямой WebSocket-push к 50K соединениям - это 50K операций записи в сокеты от одного сервиса. За несколько миллисекунд.

Fan-out через Kafka: сообщение попадает в topic, consumers (Gateway сервисы) читают и каждый доставляет сообщение своим подключённым клиентам. Gateway 1 держит 10K соединений - он отправляет тем из них, кто подписан на этот канал.

Write amplification: при 50K участниках и 10 Gateway подах каждый Gateway обрабатывает в среднем 5K соединений. Kafka message читается 10 раз (один раз каждым Gateway). Каждый Gateway делает до 5K ws.send(). Итого: 1 Kafka write -> 50K ws.send(). Это нормально - ws.send() дёшевый, Kafka дёшевый.

Как Gateway pod знает, каким из своих соединений доставить конкретное сообщение из Kafka?

Presence и Typing Indicators: ephemeral state

Presence (online/offline/away) и typing indicators - это ephemeral state: он важен прямо сейчас, но через минуту неактуален. Хранить в PostgreSQL нет смысла. Redis с TTL - идеальное решение.

Проблема масштабирования presence: канал с 50K участниками, 10K онлайн. Когда кто-то заходит - нужно проверить статус 50K пользователей. MGET 50K ключей из Redis - это несколько сотен миллисекунд и большой объём данных. Решение: presence updates через fan-out, не через polling.

Discord для серверов с 100K+ пользователями отказался от показа точного числа онлайн. Вместо этого - приблизительные данные: "1000+", "5000+". Точный presence count требует обращения к Redis с 100K ключами - это неприемлемо при каждом открытии сервера.

Почему typing indicators хранятся в Redis с коротким TTL, а не в PostgreSQL?

Масштабирование: как Discord дошёл до 26 млн concurrent

Discord рос с 10 млн пользователей в 2017 до 150 млн в 2023. Каждый скачок нагрузки вынуждал переписывать части системы. Python -> Elixir -> Rust для самых нагруженных сервисов.

Проблема Discord 2020: один популярный сервер (Minecraft, 868K+ участников) создавал thundering herd при массовом подключении. Discord Gaming Event - 100K+ человек подключаются за 30 секунд. Fan-out к 100K соединениям создавал backpressure в Kafka.

Rust переписка Message Service в Discord: Go версия показывала latency spikes из-за GC pauses при высокой нагрузке. Rust без GC дал p99 latency в 10мс вместо 500мс во время GC пауз. Подробности в "Why Discord is switching from Go to Rust" (2020).

Discord и Slack используют одинаковую архитектуру - оба просто WebSocket серверы с базой данных

Discord шардируется по servers (гильдиям), Slack по channels - принципиально разные единицы масштабирования с разными trade-offs

Discord server - social unit (все знают друг друга). Slack channel - communication unit (могут быть незнакомцы). Это определяет паттерны нагрузки: Discord hotspot на популярный server, Slack hotspot на активный channel. Разные единицы шардинга оптимальны для разных паттернов.

Как Discord решил проблему thundering herd при 100K+ одновременных подключениях?

Итоги

  • Slack: channel как unit of scale - hotspot на активный channel, не на весь workspace
  • Fan-out через Kafka: каждый Gateway читает topic и доставляет своим подписчикам через локальный индекс
  • Presence и typing indicators - ephemeral state в Redis с TTL; TTL = автоматическое "перестал печатать"
  • Discord: event bucketing батчит presence events за 500мс - снижает Kafka throughput в 500+ раз при thundering herd

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

Архитектура чат-платформ объединяет все темы real-time backend

  • Distributed Tracing — При fan-out через Kafka трейс должен пройти через все Gateway поды - correlation ID критичен
  • Chaos Engineering — Thundering herd при reconnect - это chaos сценарий который нужно тестировать заранее
  • Infrastructure (K8s) — Gateway Service шардируется по sticky workspace/channel; правильный HPA критичен для пиковых нагрузок
  • Design: Notification Platform — Push-уведомления для offline пользователей - отдельный сервис поверх той же Kafka

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

  • Как спроектировать систему чтобы один Gateway pod мог обслуживать как Slack-style channels так и Discord-style servers?
  • Что произойдёт с presence данными если Redis cluster упадёт - как обеспечить degraded mode?
  • Как организовать message ordering внутри канала при fan-out через несколько Kafka partitions?

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

  • sd-01-intro
Design: Slack/Discord

0

1

Войти