Ниже — проведу детальный разбор вопрос и ответов из этого интервью по System Design:

Mock-собеседование от Team Lead из Ozon секции по языку Golang на платформе it-interview.io.

Общий вывод: Кандидат (Александр) показал очень высокий уровень подготовки.

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

Основные замечания касаются тайм-менеджмента (повторы) и изначально неверного выбора ключа шардирования, который, однако, был вовремя исправлен.


Схема архитектуры (спроектированной):

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

  • Вопрос собеседующего: Что именно должен делать сервис? Классический редирект или просто возвращать ссылку? Нужна ли аутентификация?
  • Ответ интервьюера (в интерпретации): Мы будем делать сервис, который по длинной ссылке возвращает короткую. По GET-запросу на короткую ссылку мы должны вернуть длинную (без редиректа). У ссылок есть срок хранения. Пользователи могут задавать кастомные имена (alias). Аутентификация есть, но мы ее не проектируем, просто обозначим на схеме. Нужен rate limiting и базовая аналитика (количество переходов).
  • Анализ ответа: Верно. Кандидат четко отделил «классический» сценарий от требуемого (возврат ссылки, а не редирект), что важно для проектирования API. Он правильно вынес аутентификацию за скобки, но учел ее наличие.
  • Полный правильный ответ: Наш сервис будет предоставлять два основных API:
    1. POST /shorten: принимает long_url и опциональный custom_alias, возвращает short_key. Требует аутентификации.
    2. GET /{short_key}: принимает короткий ключ, возвращает оригинальную длинную ссылку (200 OK) или ошибку (404/410), если ссылка не найдена или истекла.
      Также нам необходимы: ограничение частоты запросов (rate limiting) для защиты API и сбор аналитики по количеству обращений к каждому short_key (например, для последующего построения графиков).

Вопрос 2: Сбор нефункциональных требований и оценка нагрузки

  • Вопрос собеседующего: Какая предполагается нагрузка? Какое соотношение чтения и записи? Требования к задержкам и доступности?
  • Ответ интервьюера (в интерпретации): Будем считать от количества создаваемых ссылок: 100 млн новых ссылок в месяц. Соотношение чтения к записи — 100:1 (т.е. 10 млрд переходов/чтений в месяц). Хранение — 1 год (позже скорректировано на 10 лет по совету интервьюера). Задержка на чтение — 1 мс (очень жесткое требование). Доступность — высокая, система должна масштабироваться.
  • Анализ ответа: Верно. Кандидат использует подход «от записи», что логично для такого сервиса. Он корректирует цифры подсказки интервьюера, что показывает гибкость. Выбор метрик (1 мс, высокая доступность) задает тон для проектирования высоконагруженной системы.
  • Полный правильный ответ: Исходя из требования в 100 млн новых ссылок в месяц и соотношении Read:Write = 100:1, мы получаем:
    • Запись: ~100 млн операций в месяц → (100e6 / (30*86400)) ≈ ~40 RPS (запросов в секунду). Пиковый можно умножить на 2-3.
    • Чтение: ~10 млрд операций в месяц → ~4000 RPS.
      Система критична к задержкам на чтение (target 1 мс), так как это пользовательский опыт. Доступность должна быть высокой (цель 99.95% – 99.99%), так как сервис используется другими системами. Масштабируемость обязательна, так как нагрузка может расти.

 

Список требований:


Вопрос 3: Схема данных и расчет объема хранилища

  • Вопрос собеседующего: Как будет выглядеть модель данных? Какой объем данных мы будем хранить?
  • Ответ интервьюера (в интерпретации): Таблица/коллекция links будет содержать: long_url (256 байт), short_key (8 байт, если использовать base64), user_id (10 байт), created_at (6 байт). Итого ~300 байт на запись. При 100 млн записей в месяц и сроке хранения 10 лет, общий объем: 100 млн * 12 месяцев * 10 лет * 300 байт ≈ 3.6 ТБ.
  • Анализ ответа: Отлично. Кандидат не просто называет цифры, а объясняет, из чего они складываются. Расчет объема хранилища (3.6 ТБ за 10 лет) показывает, что данные не являются проблемой и могут поместиться на относительно небольшом кластере серверов, что оправдывает выбранные подходы к шардированию.
  • Полный правильный ответ: Для хранения метаданных ссылок нам потребуется примерно 300-350 байт на запись (с учетом индексов и служебной информации). При 100 млн новых ссылок в месяц и хранении в течение 10 лет, общий объем данных составит около 4 ТБ. Это довольно скромный объем, что позволяет нам рассматривать относительно простые решения для шардирования. Трафик: запись ~ 40 RPS * 300 байт ≈ 12 КБ/с (входящий), чтение ~ 4000 RPS * 300 байт ≈ 1.2 МБ/с (исходящий).

