open atlas
← Все проекты

fullstack · advanced · 10d

URL-сокращатель под нагрузкой

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

Это капстоун: возьми всё со spine-треков и доведи одну небольшую систему от и до. URL-сокращатель кажется тривиальным — POST длинного URL, GET редиректа — но путь редиректа read-heavy, критичен по латентности и подвержен злоупотреблениям, так что он вынуждает принимать реальные решения по схеме, кэшу, деплою и дежурству. Ты спроектируешь его, построишь горячий путь, закэшируешь и защитишь, протестируешь под нагрузкой, задеплоишь, снимешь телеметрию, а затем переживёшь и разберёшь реальный инцидент.

Результат

Задеплоенный сервис редиректов с индексированным хранилищем, read-through кэшем, CI/CD-пайплайном, RED-дашбордами с SLO и написанным пост-мортемом инцидента cache-stampede.

Этапы

0/8 · 0%
  1. 01Очерти систему: масштаб, SLO, non-goals

    До любого кода оцени задачу. Сокращатель — это подавляюще чтения (редиректы) над записями (создания), часто 100:1 и хуже, поэтому именно путь редиректа должен защищать твой дизайн. Сделай прикидку на салфетке (целевой QPS, число ссылок, объём хранилища, рабочее множество кэша), затем выпиши SLO (например, p99 редиректа < 50 мс, доступность 99.9%) и явные non-goals, чтобы не вылизывать путь создания.

    Критерии готовности
    • У тебя есть числа: целевой QPS редиректов, QPS создания, всего ссылок, байт на ссылку и вытекающие оценки хранилища и памяти под кэш.
    • Ты выписал 2–3 SLO для пути редиректа и минимум два явных non-goal.
  2. 02Спроектируй API, схему и короткий код

    Реши, как генерируются и хранятся короткие коды. Монотонный счётчик + base62 даёт плотные, бесколлизионные коды, но раскрывает объём и порядок; случайные/хешированные коды это скрывают, но требуют проверки уникальности и ретрая при коллизии. Выбери один вариант и обоснуй его, затем спроектируй таблицу и единственный индекс, по которому пойдёт lookup редиректа, и реши, где сидят кэш и CDN.

    Критерии готовности
    • Твой API-контракт покрывает создание и резолв, и ты можешь сформулировать поведение при коллизии для выбранной генерации кодов.
    • В схеме есть ровно тот индекс, что нужен запросу редиректа, и ты обосновал его правилом ведущего столбца.
  3. 03Построй горячий путь редиректа

    Реализуй создание и резолв по-настоящему. Путь резолва — горячий: это должен быть один индексированный lookup, возвращающий 301/302, с лёгким жизненным циклом запроса (accept → route → handle → serialize). Замерь холодный (некэшированный) редирект от и до, чтобы знать латентность, которую ты собираешься убрать кэшем.

    Критерии готовности
    • Редирект резолвится одним индексированным запросом (ты подтвердил это через EXPLAIN, а не sequential scan).
    • Ты записал p50/p99 холодного редиректа без кэша как базовую линию.
  4. 04Закэшируй чтения, защити записи

    Поставь read-through кэш перед резолвом и сделай создание безопасным. Кэш превращает горячий путь из запроса к БД в попадание в память, но вносит инвалидацию и будущий риск stampede, за который ты заплатишь позже. На стороне записи сделай создание идемпотентным и ограничь по частоте злоупотребляющее создание, чтобы скрипт не исчерпал твоё пространство кодов или твою базу.

    Критерии готовности
    • Кэшированные редиректы показывают явное падение p99 относительно холодной базовой линии, и ты можешь объяснить своё правило инвалидации.
    • Создание идемпотентно для одного входа и ограничено по частоте на клиента, с 429 + Retry-After при злоупотреблении.
  5. 05Протестируй: unit, integration, contract, load

    Построй пирамиду тестов для сервиса. Покрой юнитами генерацию кодов и математику пополнения/лимита, integration-тестами — путь резолва против реального Postgres + кэша, contract-тестами — API, чтобы клиенты не ломались на изменении, и нагрузочным тестом — путь редиректа, чтобы найти, где деградирует p99. Именно нагрузочный тест делает следующие два этапа честными.

    Критерии готовности
    • Unit-, integration- и contract-тесты идут зелёными одной командой, а flaky-тест карантинится, а не ретраится до зелёного.
    • Нагрузочный тест сообщает QPS редиректов, при котором p99 пересекает твой SLO.
  6. 06Задеплой: контейнер, пайплайн, выкатка

    Выкати через пайплайн, а не руками. Контейнеризуй лёгким образом, собери CI/CD-пайплайн, который гоняет тесты как гейты до сборки, выкатывай стратегией, умеющей безопасно падать (canary или blue-green), поставь редиректы за CDN/балансировщик и держи секреты вне образа. Плохая выкатка должна откатываться за секунды, а не передеплоем.

    Критерии готовности
    • Пуш гоняет тесты → собирает образ → деплоит через пайплайн, и ты можешь откатиться без пересборки.
    • Секреты инжектятся при деплое, а не запекаются в образ, и редиректы отдаются за CDN или балансировщиком.
  7. 07Наблюдай: RED, трейсы, SLO

    Сделай работающую систему читаемой. Снимай RED-метрики (rate, errors, duration) на пути редиректа, структурные логи, которые реально можно запрашивать, и трейсы, проброшенные через переходы кэш→БД, чтобы медленный редирект указывал на медленный span. Преврати свои SLO в дашборды и error budget, чтобы находить инцидент раньше, чем о нём напишут пользователи.

    Критерии готовности
    • Дашборд показывает rate редиректов, error rate и p50/p99 длительности, привязанные к твоим SLO и error budget.
    • Трейс одного медленного редиректа показывает span'ы кэша и БД, так что ты локализуешь латентность без угадывания.
  8. 08Переживи cache-stampede, затем напиши пост-мортем

    Запись кэша популярной ссылки истекает на пике трафика; тысячи одновременных промахов разом бьют в Postgres, p99 латентности редиректа взлетает, а origin насыщается. Обнаружь это по своим метрикам, смягчи вживую, найди корневую причину и напиши пост-мортем. Механизм, который тебя спасает, — это коалесценция запросов (single-flight) плюс вероятностное раннее обновление, а не просто более длинный TTL, который лишь сдвигает обрыв.

    Критерии готовности
    • Ты воспроизвёл stampede нагрузочным тестом (N одновременных промахов на одном горячем ключе) и зафиксировал всплеск p99 и QPS origin на дашборде.
    • Ты смягчил его через single-flight (один поход в origin на ключ) плюс jitter раннего обновления и показал возврат p99 к SLO.
    • Твой пост-мортем называет триггер, способствующие факторы, радиус поражения, фикс и одну превенцию, которая не «поднять TTL».
    Самопроверка

    Вставь корневую причину из пост-мортема и пункт превенции; senior-ревьюер проверяет, что назван механизм stampede (коалесценция / раннее обновление), а не только симптом (высокая латентность).

