Зомби-апокалипсис в Телемосте: как мы проводим нагрузочное тестирование видеоконференцсвязи

Привет! Меня зовут Иван, я разработчик в команде бэкенда Яндекс Телемоста — сервиса для видеозвонков. В этой статье я расскажу, как мы тестируем Телемост под нагрузкой, почему нам пришлось разработать для этого свой инструмент, и что у него под капотом. Но сперва поделюсь, с какими сложностями в тестировании мы столкнулись.

Немного контекста про сервис, который тестируем. Телемост — это сервис для видеозвонков, который поддерживает до тысячи активных участников в одной встрече. Активные участники — это те, кто может общаться с другими: каждый передаёт своё видео и аудио.

Сервис работает с 2020 года, и за последний год мы прикрутили поддержку 1000 участников, шаринг экрана в 4К и продолжаем улучшать качество сервиса.

Как мы проверяем Телемост на прочность

Ручное тестирование

Допустим, мы хотим проверить базовый сценарий: я могу зайти во встречу, пригласить друга и увидеть его, как и он меня. Для этого неплохо было бы иметь напарника, который зашёл бы во встречу и рассказал, что он видит и слышит. Но можно справиться и самому: открыть вторую вкладку в браузере, зайти в ту же встречу — готово, можно проверять новые фичи.

f9abc2af76af7d763a4e7948692bd50d.png

Но как быть в тех случаях, когда двух участников нам недостаточно? Например, мы хотим проверить, как выглядит расположение 10 участников во встрече. Можно пойти проторенной дорожкой и открыть 10 вкладок:

f7b526c4a80736dc4650f1bffa47a23a.png

Казалось бы — задача решена. Но у такого подхода есть ряд минусов:

  1. Мы потратили кучу времени, открывая вкладки. Мало их открыть — нужно ещё нажать «Войти» в каждой.

  1. У некоторых участников встречи качество видео стало хуже, а в уголках появились красные пиктограммы, указывающие на низкое качество сети. Ноутбук уже не справляется с таким количеством медиапотоков — Wi‑Fi‑канал просто перегружен.

Но ни 10, ни 15 участников нам не достаточно. Мы обещаем поддержку тысячи активных пользователей в одной встрече, а значит, нужно тестировать на сотнях, если не тысячах пользователей. Нужно обзавестись таким инструментом, который позволил бы:

  1. Добавлять нужное количество участников во встречу.

  2. Управлять этими участниками, чтобы они не просто сидели статично, а выполняли какие‑то действия (размьючивали микрофон, включали камеру).

  3. Смотреть на встречу глазами каждого из участников, чтобы проверить качество видеосвязи.

Нагрузочное тестирование

Нам нужно убедиться, что сервис стабилен под нагрузкой, и выявить узкие места в ПО и инфраструктуре. Чтобы объяснить сложность нагрузочного тестирования видеоконференцсвязи в целом и Телемоста в частности, покажу упрощённую схему бэкенда Телемоста:

37c5eb9c4ac897e14025f03b73a13172.png

Бэкенд можно условно поделить на два компонента:

  1. Telemost Backend — бэкенд бизнес‑логики. Отвечает за создание и управление встречами, доступ участников, а также за различные функции, такие как чат и комната ожидания. Клиенты взаимодействуют с этой частью бэкенда по HTTP.

  2. Медиасервер — критически важный компонент, который отвечает за потоки медиаданных. Взаимодействует как с клиентами, так и с бэкендом бизнес‑логики. Для взаимодействия с клиентами используется несколько протоколов, например RTP для передачи медиа, WebSocket для команд.

В идеале нам хотелось бы интеграционно протестировать оба этих компонента — так, как они работают в продакшене. Нагрузить сервер бизнес‑логики HTTP‑запросами было несложно — для этого существует множество инструментов, мы воспользовались JMeter. Но задача нагрузочного тестирования всех компонентов вместе осталась не решена.

Коридорное тестирование, чтобы проверить Телемост под нагрузкой