Вопрос 4: Проектирование высокоуровневой архитектуры

  • Вопрос собеседующего: Предложите High-Level Design.
  • Ответ интервьюера (в интерпретации): Рисуем клиентов (разделяем на читающих и пишущих). Далее API Gateway (балансировка, TLS, аутентификация). За ним разделяем сервисы на TinyURL Creator (Post) и TinyURL Fetcher (Get) для независимого масштабирования. Далее — шардированная БД (SQL) с репликами для отказоустойчивости. Для чтения добавляем кэш (Redis) перед БД по стратегии LRU.
  • Анализ ответа: Верно и очень хорошо. Кандидат принимает ключевое архитектурное решение — разделение трафика чтения и записи на разные сервисы. Это позволяет оптимизировать стоимость и ресурсы (держать много инстансов для Get и мало для Post). Добавление кэша для чтения — обязательный элемент для достижения задержки в 1 мс.
  • Полный правильный ответ: Наша High-Level архитектура будет состоять из:
    1. Клиенты и API Gateway: Единая точка входа для балансировки нагрузки, терминирования TLS, rate limiting и аутентификации.
    2. Сервис записи (Post Service): Отвечает за генерацию и сохранение коротких ссылок. Масштабируется горизонтально, но требует меньше инстансов.
    3. Сервис чтения (Get Service): Отвечает за обработку запросов на получение длинной ссылки по короткому ключу. Масштабируется агрессивно для обработки высокого RPS.
    4. Слой кэширования (Redis Cluster): Распределенный кэш перед БД для обеспечения задержки чтения <1 мс. Используем стратегию Cache-Aside.
    5. Слой данных (Sharded SQL Database): Основное хранилище. Шардирование необходимо для распределения нагрузки и объема данных. Репликация (replicas) — для отказоустойчивости и распределения нагрузки чтения.

Вопрос 5: Выбор ключа для шардирования

  • Вопрос собеседующего: По какому ключу вы будете шардировать данные в БД?
  • Ответ интервьюера (в интерпретации): Первоначально: По хэшу от long_urlПосле уточнения: Понял ошибку, нужно шардировать по short_key, так как именно он приходит в запросе на чтение (GET /{short_key}), и нам нужно быстро определить, на каком шарде искать данные.
  • Анализ ответа: Верно (после коррекции). Кандидат демонстрирует умение слышать собеседника и исправлять ошибки на лету. Это очень ценный навык на интервью. Изначально неверный выбор ключа — распространенная ошибка.
  • Полный правильный ответ: Ключом для шардирования должен быть short_key. Рассмотрим потоки данных:
    • Запись: Мы создаем short_key -> мы вычисляем его хэш -> определяем шард -> пишем данные.
    • Чтение: Клиент приходит с short_key (GET /{short_key}) -> мы вычисляем его хэш -> определяем шард -> идем в кэш или напрямую в БД на нужный шард.
      Шардирование по short_key гарантирует, что все операции с конкретной ссылкой будут направлены на один и тот же шард, и нам не нужно делать broadcast-запросы во все шарды для поиска данных.

Вопрос 6: Генерация уникальных коротких ссылок (ключей)

  • Вопрос собеседующего: Как вы будете гарантировать уникальность short_key на всех шардах? Если два сервиса-создателя одновременно сгенерируют одинаковый ключ для разных ссылок, возникнет коллизия.
  • Ответ интервьюера (в интерпретации): Использовать хэш от long_url небезопасно из-за коллизий при обрезке до 8 символов. Предлагаю вынести генерацию в отдельный сервис/хранилище (например, Redis), которое будет выдавать заранее сгенерированные уникальные ключи, используя распределенные блокировки (Redlock) для обеспечения атомарности выдачи.
  • Анализ ответа: Верно, но сложно. Кандидат предлагает рабочее решение (pre-generated keys + distributed locks), но оно может стать узким местом и добавить лишнюю сложность. Существуют более простые и распространенные паттерны.
  • Полный правильный ответ: Есть несколько подходов:
    1. Глобальный генератор ID (Snowflake ID): Использовать распределенный генератор уникальных ID (как Twitter Snowflake), который выдает 64-битные числа. Затем эти числа можно закодировать в base62, чтобы получить короткую строку. Это исключает коллизии на уровне генерации.
    2. База данных + Hash + Probabilistic approach: Взять хэш от long_url + соль. При попытке вставки в БД по уникальному индексу на колонке short_key может произойти ошибка дубликата. В этом случае сервис повторяет попытку с новой солью. Это очень распространенный и надежный подход (особенно для не слишком высоких RPS на запись).
      Предложенный вами подход с предвыделенными ключами в Redis также рабочий, но он требует поддержки консистентности этого пула и может быть сложнее в реализации. Я бы, наверное, остановился на подходе с Snowflake-like генератором или подходе «попробуй еще раз» (retry on duplicate key), так как 40 RPS на запись позволяют это сделать без проблем.

