Elido
7 мин чтенияИнженерия

Как создать сокращатель URL: архитектура и код

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

Marius Voß
DevRel · edge infra
Диаграмма архитектуры сокращателя URL, показывающая путь записи с кодированием короткого кода и путь чтения, разрешающий редирект из кеша

Чтобы создать сокращатель URL, вам нужны четыре вещи: место для хранения маппинга короткого кода на целевой URL, способ генерации уникального кода для каждой новой ссылки, обработчик редиректа, который ищет код и возвращает HTTP-редирект, и кеш перед поисковым запросом, потому что чтений значительно больше, чем записей. Это всё ядро, и его можно поднять за afternoon.

Ловушка - думать, что версия, собранная за afternoon, это уже продукт. Редирект, работающий на вашем ноутбуке, и сервис сокращения URL, выдерживающий незнакомцев, направляющих его на вредоносное ПО, нагружающих трафиком и ожидающих четырёх девяток доступности, - это разные инженерные задачи. Первое - алгоритм. Второе - операционное обязательство.

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

Два пути в сокращателе URL: путь записи кодирует уникальный ID в короткий код и сохраняет его, путь чтения разрешает клик через кеш до редиректа

Кратко: что на самом деле делает сокращатель URL#

Сокращатель URL - это поиск по ключу-значению с HTTP-редиректом. Ключ - это короткий код, значение - длинный URL, и вся задача сводится к превращению example.com/aB3x9 в 302, указывающий на исходный адрес.

Модель данных - одна таблица:

CREATE TABLE links (
    id          BIGSERIAL PRIMARY KEY,
    short_code  TEXT NOT NULL UNIQUE,
    long_url    TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX idx_links_short_code ON links (short_code);

Через неё проходят два пути. Путь записи принимает длинный URL, генерирует короткий код и вставляет строку. Путь чтения принимает короткий код, ищет строку и возвращает редирект. Чтения преобладают в соотношении примерно 1000 к 1, поэтому почти всё инженерное внимание должно быть направлено на то, чтобы поиск был быстрым и дешёвым. Уникальный индекс на short_code - это то, что делает поиск поиском по индексу, а не полным сканированием. Вот и всё ядро.

Генерация короткого кода: Base62, случайный или хеш#

Короткий код - место, где находится интересное решение. Есть три реалистичные стратегии, и они компромиссны по длине, предсказуемости и сложности обработки коллизий.

Base62 от уникального ID - классика. Берёте автоинкрементный ID строки и кодируете его в base62, 62 символа a-z, A-Z и 0-9. Коды короткие, коллизии невозможны, так как каждый ID уникален, и они становятся на один символ длиннее примерно каждые 62x по объёму. Недостаток - они последовательны и предсказуемы, поэтому любой может обойти ваше пространство имён.

const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

// encode turns a positive integer ID into a base62 short code.
func encode(id uint64) string {
	if id == 0 {
		return string(alphabet[0])
	}
	var b []byte
	for id > 0 {
		b = append(b, alphabet[id%62])
		id /= 62
	}
	// reverse, since we built the digits least-significant first
	for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
		b[i], b[j] = b[j], b[i]
	}
	return string(b)
}

Случайные строки решают проблему предсказуемости. Генерируете короткий случайный код, например с помощью библиотеки nanoid, и проверяете его по уникальному индексу перед сохранением. При семи символах base62 у вас триллионы возможностей, так что коллизии редки, но вы всё равно должны обрабатывать редкую вставку, которая нарушает ограничение уникальности, повторно пробуя с новым кодом.

Хеширование URL - третий вариант и обычно худший. Хеш длинного URL детерминирован, что кажется удобным, но вам всё равно нужно его усекать, вы всё равно получаете коллизии, и одинаковые URL отображаются на одинаковые коды, что раскрывает информацию. Большинство продакшн-сервисов выбирают base62 для внутренних ID или случайные коды для публичных. Кастомные или брендированные слаги, коды, которые пользователь вводит вручную, проверяются по тому же уникальному индексу перед принятием.

Путь редиректа: 301 против 302 и почему это определяет вашу аналитику#

Код статуса редиректа - не косметический выбор. Он определяет, увидите ли вы второй клик.

301 Moved Permanently сообщает браузерам и прокси, что перемещение постоянное, поэтому они кешируют его. После первого посещения браузер может направлять будущие клики прямо к месту назначения, не обращаясь к вашему серверу. Отлично для скорости, но губительно для аналитики, потому что клики, которые вы больше всего хотите считать, это те, которые никогда до вас не доходят. HTTP-семантика прописана в RFC 9110, который определяет как постоянные, так и временные редиректы.

302 Found или 307 Temporary Redirect запрашивается каждый раз. Браузер обращается к вашему серверу при каждом клике, что означает возможность считать каждое посещение и изменять место назначения позже без борьбы с устаревшими кешами. Для сокращателя ссылок, вся ценность которого - в редактируемых ссылках и данных о кликах, это правильный вариант по умолчанию. Цена - один сетевой round trip на клик, который попадание в кеш делает незначительным.

Практическое правило: используйте 302, если нет конкретной причины хотеть, чтобы ссылка была заморожена и кеширована навсегда. Пост 301 против 302 редиректов подробно разбирает этот компромисс, а типы редиректов охватывает остальных представителей семейства 3xx, включая случаи, когда важны 307 и 308.