В какой‑то момент мы завели регулярную встречу, куда позвали всех своих коллег. Цель была простая: собрать как можно больше людей в одной встрече, провести там 15 минут, а затем собрать фидбэк. Плюсы такого подхода очевидны:

  1. Нагружаем все компоненты в «боевых» условиях. Это самый честный способ что‑либо проверить: пользователи используют разные браузеры, географически находятся в разных городах в сетях с разной пропускной способностью — точно так, как это бывает в продакшене.

  2. Получаем фидбэк пользователей. Когда у кого‑то возникали проблемы, коллеги подробно описывали, что они наблюдали.

Чего не хватило:

  1. Невозможно собрать 1000 участников (как бы мы ни старались).

  2. Невозможно собирать встречи так часто, как нам хотелось бы. Провести 15-минутную встречу раз в неделю по расписанию — это окей, но быстро протестировать что‑то перед релизом уже не получится.

И снова появилась мысль о том, что классно было бы иметь инструмент, который позволил бы нам нагружать Телемост тогда, когда нам это нужно.

Разработка собственного инструмента

Расскажу, что мы хотели получить от инструмента:

  • Запускать во встречу до 1000 виртуальных участников.

  • Поддержку WebRTC — как в браузере пользователей.

  • Управлять виртуальными участниками встречи.

  • Записывать встречи, чтобы можно посмотреть на встречу «глазами» каждого из участников.

Таких виртуальных участников мы назвали зомбиками. А инструмент для управления ими — Zombieland.

Zombieland в Телемосте: как он устроен

Мы создали веб‑приложение, к которому имеют доступ все сотрудники, находящиеся в корпоративной сети — как разработчики, так и тестировщики. В этом приложении можно указать встречу, задать количество зомбиков, нажать одну кнопку — и зомби начнут заходить во встречу.

2d94f605c5a0ed4f00399bd19e690997.png

Кроме того, мы добавили множество страниц со служебной информацией, которая помогает нам в тестировании и отладке. С этих же страниц можно управлять зомбиками.

98343c6d7eea3b0f68877ce5e8dfda92.png

На чём строится Zombieland: Selenoid

В Яндексе есть сервис для создания виртуальных окружений и запуска автотестов. Это фактически замена Selenium Grid, которая отлично интегрируется с нашей инфраструктурой и позволяет использовать уже имеющиеся в командах ресурсы. Фактически это такой горизонтально отмасштабированный Selenoid.

325b52b5878d0c2a296b77ffc846300d.png

По запросу поднимается виртуальное окружение, и в каждом из этих окружений запускается свой экземпляр Selenoid, который в контейнерах поднимает браузеры. Когда мы через веб‑драйвер взаимодействуем со страницей, запросы приходят на WebDriver Proxy, который направляет их в нужное виртуальное окружение. А уже там Selenoid отправляет команды нужному браузеру.

При этом взаимодействие с виртуальным драйвером у нас стандартное — это привычное API Selenium. Под капотом запускаются контейнеры со старым добрым Selenoid, так что вы сможете сделать всё то же самое на своём проекте, если у вас уже есть Selenoid.

ff3147fb5e88dab9c1a6174b22226b36.png

Вся эта сеть браузеров управляется веб‑приложением, написанным на Spring Boot.

Selenium мы используем немного нестандартно. Как обычно его используют для тестов? Поднимается браузер, при помощи веб‑драйвера ведётся взаимодействие со страницей, затем проверяется реакция страницы, после чего сессия завершается. Чем быстрее завершится такая сессия, тем лучше для тестов, так как это ускоряет их выполнение.

В нашем случае нам не требуется завершить сессию быстро. Мы хотим, чтобы сессия жила столько, сколько указано в интерфейсе Zombieland. Этот тайм‑аут передаётся в параметр session‑timeout при запуске сессии в Selenoid: sessionTimeout:5m.

Кроме этого параметра, мы используем параметр enableVNC, который позволяет включить удалённый доступ, и enableVideo, чтобы записывать видео встречи, фиксируя то, что видел зомби.

