Перейти к основному содержимому

Публичное собеседование по System Design: проектируем видео платформу

· 32 мин. чтения

Сегодня мы разберём публичное собеседование по system design, в котором кандидат Виталий проектирует архитектуру видеопортала типа YouTube под руководством интервьюера Владимира. В ходе сессии были последовательно проработаны функциональные и нефункциональные требования, оценены объёмы хранения и трафика, предложены решения для загрузки, транскодирования, отдачи видео, работы с подписками, рекомендациями, поиском, телеметрией и подсчётом просмотров, а также обсуждены нюансы масштабирования, отказоустойчивости и стоимости облачных сервисов. Интервьюер дал развёрнутый фидбэк, отметив сильные стороны кандидата — хорошее владение верхнеуровневой архитектурой и знание реальных кейсов — и указал на возможности для улучшения, включая более глубокую проработку требований к безопасности, приватности и деталям реализации отдельных компонентов.

Вопрос 1. Расскажите немного о себе и своём опыте.

Таймкод: 00:00:58

Ответ собеседника: Правильный. Кандидат рассказал об образовании по специальности вычислительные машины, комплексы, системы и сети. Начинал карьеру с PHP, затем перешёл на Python, около года назад начал писать на Go. Работал в стартапе, где занимался широким кругом задач: DevOps, администрирование баз данных и облаков, найм, проведение технических собеседований. Также упомянул участие в шоу troubleshooting.

Правильный ответ:

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

  • Образование (если релевантно).
  • Основной стек технологий и языки программирования.
  • Опыт работы: где, сколько, какие задачи решал.
  • Глубину опыта в Go: сколько времени, какие проекты, какие сложные задачи были решены.
  • Soft skills и дополнительные компетенции (DevOps, code review, менторство, проведение собеседований).

Кандидат дал полный и структурированный ответ: описал образование, путь через PHP и Python к Go, широкий круг обязанностей в стартапе и даже участие в troubleshooting-шоу, что говорит о коммуникабельности и умении работать под давлением. Ответ демонстрирует T-shaped профиль — широкий кругозор с углублением в backend-разработку.

Вопрос 2. Какой любимый язык программирования?

Таймкод: 00:02:40

Ответ собеседника: Правильный. Кандидат выбирает язык в зависимости от задач. Сейчас активно пишет на Go, но для других задач использует Python или PHP. Также отметил, что старается не программировать, а писать архитектурную документацию, design docs и ADR.

Правильный ответ:

Это вопрос-лакмусовая бумажка: интервьюер оценивает зрелость и приоритеты кандидата. Идеальный ответ демонстрирует прагматизм и понимание контекста.

Прагматичный подход к выбору языка

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

  • Go — для высоконагруженных микросервисов, CLI-инструментов, инфраструктурного кода. Сильные стороны: быстрая компиляция, встроенная конкурентность (goroutines, channels), статическая типизация, простота развёртывания (один бинарник).
  • Python — для прототипирования, data science, ML, скриптов автоматизации. Сильные стороны: экосистема, скорость разработки.
  • PHP — для веб-приложений, если проект уже на нём (WordPress, Laravel).

Рост в сторону архитектуры

Упоминание кандидатом стремления писать design docs и ADR (Architecture Decision Records) — это показатель роста от разработчика к архитектору/tech lead. Это правильная траектория: код пишется один раз, а архитектурные решения влияют на проект годами.

Пример ADR в формате Markdown:

# ADR-001: Выбор Go для сервиса обработки заказов

## Статус
Принято

## Контекст
Сервис должен обрабатывать 10k RPS с латенцией < 50ms.
Команда имеет опыт работы с Python и Go.

## Решение
Использовать Go из-за:
- Нативной поддержки конкурентности
- Предсказуемого GC
- Низкого потребления памяти

## Последствия
- Потребуется обучение команды Go-идиомам
- Меньше готовых библиотек для доменной области

Вопрос 3. С чего начать проектирование видеопортала типа YouTube? Какие функциональные требования нужно уточнить?

Таймкод: 00:05:28

Ответ собеседника: Правильный. Кандидат предложил начать с верхнеуровневой архитектуры и уточнения функциональных требований. Уточнил, что система больше похожа на YouTube (пользователи загружают видео), а не Netflix. Предложил уточнить наличие социальных функций: лайки, комментарии, рекомендации, подписки, субтитры, видеоаналитика, премодерация. По итогам обсуждения были включены: рекомендации, подписки, субтитры. Лайки и комментарии пока не включаем, премодерацию игнорируем.

Правильный ответ:

Это классический вопрос по проектированию систем (system design), который проверяет способность кандидата структурировать неопределённую задачу.

1. Уточнение функциональных требований

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

  • Кто загружает контент? Только авторы или любой пользователь (UGC vs курируемый контент)?
  • Какие социальные функции нужны? Лайки, комментарии, подписки, рекомендации, шаринг.
  • Нужна ли модерация? Премодерация контента, фильтрация запрещённого контента.
  • Какие форматы контента? Только видео или также стримы (live streaming)?
  • Нужна ли аналитика для авторов? Статистика просмотров, удержание аудитории.
  • Нужны ли субтитры? Автоматические (ASR) или ручные.
  • Монетизация? Реклама, подписки, донаты.

2. Уточнение нефункциональных требований

  • Масштаб: DAU, количество видео, объём хранилища.
  • Доступность: SLA 99.9% или 99.99%?
  • Латенция: допустимое время начала воспроизведения (time-to-first-frame).
  • География: один регион или глобальная аудитория.
  • Бюджет: ограничения по стоимости инфраструктуры.

3. Ограничение скоупа

Кандидат правильно уточнил, что это YouTube-like платформа (UGC), а не Netflix (курируемый контент). Это принципиально разные архитектуры:

  • YouTube: любой пользователь загружает контент → нужна масштабируемая обработка видео, модерация, рекомендации.
  • Netflix: контент загружают профессионалы → фокус на доставке (CDN), персонализации, качестве потока.

4. Приоритизация фич

Кандидат верно предложил обсудить приоритеты. На MVP видеопортала достаточно:

  • Загрузка и обработка видео (транскодирование)
  • Воспроизведение (через CDN)
  • Подписки на каналы
  • Базовые рекомендации
  • Субтитры

Лайки, комментарии и премодерация — это следующие итерации, которые можно добавить после запуска основного функционала.

Вопрос 4. Оцените объём хранилища для видеопортала с 100 тысячами авторов и 100 миллионами зрителей

Таймкод: 00:07:02

Ответ собеседника: Правильный. Кандидат произвёл расчёт: 100 000 авторов × 1 видео в неделю × 10 минут средняя длительность. Для трёх форматов (360p — 50 МБ, 720p — 100 МБ, 1080p — 200 МБ) получается 350 МБ на видео. Итого 35 ТБ в неделю, около 1800 ТБ в год. С учётом репликации (×3) получается 5400 ТБ в год, что составляет примерно 675 дисков по 8 ТБ. Кандидат отметил, что данные нельзя терять, и упомянул дедупликацию.

