
С самого начала мы так спланировали инженерные и продуктовые решения, чтобы Discord хорошо подходил для голосовых чатов во время игры с друзьями. Эти решения позволили сильно масштабировать систему, обладая небольшой командой и ограниченными ресурсами.
В статье рассматриваются различных технологии, которые использует Discord для аудио/видеочатов.
Для ясности всю группу пользователей и каналов мы будем называть «группа» (guild) — в клиенте они называются «серверами». Вместо этого здесь термин «сервер» относится к нашей серверной инфраструктуре.
Главные принципы
Каждый аудио/видеочат в Discord поддерживает много участников. Мы наблюдали, как в больших групповых чатах тысяча человек разговаривают по очереди. Такая поддержка требует клиент-серверной архитектуры, потому что одноранговая пиринговая сеть становится непомерно дорогой при увеличении числа участников.
Маршрутизация сетевого трафика через серверы Discord также гарантирует, что ваш IP-адрес никогда не виден — и никто не запустит DDoS-атаку. У маршрутизация через серверы есть и другие преимущества: например, модерация. Администраторы могут быстренько отключить звук и видео нарушителям.
Клиентская архитектура
Discord работает на многих платформах.
- Веб (Chrome/Firefox/Edge и т. д.)
- Автономное приложение (Windows, MacOS, Linux)
- Телефон (iOS/Android)
Все эти платформы мы можем поддерживать только одним способом: через повторное использование кода WebRTC. Эта спецификация для коммуникаций в реальном времени включает сетевые, аудио- и видеокомпоненты. Стандарт принят Консорциумом World Wide Web и Инженерной группой по Интернету. WebRTC доступен во всех современных браузерах и как нативная библиотека для внедрения в приложения.
Аудио и видео в Discord работает на WebRTC. Таким образом, браузерное приложение полагается на реализацию WebRTC в браузере. Однако приложения для десктопов, iOS и Android используют единый мультимедийный движок C++, построенный поверх собственной библиотеки WebRTC, специально адаптированной к потребностям наших пользователей. Это означает, что некоторые функции в приложении работают лучше, чем в браузере. Например, в наших нативных приложениях мы можем:
- Обойти приглушение громкости в Windows по умолчанию, когда все приложения автоматически приглушаются при использовании гарнитуры. Это нежелательно, когда вы с друзьями пошли в рейд и координируете действия в чате Discord.
- Использовать собственный регулятор громкости вместо глобального микшера операционной системы.
- Обрабатывать исходные аудиоданные для обнаружения голосовой активности и трансляции звука и видео в играх.
- Уменьшате пропускную способность и потребление ресурсов CPU в периоды тишины — даже в самых многочисленных голосовых чатов в любой момент времени одновременно говорят всего несколько человек.
- Обеспечить общесистемную функциональность режима «рации» (push to talk).
- Отправлять вместе с аудио- видеопакетами дополнительную информацию (например, индикатор приоритета в чате).
Наличие собственной версии WebRTC означает частые обновления для всех пользователей: это трудоёмкий процесс, который мы стараемся автоматизировать. Однако эти усилия окупаются благодаря специфическим функциям для наших игроков.
В Discord голосовая и видеосвязь инициируется путём ввода голосового канала или вызова. То есть связь всегда инициируется клиентом — это снижает сложность клиентской и серверной части, а также повышает устойчивость к ошибкам. В случае сбоя инфраструктуры участники могут просто повторно подключиться к новому внутреннему серверу.
Под нашим контролем
Контроль нативной библиотеки позволяет реализовать некоторые функции иначе, чем в браузерной реализации WebRTC.
Во-первых, WebRTC полагается на протокол Session Description Protocol (SDP) для согласования аудио/видео между участниками (до 10 КБ на каждый обмен пакетами). В собственной библиотеке для создания обоих потоков — входящего и исходящего — используется API более низкого уровня от WebRTC (
webrtc::Call). При подключении к голосовому каналу происходит минимальный обмен информацией. Это адрес и порт сервера бэкенда, метод шифрования, ключи, кодек и идентификация потока (около 1000 байт).webrtc::AudioSendStream* createAudioSendStream(
uint32_t ssrc,
uint8_t payloadType,
webrtc::Transport* transport,
rtc::scoped_refptr<webrtc::AudioEncoderFactory> audioEncoderFactory,
webrtc::Call* call)
{
webrtc::AudioSendStream::Config config{transport};
config.rtp.ssrc = ssrc;
config.rtp.extensions = {{"urn:ietf:params:rtp-hdrext:ssrc-audio-level", 1}};
config.encoder_factory = audioEncoderFactory;
const webrtc::SdpAudioFormat kOpusFormat = {"opus", 48000, 2};
config.send_codec_spec =
webrtc::AudioSendStream::Config::SendCodecSpec(payloadType, kOpusFormat);
webrtc::AudioSendStream* audioStream = call->CreateAudioSendStream(config);
audioStream->Start();
return audioStream;
}Кроме того, для определения наилучшего маршрута между участниками WebRTC использует Interactive Connectivity Establishment (ICE). Поскольку у нас каждый клиент подключается к серверу, нам не нужен ICE. Это позволяет обеспечить гораздо более надёжное соединение, если вы находитесь за NAT, а также сохранить ваш IP-адрес в секрете от других участников. Клиенты периодически пингуются, чтобы файрвол сохранял открытое соединение.
Наконец, WebRTC использует Secure Real-time Transport Protocol (SRTP) для шифрования носителей. Ключи шифрования устанавливаются с помощью протокола Datagram Transport Layer Security (DTLS) на основе стандартного TLS. Встроенная библиотека WebRTC позволяет реализовать собственный транспортный уровень с помощью
webrtc::Transport API.Вместо DTLS/SRTP мы решили использовать более быстрое шифрование Salsa20. Кроме того, мы не отправляем аудиоданные в периоды тишины — частое явление, особенно в больших чатах. Это приводит к значительной экономии пропускной способности и ресурсов CPU, однако и клиент, и сервер должны быть готовы в любой момент прекратить приём данных и переписать порядковые номера аудио/видеопакетов.
Поскольку веб-приложение использует браузерную реализацию WebRTC API, тут нельзя отказаться от SDP, ICE, DTLS и SRTP. Клиент и сервер обмениваются всей необходимой информацией (менее 1200 байт при обмене пакетами) — и у клиентов на основе этой информации устанавливается сессия SDP. Бэкенд отвечает за устранение различий между десктопными и браузерными приложениями.
Архитектура бэкенда
На бэкенде работает несколько сервисов для голосовых чатов, но мы сосредоточимся на трёх: Discord Gateway, Discord Guilds и Discord Voice. Все наши сигнальные серверы написаны на Elixir, что позволяет многократно повторно использовать код.
Когда вы в сети, ваш клиент поддерживает соединение WebSocket к шлюзу Discord Gateway (мы называем его шлюзовым подключением WebSocket). Через это соединение ваш клиент получает события, связанные с группами и каналами, текстовые сообщения, пакеты присутствия и т. д.
При подключении к голосовому каналу статус подключения отображается объектом состояния голосовой связи. Клиент обновляет этот объект по шлюзовому подключению.
defmodule VoiceStates.VoiceState do
@type t :: %{
session_id: String.t(),
user_id: Number.t(),
channel_id: Number.t() | nil,
token: String.t() | nil,
mute: boolean,
deaf: boolean,
self_mute: boolean,
self_deaf: boolean,
self_video: boolean,
suppress: boolean
}
defstruct session_id: nil,
user_id: nil,
token: nil,
channel_id: nil,
mute: false,
deaf: false,
self_mute: false,
self_deaf: false,
self_video: false,
suppress: false
endПри подключении к голосовому каналу вам назначают один из серверов Discord Voice. Он отвечает за передачу звука каждому участнику канала. Все голосовые каналы в группе назначаются одному серверу. Если вы первый в чате, сервер Discord Guilds отвечает за назначение сервера Discord Voice всей группе с помощью описанного ниже процесса.
Назначение сервера Discord Voice
Каждый сервер Discord Voice периодически сообщает о своём состоянии и нагрузке. Эта информация помещается в систему обнаружения сервисов (мы используем etcd), как обсуждалось в предыдущей статье.
Сервер Discord Guilds следит за системой обнаружения сервисов и назначает группе наименее используемый сервер Discord Voice в данном регионе. Когда он выбран, все объекты состояния голосовой связи (также поддерживаемые сервером Discord Guilds) передаются на сервер Discord Voice, чтобы тот мог настроить переадресацию аудио/видео. Клиенты уведомляются о выбранном сервере Discord Voice. Тогда клиент открывает второе соединение WebSocket с голосовым сервером (мы называем его голосовым соединением WebSocket), которое используется для настройки переадресации мультимедиа и индикации речи.
Когда в клиенте отображается статус Awaiting Endpoint, это означает, что сервер Discord Guilds ищет оптимальный сервер Discord Voice. Сообщение Voice Connected означает, что клиент успешно обменялся пакетами UDP с выбранным сервером Discord Voice.
Сервер Discord Voice содержит два компонента: сигнальный модуль и блок ретрансляции мультимедиа, называемый блоком избирательной пересылки, SFU (selective forwarding unit). Сигнальный модуль полностью контролирует SFU и отвечает за генерацию идентификаторов потоков и ключей шифрования, перенаправление индикаторов речи и т. д.
Наш SFU (на C++) отвечает за направление аудио- и видеотрафика между каналами. Он разработан своими силами: для нашего конкретного случая SFU обеспечивает максимальную производительность и, таким образом, самую боль��ую экономию. При модерации нарушителей (отключение звука на сервере), их аудиопакеты не обрабатываются. SFU также работает мостом между нативными и браузерными приложениями: он реализует транспорт и шифрование и для браузера и для нативных приложений, преобразуя пакеты в процессе передачи. Наконец, SFU отвечает за обработку протокола RTCP, который используется для оптимизации качества видео. SFU собирает и обрабатывает отчёты RTCP от получателей — и уведомляет отправителей, какая полоса доступна для передачи видео.
Отказоустойчивость
Поскольку напрямую из интернета у нас доступны только сервера Discord Voice, речь пойдёт о них.
Сигнальный модуль непрерывно контролирует SFU. Если тот сбоит, он мгновенно перезапускается с минимальной паузой в обслуживании (несколько потерянных пакетов). Состояние SFU восстанавливается сигнальным модулем без какого-либо взаимодействия с клиентом. Хотя сбои SFU редки, мы используем тот же механизм для обновления SFU без перерывов в обслуживании.
Когда падает сервер Discord Voice, он не отвечает на пинг — и удаляется из системы обнаружения сервисов. Клиент также замечает сбой сервера из-за разрыва голосового соединения WebSocket, тогда он запрашивает пинг голосового сервера через шлюзовое соединение WebSocket. Сервер Discord Guilds подтверждает сбой, консультируется с системой обнаружения сервисов и назначает группе новый сервер Discord Voice. Затем Гильдии Discordов отправляют все объекты состояния голоса на новый голосовой сервер. Все клиенты получают уведомление о новом сервере и подключаются к нему для запуска настройки мультимедиа.