Selenoid умеет делать всё это из коробки, поэтому дополнительных усилий от нас не потребовалось. Но, чтобы эффективно работать с сессиями, нам нужно было продумать и реализовать управляемый процесс.

RemoteWebDriver — управляем сессиями

Чтобы сделать сессии управляемыми, мы:

  1. Создаём объект RemoteWebDriver, извлекаем из него идентификатор сессии и сохраняем его в базе данных.

RemoteWebDriver driver = new RemoteWebDriver(gridUrl, options);
String sessionId = driver.getSessionId().toString();
  1. Каждый раз, когда нам нужно что-то сделать на странице зомбика, мы извлекаем этот идентификатор, пересоздаём RemoteWebDriver и подключаемся к текущей сессии.

RemoteWebDriver driver = new RemoteWebDriver(seleniumGridUrl, capabilities) {
 	{
     	setSessionId(sessionId);
 	}
 };

Зачем мы пересоздаём RemoteWebDriver? Почему бы не создать его один раз и не использовать до окончания сессии? Делаем мы это по нескольким причинам. Во‑первых, запросы занимают какое‑то время, поэтому хочется сделать их выполнение асинхронным. А во‑вторых, это нужно, чтобы автоматизировать наших зомбиков. Не все действия мы инициируем кнопками на фронтенде зомбиленда. Что‑то зомби должны уметь делать сами, по расписанию. Например — выходить из встречи.

090b1794ad7a0f88e089a772c7c28fcd.png

Мы пришли к такой схеме: запросы с фронтенда вообще не взаимодействуют напрямую с веб‑драйвером. Когда поступает запрос, мы лишь обновляем состояние зомбика в базе данных или добавляем для него команду.

Затем поднимается cron‑таска, которая ищет в базе зомбиков с изменённым состоянием или новой командой и выполняет необходимые действия.

5ae7e74f6a40c915542893285168c5fb.png

Пример:

  1. С фронтенда поступает POST‑запрос на добавление нового зомбика во встречу. На этом шаге мы просто записываем новый объект зомби в базу данных.

  1. Поднимается cron‑таска (она стартует раз в секунду). В таске мы видим, что в базе появился новый зомбик со статусом new, и понимаем, что для него нужно создать сессию в Selenoid. Сron‑таска делает это, сохраняет sessionid для зомбика в базе и завершает своё выполнение.

  2. Следующая cron‑таска, которая запустится, увидит зомбика в состоянии session created. Это значит, что у зомбика есть сессия, но он пока не в конференции — его нужно запустить, и cron‑таска выполнит это действие. Так продолжается до момента, когда очередная задача увидит, что зомби пора выводить из встречи: она возьмёт его sessionid из базы, пересоздаст объект RemoteWebDriver, подключится к сессии и кликнет по кнопке «Выйти».

Управление состоянием Зомби

f3d7d79cbc69c5c47854c9a41b66fe5e.png

Для управления состоянием мы использовали state‑машину — взяли её реализацию из Spring Framework. Мы определили состояния, в которых может находиться зомбик. Затем определили события, которые переключают состояние зомбика из одного в другое, и настроили логику переходов между ними.

.and()
.source(NEW)
.target(SESSION_CREATED)
.event(CREATE_SESSION)
.action(new CreateSessionAction(), errorAction())

Например, в этом коде мы указываем, что новый зомбик, который только добавился в базу данных и находится в состоянии NEW, должен перейти в состояние SESSION_CREATED при срабатывании события CREATE_SESSION. Также у нас есть класс CreateSessionAction с методом execute, в котором описана вся логика взаимодействия с веб‑драйвером.

Сценарии: как cron-таска узнает, какой ивент нам нужен

Когда мы начинаем тестирование, в момент запуска мы создаём сценарий. Для них в базе данных есть отдельная таблица. Каждый сценарий состоит из списка шагов, для каждого шага задан ивент, а также порядковый номер, чтобы мы понимали последовательность. Если шаг должен выполниться не сразу, а спустя какое‑то время, то мы указываем задержку в секундах в поле pause.