Вопрос 7: Идемпотентность создания ссылки

  • Вопрос собеседующего: Что будет, если пользователь дважды отправит запрос на создание одной и той же длинной ссылки? Мы создадим два разных коротких ключа или должны вернуть существующий?
  • Ответ интервьюера (в интервью этот вопрос был задан в самом конце, но логически относится к функциональности).
  • Ответ интервьюера (в интерпретации): Не был явно проработан в основной части.
  • Анализ: Кандидат упустил этот момент в требованиях. Это важный функциональный нюанс.
  • Полный правильный ответ: Нам нужно решить вопрос идемпотентности. Есть два подхода:
    1. Всегда создавать новый ключ: Это просто, но может привести к захламлению хранилища дублями.
    2. Идемпотентность на стороне сервера: Если ссылка для данного пользователя уже существует, возвращать существующий short_key. Для этого можно использовать комбинацию (user_id, long_url) как ключ для проверки дубликатов. Это удобно для пользователя и экономит место. Я бы предложил реализовать именно этот подход, добавив уникальный композитный индекс в БД.

Вопрос 8: Анализ точек отказа и повторное использование кода

  • Вопрос собеседовавшего (в конце): Где у нас точки отказа?
  • Ответ интервьюера (в интерпретации): Обсудили отказ шарда, мастера БД, узлов кэша, сервисов. Предложили репликацию и механизмы перевыборов мастера.
  • Анализ: Верно. Кандидат понимает основные принципы обеспечения отказоустойчивости.
  • Замечание интервьюера по тайм-менеджменту: Кандидат несколько раз подробно рассказывал про шардирование и репликацию в разных частях интервью.
  • Полный правильный ответ (в части коммуникации): «Как мы обсуждали на этапе проектирования хранилища, для обеспечения отказоустойчивости мы используем репликацию мастер-реплика для каждого шарда. В случае отказа мастера одна из реплик будет повышена до мастера. Здесь в этом flow применим тот же самый подход, поэтому я не буду повторять детали, если вы не против.» Такой подход экономит время и показывает, что кандидат держит в голове целостную картину.

Фидбэк от интервьюера

[[ Отзыв о кандидате ]]
  • Александр во время собеседования показал себя хорошо.
  • Показал себя как специалист способный анализировать предметную область и способный подстраиваться под новые требования.
  • В качестве системы для проектирования был выбран URL Shortnet (сокращатель ссылок).
  • Кандидат собрал все требования, реализовал верхнеуровневый дизайн и постепенно углубился до компонентов.
  • Из плюсов еще могу отметить что разделил трафик на чтение и на запись и проработал оба потока данных. Проработал систему хранения, масштабирования и отказоустойчивости.
  • В ходе интерью из за того что сразу не спроектировал API ошибся с выбором ключа для шардирования данных.
  • Но после того как спроектировал две API ручки быстро сообразил в какую сторону необходимо смотреть.
  • Так же не до конца раскрыл как именно будет генерироваться уникальная ссылка.
  • Не определились со словарем и полным алгоритмом.
  • По коммуникациям приятный в общении, рассудительный.
  • Все предположения обосновывает и рассуждает в слух, что дает понять в какую сторону движется.
  • Из рекомендаций можно посоветовать следить за таймингом, чаще просматривать первоначальный требования и не повторяться на тех моментах, которые уже были озвучены ранее.

(с) Валерий Бабушкин. System Design. Карьера в IT. Karpov courses. Яндекс алгоритмы.


 

 

Tags

Нет Ответов

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Рубрики


Подпишись на новости
👋

Есть вопросы?