Не так давно я писал про волейбольный сервис, теперь пришло время описать его с технической точки зрения.
Возможно, общественное сознание найдет изъяны в архитектуре и подтолкнет к лучшим решениям.
Краткое описание функциональности:
Например, такими:
![](https://files.vball.io/mmvl_final-187.gif)
Теперь, как это все работает.
Все написано на python, веб-сервис — Django/Gunicorn.
Интенсивно используются OpenCV и FFMpeg.
База данных — Postgres.
Кэш и очередь — Redis.
В самой первой версии было 3 компонента:
![](https://habrastorage.org/r/w780q1/webt/3_/j_/j7/3_j_j7r5_iynmvjhbnkgj0dnd8y.jpeg)
Разработка велась локально, я поставил на домашний десктоп Ubuntu, а на нее microk8s и получил маленький Кубернетес-кластер.
Здесь я столкнулся с тем, что кадры, которые видит opencv не очень совпадают с временем, которое видит ffmpeg из-за особенностей кодеков, поэтому высчитать время в оригинальном файле из номера кадра оказалаось невозможно.
Из-за этого пришлось сохранять кадры, что сильно замедлило процесс и потребовало дальнейших архитектурных изменений.
Как уже упоминалось, время обработки в 3 раза превышает время игры. Профайлер показал, что, большая часть времени тратится при записи кадров на диск, сам же разбор кадров примерно в два раза быстрее.
Логично распараллелить эти две работы.
По начальной задумке пока vproc разбирает кадры через opencv, ffmpeg параллельно записывает все на диск, а clipper собирает из них видео.
Но с ffmpeg нашлись две проблемы:
В итоге вместо ffmpeg появился пятыйэлемент компонент — Framer. Он запускается из vproc, и листает кадры в том же видеофайле, ожидая пока vproc найдет розыгрыши. Как только они появились — framer выкладывает нужные кадры в отдельную директорию.
Из дополнительных плюсов — ни одного лишнего кадра не эспортируется.
Мелочь, но все таки.
По производительности (на 10-минутном тестовом видео):
Было:
Стало:
Выигрыш в два раза, очень хорошо. Если писать в несколько потоков, то наверное можно выжать еще чуть. Если хранить кадры в памяти и генерить из них видео через API, то скорее всего можно ускориться значительно.
Дальше я стал выбирать хостинг. Понятно, что основные варианты — GKE, AWS, Azure, но многие авторы мелких проектов жалуются на непрозрачное ценообразование и, как следствие, немаленькие счета.
Основная засада здесь — цена за исходящий трафик, она составляет порядка $100/Tb, а поскольку речь идет о раздаче видео, вероятность серьезно попасть очень неиллюзорна.
Тогда я решил глянуть второй эшелон — Digital Ocean, Linode, Heroku. На самом деле Kubernetes-as-service уже не такая редкая вещь, но многие варианты не выглядят user-friendly.
Больше всего понравился Digital Ocean, потому что:
При наличии CDN веб-серверу уже не было надобности раздавать видео, однако кто-то должен был это видео опубликовать.
Так в архитектуре появился четвертый компонент — Pusher.
![](https://habrastorage.org/r/w780q1/webt/dh/h7/m3/dhh7m3kdzmndajinttp-4_nae-0.jpeg)
Однако серьезным недостатком оказалась невозможность смонтировать один и тот же диск на несколько машин одновременно.
Сам DO предлагает в таких случаях использовать NFS через выделенный контейнер, но пока решил не заморачиваться, ибо, как упоминалось выше, назревает специальное решение для видео кадров, и если уже городить отдельный контейнер, то для него.
Тем не менее, отсутствие общего диска серьезно повлияло на архитектуру.
Если с передачей загружаемого файла, можно было выкрутиться через S3, то гонять туда-сюда сотни тысяч кадров не выглядело блестящим вариантом.
В итоге, пришлось привязать все стадии обработки конкретной видео к одной ноде, что конечно уменьшило общую масштабируемость системы.
Запустив кластер в облаке, я стал искать решение для сбора логов и метрик. Самостоятельно возиться с хостингом этого добра не хотелось, поэтому целью был free-tier в каком-нибудь облаке.
Такое есть не у всех: Модная Grafana хочет $50 в месяц, выходящий из моды Elastic — $16, Splunk даже прямо не говорит.
Зато внезапно оказалось что New Relic, также известный своими негуманными ценами, теперь предоставляет первые 100G в месяц бесплатно.
Вполне естественным решением виделось разделить кластер на два node-pool'а:
Фронтовой пул, понятно, всегда онлайн, а вот процессинговый не так прост.
Во-первых, обработка видео требует серьезных ресурсов (= денег), а во-вторых, обрабатывать приходится нечасто (особенно в стадии зарождения проекта).
Поэтому хочется включать процессинг только, чтобы обработать видео (время обработки — 3x от продолжительности действа).
Кубернетес формально поддерживает autoscale 0, но как именно это реализуется — я не нашел, зато нашлась такая дискуссия на Stack Overflow.
В итоге пришлось нагородить еще один под во фронтовом пуле, единственной задачей которого является посматривать в постгрес иногда и включать/выключать процессинговый пул в зависимости от того, что в базе оказалось.
У DigitalOcean есть неофициальный клиент для питона, но он уже давно не обновлялся, а Kubernetes API там не присутствует в принципе.
Я сделал на коленке клиент для себя, он покрывает процентов десять возможностей, но вдруг кому пригодится для старта.
В итоге диаграмм разрослась вот так:
![](https://habrastorage.org/r/w780q1/webt/5v/py/ql/5vpyqlewmh7sb3me21zr44o2wua.jpeg)
Несмотря на великое множество CI/CD инструментов, в разработке не нашлось ничего удобнее Jenkins.
А для управления DigitalOcean'ом идеально подошли Github Actions.
Возможно, общественное сознание найдет изъяны в архитектуре и подтолкнет к лучшим решениям.
Краткое описание функциональности:
- пользователь загружает видео с записью волейбольной игры
- один хитрый алгоритм распознает мяч на кадрах
- другой хитрый алгоритм выделяет розыгрыши
- розыгрыши компонуются в отдельные видеофайлы
- файлы с розыгрышами собираются в дайджест всей игры
- все видео заливается в облако
- пользователи смотрят/качают/шарят клипы с самыми классными розыгрышами
Например, такими:
![](https://files.vball.io/mmvl_final-187.gif)
Теперь, как это все работает.
Технологии
Все написано на python, веб-сервис — Django/Gunicorn.
Интенсивно используются OpenCV и FFMpeg.
База данных — Postgres.
Кэш и очередь — Redis.
Альфа
В самой первой версии было 3 компонента:
- Front — Веб сервис (Django), с которым взаимодействуют конечные пользователи
- Videoproc (Vproc) — Ядро алгоритма, python + opencv, которое содержит все алгоритмы треккинга мяча и логику нарезки на розыгрыши
- Clipper — Сервис генерации видео на основе выхлопа Vproc, используя ffmpeg
![](https://habrastorage.org/webt/3_/j_/j7/3_j_j7r5_iynmvjhbnkgj0dnd8y.jpeg)
Разработка велась локально, я поставил на домашний десктоп Ubuntu, а на нее microk8s и получил маленький Кубернетес-кластер.
Здесь я столкнулся с тем, что кадры, которые видит opencv не очень совпадают с временем, которое видит ffmpeg из-за особенностей кодеков, поэтому высчитать время в оригинальном файле из номера кадра оказалаось невозможно.
Из-за этого пришлось сохранять кадры, что сильно замедлило процесс и потребовало дальнейших архитектурных изменений.
Параллельная обработка
Как уже упоминалось, время обработки в 3 раза превышает время игры. Профайлер показал, что, большая часть времени тратится при записи кадров на диск, сам же разбор кадров примерно в два раза быстрее.
Логично распараллелить эти две работы.
По начальной задумке пока vproc разбирает кадры через opencv, ffmpeg параллельно записывает все на диск, а clipper собирает из них видео.
Но с ffmpeg нашлись две проблемы:
- Кадры из ffmpeg не идентичны кадрам из opencv (это не всегда так, зависит от кодека видеофайла)
- Количество кадров в записи может быть слишком большим — например час видео при хорошем fps — это порядка 200K файлов, что многовато для одного каталога, даже если это ext4. Городить разбиение на поддиректории и потом склеивать при компоновке видео — не хотелось усложнять
В итоге вместо ffmpeg появился пятый
Из дополнительных плюсов — ни одного лишнего кадра не эспортируется.
Мелочь, но все таки.
По производительности (на 10-минутном тестовом видео):
Было:
Completed file id=73, for game=test, frames=36718, fps=50, duration=600 in 1677 sec
Стало:
Completed file id=83, for game=test, frames=36718, fps=50, duration=600 in 523 sec + framer time 303
Выигрыш в два раза, очень хорошо. Если писать в несколько потоков, то наверное можно выжать еще чуть. Если хранить кадры в памяти и генерить из них видео через API, то скорее всего можно ускориться значительно.
Digital Ocean
Дальше я стал выбирать хостинг. Понятно, что основные варианты — GKE, AWS, Azure, но многие авторы мелких проектов жалуются на непрозрачное ценообразование и, как следствие, немаленькие счета.
Основная засада здесь — цена за исходящий трафик, она составляет порядка $100/Tb, а поскольку речь идет о раздаче видео, вероятность серьезно попасть очень неиллюзорна.
Тогда я решил глянуть второй эшелон — Digital Ocean, Linode, Heroku. На самом деле Kubernetes-as-service уже не такая редкая вещь, но многие варианты не выглядят user-friendly.
Больше всего понравился Digital Ocean, потому что:
- Managed Kubernetes
- Managed Postgres
- S3 хранилище с бесплатным(!) CDN + 1 TB/месяц
- Закрытый docker registry
- Все операции можно делать через API
- Датацентры по всему миру
При наличии CDN веб-серверу уже не было надобности раздавать видео, однако кто-то должен был это видео опубликовать.
Так в архитектуре появился четвертый компонент — Pusher.
![](https://habrastorage.org/webt/dh/h7/m3/dhh7m3kdzmndajinttp-4_nae-0.jpeg)
Однако серьезным недостатком оказалась невозможность смонтировать один и тот же диск на несколько машин одновременно.
Сам DO предлагает в таких случаях использовать NFS через выделенный контейнер, но пока решил не заморачиваться, ибо, как упоминалось выше, назревает специальное решение для видео кадров, и если уже городить отдельный контейнер, то для него.
Тем не менее, отсутствие общего диска серьезно повлияло на архитектуру.
Если с передачей загружаемого файла, можно было выкрутиться через S3, то гонять туда-сюда сотни тысяч кадров не выглядело блестящим вариантом.
В итоге, пришлось привязать все стадии обработки конкретной видео к одной ноде, что конечно уменьшило общую масштабируемость системы.
Логи и метрики
Запустив кластер в облаке, я стал искать решение для сбора логов и метрик. Самостоятельно возиться с хостингом этого добра не хотелось, поэтому целью был free-tier в каком-нибудь облаке.
Такое есть не у всех: Модная Grafana хочет $50 в месяц, выходящий из моды Elastic — $16, Splunk даже прямо не говорит.
Зато внезапно оказалось что New Relic, также известный своими негуманными ценами, теперь предоставляет первые 100G в месяц бесплатно.
Видео
Вполне естественным решением виделось разделить кластер на два node-pool'а:
- front — на котором крутятся веб-сервера
- vproc — где обрабатывается видео
Фронтовой пул, понятно, всегда онлайн, а вот процессинговый не так прост.
Во-первых, обработка видео требует серьезных ресурсов (= денег), а во-вторых, обрабатывать приходится нечасто (особенно в стадии зарождения проекта).
Поэтому хочется включать процессинг только, чтобы обработать видео (время обработки — 3x от продолжительности действа).
Кубернетес формально поддерживает autoscale 0, но как именно это реализуется — я не нашел, зато нашлась такая дискуссия на Stack Overflow.
В итоге пришлось нагородить еще один под во фронтовом пуле, единственной задачей которого является посматривать в постгрес иногда и включать/выключать процессинговый пул в зависимости от того, что в базе оказалось.
У DigitalOcean есть неофициальный клиент для питона, но он уже давно не обновлялся, а Kubernetes API там не присутствует в принципе.
Я сделал на коленке клиент для себя, он покрывает процентов десять возможностей, но вдруг кому пригодится для старта.
В итоге диаграмм разрослась вот так:
![](https://habrastorage.org/webt/5v/py/ql/5vpyqlewmh7sb3me21zr44o2wua.jpeg)
DevOps
Несмотря на великое множество CI/CD инструментов, в разработке не нашлось ничего удобнее Jenkins.
А для управления DigitalOcean'ом идеально подошли Github Actions.