Стартер

  • README.md
  • src/shortener.ts
  • test/shortener.test.ts
Скачать стартер (.zip)

Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: bun test

Рубрика

Джуниор Миддл Сеньор
Корректность кодека Реализует encodeBase62/decodeBase62 с round-trip для малых значений; может пропустить граничные случаи вроде n=0 или однозначного base62. Полный round-trip для всех неотрицательных целых включая 0 и большие значения; правильный порядок символов (нет leading-zero неоднозначности от использования '0' как первого символа). Может объяснить, почему порядок символов важен для URL-безопасности, почему base62 лучше base64 для коротких кодов в URL, и какова максимальная длина кода при заданном потолке счётчика (например, 62^6 ≈ 56 млрд ссылок до переполнения 6-символьных кодов).
Обработка коллизий и истечения срока Счётчик инкрементируется при каждом create; resolve возвращает null для неизвестных кодов. Сравнение TTL может быть off-by-one или использовать >= вместо >. Детерминированная уникальная генерация кодов без коллизий по построению; TTL правильно различает неизвестный (никогда не существовавший) и истёкший (когда-то существовавший) коды; createdAt хранится в момент create, а не resolve. Рассуждает о стоимости координации общего счётчика в распределённой системе (единая точка, требует атомарного инкремента или стратегии flake-ID), стоимости lookup-before-insert при случайной генерации кодов и о том, когда каждый подход предпочтительнее (write-heavy vs. анонимность vs. предсказуемость).
Масштабирование пути чтения Понимает, что редирект — горячий путь и кэш помогает; использует 301 по умолчанию, не задумываясь об импликациях. Объясняет, что 301 постоянно кэшируется браузером/CDN (быстро, но трудно отозвать), тогда как 302 запрашивается каждый раз (управляемо, но медленнее при нагрузке); выбирает правильный вариант для каждого сценария и обосновывает выбор. Определяет риск cache-stampede на горячих кодах (много одновременных промахов при истечении срока), предлагает single-flight (коалесценцию запросов) и вероятностное раннее обновление как меры смягчения — не просто более длинный TTL. Обсуждает кэширование 301 на CDN edge как путь с нулевой нагрузкой на бэкенд и радиус поражения неправильно настроенного постоянного редиректа.
Эталонный разбор (спойлер)