Хранение и кеширование: проектирование для соотношения чтения/записи 1000:1#

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

func resolve(ctx context.Context, code string) (string, error) {
	if url, ok := cache.Get(code); ok {
		return url, nil // hot path: served from memory
	}
	url, err := db.LookupLongURL(ctx, code)
	if err != nil {
		return "", err
	}
	cache.Set(code, url) // populate for the next click
	return url, nil
}

В продакшне это обычно становится двухуровневым: небольшой внутрипроцессный кеш для самых горячих ссылок, поддерживаемый общим хранилищем в памяти, например Redis, чтобы каждый экземпляр сервера получал пользу от поиска, выполненного любым из них. База данных, источник истины, затрагивается только при реальном холодном промахе. Настройте этот слой правильно, и один скромный сервер обработает огромный объём кликов. Пост стратегия кеширования для URL-редиректов подробно разбирает решения по вытеснению и размерам, а краеугольная статья о достижении p95 менее 15 мс показывает, как выглядит настроенный путь редиректа под нагрузкой.

Если вы предпочитаете не запускать всё это самостоятельно, API Elido предоставляет уровень редиректа, кеш и внутрирегиональную EU-доставку с p95 менее 15 мс при попадании в кеш - за один вызов. Начните бесплатно и избавьте себя от операционных вопросов.

Подсчёт кликов без замедления редиректа#

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

Разделите их. Обработчик немедленно выполняет редирект, а затем отправляет событие клика в надёжный журнал или очередь сообщений по принципу "выстрелил и забыл". Отдельный потребитель читает этот поток и записывает события в аналитическое хранилище по собственному расписанию. Посетитель никогда не ждёт, а аналитический запрос, сканирующий миллионы строк кликов, никогда не конкурирует с путём редиректа за ресурсы. Колоночная аналитическая база данных обрабатывает такие агрегатные запросы значительно лучше, чем строковые хранилища, вот почему события кликов обычно попадают куда-то отдельно от таблицы ссылок. Пост приём кликов по принципу "выстрелил и забыл" подробно описывает сторону очереди, а статья почему колоночное хранилище лучше Postgres для аналитики кликов объясняет выбор хранилища. Аналитика Elido следует этой схеме, поэтому клики можно запрашивать за секунды, не добавляя миллисекунд к редиректу.

Продакшн-бэклог за пределами рабочего редиректа: сканирование на злоупотребления, ограничение запросов, TLS для кастомных доменов, GDPR-безопасные данные кликов и высокая доступность

Что ещё нужно построить: сложные 80 процентов#

Вот часть, которую пропускают руководства по системному дизайну. Рабочий редирект - это, возможно, пятая часть реального сервиса сокращения URL. Остальное - всё то, что превращает демо во что-то, что можно выставить в публичный интернет.

  • Сканирование на злоупотребления и безопасность. Публичный сокращатель становится мишенью для фишинга в течение нескольких часов после запуска. Нужно проверять пункты назначения по базе угроз, например Google Safe Browsing, и повторно сканировать, потому что чистый URL при создании может позже стать вредоносным. Контрольный список безопасности сокращателя URL - полный перечень.
  • Ограничение запросов и идемпотентность. Открытый эндпоинт создания ссылок немедленно подвергается скриптовым атакам. Нужны лимиты на ключ и идемпотентность, чтобы повторный запрос не создавал дублирующиеся ссылки. Механика - в статье ограничение API запросов и идемпотентность.
  • Кастомные домены с TLS. Брендированные ссылки означают выпуск сертификатов для доменов, которыми вы не владеете, по требованию, без ручных шагов.
  • GDPR-безопасные данные кликов. С момента, когда вы начинаете логировать клики, вы обрабатываете персональные данные. Усечение IP-адресов и документирование сроков хранения не опционально в EU, как показывает статья GDPR для сокращателей URL.
  • Высокая доступность. Ваш редирект теперь находится на критическом пути каждой ссылки, которой кто-либо поделился. Простой ломает контент других людей, поэтому требования к доступности выше, чем для большинства приложений.

Ничто из этого не является экзотикой. Это просто большой объём непрерывной работы, которая никогда не заканчивается, - и это честная причина, по которой большинство команд останавливается на MVP и обращается к чему-то готовому.

Создать, купить или развернуть самостоятельно#

Создать собственный сокращатель - лучший способ понять редиректы, кодирование и кеширование, а для закрытого внутреннего инструмента MVP может быть всем, что когда-либо понадобится. Создайте его. За выходные узнаете больше, чем даст любая подготовка к собеседованию.

Для всего публичного или бизнес-ориентированного честно оцените расходы на поддержку. Редирект бесплатен; обработка злоупотреблений, TLS для кастомных доменов, аналитический пайплайн и дежурство - нет. Если хотите контроль без написания с нуля, можно развернуть существующий сервис: Elido предлагает путь для самостоятельного развёртывания, а пост опции с открытым исходным кодом сравнивает их. Если предпочитаете делегировать полностью, решение для разработчиков и быстрый старт с API и SDK дадут вам продакшн-уровень редиректа без бэклога выше.

Читайте также в блоге#

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
build a url shortener
url shortener system design
short code generation
base62 encoding
url redirect
url shortener architecture
link shortener api

Читать дальше