Правильный ответ:

Оценка хранилища — это базовый навык для проектирования систем. Давайте разберём расчёт детальнее и дополним его.

1. Расчёт объёма исходного контента

Исходные данные:

  • 100 000 авторов
  • 1 видео в неделю на автора
  • Средняя длительность: 10 минут
  • Три качества: 360p, 720p, 1080p

Примерные размеры 10-минутного видео:

КачествоБитрейтРазмер 10 мин
360p0.5 Мбит/с~37 МБ
720p2 Мбит/с~150 МБ
1080p5 Мбит/с~375 МБ

Итого на одно видео: ~562 МБ (с округлением ~550-600 МБ).

В неделю: 100 000 × 600 МБ = 60 ТБ. В год: 60 × 52 = 3 120 ТБ ≈ 3.1 ПБ.

2. Репликация и отказоустойчивость

Для надёжности данные хранятся с репликацией. Типичные стратегии:

  • x3 репликация (HDFS, self-managed): 3.1 ПБ × 3 = 9.3 ПБ.
  • Erasure coding (S3, Cэффективнее): overhead ~1.5x вместо 3x → 3.1 × 1.5 = 4.65 ПБ.

Облачные провайдеры (S3, GCS) используют erasure coding автоматически, поэтому при использовании облачного хранилища overhead меньше.

3. Дополнительные данные

Кроме самих видео нужно хранить:

  • Метаданные: название, описание, теги, миниатюры. ~10 КБ на видео → незначительно.
  • Миниатюры (thumbnails): 3-5 штук на видео, ~50 КБ каждое → ~250 КБ на видео.
  • Субтитры: ~100 КБ на видео.
  • Промежуточные файлы транскодирования: можно удалить после обработки.
  • Аналитика просмотров: может быть объёмнее самих видео при 100M DAU.

4. Оптимизации

  • Дедупликация: актуальна для популярного контента, который перезаливают. Экономия 10-30%.
  • Tiered storage: старые видео с низкими просмотрами перемещать в cold storage (S3 Glacier, дешевле в 5-10 раз).
  • Адаптивный битрейт (ABR): HLS/DASH — сегменты по 2-10 секунды вместо цельных файлов, что экономит трафик при досмотре.

5. Итоговая оценка

СценарийОбъём в год
Без репликации~3 ПБ
С erasure coding (1.5x)~4.5 ПБ
С тройной репликацией~9 ПБ
С tiered storage (50% в cold)~2-4 ПБ эффективно

Кандидат верно оценил порядок величин и упомянул ключевые аспекты: репликацию, дедупликацию и невозможность потери данных. Это демоннимание реальных инженерских компромиссов.

Вопрос 5. Какой объём входящего трафика ожидается при загрузке видео и как спроектировать архитектуру верхнего уровня для видеопортала?

Таймкод: 00:11:35

Ответ собеседника: Правильный. Кандидат рассчитал входящий трафик: 35 ТБ в неделю — это примерно 50 МБ/секунду (с запасом до 100 МБ/с). Предложил архитектуру: DNS с GeoDNS для маршрутизации к ближайшему дата-центру, балансировщики нагрузки, API Gateway для формирования подписанных ссылок на загрузку, загрузка видео чанками в S3-хранилище (сырые и обработанные — разные бакеты с разными политиками хранения и безопасности), шина событий для уведомления Video Encoder о новых файлах. Video Encoder — отдельный кластер с GPU для конвертации видео в разные форматы. Готовые файлы складываются в отдельный S3-кластер. Видео хранится чанками для удобства отдачи на просмотр и перемотки. Video API сервис связывает метаданные с файлами, Video Metadata хранилище хранит метаданные видео.

Правильный ответ:

1. Расчёт входящего трафика

35 ТБ в неделю = 35 × 10^12 Б / (7 × 24 × 3600) ≈ 57.9 МБ/с.

С учётом неравномерности (пиковые часы в 3-5 раза выше среднего) и запаса на рост: ~200-300 МБ/с — реалистичная пиковая нагрузка.

2. Архитектура верхнего уровня

Кандидат описал отличную архитектуру. Дополним деталями:

[Пользователь]


[GeoDNS / Anycast] ─── маршрутизация к ближайшему DC


[CDN / Load Balancer] ─── L7 балансировка (nginx/Envoy)

├──▶ [API Gateway] ─── аутентификация, rate limiting, маршрутизация
│ │
│ ├──▶ [Upload Service] ─── генерация signed URLs
│ │ │
│ │ ▼
│ │ [S3 Raw Bucket] ─── исходные файлы
│ │ │
│ │ ▼
│ │ [Message Queue / Kafka] ─── событие "новое видео"
│ │ │
│ │ ▼
│ │ [Video Encoder Cluster] ─── GPU-ноды, транскодирование
│ │ │
│ │ ▼
│ │ [S3 Processed Bucket] ─── HLS/DASH сегменты
│ │
│ ├──▶ [Video API Service] ─── метаданные + ссылки на файлы
│ │ │
│ │ ▼
│ │ [Metadata DB] ─── PostgreSQL / MongoDB
│ │
│ └──▶ [Recommendation Service] ─── рекомендации


[Playback CDN] ─── отдача видео зрителям

3. Ключевые архитектурные решения

Подписанные URL для загрузки (Presigned URLs)

Вместо проксирования файлов через сервер — клиент загружает напрямую в S3 по подписанной ссылке. Это снимает нагрузку с бэкенда.

// Пример генерации presigned URL на Go
import (
"context"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)

func GenerateUploadURL(ctx context.Context, client *s3.PresignClient, bucket, key string) (string, error) {
req, err := client.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(15*time.Minute))
if err != nil {
return "", err
}
return req.URL, nil
}

Разделение бакетов

  • raw-uploads — исходные файлы, доступ только для Upload Service и Encoder. Lifecycle policy: удалить после успешного транскодирования.
  • processed-videos — готовые HLS/DASH сегменты, доступ через CDN. Политика хранения: долгосрочная.

Чанковая загрузка

Большие файлы загружаются частями (multipart upload). Это обеспечивает:

  • Возможность догрузки при обрыве соединения.
  • Параллельную загрузку частей.
  • Поддержку файлов больше 5 ГБ.

Шина событий

Kafka или SQS для асинхронной обработки. При загрузке файла публикуется событие:

{
"event_type": "video.uploaded",
"video_id": "abc-123",
"raw_s3_key": "raw/abc-123/original.mp4",
"uploaded_at": "2025-01-15T10:30:00Z",
"author_id": "user-456"
}