Довольно часто серверы Discord Voice попадают под DDoS (мы видим это по быстрому увеличению входящих IP-пакетов). В этом случае мы выполняем такую же процедуру, как при сбое сервера: удаляем его из системы обнаружения сервисов, выбираем новый сервер, переводим на него все объекты состояния голосовой связи и уведомляем клиентов о новом сервере. Когда DDoS-атака утихает, сервер возвращается обратно в систему обнаружения служб.
Если владелец группы решает выбрать новый регион для голоса, мы выполняем очень похожую процедуру. Сервер Discord Guilds выбирает наилучший доступный голосовой сервер в новом регионе, консультируясь с системой обнаружения сервисов. Затем он переводим на него все объекты состояния голосовой связи и уведомляем клиентов о новом сервере. Клиенты разрывают текущее соединение WebSocket со старым сервером Discord Voice и создают новое соединение с новым сервером Discord Voice.
Масштабирование
Вся инфраструктура Discord Gateway, Discord Guilds и Discord Voice поддерживает горизонтальное масштабирование. Discord Gateway и Discord Guilds работают в облаке Google.
У нас более 850 голосовых серверов в 13 регионах (размещёнными более чем в 30 дата-центрах) по всему миру. Такая инфраструктура обеспечивает большую избыточность на случай сбоев в дата-центрах и DDoS. Мы работаем с несколькими партнёрами и используем свои физические серверы в их дата-центрах. Совсем недавно добавили регион Южной Африки. Благодаря инженерным усилиям как в клиентской, так и в серверной архитектуре, теперь Discord способен обслуживать одновременно более 2,6 миллиона пользователей голосового чата с исходящим трафиком более 220 Гбит/с и 120 млн пакетов в секунду.
Что дальше?
Мы постоянно следим за качеством голосовой связи (метрики поступают с клиентской стороны на серверы бэкенда). В будущем эта информация поможет в автоматическом обнаружении и устранении деградаций.
Хотя мы запустили видеочат и скринкасты год назад, но сейчас их можно использовать только в личных сообщениях. По сравнению со звуком, видео требует значительно большей мощности CPU и пропускной способности. Задача состоит в том, чтобы сбалансировать объём пропускной способности и ресурсов CPU/GPU, используемых для обеспечения наилучшего качества видео, особенно когда группа геймеров в канале находится н�� разных устройствах. Решением проблемы может стать технология масштабируемого видеокодирования Scalable Video Coding (SVC), расширение стандарта H.264/MPEG-4 AVC.
Для скринкастов нужно ещё больше полосы, чем для видео, из-за более высокого FPS и разрешения, чем у обычной веб-камеры. Мы сейчас работаем над поддержкой аппаратное кодирования видео в десктопном приложении.
