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?