Video Encoder подписан на эту очередь и обрабатывает видео асинхронно.

4. Отдача видео (Playback)

Для просмотра используется адаптивный стриминг (HLS/DASH):

  • Видео нарезано на сегменты по 2-10 секунд.
  • Для каждого сегмента есть несколько битрейтов.
  • Клиент автоматически переключает качество в зависимости от скорости соединения.
  • CDN (CloudFront, Cloudflare) кэширует сегменты на edge-серверах.

Кандидат продемонстрировал отличное понимание архитектуры: от расчёта трафика до конкретных сервисов и их взаимодействия. Упоминание разных политик безопасности для бакетов и чанкового хранения — признак опыта проектирования production-систем.

Вопрос 6. Как обеспечить быстрое начало проигрывания видео (в течение 3 секунд) и эффективную перемотку для пользователей по всему миру?

Таймкод: 00:27:42

Ответ собеседника: Правильный. Кандидат предложил: 1) GeoDNS для определения ближайшего сервера. 2) Локальные кэши (аналог Google Global Cache / Netflix Open Connect) в каждой локации для популярного контента. 3) Сервис телеметрии, собирающий данные о том, кто откуда смотрит какие видео. 4) Cache Service, который на основе телеметрии пушит видео в локальные кэши нужных дата-центров. 5) HLS (HTTP Live Streaming) — стандарт стриминга, где клиент получает маленький файл-дескриптор со ссылками на части видео в разных качествах, сам выбирает качество в зависимости от скорости интернета. Это позволяет начать проигрывание после загрузки первых секунд. Хранение видео чанками также обеспечивает эффективную перемотку.

Правильный ответ:

Кандидат дал отличный ответ, охватив все ключевые аспекты. Дополним техническими деталями.

1. Проблема: латенция и время до первого кадра (Time to First Byte)

При запросе видео пользователь ожидает мгновенного воспроизведения. Основные факторы задержки:

  • Сетевые задержки: RTT между пользователем и сервером. Из Москвы до Франкфурта ~30мс, до Сан-Паулу ~200мс.
  • Время поиска данных: если видео не в кэше, нужно читать с диска или из удалённого хранилища.
  • Время обработки: генерация манифеста, авторизация, выбор CDN-ноды.

2. Решения для быстрого старта

CDN с edge-кэшированием

Content Delivery Network размещает копии контента на серверах, географически близких к пользователям. Популярное видео кэшируется автоматически при первых запросах.

Netflix идёт дальше — размещает свои серверы Open Connect прямо у интернет-провайдеров (ISP), что минимизирует сетевой путь.

HLS (HTTP Live Streaming)

Ключевая технология для адаптивного стриминга:

# Мастер-манифест (master.m3u8)
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2400000,RESOLUTION=1280x720
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/index.m3u8

# Медиа-манифест (720p/index.m3u8)
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXTINF:4.0,
segment_000.ts
#EXTINF:4.0,
segment_001.ts
#EXTINF:4.0,
segment_002.ts

Клиент сначала загружает маленький манифест (несколько КБ), затем начинает загрузку первого сегмента. При длительности сегмента 2-4 секунды воспроизведение начинается практически мгновенно.

Адаптивный битрейт (ABR)

Клиент мониторит скорость загрузки и переключается между качествами:

// Упрощённая логика ABR-алгоритма
type ABRController struct {
bandwidthEstimator *BandwidthEstimator
availableQualities []Quality
}

type Quality struct {
Name string
Bitrate int // bits per second
Resolution string
}

func (c *ABRController) SelectQuality() Quality {
estimatedBps := c.bandwidthEstimator.Estimate()

// Выбираем максимальное качество, которое успеем загрузить
// с запасом 20% для стабильности
targetBps := int(float64(estimatedBps) * 0.8)

selected := c.availableQualities[0]
for _, q := range c.availableQualities {
if q.Bitrate <= targetBps {
selected = q
} else {
break
}
}
return selected
}

3. Эффективная перемотка

При перемотке пользователь ожидает мгновенного отклика. Чанковое хранение решает эту проблему:

  • Видео хранится как набор сегментов (файлов по 2-10 секунд).
  • При перемотке на 5:30 клиент запрашивает только сегмент, содержащий эту точку.
  • Не нужно загружать весь файл или искать по байтовому смещению.

HTTP Range Requests также поддерживаются для точного доступа:

GET /videos/abc-123/720p/segment_068.ts HTTP/1.1
Host: cdn.example.com

4. Предиктивное кэширование (Prefetch)

Сервис телеметрии анализирует паттерны просмотров:

-- Анализ популярности контента по регионам
SELECT
video_id,
region,
COUNT(*) as view_count,
AVG(watch_duration_sec) as avg_watch_time
FROM video_views
WHERE viewed_at > NOW() - INTERVAL '7 days'
GROUP BY video_id, region
ORDER BY view_count DESC
LIMIT 1000;

На основе этих данных Cache Service загружает популярный контент в edge-кэши до того, как пользователи его запросят. Это особенно важно для новых серий популярных шоу — их можно заранее распределить по всем регионам.

5. Архитектура кэширования

[Пользователь в Москве]


[Edge PoP Москва] ─── кэш L1, популярный контент
│ miss

[Regional Cache Франкфурт] ─── кэш L2, региональный контент
│ miss

[S3 Origin] ─── исходное хранилище

Кандидат верно упомянул аналог Google Global Cache и Netflix Open Connect — это реальные паттерны, используемые в индустрии. Комбинация GeoDNS, CDN, HLS и предиктивного кэширования — это стандартный подход для глобальных видеоплатформ.

Вопрос 7. Как реализовать систему подписок и ленту подписок, учитывая проблему блогеров с миллионами подписчиков?

Таймкод: 00:36:50

Ответ собеседника: Правильный. Кандидат описал архитектуру: User Service — хранение данных пользователей; Auth Service — отдельный сервис авторизации с кэшированием, чтобы не перегружать User Service; Subscription Service — хранение подписок. Для ленты подписок предложил гибридный подход по аналогии с Twitter: при публикации нового видео Subscription Worker добавляет запись в ленту подписчиков. Для блогеров-миллионников — лента не формируется полностью заранее, а популярные блогеры подтягиваются на лету при запросе ленты. Старые записи из ленты можно очищать на основе аналитики поведения пользователей (обычно не листают дальше 10 страниц). Событие о новом видео публикуется в шину событий, от которой подписывается Subscription Worker.

Правильный ответ:

Это один из классических вопросов system design, который проверяет понимание компромиссов между записью и чтением. Кандидат отлично описал гибридный подход. Разберём детальнее.

1. Два базовых подхода к формированию ленты

Fan-out on Write (Push модель)