301 против 302: постоянство, кэшируемость и возможность отзыва. 301 постоянно кэшируется браузерами и CDN — редирект происходит на стороне клиента при повторных посещениях без нагрузки на бэкенд. Это идеальный исход для пути чтения сокращателя. Цена: нельзя изменить назначение или отключить ссылку для любого клиента, уже закэшировавшего её. 302 запрашивается каждый раз, поэтому вы сохраняете полный контроль (полезно для A/B лендингов, ссылок с истечением срока или аналитики, считающей каждое обращение). Выбирайте 301 для стабильных публичных ссылок; 302 — когда нужен контроль в реальном времени.

Счётчик против случайной генерации кодов. Монотонный счётчик + base62 даёт плотные, бесколлизионные коды с генерацией за O(1) и без проверки уникальности — но раскрывает объём создания и порядок (код 'c' создан раньше 'd'). Случайные или хешированные коды скрывают эту информацию, но требуют проверки уникальности (lookup-before-insert) и ретрая при коллизии. При низкой вероятности коллизии (например, 8 случайных символов из base62 = 62^8 ≈ 218 триллионов комбинаций) ретраи редки, но вы платите одним чтением на каждую запись. В распределённой системе общий счётчик становится узким местом координации; альтернативы включают диапазоны счётчиков на узел, Snowflake-подобные ID (timestamp + node id + sequence) или UUID, сокращённые до base62.

Cache stampede на горячем коде. Когда запись кэша популярной ссылки истекает, множество одновременных запросов все сразу промахиваются мимо кэша и веером идут в базу данных. Origin получает всплеск, пропорциональный конкурентному трафику, а не установившемуся RPS. Два стандартных способа смягчения: (1) single-flight / коалесценция запросов — только один поток идёт в БД, пока остальные ждут результата единственного in-flight запроса; (2) вероятностное раннее обновление (XFetch) — с вероятностью, пропорциональной оставшемуся TTL, первый запрос, оценивающий вероятность, запускает фоновое обновление до истечения срока, так что кэш фактически никогда не остывает под трафиком. Более длинный TTL откладывает проблему, но не устраняет её.

Почему base62, а не base64 для кодов в URL. Base64 использует '+', '/' и иногда '=' — символы, которые должны быть percent-encoded в сегментах пути URL, что делает коды длиннее и труднее читаемыми. Base62 (a–z, A–Z, 0–9) безопасен для URL без зарезервированных символов. Потеря информационной плотности мала: 6-символьный base62 кодирует log2(62^6) ≈ 35.7 бит против ≈ 36 бит у base64 — незначительно. Порядок символов важен: если '0' — первый символ, encodeBase62(0) = '0' и односимвольные коды коллидируют со строками, похожими на числа. Начало символьного набора со строчных букв избегает этого и даёт более читаемые коды.

Тип редиректа как решение в момент создания, а не глобально. Хранение типа редиректа на ссылку (не только на уровне экземпляра Shortener) позволяет смешанные политики в одном сервисе: постоянные короткие коды (301) для стабильных кампаний и временные трекинговые ссылки (302) для A/B-тестов сосуществуют в одном хранилище. Путь резолва всегда читает сохранённый тип, так что клиенты получают именно то, что было задекларировано при создании. Изменение с 301 на 302 для уже распространённого кода невозможно отозвать из кэшей браузеров — считайте тип редиректа неизменным после установки.

Сделай по-сеньорски

  • Сделай multi-region: отдавай редиректы read-local и явно рассуждай о компромиссе консистентности на пути создания.
  • Добавь пайплайн аналитики кликов (события → очередь → роллапы), который не добавляет латентности пути редиректа.
  • Поддержи кастомные домены с автоматической выдачей TLS и изоляцией по тенантам.
  • Обнаруживай и тротли злоупотребляющие паттерны создания и редиректов, не задевая легитимные всплески трафика.

Навыки

API designschema and indexingcaching strategyload testingCI/CDcontainers and k8sobservability (RED/SLO)incident responsepost-mortems

Рекомендуемый стек

PostgresRedisa reverse proxy or CDNyour backend languagea container runtimean OpenTelemetry-compatible tracer