Системный дизайн: клипы с пандами

ЗаданиеИнтервьюер

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

Аналогия интервьюера: ты приходишь в зоопарк, записываешь видео, загружаешь его в систему → система автоматически вырезает из него куски, где в кадре есть панды. Дальше эти клипы можно тегать, смотреть, шарить и искать (как YouTube).

Второй разбор задачи целиком — и другая задача. У сервиса скрейпинг-джоб всё решала очередь: 28 джоб в секунду, пик, пул, приоритеты. Здесь поток — доли видео в секунду, и очередь не проблема вовсе.

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

Порядок прежний:

  1. Понять, что просят. Пересказать задачу как набор обещаний системы.
  2. Спросить. Выяснить всё, чего нет в постановке, но что нужно для решения.
  3. Записать требования. Функциональные — что система умеет; нефункциональные — с какими свойствами.
  4. Посчитать. GPU, байты на диске, байты в раздаче.
  5. Нарисовать. C1, C2 и ниже, обосновывая каждое решение числами.
  1. Шаг 1 из 33

    Что на самом деле просят

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

    • Принять видео и отпустить. Пользователь загружает ролик и уходит. Клипы появятся потом: между загрузкой и результатом стоит обработка, которая длится дольше, чем кто-то готов держать открытым соединение.
    • Найти панду самостоятельно. Никто не размечает видео руками. Значит, внутри нейросетевой детектор, а у него есть уверенность, ошибки и цена в GPU.
    • Хранить клипы и давать их смотреть. В задании сказано «вырезает», но это не значит «создаёт файл». Клип можно хранить как пару таймкодов, а показывать как кусок оригинала. Это одно из главных решений разбора, и оно принимается числами, а не словами из условия.
    • Тегировать и искать. Слова ставят люди, по словам ищут другие люди. Нужен словарь тегов и поиск, который видит свежий тег.

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

    Чего в условии нет: сколько видео, какой длины, с какого устройства, как скоро нужны клипы и что такое клип — файл или отметка. Это следующий шаг.

  2. Шаг 2 из 33

    Вопросы, без ответов на которые считать нечего

    Под эту формулировку подходит и одна машина с видеокартой, и ферма GPU в трёх регионах. Какой вариант нужен, решают ответы, которых в задании нет.

    • Сколько видео и какой длины? Пять тысяч пятиминутных роликов в сутки и пять тысяч часовых — задачи с разницей в порядок по GPU и по диску. Без этого числа ни один расчёт не сходится.
    • С чего снимают? Телефон отдаёт 1080p примерно в 10 Мбит/с: пять минут — 375 МБ, час — четыре с половиной гигабайта. Один запрос такого размера через API не пройдёт. И если телефон пишет HEVC, браузер его может не проиграть — это вопрос про разовую перекодировку при загрузке.
    • Клип — отдельный файл или таймкоды в оригинале? От этого зависит всё хранение: резать и хранить только куски — дёшево по диску, но оригинал потерян; хранить оригинал и отмечать куски — дороже по диску, зато любое видео можно прогнать ещё раз.
    • Как скоро нужны клипы? Минуты — обычный пул. Секунды — обработка начинается до конца загрузки, и это другая система.
    • Что делать, если панд не нашлось? Это обычный исход, и у него должно быть своё состояние и сообщение пользователю.
    • Что считать клипом? Минимальная длина, запас по краям, пауза, после которой панда «ушла из кадра». Это правила детектора, и их задаёт продукт.
    • Кто смотрит и откуда? Публично и со всего мира — значит, байты раздаёт CDN, а не наш сервер.
    • Теги — свободные слова или словарь? Кто их ставит? От этого зависит поиск и то, нужна ли модерация.

    Словесные ответы пойдут в требования, числовые — в расчёты. Где продукт отвечает «не знаю», записываем допущение и проверяем его числом.

  3. Шаг 3 из 33

    Кто пользуется системой

    Три типа пользователей, и потребности у них разные.

    • Загружающий. Снял в зоопарке, нажал «загрузить», ушёл. Вернувшись, хочет увидеть клипы своего видео, подписать их и понять, почему их нет, если их нет. Ему важно, чтобы часовое видео долилось с плохого мобильного интернета.
    • Зритель. Открыл галерею или ссылку из мессенджера, ищет по тегу, смотрит, ставит свой тег. Зрителей в сотню раз больше, чем загружающих, и ждать плеер они не станут.
    • Дежурный. Приходит, когда видео час висит «в обработке». Ему нужны стадия, номер попытки и последняя ошибка.

    Отсюда два требования к схеме. У видео и у клипа есть идентификатор, по которому все трое читают одно и то же состояние. И путь загружающего отличается от пути зрителя не только ручками, но и байтами: один льёт сотни мегабайт, другие тянут мегабайты, и ни то, ни другое не должно проходить через наш API.

  4. Шаг 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. Шаг 5 из 33

    Что вне рамок

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

    • Модерация. Кто-то загрузит видео без панды и с чем-то, чего в галерее быть не должно. Это отдельный контур с людьми, правилами и жалобами.
    • Рекомендации. Лента первой версии — по времени и по тегу. «Подобрать по вкусу» — отдельный продукт со своими данными.
    • Другие животные. Детектор один и ищет панду. Модель на каждое животное — другой пайплайн и другой бюджет.
    • Ручной монтаж. Подвинуть границу клипа на секунду. Если клип — таймкоды, это дёшево: два числа в строке. В первой версии всё равно не делаем, но дверь не закрыта — в отличие от схемы с нарезкой.
    • Прямые трансляции. Обработка потока, а не файла, — другая система.

    Справа десять утверждений из того же разговора. Разложи их по видам: часть свойств на слух похожа на функции, и на ревью спорят как раз о них.

    Десять утверждений из разговора с интервьюером. Разложи их: что система умеет, с какими свойствами она это делает и чего не делает вовсе.

    1. Загрузить видео и получить его идентификатор
    2. Смотреть клип по ссылке
    3. Поставить клипу тег и описание
    4. Найти клипы по тегу
    5. Часовое видео загружается до конца и докачивается после обрыва
    6. Клип начинает играть быстрее двух секунд
    7. Архив зоопарка не задерживает ролик туриста
    8. Поставленный тег виден в поиске сразу
    9. Убирать видео, где вместо панды что-то неприличное
    10. Подбирать зрителю клипы по вкусу
    Разложено 0 из 10
  6. Шаг 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. Шаг 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. Шаг 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. Шаг 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. Шаг 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. Шаг 11 из 33

    C1: система и всё, что с ней разговаривает

    Теперь можно рисовать. Первый кадр — C1: система одним блоком.

    Слева пользователь: он и загружает, и смотрит, и тегирует. Справа CDN — чужая сеть кэшей, через которую зритель получает байты клипа. Мы её не эксплуатируем, а только настраиваем, и её доступность в наш SLO не входит — но NFR-5 без неё не выполнить.

    Две линии от пользователя — легенда цвета на всех следующих кадрах: запись уходит в сервис, чтение приходит с CDN. То, что оба пути байтов идут мимо наших сервисов, станет видно на C2.

  12. Шаг 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. Шаг 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. Шаг 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();
  15. Шаг 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берёмRabbitMQKafkaSQS
    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 / 84.5 / 82.5 / 85 / 8

    ✓ есть из коробки~ можно, но руками✗ нет

  16. Шаг 16 из 33

    От оригинала до таймкодов

    Пайплайн взял задачу. Дальше три действия, и ни одно из них не создаёт нового видео.

    Оригинал скачивается из videos-s3 один раз, на локальный диск воркера. Детектор сэмплирует из него кадры, прогоняет через модель и склеивает попадания в отрезки: начало и конец в миллисекундах, уверенность. Из кадра с самой уверенной пандой делается превью и кладётся в videos-s3 рядом с видео.

    Потом одна транзакция в clips-pg: строки клипов с таймкодами и статус видео ready. Пока она не закоммичена, для всей системы видео ещё в обработке, и клипов у него нет.

    Байты клипа — это байты оригинала, который уже лежит там, куда его положил пользователь. Пайплайн ничего не режет и не перекодирует: ffmpeg здесь только достаёт кадры.

  17. Шаг 17 из 33

    Воркер умер с видео в руках

    Обработка длится минуту, иногда десять. За это время воркер может умереть: модель упала по памяти на 4K-кадре, облако забрало спотовую машину, кто-то выкатил релиз. Видео остаётся processing, и дашборд его не покажет, потому что формально оно обрабатывается.

    Поэтому у задачи аренда. Взяв видео, воркер ставит lease_until на десять минут вперёд и продлевает его каждую минуту, пока работает. Тот же clip-pipeline раз в минуту ищет строки processing с истёкшей арендой и возвращает их в uploaded: попытка плюс один, next_run_at через паузу, которая растёт с каждой попыткой. После третьей — failed с текстом последней ошибки.

    Повторная попытка должна быть безопасной. Клипы пишутся с ключом (video_id, start_ms), превью — с детерминированными именами: половина клипов от прошлой попытки перезапишется, а не удвоится. Это та же идемпотентность, что у приёма, только внутри пайплайна. Оригинал повтор не трогает вовсе — он только читается.

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

    В таблицу требований
  18. Шаг 18 из 33

    Чтение: галерея, страница, поиск

    Читающая сторона — тот же video-api. У скрейпера чтение и запись были разведены по сервисам; здесь 28 чтений в секунду на пике не оправдывают второй сервис, и разделение откладывается до тех пор, пока числа не изменятся.

    Три запроса. Галерея — клипы по времени, страницами по курсору. Страница видео — клипы одного video_id плюс стадия и попытки, если видео ещё в обработке: это и есть FR-2 и половина NFR-14. Поиск — клипы по тегам, о нём отдельный шаг.

    Что отдаёт API в каждом из них: метаданные, адрес оригинала и два таймкода. Ни одного байта видео в ответах нет. Где эти байты — на следующем кадре.

  19. Шаг 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. Шаг 20 из 33

    C2 целиком

    Сервис собран целиком. Здесь только связи; стрелки с двух концов — связь в обе стороны.

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

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

    Чего на схеме пока нет: как детектор решает, что в кадре панда, как пул делится между загружающими и где именно живёт поиск. Это дальше.

  21. Шаг 21 из 33

    Загрузка по шагам

    Схема C2 показывает, кто с кем связан, но не в каком порядке. Здесь одна загрузка сверху вниз, в трёх рамках.

    Старт: POST /videos, строка в базе, подписанные ссылки в ответе. Между первой и второй рамкой пользователь льёт части в videos-s3 сам — минуты, иногда часы, — и ни один наш сервис в этом не участвует.

    Завершение: complete, статус uploaded, 202. В этот момент видео ещё никуда не поехало; оно — строка в таблице, которую возьмёт первый свободный воркер.

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

  22. Шаг 22 из 33

    Просмотр по шагам

    Запрос зрителя короче. Поиск по тегу — один запрос в базу по индексу, ответ — список клипов: адрес оригинала на CDN и два таймкода у каждого.

    Дальше плеер общается только с CDN: просит диапазон байтов, с которого начинается клип, и дальше по мере просмотра. Узел CDN ходит в videos-s3 на промахе, один раз на диапазон, и следующим зрителям отдаёт из кэша. Наших сервисов во второй половине последовательности нет вообще — а это, если считать по байтам, почти вся последовательность.

  23. Шаг 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. Шаг 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.

    videosВидео и его задача на обработку: статус, попытка, аренда — в одной строке.
    iduuid
    owner_iduuid
    statustext
    video_keytext
    duration_sint
    attemptint
    lease_untiltimestamptz
    next_run_attimestamptz
    errortext
    created_attimestamptz
    clipsКлип: два таймкода в оригинале, уверенность детектора, превью и теги для поиска. Файла у клипа нет.
    iduuid
    video_iduuid
    start_msint
    end_msint
    scorereal
    thumb_keytext
    titletext
    descriptiontext
    tagstext[]
    created_attimestamptz
    tagsСловарь тегов: одно нормализованное слово — одна строка.
    idbigint
    nametext
    clip_tagsКто какой тег поставил: по этой таблице тег можно снять.
    clip_iduuid
    tag_idbigint
    user_iduuid
    created_attimestamptz
  25. Шаг 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. Шаг 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. Шаг 27 из 33

    Детекция: порог, длина, ошибки

    Нейронка — не чёрный ящик с ответом «да/нет», а источник уверенности на кадр. Правила, которые превращают её в клипы, задаёт продукт, и они ложатся в таблицу требований.

    Порог. Кадр считается кадром с пандой при уверенности выше 0,7. Ниже — больше ложных клипов; выше — больше пропусков. Порог подбирают на размеченной выборке и меняют без деплоя.

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

    Ошибки двух родов. Ложное срабатывание — клип с енотом — исправляет загружающий через FR-8, и это дёшево: уходит строка. Пропуск — панда есть, клипа нет — исправляет повторный прогон: оригинал на месте, «пройти ещё раз» — обычная задача в той же очереди, с новым порогом или новой моделью. У схемы с нарезкой этого пути нет.

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

    В таблицу требований
    scraper/docs/REQUIREMENTS.md
    Функциональные0
    #Система умеетКто вызывает
    Пока пусто
    Нефункциональные0
    #СвойствоЦель
    NFR-13Перетащи сюда из текста слева
  28. Шаг 28 из 33

    Шумный сосед на GPU

    Изоляция загружающих записана одной строкой. Симулятор справа показывает, что она значит.

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

    Переключи на «поровну». Пул тот же, изменилось только правило выбора следующей задачи: у кого меньше всего в работе, тот и следующий. Турист получает клипы через секунды, зоопарк — треть пула.

    В таблице это один ORDER BY в запросе поллера: сначала владельцы, у которых в работе меньше, потом по времени загрузки. То, ради чего в брокере пришлось бы заводить очередь на владельца и свой планировщик, здесь — строка SQL. Веса — платному зоопарку больше — та же строка с коэффициентом.

    В таблицу требований
    Порядок00:00

    Запусти и смотри на цвета слотов в пуле — по ним видно, чьё видео его занимает.

  29. Шаг 29 из 33

    Где живёт поиск

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

    Тег нормализуется на входе: нижний регистр, обрезанные пробелы, от двух до тридцати двух символов. Нормализованные слова лежат в clips.tags, по ним построен индекс GIN. Запрос «клипы с тегами панда и детёныш» — WHERE tags @> ARRAY['панда','детёныш'], один проход по индексу и сортировка по времени. На 28 чтениях в секунду это миллисекунды.

    Свежесть получается бесплатно: тег пишется в clips.tags в той же транзакции, что и в clip_tags, и виден поиску сразу после коммита. NFR-6 просил пять секунд — получилось ноль.

    Подсказки при вводе — отдельный запрос к словарю tags по префиксу, с индексом под LIKE 'пан%'.

    Когда понадобится OpenSearch или подобное: полнотекстовый поиск по описаниям с опечатками, ранжирование по популярности, тысячи поисков в секунду. Калькулятор раздачи показывает, что до тысяч ещё далеко. До тех пор движок — лишний компонент со своей синхронизацией и своим отставанием, которое как раз и сломало бы NFR-6.

  30. Шаг 30 из 33

    Что хранить и сколько

    Три класса данных с разной судьбой.

    Оригиналы. Живут, пока живёт видео: это и есть клипы. 622 ТБ в год при нынешнем потоке — главная статья расходов, и ею управляет не код, а правило хранилища. Первые тридцать дней, пока видео смотрят, оно лежит в классе для частого чтения; потом переезжает в класс с редким доступом, где хранение дешевле, а каждое чтение дороже. Повторы чтений при этом закрывает CDN: холодный оригинал поднимается из хранилища по разу на узел. Удаление видео уносит оригинал, превью и строки клипов и чистит CDN по адресам. Заброшенные загрузки — строка без complete сутки — подбирает чистка по сроку.

    Превью. Килобайты на клип, их объём в счёте не виден.

    Строки в базе. Миллионы клипов в год — таблица, которую Postgres даже не замечает. Отдельного внимания требует videos: статус меняется несколько раз за жизнь строки, и мёртвые версии копятся. Автовакуум с этим справляется, но за ним нужно смотреть, потому что по этой же таблице работает очередь.

    Число для NFR-10 — в таблицу: оригинал живёт, пока живёт видео, около 622 ТБ в год, через тридцать дней — класс с редким доступом.

    В таблицу требований
    scraper/docs/REQUIREMENTS.md
    Функциональные0
    #Система умеетКто вызывает
    Пока пусто
    Нефункциональные0
    #СвойствоЦель
    NFR-10Перетащи сюда из текста слева
  31. Шаг 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. Шаг 32 из 33

    Чем заплатили

    Система выполняет все четыре обещания из задания. Вот что за это отдано.

    Хранилище — главная строка счёта. Клип — таймкоды, поэтому оригинал хранится целиком: 622 ТБ в год против 37, если бы клипы резали в файлы. Это решение ради пересчёта и простоты, и его стоит записать с числом, чтобы через год никто не спрашивал, зачем столько.

    Трафик в битрейте телефона. Зритель получает кусок оригинала как есть: 341 ТБ в месяц вместо 102 при разовой перекодировке в битрейт раздачи. Когда счёт за трафик обгонит счёт за хранение, перекодировка при загрузке встанет отдельным модулем — один файл на видео, по-прежнему без нарезки.

    Очередь живёт в базе. Одна база — один домен отказа: лежит Postgres — лежит и приём, и обработка, и галерея. Таблица очереди требует внимания к вакууму, а рост до тысяч задач в секунду потребует брокер и outbox.

    Один сервис на запись и чтение. Масштабируются вместе. При росте просмотров на два порядка читающую сторону придётся выделить и поставить перед ней кэш.

    Качество продукта упирается в модель. Порог и правила склейки можно крутить, а архив — прогнать заново, но «не та панда» решается только лучшей моделью.

    Решение пересмотрят, если появятся приватные видео (подписанные ссылки CDN), прямые трансляции (другая обработка), часовые видео в 4K (нарезка возвращается на стол) или поток в сотни видео в секунду (брокер и разделение пайплайна на пулы).

  33. Шаг 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По видео видно, на какой стадии оно и почему застряло

Листать шаги можно стрелками ← и →.

Проверь себя

Почему в этой схеме нет брокера сообщений?

Что хранится для клипа?

Почему байты видео не проходят через video-api?

Где живёт поиск по тегам?

Какую проблему решает аренда задачи?

Отвечено 0 из 5