Спроектируй YouTube
Спроектируй YouTube: путь записи, что принимает и транскодирует одну загрузку в множество рендиций, и путь чтения, отдающий миллиарды просмотров с CDN. Байты не касаются серверов приложения, БД метаданных не держит видео, а счётчики просмотров намеренно eventually consistent.
Новичка просят «спроектируй YouTube», и он рисует форму загрузки, которая POST-ит видео на веб-сервер, а тот кладёт его строкой в базу. Два уточняющих вопроса топят это. Первый: загрузка в 4K — это 20 ГБ; твой сервер приложения теперь держит запрос на 20 ГБ в памяти, а строка Postgres хранит блоб на 20 ГБ. Второй: когда у этого видео десять миллионов просмотров, каждый зритель в своей сети, на своём устройстве, со своей шириной канала — ты правда будешь стримить тот же файл на 20 ГБ пользователю на телефоне через 3G? Реальный дизайн распадается на две половины, что почти не разговаривают друг с другом: тяжёлый, медленный, асинхронный путь записи, превращающий одну загрузку в десятки файлов, и ослепительно быстрый путь чтения, отдающий эти файлы с машин рядом со зрителем. Поймёшь этот раскол правильно — остальное детали; поймёшь неправильно — никакой тюнинг базы не спасёт.
Требования
Через десять минут ты увидишь, почему этот раскол запись/чтение — единственное самое весомое архитектурное решение, и почему каждый срезанный угол на нём всплывает как инцидент на проде. Сеньорский ответ начинается с фиксации границ, ведь «спроектируй YouTube» прячет три разные системы (видео, комментарии, рекомендации), и стоит объявить, какую ты строишь.
Функциональные — автор загружает видео; система хранит его надёжно, транскодирует в несколько разрешений и делает смотрибельным. Зритель ищет или листает, открывает страницу просмотра и плавно стримит видео на любом устройстве и канале. Вторично: счётчики просмотров, лайки, комментарии. Вне границ этого урока: рекомендации и монетизация (свои системы).
Нефункциональные — это и есть часть, формирующая архитектуру. Нагрузка сильно перекошена на чтение: одну загрузку смотрят тысячи–миллионы раз, так что чтения превосходят записи примерно 100:1–1000:1. Загрузки могут быть медленными и асинхронными (автор терпит «обрабатывается, зайдите позже»); воспроизведение должно быть быстрым и плавным (буферизация — кардинальный грех). Хранилище должно быть дешёвым и долговечным, ведь каталог только растёт. И воспроизведение должно адаптироваться к телефону на сотовой и телевизору на оптике из одного исходника.
Эти четыре факта — read-heavy, async-write, дёшево-долговечное хранилище, адаптивное воспроизведение — и есть весь дизайн. Всё ниже — их следствие.
Оценка
Цифры говорят, какой компонент сломается первым. Возьмём 2 миллиарда пользователей; округляем всё до одной значащей цифры (входные данные — догадки).
Загрузки: ~1 час видео загружается в секунду глобально (величина отраслевого масштаба)
→ скажем, 500 часов/минуту → ~30 000 часов/час
Просмотр: ~1 миллиард часов просмотрено в день
→ 1e9 ч/день ÷ ~1e5 с/день ≈ 10 000 часов-видео стартует в секунду
Чтение:запись: просмотры (~5e9/день) vs загрузки (~1e6/день) → ~1000:1 read-heavy
Хранилище: один исходник → ~5 рендиций (240p…4K) + превью
10-мин 1080p видео ≈ 0,5 ГБ исходник; все рендиции ≈ 1 ГБ
миллионы новых видео/день → петабайты/день новых хранимых байт
Egress: вот реальный счёт. ~1e9 часов-просмотра/день при ~3 Мбит/с в среднем
≈ эксабайты/месяц CDN-egress — стоимость диктует канал, а не хранилищеОценка уже отвергает два дизайна: нельзя отдавать egress такого масштаба из своего ЦОД (нужен CDN, и вероятно собственный edge-флот), и нельзя класть байты видео в базу (петабайты/день в Postgres абсурдны — байты идут в объектное хранилище). Метаданные крошечны на этом фоне: запись видео — несколько килобайт, так что миллиарды видео спокойно умещаются в шардированном реляционном хранилище.
Высокоуровневый дизайн
Система раскалывается на путь записи (загрузка → транскод → хранение) и путь чтения (просмотр → CDN → стрим), соединённые лишь объектным хранилищем и БД метаданных.
Форму надо усвоить: сервер API — на пути управления, никогда на пути данных. Он выдаёт presigned URL, чтобы клиент грузил байты напрямую в объектное хранилище; пишет малую строку метаданных; кладёт задачу в очередь. Он никогда не проксирует видео. Симметрично, на чтении зритель тянет манифест и сегменты с CDN, который при промахе тянет из объектного хранилища — сервер приложения и в этом цикле не участвует. Это самое важное решение, и именно его наивный дизайн портит.
Глубокое погружение: fan-out транскодинга
Сырая загрузка — один файл в одном кодеке и одном разрешении. Он бесполезен для воспроизведения разнородной аудитории, поэтому пайплайн транскодинга превращает его в лестницу рендиций: тот же контент в 240p, 360p, 480p, 720p, 1080p, 4K, каждая с подходящим битрейтом, часто в нескольких кодеках (H.264 ради совместимости, VP9/AV1 ради эффективности). Каждая рендиция сама режется на короткие сегменты (2–10 секунд), а манифест (HLS .m3u8 или DASH .mpd) перечисляет для каждой рендиции URL её сегментов.
Транскодинг тяжёл по CPU и до неприличия параллелен, что и есть ключ к скорости. Вместо последовательного транскода всего видео разбей исходник на чанки, транскодируй чанки параллельно по флоту воркеров, затем сшей. Netflix и YouTube оба описывают пайплайны на этой форме fan-out-затем-merge; именно она позволяет долгой загрузке завершиться за минуты, а не часы, и делает «обрабатывается…» приемлемым async-состоянием, а не блокирующим ожиданием.
▸Почему это работает
Зачем вообще транскодировать в лестницу, а не отдавать исходник? Потому что ширина канала зрителя — не в твоей власти и меняется от секунды к секунде: телефон в поезде падает с LTE на 3G посреди видео. Adaptive bitrate streaming (ABR) существует ровно ради этого: плеер скачивает манифест, затем для каждого короткого сегмента выбирает наивысшую рендицию, какую вытянет текущий измеренный канал, спускаясь до 360p при провале сети и обратно до 1080p при восстановлении. Поскольку каждая рендиция нарезана по тем же границам сегментов, плеер может переключать рендицию между сегментами бесшовно. Отдавал бы ты один фиксированный файл — зритель либо буферизуется (файл велик для его канала), либо смотрит кашу (файл мал для его экрана). Лестница и делает одну загрузку смотрибельной для всех.
Глубокое погружение: путь чтения, CDN и счётчики просмотров
Воспроизведение — задача read-heavy fan-out, и ответ — слоистое кеширование с CDN на краю. Страница просмотра грузит метаданные из (кешированного) сервиса метаданных, затем плеер запрашивает манифест и тянет сегменты с узла CDN ближайшего к зрителю. Первый зритель в регионе вызывает origin pull (CDN тянет сегмент из объектного хранилища и кеширует); каждый последующий зритель в этом регионе обслуживается из edge-кеша. Поскольку популярные видео смотрят миллионы, hit rate CDN для горячего контента приближается к 100%, а объектное хранилище видит лишь длинный хвост и заполнения кеша — ровно так и отдают эксабайты egress, не расплавив origin.
Счётчики просмотров кажутся тривиальными и это классическая ловушка. Вирусное видео может притянуть сотни тысяч просмотров в секунду; если каждый просмотр — синхронный UPDATE videos SET views = views + 1, ты создал одну горячую строку, за которую борется каждый зритель — катастрофа пропускной способности записи. Сеньорский ход — сделать счётчики просмотров eventually consistent: логируй события просмотра в стрим, агрегируй их асинхронно (батчем или потоковой обработкой) и обновляй отображаемое число периодически. Никому не важно, показывает видео 1 000 000 или 1 000 200 просмотров; важно, что воспроизведение мгновенно. Размен точности свежего счёта на масштабируемость записи — верный выбор, и распознавать, какие числа могут быть приблизительными, — сеньорский инстинкт.
▸Частая ошибка
Соблазнительная, но ошибочная оптимизация: «закешируй весь файл видео в тире приложения / в Redis, чтобы чтения были быстры». Объекты видео — гигабайты; твой кеш-тир держит максимум горячий рабочий набор малых объектов, и ты сожжёшь его память на горстке видео, вытеснив всё полезное. Хуже, это возвращает байты на путь приложения, с которого ты так старательно их убирал. Верный слой кеша для крупного медиа — CDN, созданный именно чтобы кешировать большие диапазоны байт рядом с пользователями и отдавать их по HTTP range-запросам — а не твой in-memory кеш, который для метаданных, счётчиков и малых горячих объектов. Согласуй кеш с размером объекта: CDN для сегментов, Redis для килобайтной строки метаданных.
В дизайне загрузки как гигабайты видео должны попасть в хранилище и почему?
Видео виралится на ~200 000 просмотров/сек. Счётчик хранится одной строкой, обновляемой синхронно на каждый просмотр. Что ломается и в чём починка?
Поскольку канал зрителя меняется посреди видео, плеер скачивает манифест и на каждый короткий сегмент выбирает наивысшую рендицию, какую вытянет текущий канал — спускаясь при провале и поднимаясь при восстановлении. Эта техника называется _______ streaming.
Узкие места и компромиссы
Доминирующая стоимость — egress-канал, не хранилище: отдача эксабайт/месяц значит, что экономика CDN правит всей системой, и очень крупные игроки строят свои edge-кеши внутри ISP, чтобы срезать транзитные расходы. Доминирующий риск задержки — буферизация, смягчённая ABR плюс агрессивным edge-кешированием сегментов. Ключевые компромиссы: транскодировать всё заранее (больше хранилища и compute на загрузку, но мгновенное воспроизведение в любой рендиции) против транскодировать по требованию (дешевле хранилище, но холодная рендиция тормозит первого зрителя) — большинство крупных платформ пред-транскодируют популярные форматы и лениво генерят редкие. И хранилище vs compute для лестницы: больше рендиций и кодеков (AV1) дают лучшую компрессию и охват устройств, но множат счёт транскода. Наконец, принимай eventual consistency везде, где человек не отличит — счётчики просмотров, суммы лайков, сигналы рекомендаций — и трать бюджет согласованности лишь там, где это важно (автор должен видеть появление своей загрузки).
- 01Почему байты видео должны обходить серверы приложения и базу и как они попадают в хранилище?
- 02Что производит транскодинг и зачем лестница рендиций плюс манифест?
- 03Почему счётчики просмотров eventually consistent и что диктует стоимость пути чтения?
«Спроектируй YouTube» — на деле две системы, соединённые объектным хранилищем и крошечной БД метаданных. Путь записи тяжёл и асинхронен: клиент грузит байты напрямую в объектное хранилище через presigned URL, API пишет лишь малую строку метаданных и кладёт задачу, а флот воркеров транскодирует исходник — разворачивая его в лестницу рендиций (240p…4K, несколько кодеков), нарезанную на короткие сегменты с HLS/DASH манифестом, делая это параллельным транскодом чанков с последующей сшивкой, так что долгое видео завершается за минуты. Путь чтения быстр: зритель тянет манифест и сегменты с CDN рядом с собой, который origin-pull-ит из объектного хранилища лишь при промахе, давая hit rate близкий к 100% для популярного контента. Два сеньорских хода держат дизайн: оставь сервер приложения на пути управления, никогда на пути данных (байты не касаются ни его, ни базы) и сделай счётчики просмотров eventually consistent (логируй события, агрегируй async), ведь горячий синхронный счётчик расплавился бы под вирусной нагрузкой. Нефункциональные факты — read-heavy ~1000:1, async-записываемость, дёшево-долговечное хранилище, адаптивное воспроизведение — не сноски; они и есть архитектура, а egress-канал, не хранилище, — счёт, доминирующий надо всем. Теперь, когда встретишь задачу на стриминг или доставку медиа, твои первые два вопроса: где реально движутся байты и какие счётчики можно позволить сделать приблизительными?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.