При публикации нового видео мы сразу добавляем его в ленты всех подписчиков.

  • Плюсы: быстрое чтение ленты — просто SELECT из предзаполненной таблицы.
  • Минусы: медленная запись для популярных авторов. Автор с 10M подписчиков = 10M записей за одно видео.

Fan-out on Read (Pull модель)

При запросе ленты мы собираем свежие видео от всех подписок пользователя.

  • Плюсы: быстрая публикация — записали видео и забыли.
  • Минусы: медленное чтение — нужно агрегировать из множества источников.

2. Гибридный подход (правильный ответ)

Кандидат верно предложил комбинированную стратегию:

  • Обычные авторы (< 100K подписчиков): Fan-out on Write — при публикации пушим в ленты подписчиков.
  • Популярные авторы (> 100K подписчиков): Fan-out on Read — подтягиваем на лету при запросе ленты.

Порог в 100K — эмпирический, зависит от инфраструктуры. Twitter использует ~1M как порог.

3. Архитектура

// Событие о новом видео
type VideoPublishedEvent struct {
VideoID string `json:"video_id"`
AuthorID string `json:"author_id"`
Title string `json:"title"`
PublishedAt time.Time `json:"published_at"`
}

// Subscription Worker — обработчик событий
type SubscriptionWorker struct {
subscriptionRepo SubscriptionRepository
feedRepo FeedRepository
authorService AuthorService
threshold int // порог для fan-out on write
}

func (w *SubscriptionWorker) HandleVideoPublished(ctx context.Context, event VideoPublishedEvent) error {
followers, err := w.subscriptionRepo.GetFollowersCount(ctx, event.AuthorID)
if err != nil {
return err
}

if followers < w.threshold {
// Fan-out on Write для обычных авторов
return w.pushToFollowersFeeds(ctx, event)
}

// Для популярных авторов — ничего не делаем заранее
// Их видео будут подтянуты при чтении ленты
return nil
}

func (w *SubscriptionWorker) pushToFollowersFeeds(ctx context.Context, event VideoPublishedEvent) error {
// Пагинация для обработки больших списков
var cursor string
for {
followers, nextCursor, err := w.subscriptionRepo.GetFollowers(ctx, event.AuthorID, cursor, 1000)
if err != nil {
return err
}

feedItems := make([]FeedItem, len(followers))
for i, follower := range followers {
feedItems[i] = FeedItem{
UserID: follower.UserID,
VideoID: event.VideoID,
AuthorID: event.AuthorID,
CreatedAt: event.PublishedAt,
}
}

if err := w.feedRepo.BatchInsert(ctx, feedItems); err != nil {
return err
}

if nextCursor == "" {
break
}
cursor = nextCursor
}
return nil
}

4. Чтение ленты

func (s *FeedService) GetFeed(ctx context.Context, userID string, limit int, offset int) ([]FeedItem, error) {
// 1. Получаем предзаполненную ленту (от обычных авторов)
cachedFeed, err := s.feedRepo.GetFeed(ctx, userID, limit, offset)
if err != nil {
return nil, err
}

// 2. Получаем список популярных авторов, на которых подписан пользователь
celebrityAuthors, err := s.subscriptionRepo.GetCelebritySubscriptions(ctx, userID)
if err != nil {
return nil, err
}

// 3. Подтягиваем свежие видео от популярных авторов
var celebrityVideos []FeedItem
for _, author := range celebrityAuthors {
videos, err := s.videoRepo.GetRecentByAuthor(ctx, author.AuthorID, 5)
if err != nil {
return nil, err
}
celebrityVideos = append(celebrityVideos, videos...)
}

// 4. Мержим и сортируем по времени
return mergeAndSort(cachedFeed, celebrityVideos, limit, offset), nil
}

5. Хранение данных

-- Таблица подписок
CREATE TABLE subscriptions (
subscriber_id BIGINT NOT NULL,
author_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (subscriber_id, author_id)
);

CREATE INDEX idx_subscriptions_author ON subscriptions(author_id);

-- Таблица ленты (предзаполненная)
CREATE TABLE feed_items (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
video_id BIGINT NOT NULL,
author_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL,
is_read BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_feed_user_created ON feed_items(user_id, created_at DESC);

-- Партиционирование по времени для упрощения очистки
-- PARTITION BY RANGE (created_at)

6. Очистка старых записей

Как верно отметил кандидат, пользователи редко листают ленту глубже 10 страниц. Можно настроить TTL:

-- Удаляем записи старше 30 дней или глубже 500 позиции
DELETE FROM feed_items
WHERE user_id = $1
AND id NOT IN (
SELECT id FROM feed_items
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 500
);

7. Проблема «celebrity post»

Когда популярный автор публикует видео, все его подписчики могут запросить ленту одновременно. Решения:

  • Кэширование последних видео автора в Redis с TTL 5-10 минут.
  • Rate limiting на запросы к ленте.
  • Stale-while-revalidate — отдаём кэшированную ленту и обновляем в фоне.

Кандидат продемонстрировал глубокое понимание проблемы и знание реальных паттернов (упоминание Twitter). Гибридный подход с пороговым значением — это именно то, как это работает в production у крупных компаний.

Вопрос 8. Как реализовать рекомендательную систему, поиск видео и подсчёт просмотров?

Таймкод: 00:40:55

Ответ собеседника: Правильный. Кандидат предложил: Рекомендации — отдельный сервис, который асинхронно получает данные из сервиса телеметрии о просмотрах пользователей и на их основе строит рекомендации (ML-модели, коллаборативная фильтрация и т.д.). Телеметрия и рекомендации — разделённые ответственности. Рекомендательный пайплайн должен собирать данные из разных источников, реплицировать их в data warehouse и в оффлайне формировать рекомендации. Поиск — через Elasticsearch, куда загружаются метаданные видео (title, description), а Video API сервис обращается к нему при поисковых запросах. Подсчёт просмотров — кандидат упомянул проблему с постоянным инкрементом счётчика (как у YouTube с числом 301), проблему горячих строк в БД при частых обновлениях и предложил использовать специализированные решения вместо реляционных БД. Также отметил необходимость мониторинга всех компонентов с бордами метрик.

Правильный ответ:

Кандидат отлично разложил задачу на три компонента. Дополним каждую часть техническими деталями.

1. Рекомендательная система

Архитектура рекомендательного пайплайна

[События просмотров] ──▶ [Kafka] ──▶ [Stream Processor (Flink/Spark)]


[Data Warehouse (ClickHouse/BigQuery)]


[ML Training Pipeline]


[Model Registry]


[Recommendation Service]

Два типа рекомендаций:

  • Batch-рекомендации (оффлайн): пересчитываются раз в несколько часов. Хранятся в Redis/кеше. Подходят для главной страницы.
  • Real-time рекомендации (онлайн): учитывают текущую сессию пользователя. Например, «смотрите также» на странице видео.

Коллаборативная фильтрация — упрощённый принцип:

Пользователь A смотрил: [v1, v2, v3]
Пользователь B смотрел: [v1, v2, v3, v4]
→ Рекомендуем A видео v4

Content-based фильтрация:

Рекомендуем видео, похожие по тегам, категории, автору на то, что пользователь уже смотрел.

type RecommendationService struct {
cache *redis.Client
videoRepo VideoRepository
mlClient MLServiceClient
}

func (s *RecommendationService) GetRecommendations(ctx context.Context, userID string, limit int) ([]Video, error) {
// 1. Пробуем получить из кеша (batch-рекомендации)
cached, err := s.cache.Get(ctx, fmt.Sprintf("recs:batch:%s", userID)).Result()
if err == nil {
return deserializeVideos(cached), nil
}

// 2. Если нет в кеше — генерируем на лету
return s.mlClient.GetRecommendations(ctx, userID, limit)
}

2. Поиск видео

Elasticsearch — стандартное решение для полнотекстового поиска.

// Маппинга индекса в Elasticsearch
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": { "type": "keyword" },
"russian": { "type": "text", "analyzer": "russian" }
}
},
"description": { "type": "text", "analyzer": "russian" },
"tags": { "type": "keyword" },
"author_id": { "type": "long" },
"author_name": { "type": "text" },
"category": { "type": "keyword" },
"published_at": { "type": "date" },
"view_count": { "type": "long" },
"duration_sec": { "type": "integer" }
}
}
}
// Поисковый запрос через Elasticsearch
func (s *SearchService) Search(ctx context.Context, query string, filters SearchFilters) (*SearchResult, error) {
boolQuery := elastic.NewBoolQuery()

// Полнотекстовый поиск по title и description
multiMatch := elastic.NewMultiMatchQuery(query, "title^3", "description", "tags^2", "author_name")
boolQuery.Must(multiMatch)

// Фильтры
if filters.Category != "" {
boolQuery.Filter(elastic.NewTermQuery("category", filters.Category))
}
if filters.MinDuration > 0 {
boolQuery.Filter(elastic.NewRangeQuery("duration_sec").Gte(filters.MinDuration))
}

// Сортировка: релевантность + популярность
scoreScript := elastic.NewScriptScoreQuery(boolQuery,
elastic.NewScript("doc['view_count'].value * 0.0001 + _score"))

result, err := s.esClient.Search().
Index("videos").
Query(scoreScript).
From(filters.Offset).
Size(filters.Limit).
Do(ctx)

return parseSearchResult(result), err
}

