> Как спроектировать систему url shortener на уровне микросервисов (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Black Wall Group (BWG)

Стек: Go

> Пример ответа

Короткий ответ

Проектирование url shortener на микросервисах сводится к разделению на три ключевых сервиса: API gateway (приём запросов), core service (генерация и хранение коротких кодов) и analytics service (сбор статистики переходов). Хранение - распределённая key-value база (например, Redis для кэша + Cassandra или DynamoDB для персистентности). Генерация кодов - либо счётчик с base62, либо случайные строки с проверкой коллизий. Для надёжности - репликация, шардирование по ключу, eventual consistency для аналитики.

Подробное объяснение

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

Core service - главный сервис. Принимает длинный URL, генерирует короткий код, сохраняет пару в хранилище. Код генерируется двумя способами:

  • счётчик (например, Snowflake ID или глобальный sequence) с последующим переводом в base62 - детерминированно, без коллизий, но требует координации между инстансами;
  • случайная строка фиксированной длины (например, 7 символов из алфавита [a-zA-Z0-9]) - проще, но нужна проверка на коллизии и повторная генерация при конфликте.

Для хранения используется key-value хранилище с ключом = короткий код, значением = длинный URL. Redis - для горячих данных (TTL), Cassandra/DynamoDB - для персистентного слоя. Шардирование - по хэшу короткого кода (consistent hashing).

Redirect service - принимает GET-запрос с коротким кодом, ищет длинный URL в кэше, при промахе - в базе, возвращает 302 Redirect. Для повышения производительности - локальный кэш (LRU) на каждом инстансе + Redis как общий кэш второго уровня.

Analytics service - асинхронно получает события о переходах (через message queue, например Kafka или NATS), агрегирует по времени, IP, User-Agent. Данные хранятся в columnar базе (ClickHouse) или time-series базе (InfluxDB). Не критичен к задержкам, поэтому eventual consistency.

API gateway - единая точка входа. Маршрутизирует запросы: POST /shorten → core service, GET /:code → redirect service, GET /stats/:code → analytics service. Также выполняет rate limiting, аутентификацию (если нужна), валидацию входных данных.

Межсервисное взаимодействие - синхронное (gRPC или REST) для core и redirect, асинхронное (event-driven) для analytics. Для обнаружения сервисов - Consul или etcd, для балансировки - Envoy или NGINX.

Масштабирование - каждый сервис горизонтально масштабируется независимо. Core service - stateless, вся координация в базе. Redirect service - stateless, масштабируется за счёт кэширования. Analytics - масштабируется за счёт партиционирования очереди.

Надёжность - репликация базы (Raft для Cassandra, multi-AZ для DynamoDB), circuit breaker для синхронных вызовов, retry с exponential backoff для асинхронных.

На практике

Начинаем с монолита, разбиваем на микросервисы только когда появляется реальная нагрузка. Для senior-позиции важно показать понимание trade-off: микросервисы добавляют сложность в деплой, observability и согласованность данных, поэтому оправданы только при определённом масштабе.

Практические решения:

  • генерация кодов через случайные строки с проверкой в Redis (SETNX) - проще, чем глобальный счётчик;
  • для редиректа - сначала локальный кэш, потом Redis, потом база. Промах в кэше - редкий случай, поэтому можно блокировать запрос на время чтения из базы;
  • аналитика - не блокирует основной поток, пишем событие в Kafka и забываем. Если Kafka недоступен - пишем в локальный файл и отправляем позже (outbox pattern);
  • rate limiting на gateway - token bucket на IP или API key.

Примерная схема деплоя: Kubernetes, каждый сервис - отдельный deployment, HPA по CPU и RPS. Базы - managed (AWS RDS для реляционной части, ElastiCache для Redis, DynamoDB для основного хранилища).

Пример кода

Генерация короткого кода на Go:

GO
package main
import (
"crypto/rand"
"encoding/base64"
)
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func generateCode(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = alphabet[int(b)%len(alphabet)]
}
return string(bytes), nil
}

Редирект с кэшем:

GO
func (s *Server) Redirect(w http.ResponseWriter, r *http.Request) {
code := r.URL.Path[1:]
// локальный кэш
if url, ok := s.localCache.Get(code); ok {
http.Redirect(w, r, url, http.StatusFound)
return
}
// redis
url, err := s.redis.Get(r.Context(), code).Result()
if err == nil {
s.localCache.Set(code, url, time.Hour)
http.Redirect(w, r, url, http.StatusFound)
return
}
// база
url, err = s.db.GetLongURL(r.Context(), code)
if err != nil {
http.NotFound(w, r)
return
}
s.redis.Set(r.Context(), code, url, time.Hour)
s.localCache.Set(code, url, time.Hour)
http.Redirect(w, r, url, http.StatusFound)
}

Как отвечать на собеседовании

Начни с верхнеуровневой архитектуры: три сервиса + gateway. Затем уточни детали по мере вопросов. Обязательно проговори:

  • как генерируешь код и почему выбрал этот способ;
  • как хранишь данные и как шардируешь;
  • как обеспечиваешь консистентность между кэшем и базой;
  • как обрабатываешь пиковые нагрузки на редирект;
  • как обрабатываешь недоступность аналитики.

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

Что проверяет интервьюер

  • Понимание декомпозиции системы на сервисы с чёткими границами ответственности;
  • умение обосновать выбор хранилища и стратегии кэширования;
  • знание паттернов асинхронного взаимодействия (event-driven, message queue);
  • способность проектировать под нагрузку: шардирование, репликация, горизонтальное масштабирование;
  • понимание компромиссов между консистентностью и доступностью (CAP theorem применительно к конкретным сервисам);
  • практический опыт: как деплоить, мониторить, обрабатывать отказы.

Типичные ошибки

  • предлагать единую монолитную базу для всех сервисов - это убивает смысл микросервисов;
  • использовать глобальный auto-increment ID в распределённой системе без координации - будет узкое место;
  • игнорировать кэширование для редиректа - это самый горячий путь, без кэша не выдержит нагрузку;
  • синхронно вызывать analytics при каждом переходе - это заблокирует редирект;
  • не продумывать обработку коллизий при генерации кодов;
  • забывать про rate limiting и защиту от злоупотреблений (например, спам-генерация ссылок);
  • не учитывать, что короткие коды должны быть устойчивы к угадыванию (если ссылки приватные) - случайные строки лучше счётчика.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью