Как я сделал порно в 20 раз эффективнее с потоковым видео на Python

Опубликовано: 2022-03-11

вступление

Порно — это большая индустрия. В Интернете не так много сайтов, которые могут соперничать по трафику с крупнейшими игроками.

И жонглировать этим огромным трафиком сложно. Еще больше усложняет ситуацию то, что большая часть контента с порносайтов состоит из прямых видеопотоков с малой задержкой, а не из простого статического видеоконтента. Но несмотря на все связанные с этим проблемы, я редко читал о разработчиках Python, которые их берут на себя. Поэтому я решил написать о своем собственном опыте работы.

В чем проблема?

Несколько лет назад я работал на 26-м (на тот момент) по посещаемости веб-сайте в мире — не только в порноиндустрии: во всем мире.

В то время сайт обслуживал запросы на потоковую передачу порновидео по протоколу обмена сообщениями в реальном времени (RTMP). В частности, он использовал решение Flash Media Server (FMS), созданное Adobe, для предоставления пользователям прямых трансляций. Основной процесс был следующим:

  1. Пользователь запрашивает доступ к какой-либо прямой трансляции
  2. Сервер отвечает сеансом RTMP, воспроизводящим нужный материал.

По нескольким причинам FMS не был для нас хорошим выбором, начиная с его затрат, которые включали покупку обоих:

  1. Лицензии Windows для каждой машины, на которой мы запускали FMS.
  2. Лицензии для FMS на сумму около 4 тысяч долларов, из которых нам приходилось покупать несколько сотен (и больше каждый день) из-за нашего масштаба.

Все эти сборы начали накапливаться. Помимо затрат, FMS был продуктом, которого не хватало, особенно с точки зрения его функциональности (подробнее об этом чуть позже). Поэтому я решил отказаться от FMS и написать свой собственный парсер Python RTMP с нуля.

В итоге мне удалось сделать наш сервис примерно в 20 раз эффективнее.

Начиная

Были затронуты две основные проблемы: во-первых, RTMP и другие протоколы и форматы Adobe не были открытыми (т. е. общедоступными), что затрудняло работу с ними. Как вы можете реверсировать или анализировать файлы в формате, о котором вы ничего не знаете? К счастью, в публичной сфере были доступны некоторые реверсивные попытки (разработанные не Adobe, а скорее группой под названием OS Flash, ныне несуществующей), на которых мы основывали нашу работу.

Примечание: позже Adobe выпустила «спецификации», которые содержали не больше информации, чем та, которая уже была раскрыта в реверсивной вики и документах, не созданных Adobe. Их (Adobe) спецификации были абсурдно низкого качества и делали практически невозможным фактическое использование их библиотек. Более того, сам протокол временами казался намеренно вводящим в заблуждение. Например:

  1. Они использовали 29-битные целые числа.
  2. Они везде включали заголовки протокола с форматированием с прямым порядком байтов, за исключением определенного (пока не отмеченного) поля, которое было с прямым порядком байтов.
  3. Они втиснули данные в меньший объем за счет вычислительной мощности при транспортировке видеокадров 9k, что не имело почти никакого смысла, потому что они зарабатывали биты или байты за раз — незначительный выигрыш для такого размера файла.

А во-вторых: RTMP сильно ориентирован на сессии, что сделало практически невозможным мультикаст входящего потока. В идеале, если несколько пользователей хотят смотреть один и тот же прямой эфир, мы могли бы просто передать им обратно указатели на один сеанс, в котором транслируется этот поток (это будет многоадресная потоковая передача видео). Но с RTMP нам пришлось создать совершенно новый экземпляр потока для каждого пользователя, которому нужен доступ. Это была полная трата.

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

Мое решение для многоадресной потоковой передачи видео

Имея это в виду, я решил переупаковать/разобрать типичный поток ответов в «теги» FLV (где «тег» — это просто видео, аудио или метаданные). Эти теги FLV могут перемещаться по RTMP без особых проблем.

Преимущества такого подхода:

  1. Нам нужно было переупаковать поток только один раз (переупаковка была кошмаром из-за отсутствия спецификаций и особенностей протокола, описанных выше).
  2. Мы могли бы повторно использовать любой поток между клиентами с очень небольшими проблемами, предоставив им просто заголовок FLV, в то время как внутренний указатель на теги FLV (вместе с каким-то смещением, указывающим, где они находятся в потоке) позволял доступ к содержимое.

Я начал разработку на языке, который знал лучше всего на тот момент: C. Со временем этот выбор стал громоздким; поэтому я начал изучать основы Python во время переноса своего кода на C. Процесс разработки ускорился, но после нескольких демок я быстро столкнулся с проблемой исчерпания ресурсов. Обработка сокетов в Python не предназначалась для таких ситуаций: в частности, в Python мы обнаружили, что делаем несколько системных вызовов и переключателей контекста для каждого действия, добавляя огромное количество накладных расходов.

Улучшение производительности потокового видео: сочетание Python, RTMP и C