Проблема автодополнения (autocomplete):

Для поисковых подсказок используют completion suggester в Elasticsearch или отдельный сервис на основе trie-структуры.

3. Подсчёт просмотров

Проблема горячих строк (hot rows)

При 100M DAU и популярном видео с миллионом просмотров в час — это тысячи UPDATE в секунду на одну строку. В реляционной БД это приводит к lock contention.

-- Плохо: горячая строка
UPDATE videos SET view_count = view_count + 1 WHERE id = 12345;
-- Тысячи таких запросов в секунду → row-level locks → деградация

Решение: двухуровневый счётчик

type ViewCounter struct {
redis *redis.Client
db *sql.DB
kafka sarama.AsyncProducer
}

// Уровень 1: Запись в Redis (быстрая, in-memory)
func (c *ViewCounter) IncrementView(ctx context.Context, videoID string) error {
key := fmt.Sprintf("views:%s", videoID)

// Инкремент в Redis — атомарная операция, ~1ms
_, err := c.redis.Incr(ctx, key).Result()
if err != nil {
return err
}

// Публикуем событие для аналитики
c.kafka.Input() <- &sarama.ProducerMessage{
Topic: "video-views",
Value: sarama.StringEncoder(fmt.Sprintf(`{"video_id":"%s","ts":%d}`, videoID, time.Now().Unix())),
}

return nil
}

// Уровень 2: Периодическая синхронизация с БД
func (c *ViewCounter) SyncToDB(ctx context.Context) error {
// Каждые 5 минут сбрасываем счётчики из Redis в PostgreSQL
iter := c.redis.Scan(ctx, 0, "views:*", 1000).Iterator()

for iter.Next(ctx) {
key := iter.Val()
videoID := strings.TrimPrefix(key, "views:")

count, err := c.redis.Get(ctx, key).Int64()
if err != nil {
continue
}

// Атомарный инкремент в БД — не перезапись!
_, err = c.db.ExecContext(ctx,
"UPDATE videos SET view_count = view_count + $1 WHERE id = $2",
count, videoID)
if err != nil {
log.Printf("failed to sync views for %s: %v", videoID, err)
}
}

return iter.Err()
}

Проблема YouTube с числом 301:

YouTube некоторое время замораживал счётчик на 301, пока проверял просмотры на ботов. Это часть anti-fraud системы:

func (s *ViewService) RecordView(ctx context.Context, videoID string, userID string) error {
// 1. Проверяем, не смотрел ли этот пользователь недавно
recent, err := s.redis.Get(ctx, fmt.Sprintf("viewed:%s:%s", userID, videoID)).Bool()
if recent {
return nil // Дубликат — игнорируем
}

// 2. Проверяем на бота (rate limiting, fingerprint)
if s.isSuspicious(userID) {
s.quarantineView(videoID) // В карантин для проверки
return nil
}

// 3. Записываем валидный просмотр
s.redis.Set(ctx, fmt.Sprintf("viewed:%s:%s", userID, videoID), true, 24*time.Hour)
return s.viewCounter.IncrementView(ctx, videoID)
}

4. Мониторинг

Кандидат верно упомянул необходимость мониторинга. Ключевые метрики:

  • Latency: p50, p95, p99 для всех API endpoints.
  • Throughput: RPS на каждый сервис.
  • Error rate: процент 5xx ошибок.
  • Cache hit ratio: для Redis/CDN.
  • Queue depth: длина очередей Kafka.
  • Business metrics: DAU, среднее время просмотра, конверсия рекомендаций.

Кандидат продемонстрировал системное мышление: разделение ответственности между сервисами, понимание проблемы горячих строк, знание реальных паттернов (YouTube 301, коллаборативная фильтрация). Это уровень зрелого инженера, который не только проектирует, но и думает о production-ограничениях.

Вопрос 9. Какие вопросы можно углубить при проектировании видеопортала на более низком уровне (выбор хранилищ, деплой энкодера, склейка чанков, стоимость облачных сервисов)?

Таймкод: 00:54:12