54f4d6c52cb4b660e6f2b219ce74e2f7.png

В cron‑таске выбираем зомбиков из таблицы zombies, где для каждого зомбика прописан его текущий шаг и время, когда этот шаг должен выполниться. По идентификаторам шага и сценария мы находим нужный ивент, отправляем его и переводим зомбика из одного состояния в другое, выполняя необходимые действия.

Но как быть, если мы хотим вмешаться в сценарий и выполнить действие за зомбика? Конечно, если речь идёт об одном зомби, мы можем открыть ссылку и подключиться по VNC, чтобы что‑то нажать вручную. Однако если нужно, например, замьютить сразу всех, сотню зомбиков, это становится неудобно, и требуется автоматизация.

Для таких действий у нас тоже есть ивенты. Когда зомбик находится в состоянии «online», можно вызвать события mute audio или mute video, чтобы отключить микрофон или камеру.

b3392e822d296896e9be9d1d9f251203.png

В таблице zombies для таких событий предусмотрено поле command, которое имеет высший приоритет. Когда cron‑таска запускается и видит, что это поле не пустое, она сначала выполняет команду из этого поля, отправляя нужный ивент. Следующий шаг по сценарию выполняется уже в следующей итерации, когда запускается следующая cron‑таска.

Работа с медиа в Zombieland

На хостах, где Selenoid запускает браузеры, нет ни микрофона, ни веб‑камеры. Но они нам и не нужны, потому что Chrome прекрасно умеет подменять реальные устройства видео‑ и аудиофайлами.

Для этого при запуске Chrome мы используем флаг ‑use‑fake‑device‑for‑media‑stream. Помимо него, нам также нужен флаг ‑use‑fake‑ui‑for‑media‑stream, чтобы отключить уведомления о необходимости предоставить доступ к камере.

Также есть ещё пара флагов:

Эти файлы заранее добавляются в Docker‑образ.

Файлы с медиа мы используем в формате Motion JPEG (.mjpeg). В этом формате каждый кадр сжат алгоритмом JPEG без учёта межкадровой разницы, из‑за чего файлы получаются тяжёлыми. Минутное видео в высоком разрешении может весить до сотни мегабайт. К счастью, Chrome умеет воспроизводить эти файлы по кругу, поэтому достаточно нарезать короткие клипы, чтобы они могли воспроизводиться бесконечно долго.

Форматы аудио: WAV. С помощью FFMPEG мы можем извлечь аудиодорожку из любого видео.

Изначально для подмены видео участника мы использовали короткие ролики. Важно было только, чтобы изображение менялось и не было статичным.

Затем, когда качество видео в Телемосте выросло, возникла необходимость в видеофайлах более высокого разрешения.

392d507f1a671ad3708502ca5d016d31.png

Теперь мы нарезаем короткие отрывки из фильмов, выбирая наиболее динамичные сцены. При запуске каждой партии зомби используется одно и то же видео.

Идентификация зомбиков

Чтобы понять, у какого участника возникли проблемы, их нужно как‑то различать. Делаем мы это при помощи уникальных имён. Изначально мы использовали семизначные случайные числа, что обеспечивало уникальность, но не было удобно для чтения и запоминания. Позже один из разработчиков вдохновился идеей автомобильных номеров и предложил перейти на цифро‑буквенные наименования. Например, A-1234.

Такой подход даёт нам 260 тысяч уникальных комбинаций, чего вполне хватает, так как в одной встрече мы запускаем не более тысячи зомбиков.

Сценарий нагрузочного тестирования

Теперь, когда стало понятно, как мы используем наш инструмент, расскажу про сценарий нагрузочного тестирования.

Вот что делают зомби:  

7c6b6f4a75619e14a899d49ac38bcccf.png

Что в это время делаем мы:

  • Запускаем зомби и проверяем, зашли ли они все во встречу.

  • Проверяем, передают ли они видео, оцениваем его качество и отслеживаем, не возникло ли проблем с изображением.

  • После завершения теста анализируем собранные артефакты.