После профилирования кода я решил переместить критичные к производительности функции в модуль Python, полностью написанный на C. Это был довольно низкоуровневый материал: в частности, он использовал механизм ядра epoll для обеспечения логарифмического порядка роста. .

В программировании асинхронных сокетов есть средства, которые могут предоставить вам информацию о том, доступен ли данный сокет для чтения/записи/заполнения ошибок. В прошлом разработчики использовали системный вызов select() для получения этой информации, которая плохо масштабируется. Poll() — это лучшая версия select, но она все же не так хороша, поскольку вам приходится передавать кучу дескрипторов сокетов при каждом вызове.

Epoll великолепен, так как все, что вам нужно сделать, это зарегистрировать сокет, и система запомнит этот отдельный сокет, обрабатывая все мелкие детали внутри. Таким образом, при каждом вызове нет накладных расходов на передачу аргументов. Он также намного лучше масштабируется и возвращает только те сокеты, которые вам нужны, что намного лучше, чем просмотр списка из 100 тыс. дескрипторов сокетов, чтобы увидеть, были ли у них события с битовыми масками — что вам нужно сделать, если вы используете другие решения.

Но за повышение производительности мы заплатили цену: этот подход следовал совершенно другому шаблону проектирования, чем раньше. Предыдущий подход сайта был (если я правильно помню) одним монолитным процессом, который блокировался при получении и отправке; Я разрабатывал решение, управляемое событиями, поэтому мне пришлось реорганизовать остальную часть кода, чтобы он соответствовал этой новой модели.

В частности, в нашем новом подходе у нас был основной цикл, который обрабатывал получение и отправку следующим образом:

Решение для потоковой передачи видео Python использовало комбинацию «тегов» RTMP, FLV и многоадресной потоковой передачи видео.

  1. Полученные данные передавались (в виде сообщений) на уровень RTMP.
  2. RTMP был расчленен, и теги FLV были извлечены.
  3. Данные FLV отправлялись на уровень буферизации и многоадресной рассылки, который организовывал потоки и заполнял низкоуровневые буферы отправителя.
  4. Отправитель хранил структуру для каждого клиента с последним отправленным индексом и пытался отправить клиенту как можно больше данных.

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

Системные, архитектурные и аппаратные проблемы

Но мы столкнулись с другой проблемой: переключатели контекста ядра становились обузой. В результате мы решили записывать только каждые 100 миллисекунд, а не мгновенно. Это объединило меньшие пакеты и предотвратило всплеск переключений контекста.

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

Это сработало, но с одним недостатком: хотя общий случай был обработан довольно хорошо, мы увидели ужасную производительность, когда все на сайте (или непропорциональное количество пользователей) смотрели один вещатель. Хорошая новость: это никогда не происходит вне маркетинговой кампании. Мы реализовали отдельный кластер для обработки этого сценария, но, по правде говоря, мы рассудили, что подвергать опасности опыт платящего пользователя ради маркетинговых усилий было бессмысленно — на самом деле это был не настоящий сценарий (хотя было бы неплохо обработать все мыслимые кейс).

Заключение

Немного статистики по конечному результату: Ежедневный трафик на кластере составлял около 100 тысяч пользователей в пике (60% нагрузки), в среднем ~ 50 тысяч. Я управлял двумя кластерами (HUN и US); каждый из них обслуживал около 40 машин, чтобы разделить нагрузку. Совокупная пропускная способность кластеров составляла около 50 Гбит/с, из которых они использовали около 10 Гбит/с при пиковой нагрузке. В конце концов, мне удалось легко вытолкнуть 10 Гбит/с на машину; теоретически 1 это число могло достигать 30 Гбит/с на машину, что соответствует примерно 300 тысячам пользователей, одновременно просматривающих потоки с одного сервера.

Существующий кластер FMS содержал более 200 машин, которые можно было бы заменить моими 15 — только 10 из них выполняли бы какую-либо реальную работу. Это дало нам примерно 200/10 = 20-кратное улучшение.

Вероятно, мой самый большой вывод из проекта потокового видео Python заключался в том, что я не должен позволять себе останавливаться перед перспективой изучения нового набора навыков. В частности, Python, транскодирование и объектно-ориентированное программирование — все эти понятия, с которыми у меня был очень непрофессиональный опыт, прежде чем взяться за этот многоадресный видеопроект.

Это и то, что развертывание собственного решения может принести большие деньги.

1 Позже, когда мы запустили код в производство, мы столкнулись с аппаратными проблемами, поскольку использовали более старые серверы Intel sr2500, которые не могли работать с 10-гигабитными Ethernet-картами из-за их низкой пропускной способности PCI. Вместо этого мы использовали их в бондах 1-4x1 Gbit Ethernet (агрегируя производительность нескольких сетевых карт в виртуальную карту). В конце концов, мы получили несколько новых процессоров Intel sr2600 i7, которые обслуживали 10 Гбит/с по оптике без каких-либо проблем с производительностью. Все проектные расчеты относятся к этому оборудованию.