Ответ собеседника: Правильный. Кандидат ответил на вопрос зрителя о том, что можно углубить. По склейке чанков: чанки именуются по индексам (1, 2, 3...), что позволяет Video Encoder склеивать их в правильном порядке. По выбору хранилищ: для каждого сервиса выбор хранилища — отдельная большая тема (счётчики просмотров неудобно хранить в реляционных БД, для подписок можно разделить горячие и холодные данные на разные хранилища). По деплою энкодера: варианты — виртуальные машины с установленным энкодером, serverless-функции (Lambda), или Kubernetes-кластер с Docker-контейнерами. По стоимости: кандидат пояснил, что использование Amazon S3 для хранения и отдачи огромных объёмов данных будет очень дорогим (сотни миллионов долларов за трафик), поэтому для подобных масштабов целесообразно использовать локальные хранилища. Также пояснил, почему энкодер ставится после балансировщика, а не перед ним — нужен буфер для временного хранения файлов, чтобы энкодер обрабатывал их постепенно, а пользователь сразу после загрузки мог уйти (аналогично YouTube).

Правильный ответ:

Кандидат отлично раскрыл тему. Дополним каждую подтему техническими деталями.

1. Выбор хранилищ (Polyglot Persistence)

Разные данные — разные хранилища. Это принцип polyglot persistence:

ДанныеХранилищеПочему
Метаданные видеоPostgreSQLСтруктурированные данные, сложные запросы, ACID
Счётчики просмотровRedis + ClickHouseБыстрый инкремент + аналитика
ПодпискиPostgreSQL + RedisРеляционные связи + кеш горячих данных
Лента подписокRedis (Sorted Sets)Быстрая вставка и чтение по времени
ПоискElasticsearchПолнотекстовый поиск, фасеты
ВидеофайлыS3 / объектное хранилищеМасштабируемость, durability
Аналитика просмотровClickHouse / BigQueryКолоночное хранение, быстрая агрегация
Сессии пользователейRedisTTL, быстрый доступ
// Пример: хранение подписок с кешированием
type SubscriptionService struct {
db *sql.DB
cache *redis.Client
}

func (s *SubscriptionService) IsFollowing(ctx context.Context, subscriberID, authorID int64) (bool, error) {
// 1. Проверяем кеш
key := fmt.Sprintf("sub:%d:%d", subscriberID, authorID)
cached, err := s.cache.Get(ctx, key).Bool()
if err == nil {
return cached, nil
}

// 2. Если нет в кеше — идём в БД
var exists bool
err = s.db.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM subscriptions WHERE subscriber_id = $1 AND author_id = $2)",
subscriberID, authorID).Scan(&exists)
if err != nil {
return false, err
}

// 3. Записываем в кеш
s.cache.Set(ctx, key, exists, 5*time.Minute)

return exists, nil
}

2. Деплой Video Encoder

Вариант A: Kubernetes с GPU-нодами

apiVersion: batch/v1
kind: Job
metadata:
name: video-encode-{{.VideoID}}
spec:
template:
spec:
containers:
- name: encoder
image: video-encoder:latest
resources:
limits:
nvidia.com/gpu: 1
memory: "8Gi"
cpu: "4"
env:
- name: INPUT_S3_KEY
value: "{{.RawS3Key}}"
- name: OUTPUT_S3_KEY
value: "{{.ProcessedS3Key}}"
- name: PRESETS
value: "360p,720p,1080p"
nodeSelector:
accelerator: nvidia-tesla-t4
restartPolicy: OnFailure

Вариант B: Очередь задач с автоскейлингом

type EncoderWorker struct {
queue sarama.ConsumerGroup
encoder *FFmpegEncoder
metrics *prometheus.CounterVec
}

func (w *EncoderWorker) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
var task EncodeTask
if err := json.Unmarshal(msg.Value, &task); err != nil {
log.Printf("failed to unmarshal task: %v", err)
session.MarkMessage(msg, "")
continue
}

start := time.Now()
err := w.encoder.Encode(task)
duration := time.Since(start)

w.metrics.WithLabelValues(task.Preset, strconv.FormatBool(err != nil)).Observe(duration.Seconds())

if err != nil {
// Retry с exponential backoff
w.retryTask(task)
continue
}

session.MarkMessage(msg, "")
}
return nil
}

Вариант C: Serverless (AWS Lambda + MediaConvert)

Подходит для небольших объёмов. Lambda запускает AWS MediaConvert job. Ограничение: 15 минут выполнения, нет GPU.

3. Склейка чанков и транскодирование

При multipart upload клиент загружает части параллельно. Сервер должен собрать их в правильном порядке:

type MultipartUploader struct {
s3Client *s3.Client
}

func (u *MultipartUploader) CompleteMultipartUpload(ctx context.Context, bucket, key string, uploadID string, parts []CompletedPart) error {
// Части уже отсортированы по PartNumber клиентом
_, err := u.s3Client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: aws.String(uploadID),
MultipartUpload: &types.CompletedMultipartUpload{
Parts: parts,
},
})
return err
}

Транскодирование через FFmpeg:

type FFmpegEncoder struct {
presets []Preset
}

type Preset struct {
Name string
Resolution string
Bitrate string
AudioRate string
}

func (e *FFmpegEncoder) EncodeHLSSegments(ctx context.Context, inputPath, outputDir string) error {
cmd := exec.CommandContext(ctx, "ffmpeg",
"-i", inputPath,
"-filter_complex",
"[0:v]split=3[v1][v2][v3];"+
"[v1]scale=w=640:h=360[v1out];"+
"[v2]scale=w=1280:h=720[v2out];"+
"[v3]scale=w=1920:h=1080[v3out]",
// 360p
"-map", "[v1out]", "-c:v:0", "libx264", "-b:v:0", "800k",
"-map", "0:a", "-c:a:0", "aac", "-b:a:0", "96k",
// 720p
"-map", "[v2out]", "-c:v:1", "libx264", "-b:v:1", "2400k",
"-map", "0:a", "-c:a:1", "aac", "-b:a:1", "128k",
// 1080p
"-map", "[v3out]", "-c:v:2", "libx264", "-b:v:2", "5000k",
"-map", "0:a", "-c:a:2", "aac", "-b:a:2", "192k",
// HLS настройки
"-f", "hls",
"-hls_time", "4",
"-hls_playlist_type", "vod",
"-hls_flags", "independent_segments",
"-hls_segment_type", "mpegts",
"-hls_segment_filename", outputDir+"/%v/segment_%03d.ts",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1 v:2,a:2",
outputDir+"/%v/index.m3u8",
)

output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("ffmpeg failed: %w, output: %s", err, string(output))
}
return nil
}

4. Стоимость облачных сервисов

Кандидат абсолютно прав: на масштабе YouTube облака становятся нерентабельными.

Примерная стоимость на AWS для 100M DAU:

КомпонентМесячная стоимость
S3 Storage (10 PB)~$230,000
S3 Data Transfer Out (1 PB/день)~$9,000,000
CloudFront CDN~$2,000,000
EC2 (энкодер, 100 GPU-инстансов)~$300,000
RDS PostgreSQL~$50,000
ElastiCache Redis~$30,000
MSK (Kafka)~$40,000
Итого~$11,650,000/мес