На что мы смотрим при анализе?

Метрики запросов. Мы собираем и фиксируем тайминги в трёх перцентилях, а также RPS, распределяя их по кодам ответов — 200, 400 и 500.

Логи бэкенда. Логи собираются в ClickHouse, и на их основе мы строим графики для удобства анализа.

6d33c955d91d23e0fbeb03510d230254.png

Например, мы строим график присутствия участников во встрече, чтобы понять, кто пришёл первым, кто опоздал, а кто и вовсе отвалился в середине встречи.

Однако метрики и логи — это ещё не всё. Телемост работает на WebRTC, что позволяет нам использовать WebRTC‑статистики. Немного расскажу про них.

WebRTC: peer-to-peer

В мире WebRTC бэкенд несколько утратил свое значение. Теперь он может быть просто транспортом, который помогает двум пирам договориться о встрече. После этого пиры устанавливают прямое peer‑to‑peer соединение, и бэкенд им не нужен.

a8234a61fd2f6196d8ef2c8a08603383.png

Аналогично статистику теперь можно собирать не только на стороне бэкенда — клиенты сами передают WebRTC‑статы. Мы собираем эти статы на сервере мониторинга, сохраняем в базе и затем строим графики для анализа метрик.

e05635c988862657aa46411fe22dd5d0.png

WebRTC Stats: где посмотреть

  • Спецификация. Полный список метрик доступен на сайте www.w3.org/TR/webrtc‑stats/, где их более 300, но далеко не все из них поддерживаются браузерами.

  • Посмотреть графики. Чтобы увидеть, как они выглядят, можно открыть служебную страницу WebRTC Internals в Chrome, где отображаются соответствующие графики. Вот лишь некоторые из них:

    • [packetsSent/s] / [packetsRecieved/s] — можно увидеть графики полученных и отправленных пакетов медиаданных.

    • [packetsLost] — если его рассматривать вместе с графиком [packetsReceived], можно определить процент сетевых потерь.

    • roundTripTime — это время, которое требуется пакету, чтобы пройти от нашего узла до удалённого и обратно, когда удалённый узел отправляет подтверждение о получении пакета. Если на графике мы видим, что RTT очень высокий, это может указывать на не самый оптимальный маршрут передачи пакетов.

    • audioLevel — по нему можно оценить громкость звука.

7fca608ef333e398f0b6dee63ecd7c81.png

Эти метрики собираются в трёх отчётах:

fd9ee07cc97243336d49033b148123d1.png

  • outbound‑rtp — исходящие пакеты, которые мы отправляем;

  • inbound‑rtp — входящие пакеты;

  • remote‑inbound‑rtp — статистика по пакетам, которые мы отправили, но глазами удалённого пользователя, который их получил.

Мы собираем все эти данные, получая полную статистику о каждом участнике, и затем строим графики для анализа.

3358100d3a0fc8342fbc181924619825.png

Например, на этом графике видно, что один из зомбиков перестал получать видео от другого через две минуты после начала встречи. Это совпало с моментом, когда к встрече подключился ещё один зомбик и начал передавать свои данные.

bcb7882c1f26da7e0315e1170abb5ffb.png

А на этом графике можно наблюдать кратковременное снижение разрешения видео.

Это лишь часть метрик, доступных в WebRTC‑статистике. На практике нам не все из них нужны для тестирования с Zombieland.

В статье я рассказал, как (и зачем) мы построили Zombieland — инструмент для нагрузочного тестирования Телемоста.

В конце хочется посоветовать искать готовые инструменты для своих задач — наверняка кто‑то уже создал нужное решение. Например, мы нашли JMeter и успешно нагружаем с его помощью одну из частей бэкенда.

Если подходящих инструментов нет, не бойтесь вложить время в создание собственных — это окупается. А иногда стоит взглянуть на привычные инструменты под новым углом, как мы сделали с Selenoid, создав на его основе эмулятор.

© Habrahabr.ru