С самого начала мы так спланировали инженерные и продуктовые решения, чтобы 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 и разрешения, чем у обычной веб-камеры. Мы сейчас работаем над поддержкой аппаратное кодирования видео в десктопном приложении.
Автор: m1rko