Для сравнения: YouTube использует собственную инфраструктуру (Google data centers), что в разы дешевле на таком масштабе.

Когда облако оправдано:

  • Стартапы и средние проекты (до ~1M DAU).
  • Непредсказуемый рост — облако позволяет масштабироваться без CAPEX.
  • Редкие нагрузки (энкодер нужен только при загрузке видео).

Когда своя инфраструктура лучше:

  • Стабильно высокая нагрузка (> 10M DAU).
  • Предсказуемый рост.
  • Есть команда для управления железом.

5. Буфер перед энкодером

Кандидат верно отметил необходимость буфера. Архитектура:

[Клиент] ──▶ [Load Balancer] ──▶ [Upload Service]


[S3 Raw Bucket] ◀── буфер


[Kafka] ◀── событие "загружено"


[Encoder Worker] ──▶ [S3 Processed]

Пользователь загрузил файл → получил подтверждение → ушёл. Энкодер обрабатывает асинхронно. Видео станет доступно через несколько минут (как на YouTube — «обработка»).

Кандидат продемонстрировал отличное понимание компромиссов между облаком и on-premise, знание реальных затрат и архитектурных паттернов. Упоминание конкретных технологий (Kubernetes, FFmpeg, multipart upload) показывает практический опыт.

Вопрос 10. Как решить проблему доступа автора к видео после смены локации и как обеспечить репликацию метаданных? Также как гарантировать начало проигрывания в 3 секунды если видео ещё не реплицировано в нужный регион?

Таймкод: 01:01:30

Ответ собеседника: Правильный. Кандидат ответил на вопрос зрителя. По доступу автора после смены локации: метаданные видео должны реплицироваться во все регионы (или быть доступны глобально), чтобы автор из любой локации мог получить доступ к своим видео для пост-обработки (выбор превьюшки, настройки приватности и т.д.). Альтерно — API может перенаправлять запросы в нужный регион. По гарантии 3 секунд проигрывания: кандидат пояснил, что требования вида «начало проигрывания за 3 секунды» обычно означают не 100% гарантию, а определённый перцентиль (например, 99.9%). Если видео ещё не реплицировано в нужный регион, сначала происходит репликация, и только потом отдача. Это требует дополнительного времени и аналитических недель работы для определения оптимального алгоритма предсказания, какие видео куда реплицировать. YouTube прошёл через множество итераций подобных решений.

Правильный ответ:

Кандидат дал зрелый ответ, затронув ключевые аспекты: SLA как перцентиль, а не абсолют, и необходимость итеративного подхода к оптимизации. Дополним техническими деталями.

1. Репликация метаданных

Проблема: Автор загрузил видео из Москвы (метаданные записаны в EU-регионе), затем перелетел в Нью-Йорк и хочет отредактировать описание.

Решение A: Глобальная база данных

Использовать распределённую БД с автоматической репликацией:

  • CockroachDB / YugabyteDB — PostgreSQL-совместимые распределённые БД с multi-region поддержкой.
  • Google Cloud Spanner — глобально распределённая БД с strong consistency.
  • Aurora Global Database (AWS) — до 5 регионов чтения, репликация < 1 сек.
-- CockroachDB: настройка locality для таблицы
CREATE TABLE video_metadata (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id BIGINT NOT NULL,
title STRING NOT NULL,
description STRING,
s3_key STRING NOT NULL,
status STRING DEFAULT 'processing',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
) LOCALITY GLOBAL;

-- Для данных, которые часто читаются в определённом регионе:
CREATE TABLE video_views_by_region (
video_id UUID,
region STRING,
view_count BIGINT,
PRIMARY KEY (video_id, region)
) LOCALITY REGIONAL BY ROW;

Решение B: Асинхронная репликация через CDC

Change Data Capture — при изменении записи в мастер-БД событие публикуется в шину и реплики обновляются:

[Master DB (EU)] ──CDC──▶ [Kafka] ──▶ [Replica DB (US)]
──▶ [Replica DB (Asia)]
// CDC Consumer для репликации метаданных
type MetadataReplicator struct {
consumers map[string]*sql.DB // region -> DB connection
}

func (r *MetadataReplicator) HandleChange(ctx context.Context, change CDCEvent) error {
// Отправляем изменение во все региональные реплики
var wg sync.WaitGroup
errChan := make(chan error, len(r.consumers))

for region, db := range r.consumers {
wg.Add(1)
go func(r string, db *sql.DB) {
defer wg.Done()

switch change.Operation {
case "INSERT", "UPDATE":
_, err := db.ExecContext(ctx,
`INSERT INTO video_metadata (id, author_id, title, description, s3_key, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
description = EXCLUDED.description,
status = EXCLUDED.status,
updated_at = EXCLUDED.updated_at`,
change.Data.ID, change.Data.AuthorID, change.Data.Title,
change.Data.Description, change.Data.S3Key, change.Data.Status,
change.Data.CreatedAt, change.Data.UpdatedAt)
if err != nil {
errChan <- fmt.Errorf("region %s: %w", r, err)
}
}
}(region, db)
}

wg.Wait()
close(errChan)

// Собираем ошибки
var errs []error
for err := range errChan {
errs = append(errs, err)
}

if len(errs) > 0 {
return fmt.Errorf("replication errors: %v", errs)
}
return nil
}

Решение C: API с маршрутизацией по регионам

Если метаданные хранятся регионально, API Gateway маршрутизирует запрос:

type MetadataService struct {
regionalClients map[string]MetadataClient // region -> client
}

func (s *MetadataService) GetVideoMetadata(ctx context.Context, videoID string, userRegion string) (*VideoMetadata, error) {
// Определяем, в каком регионе находятся метаданные видео
primaryRegion, err := s.getVideoPrimaryRegion(ctx, videoID)
if err != nil {
return nil, err
}

// Если пользователь в том же регионе — читаем локально
if primaryRegion == userRegion {
return s.regionalClients[userRegion].GetMetadata(ctx, videoID)
}

// Если в другом регионе — пробуем сначала локальную реплику (может быть stale)
metadata, err := s.regionalClients[userRegion].GetMetadata(ctx, videoID)
if err == nil && !s.isStale(metadata) {
return metadata, nil
}

// Если локальная копия устарела или отсутствует — идём в мастер
return s.regionalClients[primaryRegion].GetMetadata(ctx, videoID)
}

2. SLA: перцентили, а не абсолюты

Кандидат абсолютно прав: «3 секунды» — это не 100% гарантия. В реальности:

