Системный дизайн: клипы с пандами
Спроектировать сервис, который позволяет пользователям извлекать и хранить клипы с пандами из загруженных видео. Пользователи могут тегировать клипы словами, чтобы другие пользователи могли искать и смотреть эти клипы.
Аналогия интервьюера: ты приходишь в зоопарк, записываешь видео, загружаешь его в систему → система автоматически вырезает из него куски, где в кадре есть панды. Дальше эти клипы можно тегать, смотреть, шарить и искать (как YouTube).
Второй разбор задачи целиком — и другая задача. У сервиса скрейпинг-джоб всё решала очередь: 28 джоб в секунду, пик, пул, приоритеты. Здесь поток — доли видео в секунду, и очередь не проблема вовсе.
Проблема в единице работы. Одно видео — сотни мегабайт на входе, минута GPU внутри и клипы, которые смотрят тысячи раз. Поэтому разбор, повторяя тот же порядок — понять, спросить, записать, посчитать, нарисовать, — приходит к другим решениям: байты льются мимо API, очередь живёт в таблице, клип — это два таймкода в оригинале, а раздаёт CDN по диапазону.
Порядок прежний:
- Понять, что просят. Пересказать задачу как набор обещаний системы.
- Спросить. Выяснить всё, чего нет в постановке, но что нужно для решения.
- Записать требования. Функциональные — что система умеет; нефункциональные — с какими свойствами.
- Посчитать. GPU, байты на диске, байты в раздаче.
- Нарисовать. C1, C2 и ниже, обосновывая каждое решение числами.
Шаг 1 из 33
Что на самом деле просят
Задачу пересказывают своими словами и сверяют с тем, кто её принёс. В абзаце выше нет ни одного числа, зато есть четыре обещания, и у каждого своя цена.
- Принять видео и отпустить. Пользователь загружает ролик и уходит. Клипы появятся потом: между загрузкой и результатом стоит обработка, которая длится дольше, чем кто-то готов держать открытым соединение.
- Найти панду самостоятельно. Никто не размечает видео руками. Значит, внутри нейросетевой детектор, а у него есть уверенность, ошибки и цена в GPU.
- Хранить клипы и давать их смотреть. В задании сказано «вырезает», но это не значит «создаёт файл». Клип можно хранить как пару таймкодов, а показывать как кусок оригинала. Это одно из главных решений разбора, и оно принимается числами, а не словами из условия.
- Тегировать и искать. Слова ставят люди, по словам ищут другие люди. Нужен словарь тегов и поиск, который видит свежий тег.
Аналогия интервьюера с YouTube задаёт пропорцию: загружают немногие, смотрят многие. Всё, что просят, укладывается в одно слово — видео, и именно оно решает, какой будет система: ролик весит сотни мегабайт, а запрос в сервис скрейпинга весил килобайты.
Чего в условии нет: сколько видео, какой длины, с какого устройства, как скоро нужны клипы и что такое клип — файл или отметка. Это следующий шаг.
Шаг 2 из 33
Вопросы, без ответов на которые считать нечего
Под эту формулировку подходит и одна машина с видеокартой, и ферма GPU в трёх регионах. Какой вариант нужен, решают ответы, которых в задании нет.
- Сколько видео и какой длины? Пять тысяч пятиминутных роликов в сутки и пять тысяч часовых — задачи с разницей в порядок по GPU и по диску. Без этого числа ни один расчёт не сходится.
- С чего снимают? Телефон отдаёт 1080p примерно в 10 Мбит/с: пять минут — 375 МБ, час — четыре с половиной гигабайта. Один запрос такого размера через API не пройдёт. И если телефон пишет HEVC, браузер его может не проиграть — это вопрос про разовую перекодировку при загрузке.
- Клип — отдельный файл или таймкоды в оригинале? От этого зависит всё хранение: резать и хранить только куски — дёшево по диску, но оригинал потерян; хранить оригинал и отмечать куски — дороже по диску, зато любое видео можно прогнать ещё раз.
- Как скоро нужны клипы? Минуты — обычный пул. Секунды — обработка начинается до конца загрузки, и это другая система.
- Что делать, если панд не нашлось? Это обычный исход, и у него должно быть своё состояние и сообщение пользователю.
- Что считать клипом? Минимальная длина, запас по краям, пауза, после которой панда «ушла из кадра». Это правила детектора, и их задаёт продукт.
- Кто смотрит и откуда? Публично и со всего мира — значит, байты раздаёт CDN, а не наш сервер.
- Теги — свободные слова или словарь? Кто их ставит? От этого зависит поиск и то, нужна ли модерация.
Словесные ответы пойдут в требования, числовые — в расчёты. Где продукт отвечает «не знаю», записываем допущение и проверяем его числом.
Шаг 3 из 33
Кто пользуется системой
Три типа пользователей, и потребности у них разные.
- Загружающий. Снял в зоопарке, нажал «загрузить», ушёл. Вернувшись, хочет увидеть клипы своего видео, подписать их и понять, почему их нет, если их нет. Ему важно, чтобы часовое видео долилось с плохого мобильного интернета.
- Зритель. Открыл галерею или ссылку из мессенджера, ищет по тегу, смотрит, ставит свой тег. Зрителей в сотню раз больше, чем загружающих, и ждать плеер они не станут.
- Дежурный. Приходит, когда видео час висит «в обработке». Ему нужны стадия, номер попытки и последняя ошибка.
Отсюда два требования к схеме. У видео и у клипа есть идентификатор, по которому все трое читают одно и то же состояние. И путь загружающего отличается от пути зрителя не только ручками, но и байтами: один льёт сотни мегабайт, другие тянут мегабайты, и ни то, ни другое не должно проходить через наш API.
Шаг 4 из 33
Функциональные требования
Требования лежат файлом в репозитории сервиса. Таблица справа — этот файл; строки шага лежат под текстом, их нужно перенести в таблицу. Документ один на весь разбор: на шагах про SLO и расчёты в нём появятся числа и новые свойства.
Функциональное требование описывает действие системы, и у каждого действия есть вызывающий. Поэтому в таблице отдельная колонка: кто нажимает.
Нейронки в таблице нет. Она — способ выполнить FR-2, а не требование: с точки зрения загружающего система «находит панду», а чем именно, он не видит. Так же нет очереди, GPU и CDN: это решения, и они появятся на схеме, когда их оправдают числа. И «клип» в таблице — то, что зритель смотрит и тегирует; файл это или таймкоды, решится на расчёте.
Отдельно стоит FR-8 — удалить клип, где панды нет. Он выглядит мелким, но это единственный способ исправить ошибку детектора без человека с нашей стороны, и на шаге про детекцию он ещё понадобится.
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает FR-1 Перетащи сюда из текста слева FR-2 Перетащи сюда из текста слева FR-3 Перетащи сюда из текста слева FR-4 Перетащи сюда из текста слева FR-5 Перетащи сюда из текста слева FR-6 Перетащи сюда из текста слева FR-7 Перетащи сюда из текста слева FR-8 Перетащи сюда из текста слева Нефункциональные0
# Свойство Цель Пока пусто Шаг 5 из 33
Что вне рамок
Список того, чего система не делает, записывают так же явно, как список функций. Через полгода кто-нибудь спросит, почему нет рекомендаций, и ответ должен быть в документе.
- Модерация. Кто-то загрузит видео без панды и с чем-то, чего в галерее быть не должно. Это отдельный контур с людьми, правилами и жалобами.
- Рекомендации. Лента первой версии — по времени и по тегу. «Подобрать по вкусу» — отдельный продукт со своими данными.
- Другие животные. Детектор один и ищет панду. Модель на каждое животное — другой пайплайн и другой бюджет.
- Ручной монтаж. Подвинуть границу клипа на секунду. Если клип — таймкоды, это дёшево: два числа в строке. В первой версии всё равно не делаем, но дверь не закрыта — в отличие от схемы с нарезкой.
- Прямые трансляции. Обработка потока, а не файла, — другая система.
Справа десять утверждений из того же разговора. Разложи их по видам: часть свойств на слух похожа на функции, и на ревью спорят как раз о них.
Десять утверждений из разговора с интервьюером. Разложи их: что система умеет, с какими свойствами она это делает и чего не делает вовсе.
- Загрузить видео и получить его идентификатор
- Смотреть клип по ссылке
- Поставить клипу тег и описание
- Найти клипы по тегу
- Часовое видео загружается до конца и докачивается после обрыва
- Клип начинает играть быстрее двух секунд
- Архив зоопарка не задерживает ролик туриста
- Поставленный тег виден в поиске сразу
- Убирать видео, где вместо панды что-то неприличное
- Подбирать зрителю клипы по вкусу
Разложено 0 из 10Шаг 6 из 33
Нефункциональные требования
Второй раздел — свойства. Нефункциональное требование не добавляет системе действий, оно задаёт гарантию для тех, что уже есть.
Проверяются они через последствия, поэтому под каждым написано, что сломается без него. У этой задачи три свойства, которых не было у скрейпера, и все три — про байты.
Видео загружается, не проходя через наш API. Часовой ролик — это гигабайты. Запрос такого размера упирается в лимит тела, таймаут балансировщика и память сервиса, который его принимает. Байты должны идти в хранилище напрямую.
Клип начинает играть быстро из любой страны. Зритель не ждёт; и байты клипа тоже не должны идти через наш сервис.
Один загружающий не занимает весь пул. Зоопарк зальёт архив за десять лет, и турист с одним роликом будет ждать за ним сутки.
Колонка «Цель» пока пустая. Числа — на следующем шаге.
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает Пока пусто Нефункциональные0
# Свойство Цель NFR-1 Перетащи сюда из текста слева NFR-2 Перетащи сюда из текста слева NFR-3 Перетащи сюда из текста слева NFR-4 Перетащи сюда из текста слева NFR-5 Перетащи сюда из текста слева NFR-6 Перетащи сюда из текста слева NFR-7 Перетащи сюда из текста слева NFR-8 Перетащи сюда из текста слева Шаг 7 из 33
Числа, к которым привязываемся
Свойство без числа нельзя проверить. SLO задаёт долю, порог и окно.
- Приём загрузок — 99,9% за 30 дней. Старт и complete отвечают, даже когда GPU-пул лежит. Это 43 минуты в месяц, когда загрузить нельзя.
- 99% видео доходят до конечного состояния за сутки без человека. Конечные — «готово», «панд нет» и «не вышло». Видео, которое навсегда осталось «в обработке», хуже упавшего.
- 95% видео до десяти минут получают клипы в течение 15 минут. Это обещание про очередь и пул. Оно нарушается, когда загрузок больше, чем успевает разобрать GPU.
- Первый кадр за две секунды в 95 случаях из 100, из любой страны. Считаем верхнюю границу, а не среднее, и не только из своего города.
- Поиск за 300 мс в 99 случаях из 100, свежий тег виден за пять секунд. Второе число важнее первого: поиск, который не находит тег, поставленный минуту назад, выглядит сломанным.
Цели ложатся в колонку рядом с теми строками, что уже в таблице. По этим числам потом принимают решения: нарушено третье — расширяем пул, нарушено четвёртое — смотрим на CDN и на то, где в файле лежит индекс.
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает Пока пусто Нефункциональные0
# Свойство Цель NFR-1 Перетащи сюда из текста слева NFR-3 Перетащи сюда из текста слева NFR-4 Перетащи сюда из текста слева NFR-5 Перетащи сюда из текста слева NFR-6 Перетащи сюда из текста слева Шаг 8 из 33
Расчёт: сколько GPU
Калькулятор справа. Пять тысяч видео в сутки — это 0,06 видео в секунду в среднем и 0,23 на пике, если пик вчетверо выше среднего. Для сравнения: у сервиса скрейпинг-джоб было 28 джоб в секунду. Поток здесь в сто раз меньше, и очередь на таком потоке — не вопрос. Вопрос — что происходит с каждым видео.
На каждое видео детектор тратит GPU. Не на каждый кадр: модель смотрит два кадра в секунду, и пятиминутный ролик обрабатывается примерно за минуту — 0,2 секунды GPU на секунду видео. Это 83 GPU-часа в сутки. Больше обработка ничего не делает: ни резать, ни перекодировать не нужно, клип — это таймкоды.
Сколько GPU держать, считает закон Литтла: поток на пике, умноженный на время одной обработки. 0,23 видео в секунду по 60 секунд — 14 видео в работе одновременно. Видео на GPU обрабатывается по одному, так что это же число — размер пула на пике. В среднем хватит четырёх: пул растёт и сжимается по глубине очереди.
Ожидание в очереди — ползунок, а не результат: закон Литтла не говорит, каким оно будет, но переводит его в видео. Пять минут ожидания — это 69 роликов в очереди на пике и шесть минут от загрузки до клипов; NFR-4 выполняется с запасом. Подвинь ожидание к получасу — обещание нарушено.
Подвинь коэффициент GPU к единице — модель смотрит каждый кадр, — и пул вырастает до семидесяти. Один ползунок детектора стоит дороже всего остального в этой схеме, и это первый аргумент за то, чтобы сэмплировать кадры, а не прогонять все.
В таблицу требованийЧто известно
Что из этого следует
- Средний поток
- 0,06 видео/с
- Поток на пике
- 0,23 видео/с
- Одно видео на GPU
- 60 с
- GPU-часов в сутки
- 83,3 ч
- В обработке одновременно
- 14 GPU
- Ждёт очереди на пике
- 69,4 видео
- От загрузки до клипов
- 6 мин
Поток: 5 000 видео в сутки — это 0,06 в секунду, на пике 0,23. Брокер нужен там, где тысячи.
Закон Литтла: 0,23 видео/с × 60 с на GPU = 14 видео в работе разом — столько GPU и нужно.
Нужен пул GPU-воркеров, который растёт по глубине очереди отдельно от API. Таблица задач такой поток выдерживает — брокер не нужен.
Шаг 9 из 33
Расчёт: сколько байтов на диске
Теперь про размер — то, ради чего задача такая, какая есть.
Пятиминутное видео с телефона при 10 Мбит/с весит 358 МБ. Пять тысяч таких в сутки — 1,7 ТБ загрузок в день. Через наш API столько не проходит; это уже требование NFR-2, и на схеме оно станет первым решением.
Клип — таймкоды, поэтому оригинал остаётся: он и есть то, что смотрят. 1,7 ТБ в день — это 622 ТБ за год, и хранилище растёт с этой скоростью, пока живут видео. Горячими — в классе для частого чтения — держим первые тридцать дней, пока видео смотрят; дальше оно уходит в класс с редким доступом по правилу хранилища. На цифрах это 51 ТБ горячих и 571 ТБ холодных через год.
Калькулятор считает и альтернативу. Если бы клипы резали в отдельные файлы и хранили только их — панда в кадре пятую часть ролика, клип кодирован в 3 Мбит/с, — это было бы 37 ТБ в год, в 17 раз меньше. Вот цена решения «таймкоды вместо нарезки», и её надо назвать вслух.
Что за неё куплено. Нет нарезки и перекодирования — ни CPU на каждое видео, ни файлов, которые могут разойтись со строками в базе. Границу клипа можно подвинуть двумя числами. Пропущенную панду можно найти повторным прогоном, а новую модель — применить к архиву. При нарезке ничего из этого нельзя: оригинала нет.
Подвинь длину видео к часу или битрейт к 50 Мбит/с — и счёт за хранилище становится главным в системе. Тогда нарезка возвращается на стол, и это честный ответ калькулятора. В таблицу уходит строка про бюджет хранилища; число к ней — на шаге про хранение.
В таблицу требованийЧто известно
Что из этого следует
- Один оригинал
- 358 MB
- Загружают в сутки
- 1,71 TB
- Клипов в сутки
- 15 000 шт.
- Оригиналы за год
- 622 TB
- В горячем классе
- 51,2 TB
- В классе с редким доступом
- 571 TB
- Если бы резали клипы, за год
- 37,3 TB
- Таймкоды дороже нарезки в
- ×16,7
Оригинал: 5 мин × 10 Мбит/с = 358 MB; 5 000 в сутки — 1,71 TB в день и 622 TB за год.
Нарезка хранила бы 37,3 TB в год — в ×16,7 меньше. За таймкоды платим хранилищем, а не GPU и второй копией.
Сотни терабайт: горячими держим недавние видео, остальное — в класс с редким доступом по правилу хранилища. Нарезка сэкономила бы в разы, но отняла бы пересчёт.
Шаг 10 из 33
Расчёт: сколько смотрят
Третий калькулятор — про зрителей. Если каждое загруженное видео собирает сто просмотров клипов, это 500 тысяч просмотров в сутки и 23 в секунду на пике. Каждый просмотр — 20 секунд оригинала по 10 Мбит/с, 25 мегабайт: клип не перекодирован, смотрят в том битрейте, в котором снял телефон. Итого 11 ТБ в сутки и 341 ТБ в месяц.
Это трафик, который нельзя пропускать через наш сервис: его отдаёт CDN, а наш API — только адрес и два таймкода. В таблицу уходит строка NFR-12, число к ней появится на кадре про раздачу.
Битрейт при просмотре — ползунок. Подвинь его к трём мегабитам: трафик падает втрое, до 102 ТБ в месяц. Это то, что дала бы разовая перекодировка при загрузке — один файл на видео в битрейте раздачи, нарезки по-прежнему нет. В первой версии её не делаем: это CPU на каждое видео и вторая копия. Когда счёт за трафик станет больше счёта за хранение, ползунок покажет, сколько она сэкономит.
Вторая половина калькулятора — метаданные. Каждый просмотр — одно чтение страницы клипа из базы, каждый пятый зритель ещё и ищет. На пике это 28 чтений в секунду против 0,7 записей клипов: сорок чтений на одну запись. Система читающая, как и обещала аналогия с YouTube, но 28 чтений в секунду — это ничто для одной базы с индексом. Кэш и отдельная читающая сторона здесь не нужны; они понадобятся, если просмотров станет в сто раз больше, и тогда дорога известна — глава про CQRS.
Подвинь просмотры к тысяче на видео: байты вырастают до петабайтов в месяц, а чтений в базу всё ещё меньше трёхсот в секунду. Байты растут быстрее запросов, и это главная пропорция задачи.
В таблицу требованийЧто известно
Что из этого следует
- Просмотров в сутки
- 500 000 шт.
- Просмотров на пике
- 23,1 /с
- Байты зрителям в сутки
- 11,4 TB
- Байты зрителям в месяц
- 341 TB
- Поисков на пике
- 4,63 /с
- Чтений метаданных на пике
- 27,8 /с
- Записей клипов на пике
- 0,69 /с
- Чтений на одну запись
- ×40
Байты: 500 000 просмотров × 20 с × 10 Мбит/с = 11,4 TB в сутки, 341 TB в месяц — это трафик CDN, а не API.
Метаданные: 27,8 чтений/с на пике против 0,69 записей — одна база с индексом по тегам.
Терабайты в сутки: байты идут с CDN диапазонами оригинала, API отдаёт метаданные и таймкоды, а база выдерживает чтения без кэша.
Шаг 11 из 33
C1: система и всё, что с ней разговаривает
Теперь можно рисовать. Первый кадр — C1: система одним блоком.
Слева пользователь: он и загружает, и смотрит, и тегирует. Справа CDN — чужая сеть кэшей, через которую зритель получает байты клипа. Мы её не эксплуатируем, а только настраиваем, и её доступность в наш SLO не входит — но NFR-5 без неё не выполнить.
Две линии от пользователя — легенда цвета на всех следующих кадрах: запись уходит в сервис, чтение приходит с CDN. То, что оба пути байтов идут мимо наших сервисов, станет видно на C2.
Шаг 12 из 33
Вход: байты мимо API
Первые элементы внутри границы —
api-gateway,video-api, базаclips-pgи хранилище видеоvideos-s3.Загрузка идёт в три хода.
POST /videosсоздаёт строку видео в состоянииuploadingи возвращает подписанные ссылки на части. Телефон льёт части прямо вvideos-s3: по частям, с докачкой оборванной части, без лимита тела запроса на нашей стороне.POST /videos/{id}/completeговорит, что всё долито, и переводит видео вuploaded.Через
video-apiпри этом проходят два маленьких запроса, а 358 МБ видео — ни одного байта. Это NFR-2 на схеме. Сервис, который проксировал бы терабайты в сутки, пришлось бы масштабировать по трафику, а не по запросам, и он ничего не делал бы с этими байтами, кроме как перекладывал.Файл, который сюда лёг, — единственная копия видео на всю его жизнь. Из него детектор возьмёт кадры, из него же зритель будет смотреть клипы.
- Новое на схемеapi-gatewayТочка входа: проверяет, кто пришёл, держит лимиты и раздаёт запрос внутрь. Про видео не знает ничего.
- Новое на схемеvideo-apiЕдинственный сервис с ручками: заводит загрузку, принимает complete, пишет теги, отдаёт галерею и поиск. Байты видео через него не идут.
- Новое на схемеclips-pgpostgresВидео, клипы, теги. Строка видео — она же задача обработки; строка клипа — два таймкода.
- Новое на схемеvideos-s3s3Видео как загружено — единственная копия. Сюда пользователь льёт байты сам, отсюда их берёт детектор, отсюда их тянет CDN.
Шаг 13 из 33
Очередь — это таблица
После
completeвидео нужно обработать, и привычный ход — поставить брокер:video-apiпубликует «видео загружено», воркер читает. Здесь брокера нет, и это решение, а не упущение.Строка
videosи есть задача. У неё статусuploaded, номер попытки, срок аренды и время следующего запуска. Сервисclip-pipelineраз в секунду спрашивает базу:SELECT … WHERE status = 'uploaded' AND next_run_at <= now() ORDER BY … FOR UPDATE SKIP LOCKED LIMIT 1, — ставитprocessingи аренду и идёт работать. Несколько воркеров берут задачи из одной таблицы и не мешают друг другу: захваченные строки пропускаются.Чем это лучше брокера именно здесь. Поток — 0,23 видео в секунду на пике; четырнадцать воркеров по запросу в секунду — четырнадцать запросов к индексу. Состояние видео и задача — одна строка, значит, нет двойной записи «база плюс брокер», ради которой у скрейпера появился outbox. И очередь видна одним SQL-запросом: сколько ждёт, сколько в работе, какая задача самая старая.
Брокер нужен там, где тысячи сообщений в секунду, много разных потребителей или события, которые должны разойтись по сервисам. Ничего из этого в задаче нет: потребитель один, и он тяжёлый. Сама таблица и запросы к ней — на следующем шаге.
- Новое на схемеclip-pipelineШедулер с обработкой внутри: берёт следующее видео из таблицы под аренду, качает оригинал, ищет панду и записывает таймкоды клипов. Ничего не режет.
Шаг 14 из 33
Таблица videos как очередь
Вот эта таблица. Четыре колонки делают из строки видео задачу.
- status — где видео в жизненном цикле. Очередь — это строки со значением
uploaded, и только они. - next_run_at — когда задачу можно брать. У новой — сразу, после ошибки — через паузу.
- lease_until — до какого момента задача чья-то. Пока воркер работает, он двигает это время вперёд.
- attempt и error — сколько раз брали и чем кончилось.
Индекс частичный: только по строкам
uploaded, по времени запуска. В нём единицы строк, а не миллионы — и запрос поллера читает ровно голову очереди.Взятие — один
UPDATEс подзапросом.FOR UPDATE SKIP LOCKEDпропускает строки, которые уже захватил сосед;RETURNINGотдаёт воркеру всё, что нужно для работы. Транзакция короткая: взял и отпустил. Сама обработка идёт без блокировки, под арендой.Последний запрос возвращает зависшие: строка
processingс истёкшей арендой снова становитсяuploaded, с попыткой плюс один и паузой, которая растёт с каждой попыткой. Зачем это нужно — на шаге про аренду.videos.sql-- Строка видео — она же задача: status, attempt, lease_until, next_run_at. CREATE TABLE videos ( id uuid PRIMARY KEY, owner_id uuid NOT NULL, status text NOT NULL, -- uploading | uploaded | processing | ready | empty | failed video_key text NOT NULL, duration_s int, attempt int NOT NULL DEFAULT 0, lease_until timestamptz, next_run_at timestamptz NOT NULL DEFAULT now(), error text, created_at timestamptz NOT NULL DEFAULT now() ); -- Индекс только по голове очереди: строк uploaded единицы, остальные в него не попадают. CREATE INDEX videos_queue ON videos (next_run_at) WHERE status = 'uploaded'; -- Взять следующую под аренду. SKIP LOCKED: захваченные соседом строки пропускаются, а не ждутся. UPDATE videos SET status = 'processing', lease_until = now() + interval '10 minutes' WHERE id = ( SELECT id FROM videos WHERE status = 'uploaded' AND next_run_at <= now() ORDER BY next_run_at FOR UPDATE SKIP LOCKED LIMIT 1 ) RETURNING id, video_key, attempt; -- Вернуть зависшие: аренда истекла — задача снова в очереди, с попыткой плюс один и паузой. UPDATE videos SET status = 'uploaded', attempt = attempt + 1, next_run_at = now() + (interval '1 minute' * power(2, attempt)), lease_until = NULL WHERE status = 'processing' AND lease_until < now();- status — где видео в жизненном цикле. Очередь — это строки со значением
Шаг 15 из 33
Таблица или брокер
Матрица — те же требования, что в таблице, против четырёх вариантов. Строка — что нужно нам, ячейка — что даёт вариант.
Kafka отпадает первой. Это журнал: потребитель помнит смещение в партиции, а не судьбу каждого сообщения. Задача на минуту GPU держит всю партицию, отложенный повтор и очередь для неразобранного собираются руками, а честность между загружающими в журнале не живёт вовсе. И сверху всё равно нужен outbox: строка видео и сообщение — две записи.
RabbitMQ и SQS — настоящие очереди работы, и для скрейпера такая и была нужна. Но у обеих та же двойная запись, у RabbitMQ сообщение без подтверждения на десять минут — это настройка таймаутов и heartbeat, у SQS — visibility timeout, который продлевают руками. Справедливость между владельцами в обоих случаях — свой код поверх.
Таблица в Postgres закрывает каждую строку: аренда — колонка, повтор —
next_run_at, честность —ORDER BY, дашборд —GROUP BY status. Она проигрывает только по росту: сотни задач в секунду на одной таблице сSKIP LOCKEDещё работают, тысячи — уже брокер. Но к тысячам видео в секунду у нас будет другая проблема, и она будет не про очередь, а про петабайты.Что нужно нам Postgres tableберём RabbitMQ Kafka SQS NFR-9Поток: одно видео в секунду на пике ✓доли видео в секунду — хватит одного индекса ✓с запасом ✓с запасом на порядки ✓почти без предела NFR-3Задача длится минуты, и видно, кто её держит ✓lease_until в строке, продлевается во время работы ~сообщение без ack держит канал; таймаут потребителя ✗длинная задача держит всю партицию ~visibility timeout до 12 часов, продлевать руками NFR-3Статус видео и задача — одна запись ✓UPDATE одной строки ✗нужен outbox: база и брокер — две записи ✗нужен outbox ✗нужен outbox NFR-11Отложенный повтор с паузой ✓next_run_at = now() + backoff ~TTL и dead-letter или плагин задержки ✗нет: retry-топики собираются руками ✓ChangeMessageVisibility, до 15 минут NFR-7Поровну между загружающими ✓ORDER BY по владельцу в том же запросе ~очередь на владельца, выбор — своим кодом ✗нет: порядок партиции ✗нет: FIFO в пределах группы NFR-14Очередь видна одним SQL-запросом ✓SELECT status, count(*) … GROUP BY status ~длина очереди и unacked в админке, без строки видео ~лаг консьюмера, без судьбы сообщения ~возраст и глубина в метриках, без строки видео Ничего нового в эксплуатации ✓база уже есть ~кластер и quorum-очереди ✗кластер, партиции, ребалансы ✓управляет AWS Вырастет до тысяч в секунду ~до сотен в секунду; дальше — брокер ✓десятки тысяч в секунду ✓миллионы ✓почти без предела Итого 7.5 / 8 4.5 / 8 2.5 / 8 5 / 8 ✓ есть из коробки~ можно, но руками✗ нет
Шаг 16 из 33
От оригинала до таймкодов
Пайплайн взял задачу. Дальше три действия, и ни одно из них не создаёт нового видео.
Оригинал скачивается из
videos-s3один раз, на локальный диск воркера. Детектор сэмплирует из него кадры, прогоняет через модель и склеивает попадания в отрезки: начало и конец в миллисекундах, уверенность. Из кадра с самой уверенной пандой делается превью и кладётся вvideos-s3рядом с видео.Потом одна транзакция в
clips-pg: строки клипов с таймкодами и статус видеоready. Пока она не закоммичена, для всей системы видео ещё в обработке, и клипов у него нет.Байты клипа — это байты оригинала, который уже лежит там, куда его положил пользователь. Пайплайн ничего не режет и не перекодирует: ffmpeg здесь только достаёт кадры.
Шаг 17 из 33
Воркер умер с видео в руках
Обработка длится минуту, иногда десять. За это время воркер может умереть: модель упала по памяти на 4K-кадре, облако забрало спотовую машину, кто-то выкатил релиз. Видео остаётся
processing, и дашборд его не покажет, потому что формально оно обрабатывается.Поэтому у задачи аренда. Взяв видео, воркер ставит
lease_untilна десять минут вперёд и продлевает его каждую минуту, пока работает. Тот жеclip-pipelineраз в минуту ищет строкиprocessingс истёкшей арендой и возвращает их вuploaded: попытка плюс один,next_run_atчерез паузу, которая растёт с каждой попыткой. После третьей —failedс текстом последней ошибки.Повторная попытка должна быть безопасной. Клипы пишутся с ключом
(video_id, start_ms), превью — с детерминированными именами: половина клипов от прошлой попытки перезапишется, а не удвоится. Это та же идемпотентность, что у приёма, только внутри пайплайна. Оригинал повтор не трогает вовсе — он только читается.Постоянные ошибки — битый файл, не видео вовсе — повторять незачем: такие сразу в
failed. Граница проходит не по коду ошибки, а по тому, может ли повтор дать другой результат.В таблицу требованийШаг 18 из 33
Чтение: галерея, страница, поиск
Читающая сторона — тот же
video-api. У скрейпера чтение и запись были разведены по сервисам; здесь 28 чтений в секунду на пике не оправдывают второй сервис, и разделение откладывается до тех пор, пока числа не изменятся.Три запроса. Галерея — клипы по времени, страницами по курсору. Страница видео — клипы одного
video_idплюс стадия и попытки, если видео ещё в обработке: это и есть FR-2 и половина NFR-14. Поиск — клипы по тегам, о нём отдельный шаг.Что отдаёт API в каждом из них: метаданные, адрес оригинала и два таймкода. Ни одного байта видео в ответах нет. Где эти байты — на следующем кадре.
Шаг 19 из 33
Просмотр: диапазон оригинала с CDN
GET /clips/{id}возвращаетvideo_url,start_msиend_ms. Адрес ведёт не в наш сервис, а на CDN, за которым стоитvideos-s3. Плеер открывает оригинал с нужной секунды: запросом с заголовком Range он просит у CDN только байты клипа, а не файл целиком. На промахе узел CDN тянет этот диапазон из хранилища и держит у себя.Одна деталь делает это возможным. Телефон часто пишет индекс кадров — атом moov — в конец MP4, и плеер не может начать с середины, не дочитав файл до конца. Разовая перестановка индекса в начало при загрузке решает это без перекодирования: байты копируются как есть.
Зачем CDN, если есть S3? Хранилище умеет отдавать диапазоны и само, по ссылке, и при небольшом трафике это правильный ответ — калькулятор раздачи так и говорит. CDN перед ним нужен ради трёх вещей.
- Расстояние. Бакет живёт в одном регионе, а зритель из другого полушария ходил бы через океан за каждым куском. Узел CDN стоит рядом со зрителем.
- Повторы. Удачный клип смотрят сто тысяч раз. Без CDN это сто тысяч чтений из бакета; с CDN бакет отдаёт диапазон по разу на каждый узел, остальное идёт из кэша на краю.
- Деньги. Трафик из хранилища наружу тарифицируется дороже, чем трафик с CDN, и каждый запрос к бакету тоже платный.
Так закрываются два требования сразу. NFR-12: 11 ТБ в сутки не проходят ни через
video-api, ни черезapi-gateway. NFR-5: первый кадр приходит с ближайшего края, а не из одного региона. У клипа есть превью, которое показывается, пока грузится первый кусок.Клипы публичные, как на YouTube, поэтому ссылки обычные. Если продукт захочет приватные видео, на этом же месте появятся подписанные ссылки CDN — и это единственное место, которое изменится.
В таблицу требованийШаг 20 из 33
C2 целиком
Сервис собран целиком. Здесь только связи; стрелки с двух концов — связь в обе стороны.
Два пути байтов идут мимо наших сервисов: оригинал — от пользователя прямо в
videos-s3, и он же — изvideos-s3через CDN к зрителю, по диапазону. Наши сервисы трогают байты ровно один раз, внутри пайплайна, и только читают.В центре одна база: видео с задачами, клипы с таймкодами и тегами, поиск. Брокера нет, отдельной читающей стороны нет, кэша нет: каждое из этих решений числа пока не оправдывают, а каждое добавило бы компонент, который умеет ломаться.
Чего на схеме пока нет: как детектор решает, что в кадре панда, как пул делится между загружающими и где именно живёт поиск. Это дальше.
Шаг 21 из 33
Загрузка по шагам
Схема C2 показывает, кто с кем связан, но не в каком порядке. Здесь одна загрузка сверху вниз, в трёх рамках.
Старт:
POST /videos, строка в базе, подписанные ссылки в ответе. Между первой и второй рамкой пользователь льёт части вvideos-s3сам — минуты, иногда часы, — и ни один наш сервис в этом не участвует.Завершение:
complete, статусuploaded,202. В этот момент видео ещё никуда не поехало; оно — строка в таблице, которую возьмёт первый свободный воркер.Обработка: взятие под аренду, оригинал на локальный диск, превью в хранилище, таймкоды и статус в базу одной транзакцией. Оригинал на месте, нового файла с видео не появилось. Пользователь в этой рамке не участвует и узнаёт о результате со страницы видео.
Шаг 22 из 33
Просмотр по шагам
Запрос зрителя короче. Поиск по тегу — один запрос в базу по индексу, ответ — список клипов: адрес оригинала на CDN и два таймкода у каждого.
Дальше плеер общается только с CDN: просит диапазон байтов, с которого начинается клип, и дальше по мере просмотра. Узел CDN ходит в
videos-s3на промахе, один раз на диапазон, и следующим зрителям отдаёт из кэша. Наших сервисов во второй половине последовательности нет вообще — а это, если считать по байтам, почти вся последовательность.Шаг 23 из 33
C3: внутри clip-pipeline
Спускаемся на уровень ниже. C3 показывает модули одного сервиса.
clip-pipelineна C2 был одной коробкой, внутри у него три.- task-poller — берёт следующую задачу под аренду, продлевает аренду, возвращает истёкшие. Всё, что относится к очереди, живёт здесь.
- panda-detector — декодирует оригинал по два кадра в секунду, прогоняет кадры через детектор пачками и склеивает попадания в отрезки: с порогом уверенности, минимальной длиной и запасом по краям. Из лучшего кадра делает превью и кладёт его рядом с видео.
- videos-repository — единственный модуль, который ходит в базу: таймкоды и статус одной транзакцией.
Если когда-нибудь понадобится разовая перекодировка оригинала в битрейт раздачи, она встанет отдельным модулем на CPU, а не на GPU, — и сделает один файл на видео, а не на клип.
- Новое на схемеtask-pollerОчередь целиком: следующая задача под аренду, продление, возврат истёкших.
- Новое на схемеpanda-detectorКадры через нейронку, попадания — в отрезки с порогом, минимальной длиной и запасом по краям; превью из лучшего кадра.
- Новое на схемеvideos-repositoryЕдинственный модуль, который ходит в clips-pg: таймкоды и статус одной транзакцией.
Шаг 24 из 33
Данные
Четыре таблицы, и первая из них — ещё и очередь.
videos. Кому принадлежит, где файл, в каком статусе. Колонки
attempt,lease_until,next_run_atиerror— это задача: пока видео не в конечном состоянии, строка живёт в очереди. Индекс по(status, next_run_at)— весь брокер.clips. Два таймкода в оригинале, уверенность детектора, ключ превью в
videos-s3, заголовок и описание. Файла у клипа нет: байты — это оригинал по диапазону отstart_msдоend_ms. Поэтому подвинуть границу — обновить два числа, а удалить клип — удалить строку. И колонкаtags text[]— копия тегов для поиска: по ней строится индекс, и это шаг про поиск.tags и clip_tags. Словарь нормализованных слов и кто какой тег поставил. Второе нужно, чтобы снять свой тег и чтобы один тег не ставился дважды. Тег пишется в
clip_tagsи вclips.tagsодной транзакцией; две копии — цена поиска без отдельного движка.Отрезков без клипа в данных нет: если детектор что-то нашёл, это клип; если нет — у видео статус
empty.Шаг 25 из 33
Контракт: расписка и адреса
Контракт карточками. Три из них — про загрузку, и байты видео есть только в одной, которая не наша.
POST /videosотвечает201с идентификатором и подписанными ссылками на части.PUTпо такой ссылке — это S3, не мы: ключ подписи ограничивает срок и объект, и клиент докачивает оборванную часть сам.complete—202 Accepted: клипов ещё нет, есть расписка и адрес, где следить.Переход в
uploaded— условныйUPDATE … WHERE status = 'uploading'. Повторныйcompleteпосле потерянного ответа ничего не меняет и получает тот же202: обработка стартует один раз. Это NFR-8, и ключ идемпотентности здесь — сам идентификатор видео.Остальные карточки — чтение и теги.
GET /clips/{id}отдаётvideo_urlна CDN и паруstart_ms,end_ms: плеер сам просит нужный диапазон.DELETE /clips/{id}закрывает FR-8: уходит строка и превью, а оригинал — только вместе с видео.- POST
/videos201 CreatedЗавести видео. В ответе идентификатор и подписанные ссылки на части: байты пойдут по ним, мимо API.
- запрос
Idempotency-Keysizecontent_type- ответ
idstatus: uploadingupload.part_urls[]upload.expires_at
- PUT
{upload.part_url}200 OKЧасть видео — прямо в хранилище. Не наша ручка: это S3, мы её только подписали.
- запрос
part bytes- ответ
ETag
- POST
/videos/{id}/complete202 AcceptedЗагрузка закончена. Повтор с тем же id получает тот же ответ: обработка стартует один раз.
- запрос
parts[].etag- ответ
status: uploadedlinks.self
- GET
/videos/{id}200 OKЧто с видео: стадия, попытки, готовые клипы.
- ответ
statusstageattemptsclips[]
- GET
/clips?tag=&cursor=200 OKКлипы по тегу, страницами по курсору.
- ответ
items[]next_cursor
- GET
/clips/{id}200 OKСтраница клипа: описание, теги, адрес оригинала на CDN и два таймкода. Плеер сам просит нужный диапазон.
- ответ
titledescriptiontags[]video_urlstart_msend_msthumbnail_url
- POST
/clips/{id}/tags201 CreatedПоставить тег. Слово нормализуется; второй раз тот же тег не добавляется.
- запрос
tag- ответ
tags[]
- DELETE
/clips/{id}204 No ContentУбрать клип: ложное срабатывание или своё видео. Удаляется строка и превью; оригинал — только вместе с видео.
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает Пока пусто Нефункциональные0
# Свойство Цель NFR-2 Перетащи сюда из текста слева NFR-8 Перетащи сюда из текста слева Шаг 26 из 33
Жизненный цикл видео
Состояний у видео шесть, и три из них конечные.
uploading→uploaded→processing→ready. Рядом два других конца:empty, когда детектор ничего не нашёл, иfailed, когда попытки кончились или загрузка так и не завершилась за сутки.Кто переводит, видно по цвету. В
uploaded—video-apiпоcomplete. Вprocessingи дальше вreadyилиempty— пайплайн, и только после коммита таймкодов. Обратно вuploadedи вfailed— поллер по аренде и попыткам, а не сам детектор: упавший воркер ничего не переводит, за него это делает тот, кто заметил истёкшую аренду.empty— честное состояние, а не ошибка. Половина видео из зоопарка будет про вольер без панды, и пользователю об этом говорят словами, а не пустой страницей. И оригинал при этом остаётся: если детектор ошибся, видео можно поставить в очередь ещё раз — тем же переходом вuploaded.- клиент
- video-api
- пайплайн
- поллер по аренде
processingВоркер держит аренду: качает оригинал, ищет панду, пишет таймкоды. Истекла аренда — видео вернётся в uploaded.
Куда дальше
→ readyтаймкоды записаны · пайплайн→ emptyпанд нет · пайплайн→ uploadedаренда истекла или ошибка, backoff · поллер по аренде→ failedтри попытки · поллер по аренде
Шаг 27 из 33
Детекция: порог, длина, ошибки
Нейронка — не чёрный ящик с ответом «да/нет», а источник уверенности на кадр. Правила, которые превращают её в клипы, задаёт продукт, и они ложатся в таблицу требований.
Порог. Кадр считается кадром с пандой при уверенности выше 0,7. Ниже — больше ложных клипов; выше — больше пропусков. Порог подбирают на размеченной выборке и меняют без деплоя.
Длина и склейка. Попадания склеиваются в отрезок, если пауза между ними меньше двух секунд; отрезок короче пяти секунд выбрасывается; к краям добавляется по секунде, чтобы панда не входила в кадр на первом кадре клипа. Всё это — арифметика над таймкодами, и правила можно поменять, не трогая ни одного файла.
Ошибки двух родов. Ложное срабатывание — клип с енотом — исправляет загружающий через FR-8, и это дёшево: уходит строка. Пропуск — панда есть, клипа нет — исправляет повторный прогон: оригинал на месте, «пройти ещё раз» — обычная задача в той же очереди, с новым порогом или новой моделью. У схемы с нарезкой этого пути нет.
Новая модель. Применяется к новым загрузкам сразу, а к архиву — той же очередью, по одному видео, в фоне и без спешки. Это второе, что куплено за хранение оригиналов.
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает Пока пусто Нефункциональные0
# Свойство Цель NFR-13 Перетащи сюда из текста слева Шаг 28 из 33
Шумный сосед на GPU
Изоляция загружающих записана одной строкой. Симулятор справа показывает, что она значит.
Три пользователя: турист и школьная экскурсия по ролику в две секунды и зоопарк, который выложил архив — четыреста видео разом. Пул из восьми GPU по четыре секунды на видео разбирает два видео в секунду. Включи общую очередь: зоопарк занимает пул на три минуты, и у туриста за это время не готов ни один клип. Система при этом права: она берёт задачи в порядке поступления.
Переключи на «поровну». Пул тот же, изменилось только правило выбора следующей задачи: у кого меньше всего в работе, тот и следующий. Турист получает клипы через секунды, зоопарк — треть пула.
В таблице это один
ORDER BYв запросе поллера: сначала владельцы, у которых в работе меньше, потом по времени загрузки. То, ради чего в брокере пришлось бы заводить очередь на владельца и свой планировщик, здесь — строка SQL. Веса — платному зоопарку больше — та же строка с коэффициентом.В таблицу требованийПорядок00:00Запусти и смотри на цвета слотов в пуле — по ним видно, чьё видео его занимает.
Шаг 29 из 33
Где живёт поиск
Поиск по тегам — не полнотекстовый поиск. Это точное совпадение слов из словаря, и для него не нужен отдельный движок.
Тег нормализуется на входе: нижний регистр, обрезанные пробелы, от двух до тридцати двух символов. Нормализованные слова лежат в
clips.tags, по ним построен индекс GIN. Запрос «клипы с тегамипандаидетёныш» —WHERE tags @> ARRAY['панда','детёныш'], один проход по индексу и сортировка по времени. На 28 чтениях в секунду это миллисекунды.Свежесть получается бесплатно: тег пишется в
clips.tagsв той же транзакции, что и вclip_tags, и виден поиску сразу после коммита. NFR-6 просил пять секунд — получилось ноль.Подсказки при вводе — отдельный запрос к словарю
tagsпо префиксу, с индексом подLIKE 'пан%'.Когда понадобится OpenSearch или подобное: полнотекстовый поиск по описаниям с опечатками, ранжирование по популярности, тысячи поисков в секунду. Калькулятор раздачи показывает, что до тысяч ещё далеко. До тех пор движок — лишний компонент со своей синхронизацией и своим отставанием, которое как раз и сломало бы NFR-6.
Шаг 30 из 33
Что хранить и сколько
Три класса данных с разной судьбой.
Оригиналы. Живут, пока живёт видео: это и есть клипы. 622 ТБ в год при нынешнем потоке — главная статья расходов, и ею управляет не код, а правило хранилища. Первые тридцать дней, пока видео смотрят, оно лежит в классе для частого чтения; потом переезжает в класс с редким доступом, где хранение дешевле, а каждое чтение дороже. Повторы чтений при этом закрывает CDN: холодный оригинал поднимается из хранилища по разу на узел. Удаление видео уносит оригинал, превью и строки клипов и чистит CDN по адресам. Заброшенные загрузки — строка без
completeсутки — подбирает чистка по сроку.Превью. Килобайты на клип, их объём в счёте не виден.
Строки в базе. Миллионы клипов в год — таблица, которую Postgres даже не замечает. Отдельного внимания требует
videos: статус меняется несколько раз за жизнь строки, и мёртвые версии копятся. Автовакуум с этим справляется, но за ним нужно смотреть, потому что по этой же таблице работает очередь.Число для NFR-10 — в таблицу: оригинал живёт, пока живёт видео, около 622 ТБ в год, через тридцать дней — класс с редким доступом.
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает Пока пусто Нефункциональные0
# Свойство Цель NFR-10 Перетащи сюда из текста слева Шаг 31 из 33
Что мерить
Метрик много, для решений достаточно пяти.
- Возраст самой старой задачи в
uploaded. Важнее длины очереди: по нему растёт пул и по нему видно, что пул не справляется, раньше, чем нарушится NFR-4. - Время от
completeдоready, p95. Прямо соответствует SLO. - Отказы по классам ошибок. «Не хватило памяти», «не декодируется», «модель не ответила» — три разных инцидента, и смешивать их в общую долю отказов нельзя.
- Доля
empty. Выросла вдвое за день — порог сбился или модель деградировала, и пользователи ещё не пожаловались. - Доля попаданий в CDN и исходящий трафик из
videos-s3. Если трафик из хранилища растёт вместе с просмотрами, CDN не кэширует диапазоны, и счёт за хранилище скажет об этом позже, чем метрика.
И стадия с попытками на странице видео: дежурному не нужно лезть в базу, чтобы увидеть «попытка 2 из 3, упал на кадре 1 840».
В таблицу требованийscraper/docs/REQUIREMENTS.mdФункциональные0
# Система умеет Кто вызывает Пока пусто Нефункциональные0
# Свойство Цель NFR-14 Перетащи сюда из текста слева - Возраст самой старой задачи в
Шаг 32 из 33
Чем заплатили
Система выполняет все четыре обещания из задания. Вот что за это отдано.
Хранилище — главная строка счёта. Клип — таймкоды, поэтому оригинал хранится целиком: 622 ТБ в год против 37, если бы клипы резали в файлы. Это решение ради пересчёта и простоты, и его стоит записать с числом, чтобы через год никто не спрашивал, зачем столько.
Трафик в битрейте телефона. Зритель получает кусок оригинала как есть: 341 ТБ в месяц вместо 102 при разовой перекодировке в битрейт раздачи. Когда счёт за трафик обгонит счёт за хранение, перекодировка при загрузке встанет отдельным модулем — один файл на видео, по-прежнему без нарезки.
Очередь живёт в базе. Одна база — один домен отказа: лежит Postgres — лежит и приём, и обработка, и галерея. Таблица очереди требует внимания к вакууму, а рост до тысяч задач в секунду потребует брокер и outbox.
Один сервис на запись и чтение. Масштабируются вместе. При росте просмотров на два порядка читающую сторону придётся выделить и поставить перед ней кэш.
Качество продукта упирается в модель. Порог и правила склейки можно крутить, а архив — прогнать заново, но «не та панда» решается только лучшей моделью.
Решение пересмотрят, если появятся приватные видео (подписанные ссылки CDN), прямые трансляции (другая обработка), часовые видео в 4K (нарезка возвращается на стол) или поток в сотни видео в секунду (брокер и разделение пайплайна на пулы).
Шаг 33 из 33
Ответ: решения против требований
В начале разбора мы записали требования, по ходу к ним добавились числа. Осталось проверить, что на каждое есть ответ.
Слева решения, принятые по ходу разбора, справа таблица требований целиком. Положи каждую карточку на строку, которую она закрывает. У некоторых решений строк несколько, у некоторых строк — несколько решений.
Если карточка легла не туда, под ней будет написано, на что она отвечает на самом деле. Требование без карточки означает, что разбор на него не ответил и схему нужно дорабатывать.
Решения
Закрыто 0 из 22
Перетащи карточку на требование, которое она закрывает. Или щёлкни карточку, потом строку.
Функциональные
- FR-1Начать загрузку видео и получить адрес, куда лить байты
- FR-2Сообщить, что загрузка закончена, и видеть, что с видео: в обработке, готово, панд нет
- FR-3Показать клипы одного видео
- FR-4Смотреть клип
- FR-5Поставить клипу тег и описание; снять свой тег
- FR-6Искать клипы по тегам
- FR-7Поделиться клипом ссылкой
- FR-8Удалить своё видео с клипами и клип, где панды нет
Нефункциональные
- NFR-1Приём загрузок переживает остановку обработки
- NFR-2Видео любого размера загружается, не проходя через наш API
- NFR-3Загруженное видео не теряется
- NFR-4Клипы появляются без долгого ожидания
- NFR-5Клип начинает играть быстро
- NFR-6Поиск отвечает быстро и находит свежий тег
- NFR-7Один загружающий не занимает весь GPU-пул
- NFR-8Повтор «загрузка закончена» не запускает вторую обработку
- NFR-9Система выдерживает пиковый поток
- NFR-10Хранилище растёт в рамках бюджета
- NFR-11Временный отказ не роняет видео
- NFR-12Просмотр клипа не нагружает наш API байтами
- NFR-13Клип без панды можно убрать, а панду — не пропустить
- NFR-14По видео видно, на какой стадии оно и почему застряло
Функциональные0
| # | Система умеет | Кто вызывает |
|---|---|---|
| Пока пусто | ||
Нефункциональные0
| # | Свойство | Цель |
|---|---|---|
| Пока пусто | ||
Листать шаги можно стрелками ← и →.
Проверь себя
Почему в этой схеме нет брокера сообщений?
Брокер оправдан тысячами сообщений в секунду, несколькими потребителями или событиями, которые расходятся по сервисам. Здесь 0,23 видео в секунду на пике, один тяжёлый потребитель и строка videos, которая сама служит задачей: нет двойной записи, видна очередь, повтор и честность — SQL.
Что хранится для клипа?
Клип — строка с start_ms и end_ms. Плеер просит у CDN диапазон байтов оригинала, и нового файла с видео не появляется никогда. За это платим хранением оригиналов, а получаем пересчёт, правку границ и отсутствие нарезки.
Почему байты видео не проходят через video-api?
1,7 ТБ в сутки через сервис, который их только перекладывает, — это лимиты тела запроса, таймауты балансировщика и масштабирование по трафику. Подписанные ссылки на части отправляют байты в хранилище напрямую, а через API идут два маленьких запроса.
Где живёт поиск по тегам?
Поиск по тегам — точное совпадение слов, не полнотекстовый поиск. GIN по массиву тегов отвечает за миллисекунды на десятках запросов в секунду, а тег виден сразу после коммита. Отдельный движок понадобится для опечаток, ранжирования и тысяч поисков в секунду.
Какую проблему решает аренда задачи?
Без аренды видео упавшего воркера навсегда останется в processing, и дашборд этого не покажет. Аренда продлевается в работе; истекла — поллер возвращает задачу в uploaded с паузой и считает попытку.
Отвечено 0 из 5