Проектирование Discord
Discord — это платформа для голосового, видео- и текстового общения в реальном времени, запущенная в 2015 году. Изначально Discord создавался для геймеров, но со временем стал универсальной платформой для сообществ: от учебных групп и книжных клубов до крупнейших open-source проектов и AI-сообществ.
Масштаб Discord (2025)
| Метрика | Значение |
|---|---|
| Зарегистрированных пользователей | ~600 млн |
| Ежемесячно активных (MAU) | ~259 млн |
| Активных серверов (guilds) | ~32,6 млн |
| Сообщений в день | ~1,1 млрд |
| Голосовых минут в день | ~4 млрд (только gaming) |
| Среднее время в приложении | ~94 мин/день (геймеры) |
Чем Discord отличается от предыдущих систем?
| Аспект | TikTok | Discord | ||
|---|---|---|---|---|
| Тип контента | Текст/профили | Видео | Текст/ссылки | Текст + голос + видео |
| Коммуникация | Асинхронная | Асинхронная | Асинхронная | Реальное время |
| Организация | По людям | По алгоритму | По сообществам | По серверам + каналам |
| Ключевая нагрузка | Граф связей | Видео-стриминг | Голосование | WebSocket + Voice |
| Модель доставки | Request-response | CDN streaming | Request-response | Persistent connection |
Главное архитектурное отличие Discord: постоянные соединения в реальном времени. В LinkedIn, TikTok и Reddit клиент делает HTTP-запрос и получает ответ (request-response). В Discord клиент устанавливает постоянное WebSocket-соединение с сервером и получает обновления мгновенно — каждое сообщение, каждый статус, каждое голосовое событие доставляется через это соединение в реальном времени.
WebSocket — протокол связи, который обеспечивает двустороннюю (full-duplex) коммуникацию между клиентом и сервером через одно постоянное TCP-соединение. В отличие от HTTP (клиент спрашивает → сервер отвечает), WebSocket позволяет серверу самому отправлять данные клиенту в любой момент. Это идеально для мессенджеров, игр и любых real-time приложений.
Основные функции
- Серверы (Guilds) — сообщества с иерархией каналов. Один сервер может содержать сотни каналов.
- Текстовые каналы — обмен текстовыми сообщениями в реальном времени.
- Голосовые каналы — голосовое общение с низкой задержкой.
- Видеозвонки и трансляции — Go Live (стриминг экрана/игры).
- Личные сообщения (DM) — переписка между пользователями.
- Роли и права доступа — гранулярная система прав (кто может писать, читать, банить).
- Боты — программируемые боты для автоматизации.
- Статус присутствия (Presence) — онлайн, оффлайн, «не беспокоить», «играет в...»
4.2. Оценка нагрузки (Capacity Estimation)
Сообщения
- 1,1 млрд сообщений/день = ~12 700 сообщений/сек в среднем, ~40 000/сек в пике.
- Среднее сообщение: ~200 байт (текст + метаданные).
- 1,1 млрд × 200 байт = ~220 ГБ/день новых текстовых данных.
- Discord хранит триллионы сообщений — исторические данные занимают петабайты.
WebSocket-соединения
Каждый активный пользователь держит минимум одно постоянное WebSocket-соединение. При пиковой нагрузке:
- ~30 миллионов одновременных WebSocket-соединений.
- Каждое соединение потребляет серверную память (~10-50 КБ на соединение).
- 30 млн × 30 КБ = ~900 ГБ RAM только для поддержания соединений.
Голосовой трафик
- 4 млрд голосовых минут/день (только gaming).
- Аудиокодек Opus при 64 kbps: ~480 КБ/мин.
- Голосовой трафик на сервере (входящий): ~1,9 ПБ/день.
- Этот трафик не хранится — он передаётся в реальном времени и отбрасывается.
Read States
Read States — один из самых нагруженных компонентов Discord. Он отслеживает, какие каналы и сообщения прочитал каждый пользователь (аналог галочек «прочитано» в мессенджерах). Read States обращается к базе данных:
- При каждом подключении пользователя к Discord.
- При каждом отправленном сообщении.
- При каждом прочитанном сообщении.
4.3. Эволюция архитектуры и технологический стек
Polyglot-архитектура
Discord — один из лучших примеров polyglot-архитектуры, где каждый компонент написан на языке, идеально подходящем для его задачи:
| Компонент | Язык | Почему именно этот |
|---|---|---|
| Gateway (WebSocket) | Elixir/Erlang | Миллионы параллельных лёгких процессов |
| Voice Server (SFU) | C++ | Минимальная задержка, прямой контроль над памятью |
| Data Services | Rust | Нет сборщика мусора, предсказуемая латентность |
| API | Python | Быстрая разработка, богатая экосистема |
| Frontend (web) | TypeScript/React | Стандарт индустрии для веб-приложений |
| Мобильные клиенты | Kotlin / Swift | Нативная разработка для Android / iOS |
Polyglot-архитектура — подход, при котором разные части системы пишутся на разных языках программирования. Вместо того чтобы использовать один язык для всего (как Reddit долго использовал Python), каждая команда выбирает язык, оптимальный для своей задачи. Плюс: максимальная производительность каждого компонента. Минус: сложнее нанимать, труднее переключаться между кодовыми базами.
Почему Elixir для Gateway
Elixir — функциональный язык программирования, работающий на BEAM VM (виртуальной машине Erlang). BEAM была создана компанией Ericsson в 1980-х для телекоммуникационных систем, где требуется обрабатывать миллионы параллельных соединений с минимальным даунтаймом.
BEAM VM — виртуальная машина Erlang, оптимизированная для: (1) массового параллелизма — может запускать миллионы лёгких процессов (каждый занимает ~2 КБ памяти, в отличие от потоков ОС, которые занимают ~1 МБ); (2) отказоустойчивости — если один процесс упадёт, остальные продолжают работать; (3) soft real-time — гарантирует, что ни один процесс не «заморозит» систему надолго.
В Discord каждый подключённый пользователь = один процесс BEAM. Каждый сервер (guild) = тоже один процесс BEAM. Процессы общаются друг с другом через отправку сообщений (message passing):
┌────────────────────────────────────────────────────────────┐
│ BEAM VM (один узел) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Session │ │Session │ │Session │ │Session │ │
│ │Process │ │Process │ │Process │ │Process │ │
│ │(User_A) │ │(User_B) │ │(User_C) │ │(User_D) │ │
│ └─────┬────┘ └─────┬────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └──────┬──────┴─────┬──────┘ │ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌──────────┐ ┌──────────┐ │ │
│ │ Guild │ │ Guild │ │ │
│ │ Process │ │ Process │ │ │
│ │(Server_1)│ │(Server_2)│ │ │
│ └──────────┘ └──────────┘ │ │
│ │
│ Каждый процесс: ~2-3 КБ RAM, изолирован, независим │
│ Миллионы процессов на одной машине │
└────────────────────────────────────────────────────────────┘
Discord масштабировал Elixir-систему до 5 миллионов одновременных пользователей, а позже, в связке с Rust, — до 11 миллионов.
Почему Rust для Data Services
Discord использовал Go для сервиса Read States, но столкнулся с проблемой: каждые 2 минуты сборщик мусора (Garbage Collector, GC) Go приостанавливал сервис на 10-50 миллисекунд, вызывая всплески задержки.
Garbage Collector (сборщик мусора) — автоматический механизм управления памятью. Когда программа создаёт объекты в памяти и перестаёт их использовать, GC находит эти «мусорные» объекты и освобождает память. Проблема: GC периодически «останавливает мир» (stop-the-world pause) — приостанавливает все потоки программы, пока очищает память. Для большинства приложений это незаметно, но для real-time систем с миллионами пользователей — катастрофа.
Discord переписал Read States на Rust — язык без сборщика мусора. Результаты:
Производительность Read States:
Go Rust
┌───────┐ ┌───────┐
Среднее: │ ~1 ms │ │~0.15ms│ (6.5x быстрее)
├───────┤ ├───────┤
P99: │~10 ms │ │ ~1 ms │ (10x быстрее)
├───────┤ ├───────┤
Пики (GC): │~50 ms │ │ нет │ (160x стабильнее)
└───────┘ └───────┘
Rust — системный язык программирования, который обеспечивает безопасность памяти без сборщика мусора. Вместо GC, Rust использует систему ownership (владение) — компилятор на этапе сборки определяет, когда память должна быть освобождена. Это даёт предсказуемую производительность без пауз GC, но требует от программиста более строгого подхода к написанию кода.
4.4. Высокоуровневая архитектура
┌──────────────────────────────────────────────┐
│ Клиенты (Web / Desktop / iOS / Android) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ WebSocket │ │ UDP/WebRTC │ │
│ │ (текст, presence,│ │ (голос, видео) │ │
│ │ уведомления) │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
└───────────┼─────────────────────┼────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Gateway │ │ Voice Server │
│ (Elixir/BEAM) │ │ (C++ SFU) │
│ │ │ │
│ Миллионы │ │ Миллионы │
│ WebSocket- │ │ голосовых │
│ соединений │ │ потоков │
└────────┬────────┘ └──────────────────┘
│
┌────────▼────────┐
│ API Service │
│ (Python) │
└────────┬────────┘
│
┌───────┼────────────┬────────────┬──────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───────┐┌────────┐┌───────────┐┌───────────┐┌──────────────┐
│Message││ Read ││ Guild ││ Presence ││ Permissions │
│Service││ States ││ Service ││ Service ││ Service │
│(Rust) ││ (Rust) ││ (Elixir) ││ (Elixir) ││ │
└───┬───┘└──┬─────┘└─────┬─────┘└─────┬─────┘└──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐┌────────┐┌───────────┐┌───────────┐
│ScyllaDB││ScyllaDB││ Postgres ││ Redis │
│(msgs) ││(states)││ (guilds) ││(presence, │
│ ││ ││ ││ cache) │
└────────┘└────────┘└───────────┘└───────────┘
4.5. Gateway и система доставки сообщений
Как работает Gateway
Gateway — это точка входа для всех WebSocket-соединений. Когда пользователь открывает Discord:
- Клиент устанавливает WebSocket-соединение с Gateway.
- Gateway создаёт Session Process (процесс сессии) в BEAM VM для этого пользователя.
- Session Process «подписывается» на все серверы (guilds), в которых состоит пользователь.
- Любое событие (новое сообщение, смена статуса, обновление канала) доставляется через этот WebSocket.
Пользователь отправляет сообщение в #general на Server_1:
User_A
│
│ WebSocket: "Hello!"
▼
Gateway
│
▼
Session Process (User_A)
│
│ Отправляет сообщение в Guild Process
▼
Guild Process (Server_1)
│
│ 1. Сохраняет в ScyllaDB
│ 2. Определяет получателей
│ (все, у кого есть доступ к #general)
│ 3. Fan-out: рассылает всем Session
│ Processes получателей
│
├──→ Session(User_B) ──→ WebSocket ──→ User_B
├──→ Session(User_C) ──→ WebSocket ──→ User_C
├──→ Session(User_D) ──→ WebSocket ──→ User_D
└──→ ...
Проблема: Fan-out на огромных серверах
Fan-out (веерная рассылка) — процесс доставки одного события всем заинтересованным получателям. На маленьком сервере (10 человек) это тривиально. На сервере Midjourney (15+ миллионов участников, 1+ миллион онлайн одновременно) — это инженерный кошмар.
Представьте: одно сообщение в канале #general → нужно уведомить 1 миллион онлайн-пользователей. При наивном подходе Guild Process должен:
- Пройтись по списку из 1 000 000 Session Processes.
- Каждому отправить сообщение.
- Сделать это за миллисекунды (real-time!).
Решение: Maxjourney — масштабирование до 15 миллионов
Discord собрал команду инженеров (проект Maxjourney), которая решила эту проблему через серию оптимизаций:
1. Passive Sessions (пассивные сессии)
Большинство пользователей на огромных серверах неактивны — они вступили в сервер, но не заходят в него каждый день. Discord определяет «пассивных» пользователей и не отправляет им обновления.
Сервер Midjourney: 15 млн участников
Онлайн: 1 000 000 (6.7%)
Активные: 200 000 (смотрят каналы)
Пассивные: 800 000 (онлайн, но не смотрят)
Оффлайн: 14 000 000 (93.3%)
С пассивными сессиями fan-out:
Было: 1 000 000 получателей
Стало: 200 000 получателей ← 80% снижение!
Эта оптимизация сделала fan-out на 90% дешевле.
2. Relay Processes (процессы-ретрансляторы)
Вместо того чтобы Guild Process отправлял сообщения напрямую каждому Session Process, вводится промежуточный слой — Relays:
Без Relays: С Relays:
Guild Process Guild Process
│ │
├──→ Session 1 ├──→ Relay A ──→ Sessions 1-15000
├──→ Session 2 ├──→ Relay B ──→ Sessions 15001-30000
├──→ Session 3 ├──→ Relay C ──→ Sessions 30001-45000
├──→ ... └──→ ...
└──→ Session 200000
Guild отправляет ~13 сообщений
Guild отправляет вместо 200 000
200 000 сообщений
Каждый Relay обслуживает до 15 000 пользователей и берёт на себя:
- Fan-out до своей группы пользователей.
- Проверку прав доступа (permissions).
- Фильтрацию событий (не все события нужны всем).
3. ETS (Erlang Term Storage)
Для хранения списка участников сервера Discord использует ETS — встроенную in-memory базу данных BEAM VM:
ETS (Erlang Term Storage) — in-memory таблица, встроенная в BEAM VM. Она позволяет хранить данные, доступные всем процессам на одном узле, с O(1) доступом по ключу. В отличие от обычных переменных Elixir-процесса, ETS можно безопасно читать из множества процессов одновременно без блокировок.
Хранение списка из 15 миллионов участников внутри одного Guild Process невозможно — процесс занял бы слишком много памяти. ETS позволяет разделить данные между Guild Process и Relay Processes без копирования.
4. Отдельный Sender Process
Fan-out — это CPU-интенсивная операция (сериализация сообщений, отправка по сети). Чтобы не блокировать Guild Process (который должен обрабатывать новые события), fan-out делегируется отдельному Sender Process:
Guild Process (лёгкий, быстрый)
│
│ "Отправь это сообщение всем"
▼
Sender Process (тяжёлый, но не блокирует Guild)
│
├──→ Relay A
├──→ Relay B
└──→ Relay C
Результат
Благодаря этим оптимизациям Discord расширил лимит одного сервера с ~1 миллиона до 15+ миллионов участников. Midjourney продолжает работать в одном Discord-сервере без разделения.
4.6. Хранение сообщений: от Cassandra к ScyllaDB
История: MongoDB → Cassandra → ScyllaDB
Discord прошёл через три поколения хранилищ для сообщений:
Поколение 1: MongoDB (2015-2016) На старте Discord хранил сообщения в MongoDB. При маленьком масштабе это работало, но по мере роста начались проблемы с производительностью.
Поколение 2: Cassandra (2016-2022) Discord мигрировал на Apache Cassandra — распределённую NoSQL БД, оптимизированную для записи. Cassandra хорошо справлялась с растущей нагрузкой, но со временем появились проблемы:
- Hot partitions (горячие партиции) — некоторые каналы (например, #general на огромных серверах) генерировали непропорционально много данных, перегружая отдельные узлы.
- GC pauses — Cassandra написана на Java, и её сборщик мусора вызывал всплески задержки.
- Compaction — фоновый процесс объединения файлов данных (SSTable compaction) периодически замедлял чтение.
Поколение 3: ScyllaDB (2022-настоящее) Discord мигрировал на ScyllaDB — drop-in замену Cassandra, написанную на C++.
ScyllaDB — распределённая NoSQL база данных, API-совместимая с Cassandra, но написанная на C++ с использованием фреймворка Seastar для асинхронного I/O. Благодаря отсутствию JVM и сборщика мусора, ScyllaDB обеспечивает предсказуемо низкую задержку. Один узел ScyllaDB может заменить 3-5 узлов Cassandra.
Модель данных сообщений
Сообщения хранятся с партиционированием по (channel_id, bucket), где bucket — временной интервал (например, 10 дней):
Partition Key: (channel_id, bucket)
Clustering Key: message_id (содержит timestamp — Snowflake ID)
┌──────────────────────────────────────────────────────────────┐
│ Partition: channel_12345, bucket_2025-01-01 │
│ │
│ message_id │ author_id │ content │ timestamp │
│ ────────────┼───────────┼──────────────┼─────────────────── │
│ 98765432100 │ user_42 │ "Привет!" │ 2025-01-01 10:00 │
│ 98765432200 │ user_99 │ "Здорово!" │ 2025-01-01 10:01 │
│ 98765432300 │ user_42 │ "Как дела?" │ 2025-01-01 10:02 │
│ ... │ ... │ ... │ ... │
└──────────────────────────────────────────────────────────────┘
Partition Key — ключ, по которому данные распределяются между узлами кластера. Все записи с одинаковым partition key хранятся на одном узле. Это обеспечивает быстрое чтение: «все сообщения в канале за последние 10 дней» читаются с одного узла.
Clustering Key — ключ, по которому записи внутри одной партиции упорядочены.
message_idсодержит timestamp, поэтому сообщения автоматически отсортированы по времени.
Snowflake ID
Discord использует Snowflake ID — формат уникальных идентификаторов, заимствованный у Twitter:
┌──────────────────────────────────────────────────────────────┐
│ Snowflake ID (64 бита) │
│ │
│ ┌─────────────────────┬────────────┬──────┬──────────────┐ │
│ │ Timestamp │ Worker ID │ PID │ Sequence │ │
│ │ (42 бита) │ (5 бит) │(5бит)│ (12 бит) │ │
│ └─────────────────────┴────────────┴──────┴──────────────┘ │
│ │
│ Timestamp: миллисекунды с эпохи Discord (2015-01-01) │
│ Worker ID: идентификатор сервера │
│ PID: идентификатор процесса │
│ Sequence: счётчик в пределах миллисекунды (до 4096/мс) │
└──────────────────────────────────────────────────────────────┘
Snowflake ID решает несколько проблем:
- Глобальная уникальность — не нужен центральный генератор ID, каждый сервер генерирует свои.
- Временная упорядоченность — ID сортируются по времени, что даёт бесплатную сортировку сообщений.
- Компактность — 64 бита вместо 128 бит UUID.
- Не раскрывает информацию — в отличие от автоинкрементных ID, по Snowflake нельзя определить, сколько всего сообщений в системе.
Data Services (Rust) — промежуточный слой
Между приложением и ScyllaDB стоит Data Services — слой, написанный на Rust, который решает ключевую проблему: request coalescing (объединение запросов).
Request Coalescing (объединение запросов) — техника, при которой множество одинаковых запросов, пришедших примерно одновременно, объединяются в один запрос к базе данных. Результат одного запроса затем копируется всем ожидающим клиентам.
Без request coalescing: С request coalescing:
User_A → GET msg_123 User_A ─┐
User_B → GET msg_123 User_B ─┤─→ GET msg_123 (1 запрос)
User_C → GET msg_123 User_C ─┘ │
▼
3 запроса к ScyllaDB 1 запрос к ScyllaDB
Результат копируется A, B, C
Это критически важно для Discord: когда 1000 пользователей одновременно открывают один канал, они все запрашивают одни и те же сообщения. Без coalescing база данных получит 1000 одинаковых запросов. С coalescing — один.
Результаты миграции на ScyllaDB
Cassandra ScyllaDB
┌────────────┐ ┌────────────┐
Узлов кластера: │ 177 │ │ 72 │ (2.5x меньше)
├────────────┤ ├────────────┤
P99 чтение: │ 40-125 ms │ │ 15 ms │ (3-8x быстрее)
├────────────┤ ├────────────┤
P99 запись: │ 5-70 ms │ │ 5 ms │ (до 14x стабильнее)
├────────────┤ ├────────────┤
GC-паузы: │ Да ☹ │ │ Нет ☺ │
└────────────┘ └────────────┘
Миграция триллионов сообщений была выполнена с помощью кастомного инструмента на Rust за 9 дней (первоначальная оценка — 3 месяца).
4.7. Голосовая связь (Voice)
Почему голос — это сложно
Голосовая связь кардинально отличается от текстовых сообщений:
- Протокол: UDP вместо TCP. UDP не гарантирует доставку, но значительно быстрее — потеря одного пакета голоса незаметна, а задержка в 100 мс делает разговор невозможным.
- Задержка: текстовое сообщение может быть доставлено за 200 мс — пользователь не заметит. Голос должен быть доставлен за <150 мс (задержка в обе стороны), иначе разговор неестественный.
- Пропускная способность: непрерывный поток аудио (и видео), а не дискретные сообщения.
UDP (User Datagram Protocol) — транспортный протокол, который отправляет пакеты данных без установления соединения и без гарантии доставки. В отличие от TCP, UDP не ждёт подтверждения получения и не перепосылает потерянные пакеты. Для голоса это идеально: лучше потерять 20 мс аудио, чем задержать весь поток, ожидая повторной передачи.
SFU (Selective Forwarding Unit)
Discord использует архитектуру SFU для голосовой связи. SFU — это сервер, который получает аудио/видеопотоки от каждого участника и перенаправляет их остальным, не смешивая.
SFU (Selective Forwarding Unit) — сервер, который принимает медиапотоки от всех участников звонка и выборочно перенаправляет их другим участникам. В отличие от MCU (Multipoint Control Unit), который смешивает все потоки в один, SFU просто пересылает. Это экономит CPU на сервере (не нужно декодировать и микшировать), но требует больше пропускной способности у клиентов.
Три архитектуры для группового звонка:
1. Peer-to-Peer (P2P): 2. MCU: 3. SFU (Discord):
Каждый → каждому Все → Сервер → Все → Сервер →
Смешивает → Один Пересылает → Все
A ←──→ B поток всем индивидуально
A ←──→ C
B ←──→ C A ──→ ┌─────┐ ──→ A A ──→ ┌─────┐ ──→ A(B+C)
B ──→ │ MCU │ ──→ B B ──→ │ SFU │ ──→ B(A+C)
Масштаб: до 5 чел. C ──→ └─────┘ ──→ C C ──→ └─────┘ ──→ C(A+B)
(N² соединений)
Масштаб: десятки Масштаб: сотни
Дорого по CPU Дёшево по CPU
Архитектура Voice Server
┌──────────────────────────────────────────────────────────────┐
│ Voice Server │
│ │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ Signaling Component │ │ SFU Component │ │
│ │ (Elixir) │ │ (C++) │ │
│ │ │ │ │ │
│ │ - Генерация stream ID│ │ - Приём UDP-пакетов │ │
│ │ - Генерация ключей │ │ - Перенаправление │ │
│ │ шифрования │ │ аудио/видео │ │
│ │ - Управление SFU │ │ - RTCP (контроль │ │
│ │ - Speaking indication│ │ качества) │ │
│ │ │ │ - Транспортная │ │
│ │ │ │ трансляция │ │
│ └───────────────────────┘ │ (native ↔ browser) │ │
│ └───────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Voice Server состоит из двух частей:
- Signaling Component (Elixir) — управляющая плоскость: генерирует идентификаторы потоков, ключи шифрования, координирует подключение участников.
- SFU Component (C++) — плоскость данных: принимает и перенаправляет аудио/видеопакеты.
WebRTC
Discord использует WebRTC (Web Real-Time Communication) — открытый стандарт для peer-to-peer коммуникации в реальном времени:
WebRTC — набор протоколов и API для аудио/видео коммуникации в реальном времени через браузер или нативные приложения. Включает: (1) ICE (Interactive Connectivity Establishment) — установление соединения через NAT и файрволы; (2) DTLS (Datagram Transport Layer Security) — шифрование; (3) SRTP (Secure Real-time Transport Protocol) — передача зашифрованного медиа.
Discord использует:
- Для браузера: встроенную реализацию WebRTC браузера.
- Для десктопа, iOS, Android: собственный C++ медиа-движок, построенный на WebRTC native library.
SFU действует как мост между браузерными и нативными клиентами, транслируя между двумя форматами шифрования и транспорта.
Аудиокодек Opus
Discord использует Opus — аудиокодек с открытым исходным кодом:
Opus — аудиокодек, разработанный для интернет-коммуникаций. Он объединяет лучшие свойства кодеков SILK (для речи) и CELT (для музыки). Opus поддерживает битрейт от 6 kbps (телефонное качество) до 510 kbps (студийное качество) и задержку от 2.5 мс. Это «швейцарский нож» аудиокодеков.
Discord настраивает Opus адаптивно:
- В обычном голосовом чате: 64 kbps (хорошее качество речи).
- В «музыкальном» режиме: 128 kbps (для стриминга музыки).
- При плохом соединении: автоматическое снижение до 32 kbps.
4.8. Система присутствия (Presence)
Что такое Presence
Когда вы видите зелёный кружок рядом с именем друга в Discord — это Presence (присутствие). Система отслеживает:
- Статус: Online, Idle, Do Not Disturb, Invisible, Offline.
- Активность: «Играет в Valorant», «Слушает Spotify», «Стримит на YouTube».
- Custom Status: пользовательский текст с эмодзи.
Масштаб проблемы
Presence — это одна из самых нагруженных подсистем Discord:
- 259 млн MAU, десятки миллионов одновременно онлайн.
- Каждая смена статуса (онлайн → оффлайн, начал играть → перестал играть) должна быть доставлена всем друзьям пользователя.
- У среднего пользователя ~50-100 друзей, у некоторых — тысячи.
User_A меняет статус на "Играет в Valorant"
│
▼
Presence Service
│
│ Fan-out: отправить обновление всем друзьям User_A
│
├──→ Friend_1 (онлайн) → WebSocket → обновить список друзей
├──→ Friend_2 (онлайн) → WebSocket → обновить список друзей
├──→ Friend_3 (оффлайн) → пропустить
├──→ Friend_4 (онлайн) → WebSocket → обновить список друзей
└──→ ...
Ленивая загрузка Presence
Discord использует lazy loading (ленивую загрузку) для presence:
- Presence друзей загружается при подключении.
- Presence участников сервера загружается только для видимых пользователей (тех, кто в sidebar).
- На огромных серверах (>1000 онлайн) Discord показывает только ~200 пользователей в sidebar и загружает presence только для них.
4.9. Роли и права доступа (Permissions)
Гранулярная система прав
Discord имеет одну из самых гранулярных систем прав доступа среди social-платформ. Каждый сервер может настраивать десятки отдельных прав для каждой роли и каждого канала:
Права доступа (примеры из ~40+ доступных):
Текстовые каналы:
✓ Отправлять сообщения
✓ Вставлять ссылки
✓ Прикреплять файлы
✓ Добавлять реакции
✓ Упоминать @everyone
✓ Управлять сообщениями (удалять чужие)
✓ Создавать треды
✓ Создавать приватные треды
Голосовые каналы:
✓ Подключаться
✓ Говорить
✓ Стримить видео
✓ Использовать активности
✓ Выключать микрофон участникам
✓ Перемещать участников
Административные:
✓ Управлять каналами
✓ Управлять ролями
✓ Банить участников
✓ Просматривать аудит-лог
Модель вычисления прав
Права в Discord вычисляются в реальном времени при каждом действии пользователя. Система учитывает:
Итоговые права = Base permissions (из роли @everyone)
| Role permissions (из всех ролей пользователя)
& ~Channel deny overrides
| Channel allow overrides
- @everyone — базовые права, которые есть у всех.
- Роли — пользователь может иметь несколько ролей, их права объединяются (OR).
- Channel overrides — для каждого канала можно переопределить права конкретной роли.
Пример:
Роль @everyone: Отправлять сообщения = ✓
Роль "Новичок": Вставлять ссылки = ✗
Роль "Модератор": Управлять сообщениями = ✓
Канал #announcements: Отправлять сообщения = ✗ (override)
User_A (роли: @everyone + Модератор) в #announcements:
Отправлять сообщения = ✗ (channel override побеждает)
Управлять сообщениями = ✓ (из роли Модератор)
Вычисление прав — битовая операция над числами, где каждый бит соответствует одному праву. Это O(1) по времени, но при изменении ролей нужно инвалидировать кэш прав для всех затронутых пользователей.
4.10. Боты и API
Экосистема ботов
Discord имеет одну из самых развитых экосистем ботов среди social-платформ. Боты — это программы, которые подключаются к серверу и автоматизируют задачи:
- Модерация — автоматическое удаление спама, бан нарушителей.
- Музыка — воспроизведение музыки в голосовых каналах.
- Игры — мини-игры внутри Discord.
- Интеграции — уведомления от GitHub, Jira, мониторинга.
- AI — чат-боты на основе LLM.
Как боты подключаются
Бот — это обычный WebSocket-клиент с Bot Token вместо пользовательского токена. Он подключается через Gateway и получает события точно так же, как обычный пользователь:
Bot Gateway Guild Process
│ │ │
│── WebSocket IDENTIFY ─►│ │
│ (Bot Token) │ │
│ │── Subscribe ────────►│
│ │ │
│◄── MESSAGE_CREATE ─────│◄── New message ──────│
│ │ │
│── POST /channels/ │ │
│ {channelId}/messages │── Save & fan-out ───►│
│ "Бот отвечает!" │ │
Rate Limiting для ботов
Discord строго ограничивает частоту запросов ботов:
Rate Limiting — механизм, ограничивающий количество запросов, которые клиент может сделать за единицу времени. Discord использует Token Bucket алгоритм: у каждого бота есть «ведро» с токенами. Каждый запрос забирает один токен. Токены восполняются с фиксированной скоростью. Если ведро пустое — запрос отклоняется с кодом 429 (Too Many Requests).
Лимиты:
- Глобальный: 50 запросов/секунду на бота.
- На эндпоинт: разные лимиты (например, 5 сообщений/5 сек на канал).
- При превышении: бот получает
Retry-Afterзаголовок с временем ожидания.
4.11. Масштабирование и инфраструктура
Google Cloud Platform
Discord размещается в Google Cloud Platform (GCP), используя:
- Google Compute Engine — виртуальные машины для основных сервисов.
- Google Kubernetes Engine (GKE) — оркестрация контейнеров.
- Cloud Storage — хранение вложений (файлы, изображения).
- Cloudflare — CDN и DDoS-защита.
Consistent Hashing для маршрутизации
Discord использует Consistent Hashing для распределения Guild Processes по серверам:
Consistent Hashing — алгоритм распределения данных по серверам, при котором добавление или удаление сервера затрагивает минимальное количество ключей. Представьте круг (hash ring): серверы размещены в точках круга. Каждый ключ (guild_id) хешируется и «падает» на ближайший сервер по часовой стрелке. При добавлении нового сервера перемещается только часть ключей.
Consistent Hash Ring
Server A
╱ ╲
╱ ╲
╱ Guild_1 ● ╲
╱ Guild_2 ● ╲
Server D Guild_3 ● Server B
╲ ╱
╲ Guild_4 ● ╱
╲ Guild_5 ● ╱
╲ ╱
Server C
Guild_1, Guild_2 → Server A
Guild_3 → Server B
Guild_4, Guild_5 → Server C
Если Server B упадёт, только Guild_3 переедет на Server C. Остальные не затронуты.
Отказоустойчивость
- Автоматическое переподключение — при потере WebSocket-соединения клиент автоматически переподключается и получает пропущенные события.
- Session resumption — при кратковременном разрыве соединения клиент может «возобновить» сессию, получив только пропущенные события, а не полное начальное состояние.
- Graceful degradation — если voice server недоступен, текстовые каналы продолжают работать.
4.12. Сравнение с предыдущими системами
| Аспект | TikTok | Discord | ||
|---|---|---|---|---|
| Основная БД | Espresso | ByteGraph + S3 | PostgreSQL | ScyllaDB + PostgreSQL |
| Кэш | Memcached | Redis | Memcached + Redis | ETS (BEAM) + Redis |
| Очередь | Kafka | Kafka + Flink | Kafka (Kafqueue) | Kafka |
| CDN | Akamai | Multi-CDN | Fastly | Cloudflare |
| Бэкенд | Java | Go / Python | Go (с Python) | Elixir + Rust + C++ + Python |
| Протокол связи | HTTP REST | HTTP REST | HTTP REST | WebSocket + UDP |
| Хранение контента | Doc DB (Espresso) | Object Store (S3) | RDBMS (Postgres) | Wide-column (ScyllaDB) |
| Real-time | Ограниченно | Нет | Нет | Ядро системы |
| Голос/видео | Нет | Нет | Нет | C++ SFU (WebRTC) |
| Масштаб «сообщества» | ~30K (компании) | Неограничен | Неограничен | 15M в одном сервере |
4.13. Итоговая архитектура
┌─────────────────────────────────────────────────────────┐
│ Клиенты (Web / Desktop / iOS / Android) │
│ ┌────────────────────────┐ ┌───────────────────────┐ │
│ │ WebSocket (Gateway) │ │ UDP/WebRTC (Voice) │ │
│ └───────────┬────────────┘ └───────────┬───────────┘ │
└──────────────┼───────────────────────────┼──────────────┘
│ │
┌────────▼────────┐ ┌─────────▼─────────┐
│ Cloudflare CDN │ │ Voice Servers │
│ (DDoS, static) │ │ (C++ SFU, │
└────────┬────────┘ │ WebRTC, Opus) │
│ └───────────────────┘
┌────────▼────────┐
│ Gateway │
│ (Elixir/BEAM) │
│ Миллионы │
│ WebSocket- │
│ соединений │
└────────┬────────┘
│
┌────────▼────────┐
│ Guild Processes│ ← 1 процесс на сервер
│ (Elixir) │ Fan-out через Relays
│ + Relay │ + Passive Sessions
│ + Sender │ + ETS
└────────┬────────┘
│
┌────────▼────────┐
│ API Service │
│ (Python) │
└────────┬────────┘
│
┌──────────┼──────────┬───────────┬───────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐┌────────┐┌──────────┐┌───────────┐┌──────────┐
│Message ││ Read ││Permiss- ││ Presence ││ Search │
│Service ││ States ││ions Svc ││ Service ││ Service │
│(Rust) ││(Rust) ││ ││ (Elixir) ││ │
└───┬────┘└────┬───┘└─────┬────┘└─────┬─────┘└────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐┌────────┐┌──────────┐┌──────────┐┌──────────┐
│ScyllaDB││ScyllaDB││PostgreSQL││ Redis ││Elastic- │
│(msgs) ││(states)││(guilds, ││(presence,││search │
│ ││ ││ roles) ││ cache) ││ │
└────────┘└────────┘└──────────┘└──────────┘└──────────┘
┌──────────────────────────────────────┐
│ Apache Kafka │
│ (асинхронная обработка, │
│ аналитика, ML-пайплайны) │
└──────────────────────────────────────┘
4.14. Ключевые уроки и принципы
1. Правильный язык для правильной задачи
Discord не пытается решить все задачи одним языком. Elixir — для миллионов параллельных соединений. Rust — для предсказуемой латентности без GC. C++ — для real-time медиа. Python — для быстрой итерации бизнес-логики. Это сложнее в найме и поддержке, но даёт оптимальную производительность каждого компонента.
2. GC — враг real-time систем
И Cassandra (Java GC), и Read States (Go GC) страдали от пауз сборщика мусора. Discord решил это миграцией на ScyllaDB (C++) и Rust. Если ваша система должна обеспечивать стабильно низкую задержку — задумайтесь о языках без GC.
3. Request Coalescing — простая, но мощная оптимизация
Когда тысячи пользователей одновременно запрашивают одни и те же данные (сообщения в популярном канале), объединение запросов снижает нагрузку на БД в сотни раз.
4. Passive Sessions — не обновляй тех, кому не нужно
90% экономии fan-out на огромных серверах достигнуто одной идеей: не отправлять обновления пользователям, которые не смотрят на этот сервер.
5. Snowflake ID — больше, чем просто уникальный идентификатор
Snowflake ID встраивает временную метку, что даёт бесплатную сортировку по времени, устраняет необходимость в центральном генераторе ID и оптимизирует модель данных в wide-column БД.
6. SFU — оптимальный баланс для групповых звонков
P2P не масштабируется дальше 5 участников. MCU слишком дорог по CPU. SFU перекладывает часть работы на клиентов (декодирование нескольких потоков), но зато сервер просто «пересылает» — это дёшево и масштабируемо.
7. Миграция данных — это инженерная задача, а не операционная
Discord перенёс триллионы сообщений из Cassandra в ScyllaDB за 9 дней с помощью кастомного инструмента на Rust. Стандартный инструмент (Spark migrator) потребовал бы 3 месяца. Иногда быстрее написать свой инструмент, чем мучиться с универсальным.
4.15. Вопросы для самопроверки
- Почему Discord использует WebSocket, а не HTTP для доставки сообщений? В чём разница между этими протоколами?
- Объясните, как BEAM VM обеспечивает масштабирование до миллионов одновременных соединений. Чем процесс BEAM отличается от потока ОС?
- Почему Discord переписал Read States с Go на Rust? Что такое GC-паузы и как Rust их избегает?
- Что такое Request Coalescing и почему оно критически важно для Discord?
- Объясните три архитектуры группового звонка (P2P, MCU, SFU). Почему Discord выбрал SFU?
- Как Discord масштабировал один сервер до 15 миллионов пользователей? Перечислите ключевые оптимизации (Passive Sessions, Relays, ETS, Sender Process).
- Что такое Snowflake ID? Какие проблемы он решает по сравнению с UUID и автоинкрементными ID?
- Почему Discord мигрировал с Cassandra на ScyllaDB? Какие метрики улучшились?
- Как работает система прав доступа в Discord? Почему она вычисляется через битовые операции?
- Сравните модели доставки контента: HTTP request-response (LinkedIn, Reddit) vs WebSocket push (Discord) vs CDN streaming (TikTok). Для каких задач оптимальна каждая?
Источники
- How Discord Stores Trillions of Messages — Discord Blog
- How Discord Migrated Trillions of Messages from Cassandra to ScyllaDB — ScyllaDB
- Discord Migrates Trillions of Messages from Cassandra to ScyllaDB — InfoQ
- Why Discord Is Switching from Go to Rust — Discord Blog
- How Discord Scaled Elixir to 5,000,000 Concurrent Users — Discord Blog
- How Discord Used Rust to Scale Elixir to 11 Million Concurrent Users — Medium
- Maxjourney: Pushing Discord's Limits with a Million+ Online Users in a Single Server — Discord Blog
- How Discord Serves 15-Million Users on One Server — ByteByteGo
- Discord Scales to 1 Million+ Online MidJourney Users in a Single Server — InfoQ
- How Discord Handles Two and Half Million Concurrent Voice Users Using WebRTC — Discord Blog
- Discord's Voice Stack: How Rust, Elixir, and WebRTC Power 150 Million Voices — Medium
- How Discord Moved Engineering to Cloud Development Environments — Discord Blog
- Architecting for Hyperscale: An In-Depth Analysis of Discord's Billion-Message-Per-Day Infrastructure — d4dummies
- Discord Statistics 2025 — Notta
- Discord Statistics 2025 — The Social Shepherd
- Discord Statistics 2025 — SQMagazine