Разбор: сокращатель ссылок
Интуиция: гардероб с номерком
Ты сдаёшь пальто и получаешь номерок «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].
Чтобы счётчик не стал узким местом, каждый 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. Если аналитика упадёт, редиректы продолжат работать, а события подождут в очереди.
Итоговая схема
Частые ошибки
- Не делать оценку и сразу ставить шардирование. Потом не получится объяснить, зачем оно нужно.
- Брать хеш и забыть про коллизии.
- Выбрать 301 и при этом обещать точную аналитику.
- Писать статистику синхронно в ту же БД, где лежат ссылки.
- Держать один глобальный счётчик без выдачи диапазонов. Это единая точка отказа.
Это ты проговоришь сам
В следующем шаге интервьюер спросит то, что здесь сознательно не разобрано. Подумай заранее:
- Кастомные алиасы (
sh.rt/sale2024). Как они уживаются с base62-счётчиком, чтобы алиас не пересёкся со сгенерированным кодом? Что происходит, если два человека одновременно хотят один и тот же алиас? - Срок жизни и злоупотребления. Как удалять истёкшие ссылки, не сканируя 6 млрд записей? Как не дать боту за минуту создать миллион ссылок на фишинговые сайты?
Здесь нет одного правильного ответа. Интервьюер ждёт рассуждение: варианты, их компромиссы и твой выбор с объяснением.