Ctrl K
Урок

Разбор: сокращатель ссылок

Интуиция: гардероб с номерком

Ты сдаёшь пальто и получаешь номерок «47». Номерок маленький, а пальто большое. По номерку гардеробщик быстро находит вещь. Сокращатель ссылок работает так же: длинный URL — это пальто, короткий код aZ3kP9q — номерок, а редирект — момент, когда тебе отдают пальто.

Отсюда главное наблюдение. Номерки выдают редко, а по ним приходят постоянно. Ссылку создают один раз, а кликают по ней тысячи раз. Вся архитектура строится вокруг этого перекоса.

Шаг 1. Требования

Функциональные:

  • по длинному URL выдать короткий;
  • по короткому сделать редирект на длинный;
  • собирать статистику переходов.

Нефункциональные:

  • редирект быстрый, p99 < 50 мс;
  • высокая доступность чтения: мёртвая ссылка в рекламной рассылке стоит денег;
  • коды не должны легко перебираться подряд.

На собесе начни с вопроса: «Нужны ли кастомные алиасы и срок жизни ссылки?» Это сразу показывает, что ты сужаешь задачу, а не бросаешься рисовать.

Шаг 2. Оценка

Величина Расчёт Итог
Новые ссылки 100 млн/мес ÷ 2,6 млн с ~40 записей/с
Чтение (100:1) 40 × 100 ~4 000 RPS, в пике ×3 ≈ 12 000
Записей за 5 лет 100 млн × 60 мес 6 млрд
Объём 6 млрд × 500 Б ~3 ТБ
Ёмкость кода 62⁷ ≈ 3,5 трлн, запас огромный

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

Шаг 3. API

POST /api/v1/links
{"url": "https://example.com/very/long/path", "ttl_days": 365}
→ 201 {"code": "aZ3kP9q", "short_url": "https://sh.rt/aZ3kP9q"}

GET /aZ3kP9q
→ 302 Location: https://example.com/very/long/path

Шаг 4. Генерация короткого кода

Это центральная часть кейса. Есть два основных подхода.

Счётчик + base62. Берём уникальное число и записываем его в системе счисления по основанию 62: 0-9, a-z, A-Z. Пример: 125 = 2·62 + 1, то есть код состоит из двух «цифр» [2, 1].

62^1 ↓ 20
62^0 ↓ 11
125 в base62: 2·62 + 1

Чтобы счётчик не стал узким местом, каждый API-сервер берёт из координатора (ZooKeeper или отдельной таблицы) диапазон ID, например 1 000 000 – 1 999 999, и раздаёт его локально.

Хеш. Считаем md5(url) и берём первые 7 символов в base62. Коды получаются непредсказуемыми, но обрезанный хеш даёт коллизии. Работает парадокс дней рождения: при миллиардах записей совпадения неизбежны. Поэтому нужно проверить, свободен ли код, и при занятости добавить соль и пересчитать.

Счётчик + base62 Хеш
Коллизии нет есть, нужна проверка
Лишний запрос к БД нет да, при каждой записи
Предсказуемость коды идут подряд, легко перебрать случайные
Один URL → один код нет да, бесплатная дедупликация

Сильный ответ на собесе: «Возьму счётчик с выдачей диапазонов. Чтобы коды не шли подряд, перемешаю ID обратимой функцией, например умножением по модулю или Feistel-шифром, и только потом переведу в base62». Так ты закрываешь главный минус счётчика.

Шаг 5. Хранение

Доступ к данным простой: по code получить long_url. Ни JOIN-ов, ни сложных транзакций. Подходит key-value или wide-column хранилище: DynamoDB или Cassandra. Можно и PostgreSQL с шардированием по code.

Схема записи: code (PK), long_url, created_at, expires_at, owner_id. Три терабайта на одну машину не положишь, поэтому шардируем по хешу кода: нагрузка распределится равномерно.

Шаг 6. Кэш горячих ссылок

Клики распределены по правилу 80/20. Одна вирусная ссылка может собрать миллион переходов за час. Ставим Redis по схеме cache-aside: сначала ищем в кэше, при промахе идём в БД и кладём ответ в кэш.

Размер кэша: 20% ссылок, по которым кликали за сутки. Это порядка десятков ГБ, такой объём помещается в небольшой кластер. Ссылки почти не меняются, поэтому инвалидация простая: TTL плюс удаление ключа, когда ссылку удалили.

Шаг 7. Редирект: 301 или 302

301 Moved Permanently 302 Found
Кэширует браузер да нет
Нагрузка на нас ниже выше
Видим каждый клик нет да
Можно сменить цель почти нельзя можно

Раз аналитика — это требование, выбираем 302. Компромисс стоит назвать вслух: «Платим лишними запросами, зато видим все переходы и можем отключить вредную ссылку».

Шаг 8. Аналитика через очередь

Запись статистики не должна замедлять редирект. Сервис кладёт событие {code, ts, ip, user_agent, referer} в Kafka и сразу отвечает 302. Воркеры читают очередь пачками и пишут в ClickHouse. Если аналитика упадёт, редиректы продолжат работать, а события подождут в очереди.

Итоговая схема

HTTPSPOSTGET /codeдиапазонзаписьчтениепромахсобытиебатчиПользовательклиентБалансировщикLBAPI записисервисСервис редиректасервисВыдача диапазонов IDсервисRedis: горячие ссылкикэшKafka: кликиочередьCassandra: code → urlБДВоркер аналитикиворкерClickHouseБД
Чтение идёт через кэш, аналитика уходит асинхронно через очередь

Частые ошибки

  • Не делать оценку и сразу ставить шардирование. Потом не получится объяснить, зачем оно нужно.
  • Брать хеш и забыть про коллизии.
  • Выбрать 301 и при этом обещать точную аналитику.
  • Писать статистику синхронно в ту же БД, где лежат ссылки.
  • Держать один глобальный счётчик без выдачи диапазонов. Это единая точка отказа.

Это ты проговоришь сам

В следующем шаге интервьюер спросит то, что здесь сознательно не разобрано. Подумай заранее:

  1. Кастомные алиасы (sh.rt/sale2024). Как они уживаются с base62-счётчиком, чтобы алиас не пересёкся со сгенерированным кодом? Что происходит, если два человека одновременно хотят один и тот же алиас?
  2. Срок жизни и злоупотребления. Как удалять истёкшие ссылки, не сканируя 6 млрд записей? Как не дать боту за минуту создать миллион ссылок на фишинговые сайты?

Здесь нет одного правильного ответа. Интервьюер ждёт рассуждение: варианты, их компромиссы и твой выбор с объяснением.