ПерцентильЦелевое время
p50 (медиана)< 500ms
p95< 2s
p99< 3s
p99.9< 5s
// Мониторинг перцентилей с помощью Prometheus
var playbackLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "playback_start_latency_seconds",
Help: "Time from play request to first frame",
Buckets: prometheus.ExponentialBuckets(0.01, 2, 12), // 10ms to 40s
},
[]string{"region", "quality"},
)

func (s *PlaybackService) StartPlayback(ctx context.Context, videoID string, quality string) error {
start := time.Now()
defer func() {
playbackLatency.WithLabelValues(s.region, quality).Observe(time.Since(start).Seconds())
}()

// ... логика начала воспроизведения
}

3. Что делать, если видео не реплицировано

Стратегия A: Lazy replication при первом запросе

[Пользователь в Токио запрашивает видео]


[Edge PoP Токио] ── miss


[Regional Cache Токио] ── miss


[Orchestrator] ── проверяет: есть ли файл в регионе?
│ нет

[Запрос репликации из EU → Токио] ── занимает секунды-минуты


[Пользователь ждёт / получает ошибку / смотрит из удалённого региона]

Стратегия B: Predictive replication

Предсказываем, куда реплицировать:

type ReplicationPredictor struct {
telemetry TelemetryService
cache CacheManager
}

func (p *ReplicationPredictor) PredictAndReplicate(ctx context.Context) error {
// Анализируем: какие видео набирают популярность в каких регионах
trending, err := p.telemetry.GetTrendingByRegion(ctx, time.Hour)
if err != nil {
return err
}

for _, item := range trending {
// Если видео набирает просмотры в регионе, где его ещё нет
hasLocalCopy, err := p.cache.HasLocalCopy(ctx, item.VideoID, item.Region)
if err != nil || hasLocalCopy {
continue
}

// Если тренд устойчивый (не случайный всплеск)
if item.ViewVelocity > threshold {
err := p.cache.ReplicateToRegion(ctx, item.VideoID, item.Region)
if err != nil {
log.Printf("failed to replicate %s to %s: %v", item.VideoID, item.Region, err)
}
}
}

return nil
}

Стратегия C: Fallback на удалённый регион

Если видео не успело реплицироваться — отдаём из ближайшего региона, где оно есть:

func (s *CDNService) GetVideoSegment(ctx context.Context, videoID string, segment string, userRegion string) (io.ReadCloser, error) {
// 1. Пробуем локальный edge
data, err := s.edgeCache.Get(ctx, videoID, segment)
if err == nil {
return data, nil
}

// 2. Пробуем региональный кэш
data, err := s.regionalCache.Get(ctx, videoID, segment)
if err == nil {
return data, nil
}

// 3. Находим ближайший регион, где есть файл
sourceRegion := s.findNearestSource(ctx, videoID, userRegion)

// 4. Отдаём из удалённого региона (медленнее, но работает)
// + запускаем фоновую репликацию
go s.replicateAsync(videoID, sourceRegion, userRegion)

return s.fetchFromRegion(ctx, videoID, segment, sourceRegion)
}

4. Реалистичные ожидания

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

  • Нет серебряной пули. Каждое улучшение — это компромисс между стоимостью, сложностью и качеством.
  • Измеряйте всё. Без телеметрии невозможно понять, где узкое место.
  • Итеративный подход. Начните с простого решения (CDN + fallback), затем добавляйте predictive replication, tiered storage и т.д.
  • Graceful degradation. Если видео не доступно локально — покажите из удалённого региона с предупреждением, а не ошибку.

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

Вопрос 11. Насколько валидно ссылаться на опыт конкурентов и других компаний во время собеседования? Не будет ли это воспринято негативно?

Таймкод: 01:04:45

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

Правильный ответ:

Это важный мета-вопрос о подходе к проектированию и профессиональному развитию. Кандидат и ведущий дали отличный ответ. Дополню контекстом.

1. Почему ссылки на чужой опыт — это сильный сигнал

Показывает широту кругозора. Кандидат, который знает, как решают аналогичные проблемы Netflix, YouTube, Twitter, демонстрирует, что он не замкнут в своём технологическом пузыре.

Экономит ресурсы компании. Использование проверенных паттернов вместо изобретения велосипеда — это инженерная зрелость. Как сказал кандидат: «использование чужого опыта и набитых шишек позволяет тратить меньше времени».

Демонстрирует способность к анализу. Важно не просто сказать «YouTube делает так», а объяснить почему это решение подходит для данной задачи и какие есть альтернативы.

2. Как правильно ссылаться на чужой опыт

Плохо: > «Twitter использует fan-out on write, значит нам тоже нужно так сделать.»

Хорошо: > «Twitter применил fan-out on write с порогом в 1M подписчиков, потому что у них было много celebrity-аккаунтов. В нашем случае, если у нас будет больше 10K авторов с аудиторией свыше 100K, нам стоит рассмотреть гибридный подход. Давайте посмотрим на наши метрики распределения подписчиков.»

Ключевые отличия:

  • Объяснение причин, а не слепое копирование.
  • Привязка к контексту задачи.
  • Упоминание ограничений и альтернатив.

3. Где найти информацию об архитектурах

  • Engineering Blogs: Netflix Tech Blog, Uber Engineering, Cloudflare Blog, Discord Blog, Spotify Engineering.
  • Conference talks: QCon, StrangeLoop, GOTO Conf (многие на YouTube).
  • Книги: «Designing Data-Intensive Applications» (Martin Kleppmann), «System Design Interview» (Alex Xu). . Open source: Изучение архитектурных решений в проектах с открытым кодом.
  • Patents: Google, Amazon патентуют многие свои решения — они публичны.

4. Типичные ошибки начинающих

Как верно отметил ведущий, главная ошибка — «я знаю только PostgreSQL, значит всё будем хранить в PostgreSQL». Это tunnel vision.

Зрелый инженер понимает ограничения каждого инструмента:

ИнструментХорошо дляПлохо для
PostgreSQLСтруктурированные данные, ACIDВысокочастотные счётчики, полнотекстовый поиск
RedisКэш, счётчики, сессииСложные запросы, большие объёмы данных
ElasticsearchПоиск, аналитикаТранзакции, точные агрегации
ClickHouseАналитика, агрегацииOLTP, частые обновления
MongoDBГибкая схема, документыСложные JOIN, транзакции между документами

5. Баланс между опытом и адаптацией

Важно не впадать в другую крайность — выбирать технологию только потому, что «Google так делает». Контекс имеет значение:

  • Google строит для миллиардов пользователей — их решения могут be overkill для стартапа.
  • Команда должна уметь поддерживать выбранные технологии.
  • Стоимость владения (TCO) должна быть адекватна бюджету.

Кандидат продемонстрировал правильный подход: использование чужого опыта как отправной точки для анализа, а не как готового ответа. Это зрелая инженерная позиция, которая высоко ценится на собеседованиях и в работе.